From c4e2dcf077e60ab87a4f37a56b8044b30d182ffd Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 7 Jan 2019 13:43:38 -0600 Subject: [PATCH 001/505] Try to autodetect the pyjnius JAR --- scyjava/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 63357597..657c3240 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,5 +1,7 @@ import logging import os +import sys +from pathlib import Path _logger = logging.getLogger(__name__) @@ -15,14 +17,15 @@ def _init_jvm(): PYJNIUS_JAR_STR = 'PYJNIUS_JAR' if PYJNIUS_JAR_STR not in globals(): + PYJNIUS_JAR = None try: - PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] - jnius_config.add_classpath(PYJNIUS_JAR) + PYJNIUS_JAR = Path(os.environ[PYJNIUS_JAR_STR]) except KeyError as e: - if e.args[0] == PYJNIUS_JAR_STR: - _logger.error('Unable to import scyjava: %s environment variable not defined.', PYJNIUS_JAR_STR) - else: - raise e + PYJNIUS_JAR = Path(sys.prefix) / 'share/pyjnius/pyjnius.jar' + if PYJNIUS_JAR.is_file(): + jnius_config.add_classpath(PYJNIUS_JAR) + else: + _logger.error('Unable to import scyjava: pyjnius JAR not found.') return None endpoints = scyjava_config.get_endpoints() From 531e07b807256d96dbf0279d2ff1b3397edf1fcd Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 9 Jan 2019 15:19:22 -0600 Subject: [PATCH 002/505] Autodetect pyjnius/jar in a detectable location --- scyjava/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 657c3240..35ce9469 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -15,14 +15,15 @@ def _init_jvm(): import jnius return jnius + # attempt to find pyjnius.jar if the envrionment variable is not set. PYJNIUS_JAR_STR = 'PYJNIUS_JAR' if PYJNIUS_JAR_STR not in globals(): PYJNIUS_JAR = None try: - PYJNIUS_JAR = Path(os.environ[PYJNIUS_JAR_STR]) + PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] except KeyError as e: - PYJNIUS_JAR = Path(sys.prefix) / 'share/pyjnius/pyjnius.jar' - if PYJNIUS_JAR.is_file(): + PYJNIUS_JAR = sys.prefix + '/share/pyjnius/pyjnius.jar' + if Path(PYJNIUS_JAR).is_file(): jnius_config.add_classpath(PYJNIUS_JAR) else: _logger.error('Unable to import scyjava: pyjnius JAR not found.') From b7720e90e9d0c122dec53e8c8fd5af3d29bc97a2 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 9 Jan 2019 16:27:01 -0600 Subject: [PATCH 003/505] Autodetect jre (assuming conda) --- scyjava/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 35ce9469..fd8028f8 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -29,6 +29,21 @@ def _init_jvm(): _logger.error('Unable to import scyjava: pyjnius JAR not found.') return None + # attempt to set JAVA_HOME if the environment variable is not set. + JAVA_HOME_STR = 'JAVA_HOME' + if JAVA_HOME_STR not in globals(): + JAVA_HOME = None + try: + JAVA_HOME = os.environ[JAVA_HOME_STR] + except KeyError as e: + JAVA_HOME = sys.prefix + # TODO is this necessary? + if Path(JAVA_HOME).is_dir(): + os.environ['JAVA_HOME'] = JAVA_HOME + else: + _logger.error('Unable to import scyjava: jre not found') + return None + endpoints = scyjava_config.get_endpoints() repositories = scyjava_config.get_repositories() From 80c50d0a177a584569d3242497a72aea7592ff84 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 11 Jan 2019 15:19:15 -0600 Subject: [PATCH 004/505] Find JAVA_HOME using maven settings --- scyjava/__init__.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index fd8028f8..b2a85344 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,6 +1,7 @@ import logging import os import sys +import subprocess from pathlib import Path _logger = logging.getLogger(__name__) @@ -36,8 +37,26 @@ def _init_jvm(): try: JAVA_HOME = os.environ[JAVA_HOME_STR] except KeyError as e: - JAVA_HOME = sys.prefix - # TODO is this necessary? + # attempt to find the jre by interrogating maven + # (which we have because is needed by jgo) + try: + mvn = str(subprocess.check_output(['mvn', '-v'])) + except subprocess.CalledProcessError as e: + _logger.error('Unable to import scyjava, could not find Maven') + return None + try: + begin = mvn.index('Java home: ') + except ValueError as e: + # in some versions of maven it is instead called runtime + try: + begin = mvn.index('runtime: ') + except ValueError as e: + _logger.error('Unable to import scyjava, could not locate jre') + return None + # cut out 'Java home' or 'runtime' + begin = mvn.index('/', begin) + end = mvn.index('\\n', begin) + JAVA_HOME = mvn[begin:end] if Path(JAVA_HOME).is_dir(): os.environ['JAVA_HOME'] = JAVA_HOME else: From 1e6d7253044ffdb8a23fe35a9b9d087253124520 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 11 Jan 2019 15:45:47 -0600 Subject: [PATCH 005/505] Use os.path attempting to find PYJNIUS_JAR --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index b2a85344..57ad0e86 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -23,7 +23,7 @@ def _init_jvm(): try: PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] except KeyError as e: - PYJNIUS_JAR = sys.prefix + '/share/pyjnius/pyjnius.jar' + PYJNIUS_JAR = os.path.join(sys.prefix, 'share', 'pyjnius', 'pyjnius.jar') if Path(PYJNIUS_JAR).is_file(): jnius_config.add_classpath(PYJNIUS_JAR) else: From f25e2af9cab4f21eb9aa83a7db09c2da71daf20c Mon Sep 17 00:00:00 2001 From: Hadrien Mary Date: Tue, 19 Mar 2019 14:48:14 -0400 Subject: [PATCH 006/505] Convert the convert module to a folder. The API remains the same as before --- scyjava/convert/__init__.py | 1 + scyjava/{convert.py => convert/_convert.py} | 0 2 files changed, 1 insertion(+) create mode 100644 scyjava/convert/__init__.py rename scyjava/{convert.py => convert/_convert.py} (100%) diff --git a/scyjava/convert/__init__.py b/scyjava/convert/__init__.py new file mode 100644 index 00000000..26c7d9fc --- /dev/null +++ b/scyjava/convert/__init__.py @@ -0,0 +1 @@ +from ._convert import * diff --git a/scyjava/convert.py b/scyjava/convert/_convert.py similarity index 100% rename from scyjava/convert.py rename to scyjava/convert/_convert.py From c5bc0df772616c892bc52ab9ff892ea8cd945f93 Mon Sep 17 00:00:00 2001 From: Hadrien Mary Date: Tue, 19 Mar 2019 15:24:23 -0400 Subject: [PATCH 007/505] Add Pandas <-> Table converters + tests + adapte Travis build --- .travis.yml | 3 +- scyjava/convert/_convert.py | 11 ++++++ scyjava/convert/_pandas.py | 59 ++++++++++++++++++++++++++++++ tests/test_convert.py | 73 +++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 scyjava/convert/_pandas.py diff --git a/.travis.yml b/.travis.yml index 2b3fb544..89ca169d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,7 @@ install: - conda info -a - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION - source activate test-environment - - conda install -c conda-forge pyjnius - - pip install jgo + - conda install -c conda-forge pyjnius jgo pandas numpy script: - python -m unittest discover tests -v diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index 13707ca7..ca635122 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -2,6 +2,9 @@ import jnius, collections +from ._pandas import table_to_pandas +from ._pandas import pandas_to_table + String = jnius.autoclass('java.lang.String') Boolean = jnius.autoclass('java.lang.Boolean') Integer = jnius.autoclass('java.lang.Integer') @@ -63,6 +66,7 @@ def to_java(data): :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ + if isjava(data): return data @@ -88,6 +92,10 @@ def to_java(data): else: return BigDecimal(str(data)) + # Trying to get the type without importing Pandas. + if type(data).__name__ == 'DataFrame': + return pandas_to_table(data) + if isinstance(data, collections.Mapping): jmap = LinkedHashMap() for k, v in data.items(): @@ -326,6 +334,9 @@ def to_python(data): if StringClass.isInstance(data): return data.toString() + if jclass('org.scijava.table.Table').isInstance(data): + return table_to_pandas(data) + if ListClass.isInstance(data): return JavaList(data) if MapClass.isInstance(data): diff --git a/scyjava/convert/_pandas.py b/scyjava/convert/_pandas.py new file mode 100644 index 00000000..a4921bfb --- /dev/null +++ b/scyjava/convert/_pandas.py @@ -0,0 +1,59 @@ +# Pandas <-> Scijava Table converters. +import jnius +import scyjava + + +def _import_pandas(): + try: + import pandas as pd + return pd + except ImportError: + msg = "The Pandas library is missing (http://pandas.pydata.org/). " + msg += "Please instal it using: " + msg += "conda install pandas (prefered)" + msg += " or " + msg += "pip install pandas." + raise Exception(msg) + + +def table_to_pandas(table): + pd = _import_pandas() + + data = [] + headers = [] + for i, column in enumerate(table.toArray()): + data.append(column.toArray()) + headers.append(table.getColumnHeader(i)) + df = pd.DataFrame(data).T + df.columns = headers + return df + + +def pandas_to_table(df): + pd = _import_pandas() + + if len(df.dtypes.unique()) > 1: + TableClass = jnius.autoclass('org.scijava.table.DefaultGenericTable') + else: + table_type = df.dtypes.unique()[0] + if table_type.name.startswith('float'): + TableClass = jnius.autoclass('org.scijava.table.DefaultFloatTable') + elif table_type.name.startswith('int'): + TableClass = jnius.autoclass('org.scijava.table.DefaultIntTable') + elif table_type.name.startswith('bool'): + TableClass = jnius.autoclass('org.scijava.table.DefaultBoolTable') + else: + msg = "The type '{}' is not supported.".format(table_type.name) + raise Exception(msg) + + table = TableClass(*df.shape[::-1]) + + for c, column_name in enumerate(df.columns): + table.setColumnHeader(c, column_name) + + for i, (index, row) in enumerate(df.iterrows()): + for c, value in enumerate(row): + header = df.columns[c] + table.set(header, i, scyjava.to_java(value)) + + return table diff --git a/tests/test_convert.py b/tests/test_convert.py index d2605a56..d304329e 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,5 +1,26 @@ +import scyjava_config +scyjava_config.add_repositories({'imagej.public': 'https://maven.imagej.net/content/groups/public'}) +scyjava_config.add_endpoints('org.scijava:scijava-table') + import unittest +import pandas as pd +import numpy as np from scyjava.convert import jclass, to_java, to_python +import scyjava +import jnius + + +def assert_same_table(table, df): + import numpy.testing as npt + + assert len(table.toArray()) == df.shape[1] + assert len(table.toArray()[0].toArray()) == df.shape[0] + + for i, column in enumerate(table.toArray()): + npt.assert_array_almost_equal(df.iloc[:, i].values, column.toArray()) + + assert table.getColumnHeader(i) == df.columns[i] + class TestConvert(unittest.TestCase): @@ -145,5 +166,57 @@ def testMixed(self): self.assertEqual(ml, pml) self.assertEqual(str(ml), str(pml)) + def testPandasToTable(self): + # Float table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) + + df = pd.DataFrame(array, columns=columns) + table = scyjava.to_java(df) + + assert_same_table(table, df) + assert type(table) == jnius.autoclass('org.scijava.table.DefaultFloatTable') + + # Int table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) * 100 + array = array.astype('int') + + df = pd.DataFrame(array, columns=columns) + table = scyjava.to_java(df) + + assert_same_table(table, df) + assert type(table) == jnius.autoclass('org.scijava.table.DefaultIntTable') + + # Bool table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) > 0.5 + + df = pd.DataFrame(array, columns=columns) + table = scyjava.to_java(df) + + assert_same_table(table, df) + assert type(table) == jnius.autoclass('org.scijava.table.DefaultBoolTable') + + # Mixed table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) + + df = pd.DataFrame(array, columns=columns) + + # Convert column 0 to integer + df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype('int') + # Convert column 1 to bool + df.iloc[:, 1] = df.iloc[:, 1] > 0.5 + # Convert column 2 to string + df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split('\n') + + table = scyjava.to_java(df) + + # Table types cannot be the same here, unless we want to cast. + # assert_same_table(table, df) + assert type(table) == jnius.autoclass('org.scijava.table.DefaultGenericTable') + + if __name__ == '__main__': unittest.main() From ee2e7e7e1a31574b65c05a16bfc2675aa587983f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 11:40:48 -0500 Subject: [PATCH 008/505] Tweak comment --- scyjava/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 57ad0e86..ca96ce0d 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -37,8 +37,8 @@ def _init_jvm(): try: JAVA_HOME = os.environ[JAVA_HOME_STR] except KeyError as e: - # attempt to find the jre by interrogating maven - # (which we have because is needed by jgo) + # attempt to find Java by interrogating maven + # (which we have because it is needed by jgo) try: mvn = str(subprocess.check_output(['mvn', '-v'])) except subprocess.CalledProcessError as e: From 0130955f0f34f21a3c63398db13ed1932e28a895 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 11:41:03 -0500 Subject: [PATCH 009/505] Add debug logging to the initialization And enable debug logging if DEBUG variable is set. --- scyjava/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index ca96ce0d..5a9ac7c0 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -6,6 +6,14 @@ _logger = logging.getLogger(__name__) +# Enable debug logging if DEBUG environment variable is set. +try: + debug = os.environ['DEBUG'] + if debug: + _logger.setLevel(logging.DEBUG) +except KeyError as e: + pass + def _init_jvm(): import scyjava_config import jnius_config @@ -21,22 +29,29 @@ def _init_jvm(): if PYJNIUS_JAR_STR not in globals(): PYJNIUS_JAR = None try: + _logger.debug('Checking %s environment variable', PYJNIUS_JAR_STR) PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] except KeyError as e: + _logger.debug('No %s environment variable; falling back to default path', PYJNIUS_JAR_STR) PYJNIUS_JAR = os.path.join(sys.prefix, 'share', 'pyjnius', 'pyjnius.jar') if Path(PYJNIUS_JAR).is_file(): + _logger.debug('%s found at "%s"', PYJNIUS_JAR_STR, PYJNIUS_JAR) jnius_config.add_classpath(PYJNIUS_JAR) else: _logger.error('Unable to import scyjava: pyjnius JAR not found.') return None + else: + _logger.debug('%s found in globals', PYJNIUS_JAR_STR) # attempt to set JAVA_HOME if the environment variable is not set. JAVA_HOME_STR = 'JAVA_HOME' if JAVA_HOME_STR not in globals(): JAVA_HOME = None try: + _logger.debug('Checking %s environment variable', JAVA_HOME_STR) JAVA_HOME = os.environ[JAVA_HOME_STR] except KeyError as e: + _logger.debug('No %s environment variable; checking with Maven', JAVA_HOME_STR) # attempt to find Java by interrogating maven # (which we have because it is needed by jgo) try: @@ -44,6 +59,7 @@ def _init_jvm(): except subprocess.CalledProcessError as e: _logger.error('Unable to import scyjava, could not find Maven') return None + _logger.debug('Maven said: %s', mvn) try: begin = mvn.index('Java home: ') except ValueError as e: @@ -58,10 +74,13 @@ def _init_jvm(): end = mvn.index('\\n', begin) JAVA_HOME = mvn[begin:end] if Path(JAVA_HOME).is_dir(): + _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) os.environ['JAVA_HOME'] = JAVA_HOME else: _logger.error('Unable to import scyjava: jre not found') return None + else: + _logger.debug('%s found in globals', JAVA_HOME_STR) endpoints = scyjava_config.get_endpoints() repositories = scyjava_config.get_repositories() From 8b4bc9495cf6a64925491a71796a33b0ef4cea15 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 11:49:11 -0500 Subject: [PATCH 010/505] Use jre/.. when detecting Java from mvn --- scyjava/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 5a9ac7c0..f9bd433b 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -73,8 +73,12 @@ def _init_jvm(): begin = mvn.index('/', begin) end = mvn.index('\\n', begin) JAVA_HOME = mvn[begin:end] - if Path(JAVA_HOME).is_dir(): + java_path = Path(JAVA_HOME) + if java_path.is_dir(): _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) + if java_path.name is 'jre': + _logger.debug('JAVA_HOME points at jre folder; using parent instead') + JAVA_HOME = str(java_path.parent) os.environ['JAVA_HOME'] = JAVA_HOME else: _logger.error('Unable to import scyjava: jre not found') From c286a62437d9e621366da72f2d6666ae784d222a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:07:24 -0500 Subject: [PATCH 011/505] Release version 0.2.0 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 2925fb26..06dc2e79 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.1.1.dev0' +version = '0.2.0' _logger = logging.getLogger(__name__) From 4ab346a8db84387171a5d89046a67d23b65d3f8d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:17:53 -0500 Subject: [PATCH 012/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 06dc2e79..a5ff866c 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.0' +version = '0.2.1.dev0' _logger = logging.getLogger(__name__) From 301810ee3ffeb3c3250b5cc501d7ce6c308c846e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:38:59 -0500 Subject: [PATCH 013/505] Tweak None test --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index f9bd433b..deb95a4f 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -114,7 +114,7 @@ def _init_jvm(): return None jnius = _init_jvm() -if (jnius == None): +if jnius is None: raise ImportError('Unable to import scyjava dependency jnius.') from .convert import isjava, jclass, to_java, to_python From 51b118c7283164c1fec4f946d38f787cf98c3f1b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:39:23 -0500 Subject: [PATCH 014/505] Include convert subpackage Without this, the distribution does include the conversion stuff, due to restructuring in f25e2af9cab4f21eb9aa83a7db09c2da71daf20c. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 49aff707..239f67dc 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setuptools.setup( name='scyjava', python_requires='>=3', - packages=['scyjava'], + packages=['scyjava', 'scyjava.convert'], py_modules=['scyjava_config'], version=scyjava_config.version, author='Philipp Hanslovsky, Curtis Rueden', From a83b15a8bfd21a9cbd5cf41909c703fd6b3949a7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:52:29 -0500 Subject: [PATCH 015/505] Release version 0.2.1 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index a5ff866c..fba1389f 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.1.dev0' +version = '0.2.1' _logger = logging.getLogger(__name__) From e80683f94c0a3b4f6e3118112c3c05cb433c9206 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 12:53:15 -0500 Subject: [PATCH 016/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index fba1389f..e9dfee53 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.1' +version = '0.2.2.dev0' _logger = logging.getLogger(__name__) From 1fd196d199188ba71a60bca6bb75eda261395c0d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 15:12:17 -0500 Subject: [PATCH 017/505] Fall back to default logic in more cases In particular, if an environment variable is set to the empty string, that is not good enough to use; we should still try to be smart. --- scyjava/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index deb95a4f..191594e9 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -31,8 +31,11 @@ def _init_jvm(): try: _logger.debug('Checking %s environment variable', PYJNIUS_JAR_STR) PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] - except KeyError as e: - _logger.debug('No %s environment variable; falling back to default path', PYJNIUS_JAR_STR) + except KeyError: + _logger.debug('No %s environment variable', PYJNIUS_JAR_STR) + if not PYJNIUS_JAR: + # NB: This logic handles both None and empty string cases. + _logger.debug('%s still unknown; falling back to default path', PYJNIUS_JAR_STR) PYJNIUS_JAR = os.path.join(sys.prefix, 'share', 'pyjnius', 'pyjnius.jar') if Path(PYJNIUS_JAR).is_file(): _logger.debug('%s found at "%s"', PYJNIUS_JAR_STR, PYJNIUS_JAR) @@ -50,8 +53,11 @@ def _init_jvm(): try: _logger.debug('Checking %s environment variable', JAVA_HOME_STR) JAVA_HOME = os.environ[JAVA_HOME_STR] - except KeyError as e: - _logger.debug('No %s environment variable; checking with Maven', JAVA_HOME_STR) + except KeyError: + _logger.debug('No %s environment variable', JAVA_HOME_STR) + if not JAVA_HOME: + # NB: This logic handles both None and empty string cases. + _logger.debug('%s still unknown; checking with Maven', JAVA_HOME_STR) # attempt to find Java by interrogating maven # (which we have because it is needed by jgo) try: From 3902ac15302333a16061f9630a1cd7021c3ccbcf Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 15:25:55 -0500 Subject: [PATCH 018/505] Release version 0.2.2 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index e9dfee53..54bea64b 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.2.dev0' +version = '0.2.2' _logger = logging.getLogger(__name__) From 9a3fa3e67d941832184c0f56dc9befa57221fc65 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 10 Apr 2019 15:26:11 -0500 Subject: [PATCH 019/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 54bea64b..9a226295 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.2' +version = '0.2.3.dev0' _logger = logging.getLogger(__name__) From 46903e3aa7eeedb2782411b6caa30248964bd85b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 May 2019 09:48:23 -0500 Subject: [PATCH 020/505] Update maven.imagej.net -> maven.scijava.org --- README.md | 2 +- scyjava_config.py | 2 +- tests/test_convert.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 77d76b18..12dda235 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) ```python >>> import scyjava_config ->>> scyjava_config.add_repositories({'imagej.public': 'https://maven.imagej.net/content/groups/public'}) +>>> scyjava_config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) >>> scyjava_config.add_endpoints('net.imagej:imagej:2.0.0-rc-65') >>> import scyjava, jnius >>> System = jnius.autoclass('java.lang.System') diff --git a/scyjava_config.py b/scyjava_config.py index 9a226295..fe1e9f0d 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -36,7 +36,7 @@ def maven_scijava_repository(): """ :return: url for public scijava maven repo """ - return 'https://maven.imagej.net/content/groups/public' + return 'https://maven.scijava.org/content/groups/public' def add_endpoints(*endpoints): global _endpoints diff --git a/tests/test_convert.py b/tests/test_convert.py index d304329e..f07d3dc2 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,5 +1,5 @@ import scyjava_config -scyjava_config.add_repositories({'imagej.public': 'https://maven.imagej.net/content/groups/public'}) +scyjava_config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) scyjava_config.add_endpoints('org.scijava:scijava-table') import unittest From 35010bb6d0634ee6942275440b915d458eb419b3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2019 10:05:53 -0500 Subject: [PATCH 021/505] Fix scyjava problems on Windows The version of Maven packaged by conda includes both mvn and mvn.cmd; only mvn.cmd works when invoked from Python. Invoking mvn yields: FileNotFoundError: [WinError 2] The system cannot find the file specified Then, once mvn.cmd is used, the output capture has Windows-style line breaks ('\r\n') instead of Unix-style ('\n'). So we adjust for that. Then, when parsing the path, Windows uses backslash rather than forward slash, so we cannot rely on '/' as the leading path character. Instead, we look for the colon and jump ahead two characters. This assumes only one space after the colon, but Maven appears to be reliable about that. Then, when attempting to link to the JVM, we need jvm.dll on the PATH too, or else the linkage fails with: ImportError: DLL load failed: The specified module could not be found. The jvm.dll resides in a subfolder called server (for older Javas it's jre\bin\server; for newer ones it's bin\server). So we look for jvm.dll in those places, and if found, add the containing folder to the PATH. See also imagej/pyimagej#43. --- scyjava/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 191594e9..c582e69d 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,5 +1,6 @@ import logging import os +import platform import sys import subprocess from pathlib import Path @@ -61,7 +62,11 @@ def _init_jvm(): # attempt to find Java by interrogating maven # (which we have because it is needed by jgo) try: - mvn = str(subprocess.check_output(['mvn', '-v'])) + if (platform.system() == 'Windows'): + mvn = str(subprocess.check_output(['mvn.cmd', '-v'])) + mvn = mvn.replace('\\r\\n', '\\n') # Fix Windows line breaks. + else: + mvn = str(subprocess.check_output(['mvn', '-v'])) except subprocess.CalledProcessError as e: _logger.error('Unable to import scyjava, could not find Maven') return None @@ -76,7 +81,7 @@ def _init_jvm(): _logger.error('Unable to import scyjava, could not locate jre') return None # cut out 'Java home' or 'runtime' - begin = mvn.index('/', begin) + begin = mvn.index(':', begin) + 2 end = mvn.index('\\n', begin) JAVA_HOME = mvn[begin:end] java_path = Path(JAVA_HOME) @@ -92,6 +97,18 @@ def _init_jvm(): else: _logger.debug('%s found in globals', JAVA_HOME_STR) + # On Windows, add server subfolder to the PATH so jvm.dll can be found. + if (platform.system() == 'Windows'): + # Java 9 and later + jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'bin', 'server') + if Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): + os.environ['PATH'] += ';' + jvm_server_dir + else: + # Java 8 and earlier + jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'jre', 'bin', 'server') + if Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): + os.environ['PATH'] += ';' + jvm_server_dir + endpoints = scyjava_config.get_endpoints() repositories = scyjava_config.get_repositories() From b34be2e424066823b34842446f05e01263d10a9a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2019 10:18:40 -0500 Subject: [PATCH 022/505] Release version 0.2.3 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index fe1e9f0d..c3f9d6cd 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.3.dev0' +version = '0.2.3' _logger = logging.getLogger(__name__) From 9b87aa447f49711f551d471a34beb406fda13a3b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 6 Sep 2019 15:56:39 -0500 Subject: [PATCH 023/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index c3f9d6cd..431973fa 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -22,7 +22,7 @@ import jnius_config import pathlib -version = '0.2.3' +version = '0.2.4.dev0' _logger = logging.getLogger(__name__) From 63aced9b9d404e2b8b99b022973c9faecad91bd9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Oct 2019 10:02:28 -0500 Subject: [PATCH 024/505] README: document how to increase the max heap --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 12dda235..979c8e6f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,17 @@ Built on [pyjnius](https://pyjnius.readthedocs.io/en/latest/) and [jgo](https:// '1.8.0_152-release' ``` +To pass parameters to the JVM, such as an increased max heap size: + +```python +>>> import scyjava_config +>>> scyjava_config.add_options('-Xmx6g') +>>> import scyjava, jnius +>>> Runtime = jnius.autoclass('java.lang.Runtime') +>>> Runtime.getRuntime().maxMemory() / 2**30 +5.33349609375 +``` + See the [Pyjnius documentation](https://pyjnius.readthedocs.io/en/latest/) for more about calling Java from Python. ## Use Maven artifacts from remote repositories From a0a453cdfa7c7b23341cc947edf32d7d0f5c42ee Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 12:59:37 -0500 Subject: [PATCH 025/505] Expose jgo manage_dependencies flag And set it to True by default. This flag is necessary for the SciJava BOM to be respected. See also https://github.com/scijava/jgo/issues/9. --- scyjava/__init__.py | 1 + scyjava_config.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index c582e69d..df0edc81 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -121,6 +121,7 @@ def _init_jvm(): '+'.join(endpoints), m2_repo=scyjava_config.get_m2_repo(), cache_dir=scyjava_config.get_cache_dir(), + manage_dependencies=scijava_config.get_manage_deps(), repositories=repositories, verbose=scyjava_config.get_verbose() ) diff --git a/scyjava_config.py b/scyjava_config.py index 431973fa..33b0d7f8 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -6,6 +6,8 @@ 'get_repositories', 'set_verbose', 'get_verbose', + 'set_manage_deps', + 'get_manage_deps', 'set_cache_dir', 'get_cache_dir', 'set_m2_repo', @@ -29,6 +31,7 @@ _endpoints = [] _repositories = {} _verbose = 0 +_manage_deps = True _cache_dir = pathlib.Path.home() / '.jgo' _m2_repo = pathlib.Path.home() / '.m2' / 'repository' @@ -75,6 +78,17 @@ def get_verbose(): return _verbose +def set_manage_deps(manage): + global _manage_deps + _logger.debug('Setting manage deps to %d (was %d)', manage, _manage_deps) + _manage_deps = manage + + +def get_manage_deps(): + global _manage_deps + return _manage_deps + + def set_cache_dir(dir): global _cache_dir _logger.debug('Setting cache dir to %s (was %s)', dir, _cache_dir) From 72dade1b025f9b385333e2a4313d936e478dc6e8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:09:53 -0500 Subject: [PATCH 026/505] Release version 0.3.0 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 33b0d7f8..701aacbc 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.2.4.dev0' +version = '0.3.0' _logger = logging.getLogger(__name__) From 0c5c2262194b7aa88f4921511d45763efabacd64 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:10:53 -0500 Subject: [PATCH 027/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 701aacbc..6eff810c 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.3.0' +version = '0.3.1.dev0' _logger = logging.getLogger(__name__) From af09918c046c0bbb1e0c9f2844538c80fa194d54 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:20:18 -0500 Subject: [PATCH 028/505] Fix typo --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index df0edc81..789b0d34 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -121,7 +121,7 @@ def _init_jvm(): '+'.join(endpoints), m2_repo=scyjava_config.get_m2_repo(), cache_dir=scyjava_config.get_cache_dir(), - manage_dependencies=scijava_config.get_manage_deps(), + manage_dependencies=scyjava_config.get_manage_deps(), repositories=repositories, verbose=scyjava_config.get_verbose() ) From 1139c82c99ea4dd6af52a2d0c1aad0234dd5d280 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:17:54 -0500 Subject: [PATCH 029/505] Fix collections.abc warnings Hopefully this avoids issues with Python 3.8. --- README.md | 12 ++++++------ scyjava/convert/_convert.py | 32 ++++++++++++++++---------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 979c8e6f..d2c63db7 100644 --- a/README.md +++ b/README.md @@ -173,12 +173,12 @@ FUNCTIONS * Boolean -> bool * Byte, Short, Integer, Long, BigInteger -> int * Float, Double, BigDecimal -> float - * Map -> collections.MutableMapping (dict-like) - * Set -> collections.MutableSet (set-like) - * List -> collections.MutableSequence (list-like) - * Collection -> collections.Collection - * Iterable -> collections.Iterable - * Iterator -> collections.Iterator + * Map -> collections.abc.MutableMapping (dict-like) + * Set -> collections.abc.MutableSet (set-like) + * List -> collections.abc.MutableSequence (list-like) + * Collection -> collections.abc.Collection + * Iterable -> collections.abc.Iterable + * Iterator -> collections.abc.Iterator :returns: A corresponding Python object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. ``` diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index ca635122..f83c6e21 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -1,6 +1,6 @@ # General-purpose utility methods for Python <-> Java type conversion. -import jnius, collections +import jnius, collections.abc from ._pandas import table_to_pandas from ._pandas import pandas_to_table @@ -96,7 +96,7 @@ def to_java(data): if type(data).__name__ == 'DataFrame': return pandas_to_table(data) - if isinstance(data, collections.Mapping): + if isinstance(data, collections.abc.Mapping): jmap = LinkedHashMap() for k, v in data.items(): jk = to_java(k) @@ -104,14 +104,14 @@ def to_java(data): jmap.put(jk, jv) return jmap - if isinstance(data, collections.Set): + if isinstance(data, collections.abc.Set): jset = LinkedHashSet() for item in data: jitem = to_java(item) jset.add(jitem) return jset - if isinstance(data, collections.Iterable): + if isinstance(data, collections.abc.Iterable): jlist = ArrayList() for item in data: jitem = to_java(item) @@ -162,7 +162,7 @@ def __str__(self): return _jstr(self.jobj) -class JavaIterable(JavaObject, collections.Iterable): +class JavaIterable(JavaObject, collections.abc.Iterable): def __init__(self, jobj): JavaObject.__init__(self, jobj, IterableClass) @@ -173,7 +173,7 @@ def __str__(self): return '[' + ', '.join(_jstr(v) for v in self) + ']' -class JavaCollection(JavaIterable, collections.Collection): +class JavaCollection(JavaIterable, collections.abc.Collection): def __init__(self, jobj): JavaObject.__init__(self, jobj, CollectionClass) @@ -195,7 +195,7 @@ def __eq__(self, other): return False -class JavaIterator(JavaObject, collections.Iterator): +class JavaIterator(JavaObject, collections.abc.Iterator): def __init__(self, jobj): JavaObject.__init__(self, jobj, IteratorClass) @@ -205,7 +205,7 @@ def __next__(self): raise StopIteration -class JavaList(JavaCollection, collections.MutableSequence): +class JavaList(JavaCollection, collections.abc.MutableSequence): def __init__(self, jobj): JavaObject.__init__(self, jobj, ListClass) @@ -222,7 +222,7 @@ def insert(self, index, object): return to_python(self.jobj.set(index, object)) -class JavaMap(JavaObject, collections.MutableMapping): +class JavaMap(JavaObject, collections.abc.MutableMapping): def __init__(self, jobj): JavaObject.__init__(self, jobj, MapClass) @@ -259,7 +259,7 @@ def __str__(self): return '{' + ', '.join(_jstr(k) + ': ' + _jstr(v) for k,v in self.items()) + '}' -class JavaSet(JavaCollection, collections.MutableSet): +class JavaSet(JavaCollection, collections.abc.MutableSet): def __init__(self, jobj): JavaObject.__init__(self, jobj, SetClass) @@ -296,12 +296,12 @@ def to_python(data): * Boolean -> bool * Byte, Short, Integer, Long, BigInteger -> int * Float, Double, BigDecimal -> float - * Map -> collections.MutableMapping (dict-like) - * Set -> collections.MutableSet (set-like) - * List -> collections.MutableSequence (list-like) - * Collection -> collections.Collection - * Iterable -> collections.Iterable - * Iterator -> collections.Iterator + * Map -> collections.abc.MutableMapping (dict-like) + * Set -> collections.abc.MutableSet (set-like) + * List -> collections.abc.MutableSequence (list-like) + * Collection -> collections.abc.Collection + * Iterable -> collections.abc.Iterable + * Iterator -> collections.abc.Iterator :returns: A corresponding Python object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ From 37fbfdee8d73d824ece581653bbdbe7e4ac2c974 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:22:16 -0500 Subject: [PATCH 030/505] Release version 0.3.1 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 6eff810c..4f17c6eb 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.3.1.dev0' +version = '0.3.1' _logger = logging.getLogger(__name__) From d4db31b75a8afa338b95cf25753b376f7c8e8651 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Oct 2019 13:22:56 -0500 Subject: [PATCH 031/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 4f17c6eb..071b164f 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.3.1' +version = '0.3.2.dev0' _logger = logging.getLogger(__name__) From 394062e7428586d695f8618df355f48d4fc2b506 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 7 Nov 2019 15:21:12 -0600 Subject: [PATCH 032/505] Avoid hardcoded dependency on scijava-table Without scijava-table on the classpath, I was seeing: Traceback (most recent call last): File "", line 1, in File ".../scyjava/convert/_convert.py", line 337, in to_python if jclass('org.scijava.table.Table').isInstance(data): File ".../scyjava/convert/_convert.py", line 50, in jclass return jnius.find_javaclass(data) File "jnius/jnius_export_func.pxi", line 26, in jnius.find_javaclass jnius.JavaException: Class not found b'org/scijava/table/Table' Better to eat the exception if org.scijava.table.Table is not available. --- scyjava/convert/_convert.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index f83c6e21..77af4e64 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -334,8 +334,12 @@ def to_python(data): if StringClass.isInstance(data): return data.toString() - if jclass('org.scijava.table.Table').isInstance(data): - return table_to_pandas(data) + try: + if jclass('org.scijava.table.Table').isInstance(data): + return table_to_pandas(data) + except: + # No worries if scijava-table is not available. + pass if ListClass.isInstance(data): return JavaList(data) From df21679cca04e915c7c03fe2cefc8bdcc68f01c3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 7 Nov 2019 15:25:13 -0600 Subject: [PATCH 033/505] Convert arguments to Java in appropriate places And test more features of JavaList. --- scyjava/convert/_convert.py | 6 +++--- tests/test_convert.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index 77af4e64..6e907c42 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -213,13 +213,13 @@ def __getitem__(self, key): return to_python(self.jobj.get(key)) def __setitem__(self, key, value): - return to_python(self.jobj.set(key, value)) + return to_python(self.jobj.set(key, to_java(value))) def __delitem__(self, key): - return to_python(self.jobj.remove(key)) + return to_python(self.jobj.remove(to_java(key))) def insert(self, index, object): - return to_python(self.jobj.set(index, object)) + return to_python(self.jobj.set(index, to_java(object))) class JavaMap(JavaObject, collections.abc.MutableMapping): diff --git a/tests/test_convert.py b/tests/test_convert.py index f07d3dc2..374401c0 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -101,6 +101,9 @@ def testList(self): pl = to_python(jl) self.assertEqual(l, pl) self.assertEqual(str(l), str(pl)) + self.assertEqual(pl[1], 'quick') + pl[7] = 'silly' + self.assertEqual('The quick brown fox jumps over the silly dogs', ' '.join(pl)) def testSet(self): s = set(['orange', 'apple', 'pineapple', 'plum']) From f96455194b64ebb2bb1c2c473b0b28fcf5064041 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 7 Nov 2019 15:26:30 -0600 Subject: [PATCH 034/505] Make Java-to-Python conversion work in more cases When a Java data structure contains some objects that do not have native Python equivalents, we do not want conversion of that data structure to fail, nor do we want to receive conversion errors while accessing the problematic elements of the structure from Python. --- scyjava/convert/_convert.py | 37 ++++++++++++++++++++++++++++--------- scyjava_config.py | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index 6e907c42..fdb4b1b4 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -178,6 +178,7 @@ def __init__(self, jobj): JavaObject.__init__(self, jobj, CollectionClass) def __contains__(self, item): + # NB: Collection.contains returns boolean, so no need for gentleness. return to_python(self.jobj.contains(to_java(item))) def __len__(self): @@ -201,7 +202,9 @@ def __init__(self, jobj): def __next__(self): if self.jobj.hasNext(): - return to_python(self.jobj.next()) + # NB: Even if an element cannot be converted, + # we still want to support Pythonic iteration. + return to_python(self.jobj.next(), gentle=True) raise StopIteration @@ -210,16 +213,21 @@ def __init__(self, jobj): JavaObject.__init__(self, jobj, ListClass) def __getitem__(self, key): - return to_python(self.jobj.get(key)) + # NB: Even if an element cannot be converted, + # we still want Pythonic access to elements. + return to_python(self.jobj.get(key), gentle=True) def __setitem__(self, key, value): - return to_python(self.jobj.set(key, to_java(value))) + # NB: List.set(int, Object) returns inserted element, so be gentle here. + return to_python(self.jobj.set(key, to_java(value)), gentle=True) def __delitem__(self, key): + # NB: List.remove(Object) returns boolean, so no need for gentleness. return to_python(self.jobj.remove(to_java(key))) def insert(self, index, object): - return to_python(self.jobj.set(index, to_java(object))) + # NB: List.set(int, Object) returns inserted element, so be gentle here. + return to_python(self.jobj.set(index, to_java(object)), gentle=True) class JavaMap(JavaObject, collections.abc.MutableMapping): @@ -227,13 +235,17 @@ def __init__(self, jobj): JavaObject.__init__(self, jobj, MapClass) def __getitem__(self, key): - return to_python(self.jobj.get(to_java(key))) + # NB: Even if an element cannot be converted, + # we still want Pythonic access to elements. + return to_python(self.jobj.get(to_java(key)), gentle=True) def __setitem__(self, key, value): - return to_python(self.jobj.put(to_java(key), to_java(value))) + # NB: Map.put(Object, Object) returns inserted value, so be gentle here. + return to_python(self.jobj.put(to_java(key), to_java(value)), gentle=True) def __delitem__(self, key): - return to_python(self.jobj.remove(to_python(key))) + # NB: Map.remove(Object) returns the removed key, so be gentle here. + return to_python(self.jobj.remove(to_java(key)), gentle=True) def keys(self): return to_python(self.jobj.keySet()) @@ -264,9 +276,11 @@ def __init__(self, jobj): JavaObject.__init__(self, jobj, SetClass) def add(self, item): + # NB: Set.add returns boolean, so no need for gentleness. return to_python(self.jobj.add(to_java(item))) def discard(self, item): + # NB: Set.remove returns boolean, so no need for gentleness. return to_python(self.jobj.remove(to_java(item))) def __iter__(self): @@ -287,10 +301,12 @@ def __str__(self): return '{' + ', '.join(_jstr(v) for v in self) + '}' -def to_python(data): +def to_python(data, gentle=False): """ Recursively convert a Java object to a Python object. :param data: The Java object to convert. + :param gentle: If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. Supported types include: * String, Character -> str * Boolean -> bool @@ -303,7 +319,8 @@ def to_python(data): * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :raises TypeError: if the argument is not one of the aforementioned types, + and the gentle flag is not set. """ if not isjava(data): return data @@ -354,4 +371,6 @@ def to_python(data): if IteratorClass.isInstance(data): return JavaIterator(data) + if gentle: + return data raise TypeError('Unsupported data type: ' + str(type(data))) diff --git a/scyjava_config.py b/scyjava_config.py index 071b164f..ad5f6b64 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.3.2.dev0' +version = '0.4.0.dev0' _logger = logging.getLogger(__name__) From d18b776b2c893eaeaf6f38de9d413a0975e8eb94 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 8 Nov 2019 12:19:57 -0600 Subject: [PATCH 035/505] Add a unit test for gentle conversion --- tests/test_convert.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_convert.py b/tests/test_convert.py index 374401c0..2a84641e 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -169,6 +169,19 @@ def testMixed(self): self.assertEqual(ml, pml) self.assertEqual(str(ml), str(pml)) + def testGentle(self): + Object = jnius.autoclass('java.lang.Object') + unknown_thing = Object() + converted_thing = to_python(unknown_thing, gentle=True) + assert type(converted_thing) == Object + bad_conversion = None + try: + bad_conversion = to_python(unknown_thing) + except: + # NB: Failure is expected here. + pass + self.assertIsNone(bad_conversion) + def testPandasToTable(self): # Float table. columns = ["header1", "header2", "header3", "header4", "header5"] From 38b39161ad21a6f4bdfebd599a94152a013ac432 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 8 Nov 2019 12:20:05 -0600 Subject: [PATCH 036/505] Add a unit test for conversion of complex objects This test covers the case where the data structure itself can be wrapped to Python from Java, but some elements of the data structure might not be able to be converted -- when e.g. iterating over a list of arbitrary Java objects. --- tests/test_convert.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_convert.py b/tests/test_convert.py index 2a84641e..4675183a 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -182,6 +182,29 @@ def testGentle(self): pass self.assertIsNone(bad_conversion) + def testStructureWithSomeUnsupportedItems(self): + # Create Java data structure with some challenging items. + Object = jnius.autoclass('java.lang.Object') + jmap = to_java({ + 'list': ['a', Object(), 1], + 'set': {'x', Object(), 2}, + 'object': Object(), + 'foo': 'bar' + }) + self.assertEqual('java.util.LinkedHashMap', jclass(jmap).getName()) + + # Convert it back to Python. + pdict = to_python(jmap) + l = pdict['list'] + self.assertEqual(pdict['list'][0], 'a') + assert type(pdict['list'][1]) == Object + assert pdict['list'][2] == 1 + assert 'x' in pdict['set'] + assert 2 in pdict['set'] + assert len(pdict['set']) == 3 + assert type(pdict['object']) == Object + self.assertEqual(pdict['foo'], 'bar') + def testPandasToTable(self): # Float table. columns = ["header1", "header2", "header3", "header4", "header5"] From 8076a9231ae6f200a9193319bda62fd6a1e5cde9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 8 Nov 2019 14:01:24 -0600 Subject: [PATCH 037/505] README: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2c63db7..a13133fe 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) +++oo*OO######OO*oo+++++oo*OO######OO*oo+++++oo*OO######OO*oo+++ ``` -See the [jgo documentation](https://github.com/scijava/jgo) documentation for more about Maven endpoints. +See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven endpoints. ## Convert between Python and Java data structures From d69d4e9694c5d3286d52ded6d51e6336b36c33a4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 8 Nov 2019 14:22:33 -0600 Subject: [PATCH 038/505] Release version 0.4.0 --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index ad5f6b64..3d41835f 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.4.0.dev0' +version = '0.4.0' _logger = logging.getLogger(__name__) From 31af9af8a7849393283e613a3a53fb76d19a40b5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 8 Nov 2019 14:23:40 -0600 Subject: [PATCH 039/505] Bump to next development cycle --- scyjava_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava_config.py b/scyjava_config.py index 3d41835f..c30198f5 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -24,7 +24,7 @@ import jnius_config import pathlib -version = '0.4.0' +version = '0.4.1.dev0' _logger = logging.getLogger(__name__) From 8eb61ea504460c19a229cbdf72137ba5580bacfb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 15 Jan 2020 12:57:44 -0600 Subject: [PATCH 040/505] Add convenience function for gleaning stack trace --- scyjava/convert/_convert.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index fdb4b1b4..0d892d1f 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -51,6 +51,28 @@ def jclass(data): raise TypeError('Cannot glean class from data of type: ' + str(type(data))) +def jstacktrace(exc): + """ + Extract the Java-side stack trace from a wrapped Java exception. + + Example of usage: + + from jnius import autoclass + try: + Integer = autoclass('java.lang.Integer') + nan = Integer.parseInt('not a number') + except Exception as exc: + print(jstacktrace(exc)) + + :param exc: The JavaException from which to extract the stack trace. + :returns: A multi-line string containing the stack trace, or empty string + if no stack trace could be extracted. + """ + if not hasattr(exc, 'classname') or exc.classname is None: + return str(exc) + return '' if not exc.stacktrace else '\n\tat '.join(exc.stacktrace) + + def to_java(data): """ Recursively convert a Python object to a Java object. From 22e21e57f0393fc02324e054f96783c794ec6e64 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 30 Mar 2020 16:42:39 -0500 Subject: [PATCH 041/505] Let None pass through when converting to Java The pyjnius layer takes care of expressing None as null on the Java side, and vice versa. The to_python method already converts Java's null back to Python's None. Closes #16. --- scyjava/convert/_convert.py | 3 +++ tests/test_convert.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index 0d892d1f..ef389c7d 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -89,6 +89,9 @@ def to_java(data): :raises TypeError: if the argument is not one of the aforementioned types. """ + if data is None: + return None + if isjava(data): return data diff --git a/tests/test_convert.py b/tests/test_convert.py index 4675183a..4885b697 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -169,6 +169,16 @@ def testMixed(self): self.assertEqual(ml, pml) self.assertEqual(str(ml), str(pml)) + def testNone(self): + d = {'key':None, None:'value', 'foo':'bar'} + jd = to_java(d) + self.assertEqual(3, jd.size()) + self.assertEqual(None, jd.get('key')) + self.assertEqual('value', jd.get(None)) + self.assertEqual('bar', jd.get('foo')) + pd = to_python(jd) + self.assertEqual(d, pd) + def testGentle(self): Object = jnius.autoclass('java.lang.Object') unknown_thing = Object() From c59302942e6c1294708551cf6770e80688426a6e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 19 Jun 2020 08:29:31 -0500 Subject: [PATCH 042/505] isjava: detect Python-side Java classes Otherwise, recursive conversion of data structures containing Java objects implemented from Python fail with unsupported type error. --- scyjava/convert/_convert.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index ef389c7d..dac24cda 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -24,7 +24,9 @@ def isjava(data): """Return whether the given data object is a Java object.""" - return isinstance(data, jnius.JavaClass) or isinstance(data, jnius.MetaJavaClass) + return isinstance(data, jnius.JavaClass) or \ + isinstance(data, jnius.MetaJavaClass) or \ + isinstance(data, jnius.PythonJavaClass) def jclass(data): From 100688d09e20fb3f817f7e9cc337d5ce4ee8c45b Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 17 Aug 2020 11:18:29 -0500 Subject: [PATCH 043/505] Switch from PyJNIus to JPype Closes #18. See #18 for detailed discussion and rationale. --- .travis.yml | 2 +- README.md | 85 ++++++++------- scyjava/convert/_convert.py | 194 ++++++++++++++++++++-------------- scyjava/convert/_pandas.py | 59 ----------- scyjava/{ => jvm}/__init__.py | 81 +++++--------- scyjava_config.py | 48 +++------ setup.py | 2 +- tests/test_convert.py | 33 +++--- 8 files changed, 228 insertions(+), 276 deletions(-) delete mode 100644 scyjava/convert/_pandas.py rename scyjava/{ => jvm}/__init__.py (62%) diff --git a/.travis.yml b/.travis.yml index 89ca169d..6407bb44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - conda info -a - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION - source activate test-environment - - conda install -c conda-forge pyjnius jgo pandas numpy + - conda install -c conda-forge jpype1 jgo pandas numpy script: - python -m unittest discover tests -v diff --git a/README.md b/README.md index a13133fe..be7a5aa4 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,52 @@ -# scyjava - Supercharged Java access from Python. -Built on [pyjnius](https://pyjnius.readthedocs.io/en/latest/) and [jgo](https://github.com/scijava/jgo). +Built on [JPype](https://jpype.readthedocs.io/en/latest/) and [jgo](https://github.com/scijava/jgo). ## Use Java classes from Python ```python ->>> import scyjava, jnius ->>> System = jnius.autoclass('java.lang.System') +>>> import jpype +>>> import jpype.imports +>>> jpype.startJVM() +>>> System = jpype.JClass('java.lang.System') >>> System.getProperty('java.version') -'1.8.0_152-release' +'1.8.0_252' ``` To pass parameters to the JVM, such as an increased max heap size: ```python ->>> import scyjava_config ->>> scyjava_config.add_options('-Xmx6g') ->>> import scyjava, jnius ->>> Runtime = jnius.autoclass('java.lang.Runtime') +>>> import jpype +>>> import jpype.imports +>>> import scyjava.jvm +>>> scyjava.jvm.start_JVM('-Xmx6g') +>>> Runtime = jpype.JClass('java.lang.Runtime') >>> Runtime.getRuntime().maxMemory() / 2**30 5.33349609375 ``` -See the [Pyjnius documentation](https://pyjnius.readthedocs.io/en/latest/) for more about calling Java from Python. +See the [JPype documentation](https://jpype.readthedocs.io/en/latest/) for more about calling Java from Python. ## Use Maven artifacts from remote repositories ### From Maven Central ```python ->>> import sys; sys.version_info +>>> import sys +>>> sys.version_info sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0) >>> import scyjava_config >>> scyjava_config.add_endpoints('org.python:jython-standalone:2.7.1') ->>> import scyjava, jnius ->>> jython = jnius.autoclass('org.python.util.jython') +>>> import jpype +>>> import scyjava.jvm +>>> scyjava.jvm.start_JVM() +>>> jython = jpype.JClass('org.python.util.jython') >>> jython.main([]) -Jython 2.7.1 (default:0df7adb1b397, Jun 30 2017, 19:02:43) -[OpenJDK 64-Bit Server VM (JetBrains s.r.o)] on java1.8.0_152-release +Jython 2.7.1 (default:0df7adb1b397, Jun 30 2017, 19:02:43) +[OpenJDK 64-Bit Server VM (AdoptOpenJDK)] on java1.8.0_252 Type "help", "copyright", "credits" or "license" for more information. ->>> import sys; sys.version_info +>>> import sys +>>> sys.version_info sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) ``` @@ -51,13 +56,19 @@ sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) >>> import scyjava_config >>> scyjava_config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) >>> scyjava_config.add_endpoints('net.imagej:imagej:2.0.0-rc-65') ->>> import scyjava, jnius ->>> System = jnius.autoclass('java.lang.System') +>>> import scyjava.jvm +>>> import jpype +>>> import jpype.imports +>>> from jpype import JClass, JArray, JLong +>>> scyjava.jvm.start_JVM() +>>> System = JClass('java.lang.System') >>> System.setProperty('java.awt.headless', 'true') ->>> ImageJ = jnius.autoclass('net.imagej.ImageJ') +>>> ImageJ = JClass('net.imagej.ImageJ') >>> ij = ImageJ() >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" ->>> blank = ij.op().create().img([64, 16]) +>>> LongArray = JArray(JLong) +>>> dims = LongArray([64, 16]) +>>> blank = ij.op().getClass().getMethod('create').invoke(ij.op()).img(dims) >>> sinusoid = ij.op().image().equation(blank, formula) >>> print(ij.op().image().ascii(sinusoid)) ,,,--+oo******oo+--,,,,,--+oo******o++--,,,,,--+oo******o++--,,, @@ -85,16 +96,19 @@ See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven ### Convert Java collections to Python ```python ->>> import scyjava, jnius ->>> System = jnius.autoclass('java.lang.System') +>>> import jpype +>>> import jpype.imports +>>> import scyjava +>>> import scyjava.jvm +>>> scyjava.jvm.start_JVM() +>>> import scyjava.convert +>>> System = jpype.JClass('java.lang.System') >>> props = System.getProperties() >>> props -> + >>> [k for k in props] -Traceback (most recent call last): - File "", line 1, in -TypeError: 'java.util.Properties' object is not iterable ->>> [k for k in scyjava.to_python(props) if k.startswith('java.vm.')] +['java.runtime.name', 'sun.boot.library.path', 'java.vm.version', 'java.vm.vendor', 'java.vendor.url', 'path.separator', 'java.vm.name', 'file.encoding.pkg', 'user.country', 'sun.os.patch.level', 'java.vm.specification.name', 'user.dir', 'java.runtime.version', 'java.awt.graphicsenv', 'java.endorsed.dirs', 'os.arch', 'java.io.tmpdir', 'line.separator', 'java.vm.specification.vendor', 'os.name', 'sun.jnu.encoding', 'java.library.path', 'java.specification.name', 'java.class.version', 'sun.management.compiler', 'os.version', 'user.home', 'user.timezone', 'java.awt.printerjob', 'file.encoding', 'java.specification.version', 'java.class.path', 'user.name', 'java.vm.specification.version', 'java.home', 'sun.arch.data.model', 'user.language', 'java.specification.vendor', 'awt.toolkit', 'java.vm.info', 'java.version', 'java.ext.dirs', 'sun.boot.class.path', 'java.vendor', 'file.separator', 'java.vendor.url.bug', 'sun.io.unicode.encoding', 'sun.cpu.endian', 'sun.desktop', 'sun.cpu.isalist'] +>>> [k for k in scyjava.convert.to_python(props) if k.startswith('java.vm.')] ['java.vm.version', 'java.vm.vendor', 'java.vm.name', 'java.vm.specification.name', 'java.vm.specification.vendor', 'java.vm.specification.version', 'java.vm.info'] ``` @@ -108,26 +122,25 @@ TypeError: 'java.util.Properties' object is not iterable Traceback (most recent call last): File "", line 1, in AttributeError: 'list' object has no attribute 'stream' ->>> scyjava.to_java(squares).stream() -> +>>> scyjava.convert.to_java(squares).stream() + ``` ### Introspect Java classes ```python ->>> import scyjava ->>> NumberClass = scyjava.jclass('java.lang.Number') +>>> NumberClass = scyjava.convert.jclass('java.lang.Number') >>> NumberClass -> + >>> NumberClass.getName() 'java.lang.Number' ->>> NumberClass.isInstance(scyjava.to_java(5)) +>>> NumberClass.isInstance(scyjava.convert.to_java(5)) True ->>> NumberClass.isInstance(scyjava.to_java('Hello')) +>>> NumberClass.isInstance(scyjava.convert.to_java('Hello')) False ``` -## Available functions +## Available functions -- EE fix this ``` >>> import scyjava diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index dac24cda..c6599251 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -1,21 +1,18 @@ # General-purpose utility methods for Python <-> Java type conversion. -import jnius, collections.abc - -from ._pandas import table_to_pandas -from ._pandas import pandas_to_table - -String = jnius.autoclass('java.lang.String') -Boolean = jnius.autoclass('java.lang.Boolean') -Integer = jnius.autoclass('java.lang.Integer') -Long = jnius.autoclass('java.lang.Long') -BigInteger = jnius.autoclass('java.math.BigInteger') -Float = jnius.autoclass('java.lang.Float') -Double = jnius.autoclass('java.lang.Double') -BigDecimal = jnius.autoclass('java.math.BigDecimal') -LinkedHashMap = jnius.autoclass('java.util.LinkedHashMap') -LinkedHashSet = jnius.autoclass('java.util.LinkedHashSet') -ArrayList = jnius.autoclass('java.util.ArrayList') +import collections.abc +import jpype +import jpype.imports +import scyjava +import scyjava.jvm +from jpype.types import * +from _jpype import _JObject + +# Java imports: +from java.lang import Boolean, Byte, Character, Double, Float, Integer, Iterable, Long, Object, Short, String, Void +from java.math import BigDecimal, BigInteger +from java.util import ArrayList, Collection, Iterator, LinkedHashMap, LinkedHashSet, List, Map, Set + # -- Python to Java -- @@ -24,9 +21,7 @@ def isjava(data): """Return whether the given data object is a Java object.""" - return isinstance(data, jnius.JavaClass) or \ - isinstance(data, jnius.MetaJavaClass) or \ - isinstance(data, jnius.PythonJavaClass) + return isinstance(data, jpype.JClass) or isinstance(data, _JObject) def jclass(data): @@ -37,19 +32,18 @@ def jclass(data): Supported types include: A. Name of a class to look up, analogous to Class.forName("java.lang.String"); - B. A jnius.MetaJavaClass object e.g. from jnius.autoclass, analogous to - String.class; - C. A jnius.JavaClass object e.g. instantiated from a jnius.MetaJavaClass, - analogous to "Hello".getClass(). + B. A jpype.JClass object analogous to String.class; + C. A _jpype._JObject instance analogous to o.getClass(). :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. """ - if isinstance(data, jnius.JavaClass): + + if isinstance(data, jpype.JClass): + return data.class_ + if isinstance(data, _JObject): return data.getClass() - if isinstance(data, jnius.MetaJavaClass): - return jnius.find_javaclass(data.__name__) if isinstance(data, str): - return jnius.find_javaclass(data) + return jclass(jpype.JClass(data)) raise TypeError('Cannot glean class from data of type: ' + str(type(data))) @@ -147,29 +141,8 @@ def to_java(data): raise TypeError('Unsupported type: ' + str(type(data))) -# -- Java to Python -- -BooleanClass = jclass('java.lang.Boolean') -ByteClass = jclass('java.lang.Byte') -CharacterClass = jclass('java.lang.Character') -DoubleClass = jclass('java.lang.Double') -FloatClass = jclass('java.lang.Float') -IntegerClass = jclass('java.lang.Integer') -LongClass = jclass('java.lang.Long') -ShortClass = jclass('java.lang.Short') -VoidClass = jclass('java.lang.Void') - -BigIntegerClass = jclass('java.math.BigInteger') -BigDecimalClass = jclass('java.math.BigDecimal') -StringClass = jclass('java.lang.String') - -ObjectClass = jclass('java.lang.Object') -IterableClass = jclass('java.lang.Iterable') -CollectionClass = jclass('java.util.Collection') -IteratorClass = jclass('java.util.Iterator') -ListClass = jclass('java.util.List') -MapClass = jclass('java.util.Map') -SetClass = jclass('java.util.Set') +# -- Java to Python -- def _jstr(data): @@ -180,8 +153,8 @@ def _jstr(data): class JavaObject(): - def __init__(self, jobj, intended_class=ObjectClass): - if not intended_class.isInstance(jobj): + def __init__(self, jobj, intended_class=Object): + if not isinstance(jobj, intended_class): raise TypeError('Not a ' + intended_class.getName() + ': ' + jclass(jobj).getName()) self.jobj = jobj @@ -191,7 +164,7 @@ def __str__(self): class JavaIterable(JavaObject, collections.abc.Iterable): def __init__(self, jobj): - JavaObject.__init__(self, jobj, IterableClass) + JavaObject.__init__(self, jobj, Iterable) def __iter__(self): return to_python(self.jobj.iterator()) @@ -202,7 +175,7 @@ def __str__(self): class JavaCollection(JavaIterable, collections.abc.Collection): def __init__(self, jobj): - JavaObject.__init__(self, jobj, CollectionClass) + JavaObject.__init__(self, jobj, Collection) def __contains__(self, item): # NB: Collection.contains returns boolean, so no need for gentleness. @@ -225,7 +198,7 @@ def __eq__(self, other): class JavaIterator(JavaObject, collections.abc.Iterator): def __init__(self, jobj): - JavaObject.__init__(self, jobj, IteratorClass) + JavaObject.__init__(self, jobj, Iterator) def __next__(self): if self.jobj.hasNext(): @@ -237,7 +210,7 @@ def __next__(self): class JavaList(JavaCollection, collections.abc.MutableSequence): def __init__(self, jobj): - JavaObject.__init__(self, jobj, ListClass) + JavaObject.__init__(self, jobj, List) def __getitem__(self, key): # NB: Even if an element cannot be converted, @@ -259,7 +232,7 @@ def insert(self, index, object): class JavaMap(JavaObject, collections.abc.MutableMapping): def __init__(self, jobj): - JavaObject.__init__(self, jobj, MapClass) + JavaObject.__init__(self, jobj, Map) def __getitem__(self, key): # NB: Even if an element cannot be converted, @@ -300,7 +273,7 @@ def __str__(self): class JavaSet(JavaCollection, collections.abc.MutableSet): def __init__(self, jobj): - JavaObject.__init__(self, jobj, SetClass) + JavaObject.__init__(self, jobj, Set) def add(self, item): # NB: Set.add returns boolean, so no need for gentleness. @@ -352,52 +325,117 @@ def to_python(data, gentle=False): if not isjava(data): return data - if BooleanClass.isInstance(data): + if isinstance(data, JBoolean): + return bool(data) + if isinstance(data, JInt) or isinstance(data, JLong) or isinstance(data, JShort): + return int(data) + if isinstance(data, JDouble) or isinstance(data, JFloat): + return float(data) + if isinstance(data, JChar): + return str(data) + + if isinstance(data, Boolean): return data.booleanValue() - if ByteClass.isInstance(data): + if isinstance(data, Byte): return data.byteValue() - if CharacterClass.isInstance(data): + if isinstance(data, Character): return data.toString() - if DoubleClass.isInstance(data): + if isinstance(data, Double): return data.doubleValue() - if FloatClass.isInstance(data): + if isinstance(data, Float): return data.floatValue() - if IntegerClass.isInstance(data): + if isinstance(data, Integer): return data.intValue() - if LongClass.isInstance(data): + if isinstance(data, Long): return data.longValue() - if ShortClass.isInstance(data): + if isinstance(data, Short): return data.shortValue() - if VoidClass.isInstance(data): + if isinstance(data, Void): return None - if BigIntegerClass.isInstance(data): - return int(data.toString()) - if BigDecimalClass.isInstance(data): + if isinstance(data, BigInteger): + return int(str(data.toString())) + if isinstance(data, BigDecimal): return float(data.toString()) - if StringClass.isInstance(data): - return data.toString() + if isinstance(data, String): + return str(data) try: - if jclass('org.scijava.table.Table').isInstance(data): + if isinstance(data, jclass('org.scijava.table.Table')): return table_to_pandas(data) except: # No worries if scijava-table is not available. pass - if ListClass.isInstance(data): + if isinstance(data, List): return JavaList(data) - if MapClass.isInstance(data): + if isinstance(data, Map): return JavaMap(data) - if SetClass.isInstance(data): + if isinstance(data, Set): return JavaSet(data) - if CollectionClass.isInstance(data): + if isinstance(data, Collection): return JavaCollection(data) - if IterableClass.isInstance(data): + if isinstance(data, Iterable): return JavaIterable(data) - if IteratorClass.isInstance(data): + if isinstance(data, Iterator): return JavaIterator(data) if gentle: return data raise TypeError('Unsupported data type: ' + str(type(data))) + + +def _import_pandas(): + try: + import pandas as pd + return pd + except ImportError: + msg = "The Pandas library is missing (http://pandas.pydata.org/). " + msg += "Please instal it using: " + msg += "conda install pandas (prefered)" + msg += " or " + msg += "pip install pandas." + raise Exception(msg) + + +def table_to_pandas(table): + pd = _import_pandas() + + data = [] + headers = [] + for i, column in enumerate(table.toArray()): + data.append(column.toArray()) + headers.append(table.getColumnHeader(i)) + df = pd.DataFrame(data).T + df.columns = headers + return df + + +def pandas_to_table(df): + pd = _import_pandas() + + if len(df.dtypes.unique()) > 1: + TableClass = jpype.JClass('org.scijava.table.DefaultGenericTable') + else: + table_type = df.dtypes.unique()[0] + if table_type.name.startswith('float'): + TableClass = jpype.JClass('org.scijava.table.DefaultFloatTable') + elif table_type.name.startswith('int'): + TableClass = jpype.JClass('org.scijava.table.DefaultIntTable') + elif table_type.name.startswith('bool'): + TableClass = jpype.JClass('org.scijava.table.DefaultBoolTable') + else: + msg = "The type '{}' is not supported.".format(table_type.name) + raise Exception(msg) + + table = TableClass(*df.shape[::-1]) + + for c, column_name in enumerate(df.columns): + table.setColumnHeader(c, column_name) + + for i, (index, row) in enumerate(df.iterrows()): + for c, value in enumerate(row): + header = df.columns[c] + table.set(header, i, to_java(value)) + + return table diff --git a/scyjava/convert/_pandas.py b/scyjava/convert/_pandas.py deleted file mode 100644 index a4921bfb..00000000 --- a/scyjava/convert/_pandas.py +++ /dev/null @@ -1,59 +0,0 @@ -# Pandas <-> Scijava Table converters. -import jnius -import scyjava - - -def _import_pandas(): - try: - import pandas as pd - return pd - except ImportError: - msg = "The Pandas library is missing (http://pandas.pydata.org/). " - msg += "Please instal it using: " - msg += "conda install pandas (prefered)" - msg += " or " - msg += "pip install pandas." - raise Exception(msg) - - -def table_to_pandas(table): - pd = _import_pandas() - - data = [] - headers = [] - for i, column in enumerate(table.toArray()): - data.append(column.toArray()) - headers.append(table.getColumnHeader(i)) - df = pd.DataFrame(data).T - df.columns = headers - return df - - -def pandas_to_table(df): - pd = _import_pandas() - - if len(df.dtypes.unique()) > 1: - TableClass = jnius.autoclass('org.scijava.table.DefaultGenericTable') - else: - table_type = df.dtypes.unique()[0] - if table_type.name.startswith('float'): - TableClass = jnius.autoclass('org.scijava.table.DefaultFloatTable') - elif table_type.name.startswith('int'): - TableClass = jnius.autoclass('org.scijava.table.DefaultIntTable') - elif table_type.name.startswith('bool'): - TableClass = jnius.autoclass('org.scijava.table.DefaultBoolTable') - else: - msg = "The type '{}' is not supported.".format(table_type.name) - raise Exception(msg) - - table = TableClass(*df.shape[::-1]) - - for c, column_name in enumerate(df.columns): - table.setColumnHeader(c, column_name) - - for i, (index, row) in enumerate(df.iterrows()): - for c, value in enumerate(row): - header = df.columns[c] - table.set(header, i, scyjava.to_java(value)) - - return table diff --git a/scyjava/__init__.py b/scyjava/jvm/__init__.py similarity index 62% rename from scyjava/__init__.py rename to scyjava/jvm/__init__.py index 789b0d34..414d24b2 100644 --- a/scyjava/__init__.py +++ b/scyjava/jvm/__init__.py @@ -3,49 +3,23 @@ import platform import sys import subprocess +import jgo +import jpype +import jpype.imports +import scyjava_config + from pathlib import Path +# setup logger _logger = logging.getLogger(__name__) -# Enable debug logging if DEBUG environment variable is set. -try: - debug = os.environ['DEBUG'] - if debug: - _logger.setLevel(logging.DEBUG) -except KeyError as e: - pass - -def _init_jvm(): - import scyjava_config - import jnius_config - import jgo - - if jnius_config.vm_running: - _logger.warning('JVM is already running, will not add endpoints to classpath -- required classes might not be on classpath..') - import jnius - return jnius +# TODO: Pass options +def start_JVM(options=''): - # attempt to find pyjnius.jar if the envrionment variable is not set. - PYJNIUS_JAR_STR = 'PYJNIUS_JAR' - if PYJNIUS_JAR_STR not in globals(): - PYJNIUS_JAR = None - try: - _logger.debug('Checking %s environment variable', PYJNIUS_JAR_STR) - PYJNIUS_JAR = os.environ[PYJNIUS_JAR_STR] - except KeyError: - _logger.debug('No %s environment variable', PYJNIUS_JAR_STR) - if not PYJNIUS_JAR: - # NB: This logic handles both None and empty string cases. - _logger.debug('%s still unknown; falling back to default path', PYJNIUS_JAR_STR) - PYJNIUS_JAR = os.path.join(sys.prefix, 'share', 'pyjnius', 'pyjnius.jar') - if Path(PYJNIUS_JAR).is_file(): - _logger.debug('%s found at "%s"', PYJNIUS_JAR_STR, PYJNIUS_JAR) - jnius_config.add_classpath(PYJNIUS_JAR) - else: - _logger.error('Unable to import scyjava: pyjnius JAR not found.') - return None - else: - _logger.debug('%s found in globals', PYJNIUS_JAR_STR) + # if jvm JVM is already running -- break + if JVM_status() == True: + _logger.debug('The JVM is already running.') + return # attempt to set JAVA_HOME if the environment variable is not set. JAVA_HOME_STR = 'JAVA_HOME' @@ -87,7 +61,7 @@ def _init_jvm(): java_path = Path(JAVA_HOME) if java_path.is_dir(): _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) - if java_path.name is 'jre': + if java_path.name == 'jre': _logger.debug('JAVA_HOME points at jre folder; using parent instead') JAVA_HOME = str(java_path.parent) os.environ['JAVA_HOME'] = JAVA_HOME @@ -108,12 +82,15 @@ def _init_jvm(): jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'jre', 'bin', 'server') if Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): os.environ['PATH'] += ';' + jvm_server_dir - + + # retrieve endpoint and repositories from scyjava_config endpoints = scyjava_config.get_endpoints() repositories = scyjava_config.get_repositories() - _logger.debug('Adding jars from endpoints %s', endpoints) + # use the logger to notify user that endpoints are being added + _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) + # get endpoints and add to JPype class path if len(endpoints) > 0: endpoints = endpoints[:1] + sorted(endpoints[1:]) _logger.debug('Using endpoints %s', endpoints) @@ -125,20 +102,12 @@ def _init_jvm(): repositories=repositories, verbose=scyjava_config.get_verbose() ) - jnius_config.add_classpath(os.path.join(workspace, '*')) - - try: - import jnius - return jnius - except KeyError as e: - if e.args[0] == 'JAVA_HOME': - _logger.error('Unable to import scyjava: JAVA_HOME environment variable not defined, cannot import jnius.') - else: - raise e - return None + jpype.addClassPath(os.path.join(workspace, '*')) + + # Initialize JPype JVM + jpype.startJVM(options) -jnius = _init_jvm() -if jnius is None: - raise ImportError('Unable to import scyjava dependency jnius.') + return -from .convert import isjava, jclass, to_java, to_python +def JVM_status(): + return jpype.isJVMStarted() \ No newline at end of file diff --git a/scyjava_config.py b/scyjava_config.py index c30198f5..aced65a6 100644 --- a/scyjava_config.py +++ b/scyjava_config.py @@ -21,19 +21,20 @@ 'expand_classpath') import logging -import jnius_config import pathlib +import jpype -version = '0.4.1.dev0' +version = '0.4.1.dev1' _logger = logging.getLogger(__name__) -_endpoints = [] -_repositories = {} -_verbose = 0 -_manage_deps = True -_cache_dir = pathlib.Path.home() / '.jgo' -_m2_repo = pathlib.Path.home() / '.m2' / 'repository' +_endpoints = [] +_repositories = {1: 'https://maven.scijava.org/content/repositories/releases'} +_verbose = 0 +_manage_deps = True +_cache_dir = pathlib.Path.home() / '.jgo' +_m2_repo = pathlib.Path.home() / '.m2' / 'repository' +_options = "" def maven_scijava_repository(): """ @@ -46,12 +47,10 @@ def add_endpoints(*endpoints): _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) _endpoints.extend(endpoints) - def get_endpoints(): global _endpoints return _endpoints - def add_repositories(*args, **kwargs): global _repositories for arg in args: @@ -60,12 +59,10 @@ def add_repositories(*args, **kwargs): _logger.debug('Adding repositories %s to %s', kwargs, _repositories) _repositories.update(kwargs) - def get_repositories(): global _repositories return _repositories - def set_verbose(level): global _verbose _logger.debug('Setting verbose level to %d (was %d)', level, _verbose) @@ -110,32 +107,21 @@ def get_m2_repo(): global _m2_repo return _m2_repo - -# directly delegating to jnius_config def add_classpath(*path): - jnius_config.add_classpath(*path) + jpype.addClassPath(*path) def set_classpath(*path): - jnius_config.set_classpath(*path) + jpype.addClassPath(*path) def get_classpath(): - return jnius_config.get_classpath() - - -def add_options(*opts): - jnius_config.add_options(*opts) - - -def set_options(*opts): - jnius_config.set_options(*opts) + return jpype.getClassPath() +def add_options(options): + global _options + _options = options def get_options(): - return jnius_config.get_options() - - -def expand_classpath(): - return jnius_config.expand_classpath() - + global _options + return _options diff --git a/setup.py b/setup.py index 239f67dc..6373311f 100644 --- a/setup.py +++ b/setup.py @@ -20,5 +20,5 @@ long_description_content_type='text/markdown', license='Public domain', url='https://github.com/scijava/scyjava', - install_requires=['pyjnius', 'jgo'], + install_requires=['jpype1', 'jgo'], ) diff --git a/tests/test_convert.py b/tests/test_convert.py index 4885b697..da99c00c 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -5,10 +5,15 @@ import unittest import pandas as pd import numpy as np +import scyjava.jvm +import jpype +import jpype.imports +from jpype.types import * + +# EE: scyjava.convert perfroms imports that need to happen after the JVM +# has started. +scyjava.jvm.start_JVM() from scyjava.convert import jclass, to_java, to_python -import scyjava -import jnius - def assert_same_table(table, df): import numpy.testing as npt @@ -79,7 +84,7 @@ def testLong(self): def testBigInteger(self): bi = 9879999999999999789 jbi = to_java(bi) - self.assertEqual(bi, int(jbi.toString())) + self.assertEqual(bi, int(str(jbi.toString()))) pbi = to_python(jbi) self.assertEqual(bi, pbi) self.assertEqual(str(bi), str(pbi)) @@ -180,7 +185,7 @@ def testNone(self): self.assertEqual(d, pd) def testGentle(self): - Object = jnius.autoclass('java.lang.Object') + Object = jpype.JClass('java.lang.Object') unknown_thing = Object() converted_thing = to_python(unknown_thing, gentle=True) assert type(converted_thing) == Object @@ -194,7 +199,7 @@ def testGentle(self): def testStructureWithSomeUnsupportedItems(self): # Create Java data structure with some challenging items. - Object = jnius.autoclass('java.lang.Object') + Object = jpype.JClass('java.lang.Object') jmap = to_java({ 'list': ['a', Object(), 1], 'set': {'x', Object(), 2}, @@ -221,10 +226,10 @@ def testPandasToTable(self): array = np.random.random(size=(7, 5)) df = pd.DataFrame(array, columns=columns) - table = scyjava.to_java(df) + table = to_java(df) assert_same_table(table, df) - assert type(table) == jnius.autoclass('org.scijava.table.DefaultFloatTable') + assert type(table) == jpype.JClass('org.scijava.table.DefaultFloatTable') # Int table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -232,20 +237,20 @@ def testPandasToTable(self): array = array.astype('int') df = pd.DataFrame(array, columns=columns) - table = scyjava.to_java(df) + table = to_java(df) assert_same_table(table, df) - assert type(table) == jnius.autoclass('org.scijava.table.DefaultIntTable') + assert type(table) == jpype.JClass('org.scijava.table.DefaultIntTable') # Bool table. columns = ["header1", "header2", "header3", "header4", "header5"] array = np.random.random(size=(7, 5)) > 0.5 df = pd.DataFrame(array, columns=columns) - table = scyjava.to_java(df) + table = to_java(df) assert_same_table(table, df) - assert type(table) == jnius.autoclass('org.scijava.table.DefaultBoolTable') + assert type(table) == jpype.JClass('org.scijava.table.DefaultBoolTable') # Mixed table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -260,11 +265,11 @@ def testPandasToTable(self): # Convert column 2 to string df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split('\n') - table = scyjava.to_java(df) + table = to_java(df) # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) - assert type(table) == jnius.autoclass('org.scijava.table.DefaultGenericTable') + assert type(table) == jpype.JClass('org.scijava.table.DefaultGenericTable') if __name__ == '__main__': From 9bae1dd93b811a5fde0bd0a82f671adcf1e5041c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 11 Nov 2020 10:25:22 -0600 Subject: [PATCH 044/505] Use JPype's array notation As discussed at https://github.com/scijava/scyjava/pull/20#discussion_r517544640 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index be7a5aa4..c8dd59d9 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,7 @@ sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) >>> ImageJ = JClass('net.imagej.ImageJ') >>> ij = ImageJ() >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" ->>> LongArray = JArray(JLong) ->>> dims = LongArray([64, 16]) +>>> dims = JLong[64, 16] >>> blank = ij.op().getClass().getMethod('create').invoke(ij.op()).img(dims) >>> sinusoid = ij.op().image().equation(blank, formula) >>> print(ij.op().image().ascii(sinusoid)) From e41a854acd614e151f5d7000f54aaf1e2a7de59f Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 12 Nov 2020 13:42:38 -0600 Subject: [PATCH 045/505] Delete scyjava_config.py Move scyjava_config.py functions to the scyjava.config module. The contents of scyjava_config.py are the same as the scyjava.config module. --- scyjava_config.py | 127 ---------------------------------------------- 1 file changed, 127 deletions(-) delete mode 100644 scyjava_config.py diff --git a/scyjava_config.py b/scyjava_config.py deleted file mode 100644 index aced65a6..00000000 --- a/scyjava_config.py +++ /dev/null @@ -1,127 +0,0 @@ -__all__ = ( - 'maven_scijava_repository', - 'add_endpoints', - 'get_endpoints', - 'add_repositories', - 'get_repositories', - 'set_verbose', - 'get_verbose', - 'set_manage_deps', - 'get_manage_deps', - 'set_cache_dir', - 'get_cache_dir', - 'set_m2_repo', - 'get_m2_repo', - 'set_options', - 'add_options', - 'get_options', - 'set_classpath', - 'add_classpath', - 'get_classpath', - 'expand_classpath') - -import logging -import pathlib -import jpype - -version = '0.4.1.dev1' - -_logger = logging.getLogger(__name__) - -_endpoints = [] -_repositories = {1: 'https://maven.scijava.org/content/repositories/releases'} -_verbose = 0 -_manage_deps = True -_cache_dir = pathlib.Path.home() / '.jgo' -_m2_repo = pathlib.Path.home() / '.m2' / 'repository' -_options = "" - -def maven_scijava_repository(): - """ - :return: url for public scijava maven repo - """ - return 'https://maven.scijava.org/content/groups/public' - -def add_endpoints(*endpoints): - global _endpoints - _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) - _endpoints.extend(endpoints) - -def get_endpoints(): - global _endpoints - return _endpoints - -def add_repositories(*args, **kwargs): - global _repositories - for arg in args: - _logger.debug('Adding repositories %s to %s', arg, _repositories) - _repositories.update(arg) - _logger.debug('Adding repositories %s to %s', kwargs, _repositories) - _repositories.update(kwargs) - -def get_repositories(): - global _repositories - return _repositories - -def set_verbose(level): - global _verbose - _logger.debug('Setting verbose level to %d (was %d)', level, _verbose) - _verbose = level - - -def get_verbose(): - global _verbose - _logger.debug('Getting verbose level: %d', _verbose) - return _verbose - - -def set_manage_deps(manage): - global _manage_deps - _logger.debug('Setting manage deps to %d (was %d)', manage, _manage_deps) - _manage_deps = manage - - -def get_manage_deps(): - global _manage_deps - return _manage_deps - - -def set_cache_dir(dir): - global _cache_dir - _logger.debug('Setting cache dir to %s (was %s)', dir, _cache_dir) - _cache_dir = dir - - -def get_cache_dir(): - global _cache_dir - return _cache_dir - - -def set_m2_repo(dir): - global _m2_repo - _logger.debug('Setting m2 repo dir to %s (was %s)', dir, _m2_repo) - _m2_repo = dir - - -def get_m2_repo(): - global _m2_repo - return _m2_repo - -def add_classpath(*path): - jpype.addClassPath(*path) - - -def set_classpath(*path): - jpype.addClassPath(*path) - - -def get_classpath(): - return jpype.getClassPath() - -def add_options(options): - global _options - _options = options - -def get_options(): - global _options - return _options From 0968e38e6210b0dfab6843fbaf86c7eea903ae30 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 12 Nov 2020 13:48:37 -0600 Subject: [PATCH 046/505] Create scyjava.config module scyjava_config.py functions have been relocated here as a new module. The contents of old scyjava_config.py and the new scyjava.config module are the same and remain unchanged. --- scyjava/config/__init__.py | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 scyjava/config/__init__.py diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py new file mode 100644 index 00000000..aced65a6 --- /dev/null +++ b/scyjava/config/__init__.py @@ -0,0 +1,127 @@ +__all__ = ( + 'maven_scijava_repository', + 'add_endpoints', + 'get_endpoints', + 'add_repositories', + 'get_repositories', + 'set_verbose', + 'get_verbose', + 'set_manage_deps', + 'get_manage_deps', + 'set_cache_dir', + 'get_cache_dir', + 'set_m2_repo', + 'get_m2_repo', + 'set_options', + 'add_options', + 'get_options', + 'set_classpath', + 'add_classpath', + 'get_classpath', + 'expand_classpath') + +import logging +import pathlib +import jpype + +version = '0.4.1.dev1' + +_logger = logging.getLogger(__name__) + +_endpoints = [] +_repositories = {1: 'https://maven.scijava.org/content/repositories/releases'} +_verbose = 0 +_manage_deps = True +_cache_dir = pathlib.Path.home() / '.jgo' +_m2_repo = pathlib.Path.home() / '.m2' / 'repository' +_options = "" + +def maven_scijava_repository(): + """ + :return: url for public scijava maven repo + """ + return 'https://maven.scijava.org/content/groups/public' + +def add_endpoints(*endpoints): + global _endpoints + _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) + _endpoints.extend(endpoints) + +def get_endpoints(): + global _endpoints + return _endpoints + +def add_repositories(*args, **kwargs): + global _repositories + for arg in args: + _logger.debug('Adding repositories %s to %s', arg, _repositories) + _repositories.update(arg) + _logger.debug('Adding repositories %s to %s', kwargs, _repositories) + _repositories.update(kwargs) + +def get_repositories(): + global _repositories + return _repositories + +def set_verbose(level): + global _verbose + _logger.debug('Setting verbose level to %d (was %d)', level, _verbose) + _verbose = level + + +def get_verbose(): + global _verbose + _logger.debug('Getting verbose level: %d', _verbose) + return _verbose + + +def set_manage_deps(manage): + global _manage_deps + _logger.debug('Setting manage deps to %d (was %d)', manage, _manage_deps) + _manage_deps = manage + + +def get_manage_deps(): + global _manage_deps + return _manage_deps + + +def set_cache_dir(dir): + global _cache_dir + _logger.debug('Setting cache dir to %s (was %s)', dir, _cache_dir) + _cache_dir = dir + + +def get_cache_dir(): + global _cache_dir + return _cache_dir + + +def set_m2_repo(dir): + global _m2_repo + _logger.debug('Setting m2 repo dir to %s (was %s)', dir, _m2_repo) + _m2_repo = dir + + +def get_m2_repo(): + global _m2_repo + return _m2_repo + +def add_classpath(*path): + jpype.addClassPath(*path) + + +def set_classpath(*path): + jpype.addClassPath(*path) + + +def get_classpath(): + return jpype.getClassPath() + +def add_options(options): + global _options + _options = options + +def get_options(): + global _options + return _options From d65262b62a70cdaed589cd25bd716c2f1996ab63 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 12 Nov 2020 13:52:52 -0600 Subject: [PATCH 047/505] Change scyjava_config to scyjava.config module Update the jvm module to reflect the change from scyjava_config to use scyjava.config module. --- scyjava/jvm/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scyjava/jvm/__init__.py b/scyjava/jvm/__init__.py index 414d24b2..733f20fa 100644 --- a/scyjava/jvm/__init__.py +++ b/scyjava/jvm/__init__.py @@ -6,7 +6,7 @@ import jgo import jpype import jpype.imports -import scyjava_config +import scyjava.config from pathlib import Path @@ -84,8 +84,8 @@ def start_JVM(options=''): os.environ['PATH'] += ';' + jvm_server_dir # retrieve endpoint and repositories from scyjava_config - endpoints = scyjava_config.get_endpoints() - repositories = scyjava_config.get_repositories() + endpoints = scyjava.config.get_endpoints() + repositories = scyjava.config.get_repositories() # use the logger to notify user that endpoints are being added _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) @@ -96,11 +96,11 @@ def start_JVM(options=''): _logger.debug('Using endpoints %s', endpoints) _, workspace = jgo.resolve_dependencies( '+'.join(endpoints), - m2_repo=scyjava_config.get_m2_repo(), - cache_dir=scyjava_config.get_cache_dir(), - manage_dependencies=scyjava_config.get_manage_deps(), + m2_repo=scyjava.config.get_m2_repo(), + cache_dir=scyjava.config.get_cache_dir(), + manage_dependencies=scyjava.config.get_manage_deps(), repositories=repositories, - verbose=scyjava_config.get_verbose() + verbose=scyjava.config.get_verbose() ) jpype.addClassPath(os.path.join(workspace, '*')) From a7db57cd177847eb3ad0c54fc25e4504a2e6ea7b Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 16 Nov 2020 09:00:06 -0600 Subject: [PATCH 048/505] Move scyjava.jvm functions into scyjava.config The scyjava.jvm functions/methods have been moved to the scyjava.config module. --- scyjava/config/__init__.py | 104 ++++++++++++++++++++++++++++++++++ scyjava/jvm/__init__.py | 113 ------------------------------------- 2 files changed, 104 insertions(+), 113 deletions(-) delete mode 100644 scyjava/jvm/__init__.py diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index aced65a6..c07f63e0 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -21,8 +21,13 @@ 'expand_classpath') import logging +import os +import platform import pathlib +import jgo import jpype +import jpype.imports +import subprocess version = '0.4.1.dev1' @@ -36,6 +41,105 @@ _m2_repo = pathlib.Path.home() / '.m2' / 'repository' _options = "" + +def start_JVM(options=''): + # if jvm JVM is already running -- break + if JVM_status() == True: + _logger.debug('The JVM is already running.') + return + + # attempt to set JAVA_HOME if the environment variable is not set. + JAVA_HOME_STR = 'JAVA_HOME' + if JAVA_HOME_STR not in globals(): + JAVA_HOME = None + try: + _logger.debug('Checking %s environment variable', JAVA_HOME_STR) + JAVA_HOME = os.environ[JAVA_HOME_STR] + except KeyError: + _logger.debug('No %s environment variable', JAVA_HOME_STR) + if not JAVA_HOME: + # NB: This logic handles both None and empty string cases. + _logger.debug('%s still unknown; checking with Maven', JAVA_HOME_STR) + # attempt to find Java by interrogating maven + # (which we have because it is needed by jgo) + try: + if (platform.system() == 'Windows'): + mvn = str(subprocess.check_output(['mvn.cmd', '-v'])) + mvn = mvn.replace('\\r\\n', '\\n') # Fix Windows line breaks. + else: + mvn = str(subprocess.check_output(['mvn', '-v'])) + except subprocess.CalledProcessError as e: + _logger.error('Unable to import scyjava, could not find Maven') + return None + _logger.debug('Maven said: %s', mvn) + try: + begin = mvn.index('Java home: ') + except ValueError as e: + # in some versions of maven it is instead called runtime + try: + begin = mvn.index('runtime: ') + except ValueError as e: + _logger.error('Unable to import scyjava, could not locate jre') + return None + # cut out 'Java home' or 'runtime' + begin = mvn.index(':', begin) + 2 + end = mvn.index('\\n', begin) + JAVA_HOME = mvn[begin:end] + java_path = pathlib.Path(JAVA_HOME) + if java_path.is_dir(): + _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) + if java_path.name == 'jre': + _logger.debug('JAVA_HOME points at jre folder; using parent instead') + JAVA_HOME = str(java_path.parent) + os.environ['JAVA_HOME'] = JAVA_HOME + else: + _logger.error('Unable to import scyjava: jre not found') + return None + else: + _logger.debug('%s found in globals', JAVA_HOME_STR) + + # On Windows, add server subfolder to the PATH so jvm.dll can be found. + if (platform.system() == 'Windows'): + # Java 9 and later + jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'bin', 'server') + if pathlib.Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): + os.environ['PATH'] += ';' + jvm_server_dir + else: + # Java 8 and earlier + jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'jre', 'bin', 'server') + if pathlib.Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): + os.environ['PATH'] += ';' + jvm_server_dir + + # retrieve endpoint and repositories from scyjava_config + endpoints = get_endpoints() + repositories = get_repositories() + + # use the logger to notify user that endpoints are being added + _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) + + # get endpoints and add to JPype class path + if len(endpoints) > 0: + endpoints = endpoints[:1] + sorted(endpoints[1:]) + _logger.debug('Using endpoints %s', endpoints) + _, workspace = jgo.resolve_dependencies( + '+'.join(endpoints), + m2_repo=get_m2_repo(), + cache_dir=get_cache_dir(), + manage_dependencies=get_manage_deps(), + repositories=repositories, + verbose=get_verbose() + ) + jpype.addClassPath(os.path.join(workspace, '*')) + + # Initialize JPype JVM + jpype.startJVM(options) + + return + + +def JVM_status(): + return jpype.isJVMStarted() + def maven_scijava_repository(): """ :return: url for public scijava maven repo diff --git a/scyjava/jvm/__init__.py b/scyjava/jvm/__init__.py deleted file mode 100644 index 733f20fa..00000000 --- a/scyjava/jvm/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -import os -import platform -import sys -import subprocess -import jgo -import jpype -import jpype.imports -import scyjava.config - -from pathlib import Path - -# setup logger -_logger = logging.getLogger(__name__) - -# TODO: Pass options -def start_JVM(options=''): - - # if jvm JVM is already running -- break - if JVM_status() == True: - _logger.debug('The JVM is already running.') - return - - # attempt to set JAVA_HOME if the environment variable is not set. - JAVA_HOME_STR = 'JAVA_HOME' - if JAVA_HOME_STR not in globals(): - JAVA_HOME = None - try: - _logger.debug('Checking %s environment variable', JAVA_HOME_STR) - JAVA_HOME = os.environ[JAVA_HOME_STR] - except KeyError: - _logger.debug('No %s environment variable', JAVA_HOME_STR) - if not JAVA_HOME: - # NB: This logic handles both None and empty string cases. - _logger.debug('%s still unknown; checking with Maven', JAVA_HOME_STR) - # attempt to find Java by interrogating maven - # (which we have because it is needed by jgo) - try: - if (platform.system() == 'Windows'): - mvn = str(subprocess.check_output(['mvn.cmd', '-v'])) - mvn = mvn.replace('\\r\\n', '\\n') # Fix Windows line breaks. - else: - mvn = str(subprocess.check_output(['mvn', '-v'])) - except subprocess.CalledProcessError as e: - _logger.error('Unable to import scyjava, could not find Maven') - return None - _logger.debug('Maven said: %s', mvn) - try: - begin = mvn.index('Java home: ') - except ValueError as e: - # in some versions of maven it is instead called runtime - try: - begin = mvn.index('runtime: ') - except ValueError as e: - _logger.error('Unable to import scyjava, could not locate jre') - return None - # cut out 'Java home' or 'runtime' - begin = mvn.index(':', begin) + 2 - end = mvn.index('\\n', begin) - JAVA_HOME = mvn[begin:end] - java_path = Path(JAVA_HOME) - if java_path.is_dir(): - _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) - if java_path.name == 'jre': - _logger.debug('JAVA_HOME points at jre folder; using parent instead') - JAVA_HOME = str(java_path.parent) - os.environ['JAVA_HOME'] = JAVA_HOME - else: - _logger.error('Unable to import scyjava: jre not found') - return None - else: - _logger.debug('%s found in globals', JAVA_HOME_STR) - - # On Windows, add server subfolder to the PATH so jvm.dll can be found. - if (platform.system() == 'Windows'): - # Java 9 and later - jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'bin', 'server') - if Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): - os.environ['PATH'] += ';' + jvm_server_dir - else: - # Java 8 and earlier - jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'jre', 'bin', 'server') - if Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): - os.environ['PATH'] += ';' + jvm_server_dir - - # retrieve endpoint and repositories from scyjava_config - endpoints = scyjava.config.get_endpoints() - repositories = scyjava.config.get_repositories() - - # use the logger to notify user that endpoints are being added - _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) - - # get endpoints and add to JPype class path - if len(endpoints) > 0: - endpoints = endpoints[:1] + sorted(endpoints[1:]) - _logger.debug('Using endpoints %s', endpoints) - _, workspace = jgo.resolve_dependencies( - '+'.join(endpoints), - m2_repo=scyjava.config.get_m2_repo(), - cache_dir=scyjava.config.get_cache_dir(), - manage_dependencies=scyjava.config.get_manage_deps(), - repositories=repositories, - verbose=scyjava.config.get_verbose() - ) - jpype.addClassPath(os.path.join(workspace, '*')) - - # Initialize JPype JVM - jpype.startJVM(options) - - return - -def JVM_status(): - return jpype.isJVMStarted() \ No newline at end of file From e6d82911df3192cea9398af1a9b5c5cdbe54a9df Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 16 Nov 2020 09:01:23 -0600 Subject: [PATCH 049/505] Remove scyjava_config and scyjava.jvm imports scyjava_Config and scyjava.jvm are no longer used and have been moved to scyjava.config. This patch reflects these changes. --- scyjava/convert/_convert.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scyjava/convert/_convert.py b/scyjava/convert/_convert.py index c6599251..fe68bef6 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/convert/_convert.py @@ -3,8 +3,6 @@ import collections.abc import jpype import jpype.imports -import scyjava -import scyjava.jvm from jpype.types import * from _jpype import _JObject From a2546f8fc46904106480c660383662edc9bbfaa6 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 16 Nov 2020 09:03:00 -0600 Subject: [PATCH 050/505] Replace scyjava_config with scyjava.config module scyjava_config is no longer used and has been replaced with scyjava.config. --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 6373311f..14633589 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ import setuptools -import scyjava_config +import scyjava.config from os import path here = path.abspath(path.dirname(__file__)) @@ -11,8 +11,8 @@ name='scyjava', python_requires='>=3', packages=['scyjava', 'scyjava.convert'], - py_modules=['scyjava_config'], - version=scyjava_config.version, + py_modules=['scyjava.config'], + version=scyjava.config.version, author='Philipp Hanslovsky, Curtis Rueden', author_email='hanslovskyp@janelia.hhmi.org', description='scyjava', From b61d78b3283a3daba388ea2a629fed11990dc4ef Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 18 Nov 2020 15:55:19 -0600 Subject: [PATCH 051/505] Enable set_options to supersede default jvm options This commit enables a user to set the jvm options manually which will supersede the default options. This also supports adding additional options (which are appended to the jvm option string regardless if it is default or user defined). --- scyjava/config/__init__.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index c07f63e0..6b84856f 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -17,8 +17,7 @@ 'get_options', 'set_classpath', 'add_classpath', - 'get_classpath', - 'expand_classpath') + 'get_classpath') import logging import os @@ -39,10 +38,15 @@ _manage_deps = True _cache_dir = pathlib.Path.home() / '.jgo' _m2_repo = pathlib.Path.home() / '.m2' / 'repository' -_options = "" +_options = '' +_add_options = '' def start_JVM(options=''): + # set _options to the default options from pyimagej if none are specified + global _options + _options = options + # if jvm JVM is already running -- break if JVM_status() == True: _logger.debug('The JVM is already running.') @@ -132,7 +136,17 @@ def start_JVM(options=''): jpype.addClassPath(os.path.join(workspace, '*')) # Initialize JPype JVM - jpype.startJVM(options) + jvm_options = _options + + # append any additional options + if _add_options == '': + pass + else: + jvm_options = jvm_options + ' ' + _add_options + + # store options used for the jvm in _options -- user can check what was used + _options = jvm_options + jpype.startJVM(jvm_options) return @@ -223,9 +237,13 @@ def get_classpath(): return jpype.getClassPath() def add_options(options): - global _options - _options = options + global _add_options + _add_options = options def get_options(): global _options return _options + +def set_options(options): + global _options + _options = options From f776b976a258e7a7f6dca084f90e8e93e6852f91 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 19 Nov 2020 11:09:24 -0600 Subject: [PATCH 052/505] Update the tests to support scyjava.config change This commit updates the test_convert.py tests to work with the scyjava_config to scyjava.config module reorganization change. --- tests/test_convert.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index da99c00c..bd542cf2 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,18 +1,18 @@ -import scyjava_config -scyjava_config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) -scyjava_config.add_endpoints('org.scijava:scijava-table') +import scyjava.config +scyjava.config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) +scyjava.config.add_endpoints('org.scijava:scijava-table') import unittest import pandas as pd import numpy as np -import scyjava.jvm import jpype import jpype.imports from jpype.types import * # EE: scyjava.convert perfroms imports that need to happen after the JVM # has started. -scyjava.jvm.start_JVM() +scyjava.config.set_options('-Djava.awt.headless=true') +scyjava.config.start_JVM() from scyjava.convert import jclass, to_java, to_python def assert_same_table(table, df): From 5fea42d2f644a6e59fd1d15cc2fef5de3eca10a8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 24 Nov 2020 13:13:19 -0600 Subject: [PATCH 053/505] Fix problem with setup.py importing the module It creates a circular dependency, because `pip install scyjava` on a git hash tries to run setup.py from the working copy, which tries to import scyjava, which isn't installed yet, generating an error message. More precisely, the error message is caused by the scyjava.config import then importing dependencies jgo and jpype, which aren't installed yet. Let's not allow setup.py to import scyjava. :-) While we're at it, bump the development version to 1.0.0.dev0, since that's what will be released shortly. --- scyjava/config/__init__.py | 2 -- setup.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 6b84856f..945779bd 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -28,8 +28,6 @@ import jpype.imports import subprocess -version = '0.4.1.dev1' - _logger = logging.getLogger(__name__) _endpoints = [] diff --git a/setup.py b/setup.py index 14633589..3fb932d5 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ import setuptools -import scyjava.config from os import path here = path.abspath(path.dirname(__file__)) @@ -12,7 +11,7 @@ python_requires='>=3', packages=['scyjava', 'scyjava.convert'], py_modules=['scyjava.config'], - version=scyjava.config.version, + version='1.0.0.dev0', author='Philipp Hanslovsky, Curtis Rueden', author_email='hanslovskyp@janelia.hhmi.org', description='scyjava', From 2cb4af8e77eba82efe66fde45bdaf2feb674ba65 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 24 Nov 2020 14:44:03 -0600 Subject: [PATCH 054/505] Fix packages declaration --- setup.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 3fb932d5..6053cc6d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -import setuptools +from setuptools import setup, find_packages from os import path here = path.abspath(path.dirname(__file__)) @@ -6,11 +6,10 @@ with open(path.join(here, 'README.md')) as f: scyjava_long_description = f.read() -setuptools.setup( +setup( name='scyjava', python_requires='>=3', - packages=['scyjava', 'scyjava.convert'], - py_modules=['scyjava.config'], + packages=find_packages(), version='1.0.0.dev0', author='Philipp Hanslovsky, Curtis Rueden', author_email='hanslovskyp@janelia.hhmi.org', From f60166c402b6d1e341fb183b4c4a94be00f757cb Mon Sep 17 00:00:00 2001 From: Mark Hiner Date: Tue, 24 Nov 2020 15:37:52 -0600 Subject: [PATCH 055/505] Use find_namespace_packages scyjava has no __init__.py, thus is a namespace package and not compatible with find_packages --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6053cc6d..4427c989 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import setup, find_namespace_packages from os import path here = path.abspath(path.dirname(__file__)) @@ -9,7 +9,7 @@ setup( name='scyjava', python_requires='>=3', - packages=find_packages(), + packages=find_namespace_packages(include=['scyjava.*']), version='1.0.0.dev0', author='Philipp Hanslovsky, Curtis Rueden', author_email='hanslovskyp@janelia.hhmi.org', From fe1914be3a5bfc2ea090df892812f74d4170bbad Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 15:20:54 -0600 Subject: [PATCH 056/505] Update comments --- scyjava/config/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 945779bd..b8b7214e 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -41,11 +41,10 @@ def start_JVM(options=''): - # set _options to the default options from pyimagej if none are specified global _options _options = options - # if jvm JVM is already running -- break + # if JVM is already running -- break if JVM_status() == True: _logger.debug('The JVM is already running.') return @@ -112,7 +111,7 @@ def start_JVM(options=''): if pathlib.Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): os.environ['PATH'] += ';' + jvm_server_dir - # retrieve endpoint and repositories from scyjava_config + # retrieve endpoint and repositories from scyjava config endpoints = get_endpoints() repositories = get_repositories() From c31d63373389d498b17550d6fb55b98a02a03e86 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 15:21:31 -0600 Subject: [PATCH 057/505] Remove obsolete JDK detection logic The JPype project does it much better. --- scyjava/config/__init__.py | 62 -------------------------------------- 1 file changed, 62 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index b8b7214e..3a4e77a5 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -49,68 +49,6 @@ def start_JVM(options=''): _logger.debug('The JVM is already running.') return - # attempt to set JAVA_HOME if the environment variable is not set. - JAVA_HOME_STR = 'JAVA_HOME' - if JAVA_HOME_STR not in globals(): - JAVA_HOME = None - try: - _logger.debug('Checking %s environment variable', JAVA_HOME_STR) - JAVA_HOME = os.environ[JAVA_HOME_STR] - except KeyError: - _logger.debug('No %s environment variable', JAVA_HOME_STR) - if not JAVA_HOME: - # NB: This logic handles both None and empty string cases. - _logger.debug('%s still unknown; checking with Maven', JAVA_HOME_STR) - # attempt to find Java by interrogating maven - # (which we have because it is needed by jgo) - try: - if (platform.system() == 'Windows'): - mvn = str(subprocess.check_output(['mvn.cmd', '-v'])) - mvn = mvn.replace('\\r\\n', '\\n') # Fix Windows line breaks. - else: - mvn = str(subprocess.check_output(['mvn', '-v'])) - except subprocess.CalledProcessError as e: - _logger.error('Unable to import scyjava, could not find Maven') - return None - _logger.debug('Maven said: %s', mvn) - try: - begin = mvn.index('Java home: ') - except ValueError as e: - # in some versions of maven it is instead called runtime - try: - begin = mvn.index('runtime: ') - except ValueError as e: - _logger.error('Unable to import scyjava, could not locate jre') - return None - # cut out 'Java home' or 'runtime' - begin = mvn.index(':', begin) + 2 - end = mvn.index('\\n', begin) - JAVA_HOME = mvn[begin:end] - java_path = pathlib.Path(JAVA_HOME) - if java_path.is_dir(): - _logger.debug('%s found at "%s"', JAVA_HOME_STR, JAVA_HOME) - if java_path.name == 'jre': - _logger.debug('JAVA_HOME points at jre folder; using parent instead') - JAVA_HOME = str(java_path.parent) - os.environ['JAVA_HOME'] = JAVA_HOME - else: - _logger.error('Unable to import scyjava: jre not found') - return None - else: - _logger.debug('%s found in globals', JAVA_HOME_STR) - - # On Windows, add server subfolder to the PATH so jvm.dll can be found. - if (platform.system() == 'Windows'): - # Java 9 and later - jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'bin', 'server') - if pathlib.Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): - os.environ['PATH'] += ';' + jvm_server_dir - else: - # Java 8 and earlier - jvm_server_dir = os.path.join(os.environ['JAVA_HOME'], 'jre', 'bin', 'server') - if pathlib.Path(os.path.join(jvm_server_dir, 'jvm.dll')).is_file(): - os.environ['PATH'] += ';' + jvm_server_dir - # retrieve endpoint and repositories from scyjava config endpoints = get_endpoints() repositories = get_repositories() From 6c8d6cb6f262e14c5ba6e4ca92f724489a90240b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 15:51:32 -0600 Subject: [PATCH 058/505] Revise the scyjava API 1. Move start_JVM and JVM_status methods to top level And rename them start_jvm and jvm_started, respectively. Lower case is more Pythonic, and the name jvm_started is clearer. These methods are not "configuration" per se, but actually do work. Writing: import scyjava.config scyjava.config.start_JVM() looks very odd. Whereas writing: import scyjava scyjava.start_jvm() makes total sense to me. 2. Eliminate scyjava.convert in favor of putting those functions into scyjava directly. A crucial purpose of scyjava is these functions. 3. Hide usages of jpype and initialize JVM lazily Now there is a scyjava.jimport command that wraps the jpype.JClass constructor. I think it reads much more nicely: from scyjava import jimport System = jimport('java.lang.System') print(System.getProperty('java.home')) So you don't even have to call scyjava.start_jvm() explicitly. Though of course you still can, if you want to control the startup. 4. Fix the JVM options API Calling add_options should actually append options, not overwrite. And while we're at it, let's differentiate between adding a single option (a string) versus multiple additional options (a list). 5. Remove the scyjava.config set_classpath and set_options functions Typical use case is to build up classpath and build up options, not overwrite it wholesale. If we do decide to add a means of overwriting the whole enchilada in the future, OK, but as written set_classpath didn't do that -- it still appended. 6. Fix the scyjava.config.__all__ list I have no idea what it's for, but it was out of sync. 7. Clean up code style a bit Two blank lines between functions. Alphabetized imports. 8. Split the pandas test into its own test file That way, if you don't have numpy and/or pandas installed, you can still test the vanilla conversion routines freely. --- scyjava/{convert/_convert.py => __init__.py} | 127 +++++++++++++++---- scyjava/config/__init__.py | 91 +++---------- scyjava/convert/__init__.py | 1 - setup.py | 4 +- tests/test_convert.py | 73 +---------- tests/test_pandas.py | 77 +++++++++++ 6 files changed, 209 insertions(+), 164 deletions(-) rename scyjava/{convert/_convert.py => __init__.py} (75%) delete mode 100644 scyjava/convert/__init__.py create mode 100644 tests/test_pandas.py diff --git a/scyjava/convert/_convert.py b/scyjava/__init__.py similarity index 75% rename from scyjava/convert/_convert.py rename to scyjava/__init__.py index fe68bef6..2251586f 100644 --- a/scyjava/convert/_convert.py +++ b/scyjava/__init__.py @@ -1,15 +1,85 @@ -# General-purpose utility methods for Python <-> Java type conversion. - import collections.abc +import jgo import jpype -import jpype.imports +import logging +import os +import scyjava.config from jpype.types import * from _jpype import _JObject -# Java imports: -from java.lang import Boolean, Byte, Character, Double, Float, Integer, Iterable, Long, Object, Short, String, Void -from java.math import BigDecimal, BigInteger -from java.util import ArrayList, Collection, Iterator, LinkedHashMap, LinkedHashSet, List, Map, Set +_logger = logging.getLogger(__name__) + + +# -- JVM setup -- + +def start_jvm(options=scyjava.config.get_options()): + """ + Explicitly connect to the Java virtual machine (JVM). Only one JVM can + be active; does nothing if the JVM has already been started. Calling + this function directly is typically not necessary, because the first + time a scyjava function needing a JVM is invoked, one is started on the + fly with the configuration specified via the scijava.config mechanism. + + :param options: List of options to pass to the JVM. For example: + ['-Djava.awt.headless=true', '-Xmx4g'] + """ + # if JVM is already running -- break + if jvm_started(): + _logger.debug('The JVM is already running.') + return + + # retrieve endpoint and repositories from scyjava config + endpoints = scyjava.config.get_endpoints() + repositories = scyjava.config.get_repositories() + + # use the logger to notify user that endpoints are being added + _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) + + # get endpoints and add to JPype class path + if len(endpoints) > 0: + endpoints = endpoints[:1] + sorted(endpoints[1:]) + _logger.debug('Using endpoints %s', endpoints) + _, workspace = jgo.resolve_dependencies( + '+'.join(endpoints), + m2_repo=scyjava.config.get_m2_repo(), + cache_dir=scyjava.config.get_cache_dir(), + manage_dependencies=scyjava.config.get_manage_deps(), + repositories=repositories, + verbose=scyjava.config.get_verbose() + ) + jpype.addClassPath(os.path.join(workspace, '*')) + + # Initialize JPype JVM + jpype.startJVM(*options) + + # Grab needed Java classes. + global Boolean; Boolean = jimport('java.lang.Boolean') + global Byte; Byte = jimport('java.lang.Byte') + global Character; Character = jimport('java.lang.Character') + global Double; Double = jimport('java.lang.Double') + global Float; Float = jimport('java.lang.Float') + global Integer; Integer = jimport('java.lang.Integer') + global Iterable; Iterable = jimport('java.lang.Iterable') + global Long; Long = jimport('java.lang.Long') + global Object; Object = jimport('java.lang.Object') + global Short; Short = jimport('java.lang.Short') + global String; String = jimport('java.lang.String') + global Void; Void = jimport('java.lang.Void') + global BigDecimal; BigDecimal = jimport('java.math.BigDecimal') + global BigInteger; BigInteger = jimport('java.math.BigInteger') + global ArrayList; ArrayList = jimport('java.util.ArrayList') + global Collection; Collection = jimport('java.util.Collection') + global Iterator; Iterator = jimport('java.util.Iterator') + global LinkedHashMap; LinkedHashMap = jimport('java.util.LinkedHashMap') + global LinkedHashSet; LinkedHashSet = jimport('java.util.LinkedHashSet') + global List; List = jimport('java.util.List') + global Map; Map = jimport('java.util.Map') + global Set; Set = jimport('java.util.Set') + + +def jvm_started(): + """Return true iff a Java virtual machine (JVM) has been started.""" + return jpype.isJVMStarted() # -- Python to Java -- @@ -35,16 +105,27 @@ def jclass(data): :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. """ - if isinstance(data, jpype.JClass): return data.class_ if isinstance(data, _JObject): return data.getClass() if isinstance(data, str): - return jclass(jpype.JClass(data)) + return jclass(jimport(data)) raise TypeError('Cannot glean class from data of type: ' + str(type(data))) +def jimport(class_name): + """ + Import a class from Java to Python. + + :param class_name: Name of the class to import. + :returns: A pointer to the class, which can be used to + e.g. instantiate objects of that class. + """ + start_jvm() + return jpype.JClass(class_name) + + def jstacktrace(exc): """ Extract the Java-side stack trace from a wrapped Java exception. @@ -82,6 +163,7 @@ def to_java(data): :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ + start_jvm() if data is None: return None @@ -113,7 +195,7 @@ def to_java(data): # Trying to get the type without importing Pandas. if type(data).__name__ == 'DataFrame': - return pandas_to_table(data) + return _pandas_to_table(data) if isinstance(data, collections.abc.Mapping): jmap = LinkedHashMap() @@ -151,7 +233,9 @@ def _jstr(data): class JavaObject(): - def __init__(self, jobj, intended_class=Object): + def __init__(self, jobj, intended_class=None): + if intended_class is None: + intended_class = Object if not isinstance(jobj, intended_class): raise TypeError('Not a ' + intended_class.getName() + ': ' + jclass(jobj).getName()) self.jobj = jobj @@ -320,6 +404,8 @@ def to_python(data, gentle=False): :raises TypeError: if the argument is not one of the aforementioned types, and the gentle flag is not set. """ + start_jvm() + if not isjava(data): return data @@ -360,7 +446,7 @@ def to_python(data, gentle=False): try: if isinstance(data, jclass('org.scijava.table.Table')): - return table_to_pandas(data) + return _table_to_pandas(data) except: # No worries if scijava-table is not available. pass @@ -389,14 +475,11 @@ def _import_pandas(): return pd except ImportError: msg = "The Pandas library is missing (http://pandas.pydata.org/). " - msg += "Please instal it using: " - msg += "conda install pandas (prefered)" - msg += " or " - msg += "pip install pandas." + msg += "Please install it before using this function." raise Exception(msg) -def table_to_pandas(table): +def _table_to_pandas(table): pd = _import_pandas() data = [] @@ -409,19 +492,19 @@ def table_to_pandas(table): return df -def pandas_to_table(df): +def _pandas_to_table(df): pd = _import_pandas() if len(df.dtypes.unique()) > 1: - TableClass = jpype.JClass('org.scijava.table.DefaultGenericTable') + TableClass = jimport('org.scijava.table.DefaultGenericTable') else: table_type = df.dtypes.unique()[0] if table_type.name.startswith('float'): - TableClass = jpype.JClass('org.scijava.table.DefaultFloatTable') + TableClass = jimport('org.scijava.table.DefaultFloatTable') elif table_type.name.startswith('int'): - TableClass = jpype.JClass('org.scijava.table.DefaultIntTable') + TableClass = jimport('org.scijava.table.DefaultIntTable') elif table_type.name.startswith('bool'): - TableClass = jpype.JClass('org.scijava.table.DefaultBoolTable') + TableClass = jimport('org.scijava.table.DefaultBoolTable') else: msg = "The type '{}' is not supported.".format(table_type.name) raise Exception(msg) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 3a4e77a5..05ef1229 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -12,21 +12,15 @@ 'get_cache_dir', 'set_m2_repo', 'get_m2_repo', - 'set_options', - 'add_options', - 'get_options', - 'set_classpath', 'add_classpath', - 'get_classpath') + 'get_classpath', + 'add_option', + 'add_options', + 'get_options') import logging -import os -import platform import pathlib -import jgo import jpype -import jpype.imports -import subprocess _logger = logging.getLogger(__name__) @@ -36,58 +30,8 @@ _manage_deps = True _cache_dir = pathlib.Path.home() / '.jgo' _m2_repo = pathlib.Path.home() / '.m2' / 'repository' -_options = '' -_add_options = '' - - -def start_JVM(options=''): - global _options - _options = options - - # if JVM is already running -- break - if JVM_status() == True: - _logger.debug('The JVM is already running.') - return - - # retrieve endpoint and repositories from scyjava config - endpoints = get_endpoints() - repositories = get_repositories() - - # use the logger to notify user that endpoints are being added - _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) +_options = [] - # get endpoints and add to JPype class path - if len(endpoints) > 0: - endpoints = endpoints[:1] + sorted(endpoints[1:]) - _logger.debug('Using endpoints %s', endpoints) - _, workspace = jgo.resolve_dependencies( - '+'.join(endpoints), - m2_repo=get_m2_repo(), - cache_dir=get_cache_dir(), - manage_dependencies=get_manage_deps(), - repositories=repositories, - verbose=get_verbose() - ) - jpype.addClassPath(os.path.join(workspace, '*')) - - # Initialize JPype JVM - jvm_options = _options - - # append any additional options - if _add_options == '': - pass - else: - jvm_options = jvm_options + ' ' + _add_options - - # store options used for the jvm in _options -- user can check what was used - _options = jvm_options - jpype.startJVM(jvm_options) - - return - - -def JVM_status(): - return jpype.isJVMStarted() def maven_scijava_repository(): """ @@ -95,15 +39,18 @@ def maven_scijava_repository(): """ return 'https://maven.scijava.org/content/groups/public' + def add_endpoints(*endpoints): global _endpoints _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) _endpoints.extend(endpoints) + def get_endpoints(): global _endpoints return _endpoints + def add_repositories(*args, **kwargs): global _repositories for arg in args: @@ -112,10 +59,12 @@ def add_repositories(*args, **kwargs): _logger.debug('Adding repositories %s to %s', kwargs, _repositories) _repositories.update(kwargs) + def get_repositories(): global _repositories return _repositories + def set_verbose(level): global _verbose _logger.debug('Setting verbose level to %d (was %d)', level, _verbose) @@ -160,25 +109,25 @@ def get_m2_repo(): global _m2_repo return _m2_repo -def add_classpath(*path): - jpype.addClassPath(*path) - -def set_classpath(*path): +def add_classpath(*path): jpype.addClassPath(*path) def get_classpath(): return jpype.getClassPath() + +def add_option(option): + global _options + _options.append(option) + + def add_options(options): - global _add_options - _add_options = options + global _options + _options.extend(options) + def get_options(): global _options return _options - -def set_options(options): - global _options - _options = options diff --git a/scyjava/convert/__init__.py b/scyjava/convert/__init__.py deleted file mode 100644 index 26c7d9fc..00000000 --- a/scyjava/convert/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._convert import * diff --git a/setup.py b/setup.py index 4427c989..6053cc6d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_namespace_packages +from setuptools import setup, find_packages from os import path here = path.abspath(path.dirname(__file__)) @@ -9,7 +9,7 @@ setup( name='scyjava', python_requires='>=3', - packages=find_namespace_packages(include=['scyjava.*']), + packages=find_packages(), version='1.0.0.dev0', author='Philipp Hanslovsky, Curtis Rueden', author_email='hanslovskyp@janelia.hhmi.org', diff --git a/tests/test_convert.py b/tests/test_convert.py index bd542cf2..3e991f16 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,23 +1,11 @@ import scyjava.config -scyjava.config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) -scyjava.config.add_endpoints('org.scijava:scijava-table') - import unittest -import pandas as pd -import numpy as np -import jpype -import jpype.imports -from jpype.types import * +from scyjava import jclass, jimport, to_java, to_python -# EE: scyjava.convert perfroms imports that need to happen after the JVM -# has started. -scyjava.config.set_options('-Djava.awt.headless=true') -scyjava.config.start_JVM() -from scyjava.convert import jclass, to_java, to_python +scyjava.config.add_endpoints('org.scijava:scijava-table') +scyjava.config.add_option('-Djava.awt.headless=true') def assert_same_table(table, df): - import numpy.testing as npt - assert len(table.toArray()) == df.shape[1] assert len(table.toArray()[0].toArray()) == df.shape[0] @@ -185,7 +173,7 @@ def testNone(self): self.assertEqual(d, pd) def testGentle(self): - Object = jpype.JClass('java.lang.Object') + Object = jimport('java.lang.Object') unknown_thing = Object() converted_thing = to_python(unknown_thing, gentle=True) assert type(converted_thing) == Object @@ -199,7 +187,7 @@ def testGentle(self): def testStructureWithSomeUnsupportedItems(self): # Create Java data structure with some challenging items. - Object = jpype.JClass('java.lang.Object') + Object = jimport('java.lang.Object') jmap = to_java({ 'list': ['a', Object(), 1], 'set': {'x', Object(), 2}, @@ -220,57 +208,6 @@ def testStructureWithSomeUnsupportedItems(self): assert type(pdict['object']) == Object self.assertEqual(pdict['foo'], 'bar') - def testPandasToTable(self): - # Float table. - columns = ["header1", "header2", "header3", "header4", "header5"] - array = np.random.random(size=(7, 5)) - - df = pd.DataFrame(array, columns=columns) - table = to_java(df) - - assert_same_table(table, df) - assert type(table) == jpype.JClass('org.scijava.table.DefaultFloatTable') - - # Int table. - columns = ["header1", "header2", "header3", "header4", "header5"] - array = np.random.random(size=(7, 5)) * 100 - array = array.astype('int') - - df = pd.DataFrame(array, columns=columns) - table = to_java(df) - - assert_same_table(table, df) - assert type(table) == jpype.JClass('org.scijava.table.DefaultIntTable') - - # Bool table. - columns = ["header1", "header2", "header3", "header4", "header5"] - array = np.random.random(size=(7, 5)) > 0.5 - - df = pd.DataFrame(array, columns=columns) - table = to_java(df) - - assert_same_table(table, df) - assert type(table) == jpype.JClass('org.scijava.table.DefaultBoolTable') - - # Mixed table. - columns = ["header1", "header2", "header3", "header4", "header5"] - array = np.random.random(size=(7, 5)) - - df = pd.DataFrame(array, columns=columns) - - # Convert column 0 to integer - df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype('int') - # Convert column 1 to bool - df.iloc[:, 1] = df.iloc[:, 1] > 0.5 - # Convert column 2 to string - df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split('\n') - - table = to_java(df) - - # Table types cannot be the same here, unless we want to cast. - # assert_same_table(table, df) - assert type(table) == jpype.JClass('org.scijava.table.DefaultGenericTable') - if __name__ == '__main__': unittest.main() diff --git a/tests/test_pandas.py b/tests/test_pandas.py new file mode 100644 index 00000000..95845d12 --- /dev/null +++ b/tests/test_pandas.py @@ -0,0 +1,77 @@ +import numpy as np +import numpy.testing as npt +import pandas as pd +import scyjava.config +import unittest +from scyjava import jimport, to_java + +scyjava.config.add_endpoints('org.scijava:scijava-table') +scyjava.config.add_option('-Djava.awt.headless=true') + + +def assert_same_table(table, df): + assert len(table.toArray()) == df.shape[1] + assert len(table.toArray()[0].toArray()) == df.shape[0] + + for i, column in enumerate(table.toArray()): + npt.assert_array_almost_equal(df.iloc[:, i].values, column.toArray()) + + assert table.getColumnHeader(i) == df.columns[i] + + +class TestPandas(unittest.TestCase): + + def testPandasToTable(self): + # Float table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) + + df = pd.DataFrame(array, columns=columns) + table = to_java(df) + + assert_same_table(table, df) + assert type(table) == jimport('org.scijava.table.DefaultFloatTable') + + # Int table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) * 100 + array = array.astype('int') + + df = pd.DataFrame(array, columns=columns) + table = to_java(df) + + assert_same_table(table, df) + assert type(table) == jimport('org.scijava.table.DefaultIntTable') + + # Bool table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) > 0.5 + + df = pd.DataFrame(array, columns=columns) + table = to_java(df) + + assert_same_table(table, df) + assert type(table) == jimport('org.scijava.table.DefaultBoolTable') + + # Mixed table. + columns = ["header1", "header2", "header3", "header4", "header5"] + array = np.random.random(size=(7, 5)) + + df = pd.DataFrame(array, columns=columns) + + # Convert column 0 to integer + df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype('int') + # Convert column 1 to bool + df.iloc[:, 1] = df.iloc[:, 1] > 0.5 + # Convert column 2 to string + df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split('\n') + + table = to_java(df) + + # Table types cannot be the same here, unless we want to cast. + # assert_same_table(table, df) + assert type(table) == jimport('org.scijava.table.DefaultGenericTable') + + +if __name__ == '__main__': + unittest.main() From 2fb6ccc3447af2217a9852e29b5133ae986e09d1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 20:00:44 -0600 Subject: [PATCH 059/505] Fix jstacktrace function It was still coded for pyjnius, not jpype. --- scyjava/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 2251586f..1461b3c0 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -128,24 +128,29 @@ def jimport(class_name): def jstacktrace(exc): """ - Extract the Java-side stack trace from a wrapped Java exception. + Extract the Java-side stack trace from a Java exception. Example of usage: - from jnius import autoclass + from scyjava import jimport, jstacktrace try: - Integer = autoclass('java.lang.Integer') + Integer = jimport('java.lang.Integer') nan = Integer.parseInt('not a number') except Exception as exc: print(jstacktrace(exc)) - :param exc: The JavaException from which to extract the stack trace. + :param exc: The Java Throwable from which to extract the stack trace. :returns: A multi-line string containing the stack trace, or empty string if no stack trace could be extracted. """ - if not hasattr(exc, 'classname') or exc.classname is None: - return str(exc) - return '' if not exc.stacktrace else '\n\tat '.join(exc.stacktrace) + try: + StringWriter = jimport('java.io.StringWriter') + PrintWriter = jimport('java.io.PrintWriter') + sw = StringWriter() + exc.printStackTrace(PrintWriter(sw, True)) + return sw.toString() + except: + return '' def to_java(data): From 04a649101bf9e4d8b48ee501b488006c71ed892a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 15:40:59 -0600 Subject: [PATCH 060/505] Update the README to reflect reality --- README.md | 172 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 106 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index c8dd59d9..6262ded8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ Supercharged Java access from Python. -Built on [JPype](https://jpype.readthedocs.io/en/latest/) and [jgo](https://github.com/scijava/jgo). +Built on [JPype](https://jpype.readthedocs.io/en/latest/) +and [jgo](https://github.com/scijava/jgo). ## Use Java classes from Python ```python ->>> import jpype ->>> import jpype.imports ->>> jpype.startJVM() ->>> System = jpype.JClass('java.lang.System') +>>> from scyjava import jimport +>>> System = jimport('java.lang.System') >>> System.getProperty('java.version') '1.8.0_252' ``` @@ -16,16 +15,16 @@ Built on [JPype](https://jpype.readthedocs.io/en/latest/) and [jgo](https://gith To pass parameters to the JVM, such as an increased max heap size: ```python ->>> import jpype ->>> import jpype.imports ->>> import scyjava.jvm ->>> scyjava.jvm.start_JVM('-Xmx6g') ->>> Runtime = jpype.JClass('java.lang.Runtime') +>>> from scyjava import jimport +>>> import scyjava.config +>>> scyjava.config.add_option('-Xmx6g') +>>> Runtime = jimport('java.lang.Runtime') >>> Runtime.getRuntime().maxMemory() / 2**30 5.33349609375 ``` -See the [JPype documentation](https://jpype.readthedocs.io/en/latest/) for more about calling Java from Python. +See the [JPype documentation](https://jpype.readthedocs.io/en/latest/) +for all the gritty details on how this wrapping works. ## Use Maven artifacts from remote repositories @@ -34,40 +33,35 @@ See the [JPype documentation](https://jpype.readthedocs.io/en/latest/) for more ```python >>> import sys >>> sys.version_info -sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0) ->>> import scyjava_config ->>> scyjava_config.add_endpoints('org.python:jython-standalone:2.7.1') ->>> import jpype ->>> import scyjava.jvm ->>> scyjava.jvm.start_JVM() ->>> jython = jpype.JClass('org.python.util.jython') +sys.version_info(major=3, minor=8, micro=5, releaselevel='final', serial=0) +>>> import scyjava.config +>>> scyjava.config.add_endpoints('org.python:jython-slim:2.7.2') +>>> from scyjava import jimport +>>> jython = jimport('org.python.util.jython') >>> jython.main([]) -Jython 2.7.1 (default:0df7adb1b397, Jun 30 2017, 19:02:43) -[OpenJDK 64-Bit Server VM (AdoptOpenJDK)] on java1.8.0_252 +Jython 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:12:24) +[OpenJDK 64-Bit Server VM (JetBrains s.r.o)] on java1.8.0_152-release Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.version_info sys.version_info(major=2, minor=7, micro=1, releaselevel='final', serial=0) +>>> from java.lang import System +>>> System.getProperty('java.version') +u'1.8.0_152-release' ``` ### From other Maven repositories ```python ->>> import scyjava_config ->>> scyjava_config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) ->>> scyjava_config.add_endpoints('net.imagej:imagej:2.0.0-rc-65') ->>> import scyjava.jvm ->>> import jpype ->>> import jpype.imports ->>> from jpype import JClass, JArray, JLong ->>> scyjava.jvm.start_JVM() ->>> System = JClass('java.lang.System') ->>> System.setProperty('java.awt.headless', 'true') ->>> ImageJ = JClass('net.imagej.ImageJ') +>>> import scyjava.config +>>> scyjava.config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) +>>> scyjava.config.add_endpoints('net.imagej:imagej:2.1.0') +>>> from scyjava import jimport +>>> ImageJ = jimport('net.imagej.ImageJ') >>> ij = ImageJ() >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" ->>> dims = JLong[64, 16] ->>> blank = ij.op().getClass().getMethod('create').invoke(ij.op()).img(dims) +>>> ArrayImgs = jimport('net.imglib2.img.array.ArrayImgs') +>>> blank = ArrayImgs.floats(64, 16) >>> sinusoid = ij.op().image().equation(blank, formula) >>> print(ij.op().image().ascii(sinusoid)) ,,,--+oo******oo+--,,,,,--+oo******o++--,,,,,--+oo******o++--,,, @@ -95,20 +89,24 @@ See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven ### Convert Java collections to Python ```python ->>> import jpype ->>> import jpype.imports ->>> import scyjava ->>> import scyjava.jvm ->>> scyjava.jvm.start_JVM() ->>> import scyjava.convert ->>> System = jpype.JClass('java.lang.System') ->>> props = System.getProperties() ->>> props - ->>> [k for k in props] -['java.runtime.name', 'sun.boot.library.path', 'java.vm.version', 'java.vm.vendor', 'java.vendor.url', 'path.separator', 'java.vm.name', 'file.encoding.pkg', 'user.country', 'sun.os.patch.level', 'java.vm.specification.name', 'user.dir', 'java.runtime.version', 'java.awt.graphicsenv', 'java.endorsed.dirs', 'os.arch', 'java.io.tmpdir', 'line.separator', 'java.vm.specification.vendor', 'os.name', 'sun.jnu.encoding', 'java.library.path', 'java.specification.name', 'java.class.version', 'sun.management.compiler', 'os.version', 'user.home', 'user.timezone', 'java.awt.printerjob', 'file.encoding', 'java.specification.version', 'java.class.path', 'user.name', 'java.vm.specification.version', 'java.home', 'sun.arch.data.model', 'user.language', 'java.specification.vendor', 'awt.toolkit', 'java.vm.info', 'java.version', 'java.ext.dirs', 'sun.boot.class.path', 'java.vendor', 'file.separator', 'java.vendor.url.bug', 'sun.io.unicode.encoding', 'sun.cpu.endian', 'sun.desktop', 'sun.cpu.isalist'] ->>> [k for k in scyjava.convert.to_python(props) if k.startswith('java.vm.')] -['java.vm.version', 'java.vm.vendor', 'java.vm.name', 'java.vm.specification.name', 'java.vm.specification.vendor', 'java.vm.specification.version', 'java.vm.info'] +>>> from scyjava import jimport +>>> HashSet = jimport('java.util.HashSet') +>>> moves = set(('jump', 'duck', 'dodge')) +>>> fish = set(('walleye', 'pike', 'trout')) +>>> jbirds = HashSet() +>>> for bird in ('duck', 'goose', 'swan'): jbirds.add(bird) +... +True +True +True +>>> jbirds.isdisjoint(moves) +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'java.util.HashSet' object has no attribute 'isdisjoint' +>>> j2p(jbirds).isdisjoint(moves) +False +>>> j2p(jbirds).isdisjoint(fish) +True ``` ### Convert Python collections to Java @@ -121,29 +119,34 @@ See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven Traceback (most recent call last): File "", line 1, in AttributeError: 'list' object has no attribute 'stream' ->>> scyjava.convert.to_java(squares).stream() +>>> from scyjava import to_java as p2j +>>> p2j(squares).stream() ``` -### Introspect Java classes - ```python ->>> NumberClass = scyjava.convert.jclass('java.lang.Number') ->>> NumberClass - ->>> NumberClass.getName() -'java.lang.Number' ->>> NumberClass.isInstance(scyjava.convert.to_java(5)) +>>> from scyjava import jimport +>>> HashSet = jimport('java.util.HashSet') +>>> jset = HashSet() +>>> pset = set((1, 2, 3)) +>>> jset.addAll(pset) +Traceback (most recent call last): + File "", line 1, in +TypeError: No matching overloads found for java.util.Set.addAll(set), options are: + public abstract boolean java.util.Set.addAll(java.util.Collection) +>>> from scyjava import to_java as p2j +>>> jset.addAll(p2j(pset)) True ->>> NumberClass.isInstance(scyjava.convert.to_java('Hello')) -False +>>> jset.toString() +'[1, 2, 3]' ``` -## Available functions -- EE fix this + +## Available functions ``` >>> import scyjava ->>> help(scyjava.convert) +>>> help(scyjava) ... FUNCTIONS isjava(data) @@ -156,13 +159,47 @@ FUNCTIONS Supported types include: A. Name of a class to look up, analogous to Class.forName("java.lang.String"); - B. A jnius.MetaJavaClass object e.g. from jnius.autoclass, analogous to - String.class; - C. A jnius.JavaClass object e.g. instantiated from a jnius.MetaJavaClass, - analogous to "Hello".getClass(). + B. A jpype.JClass object analogous to String.class; + C. A _jpype._JObject instance analogous to o.getClass(). :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. + jimport(class_name) + Import a class from Java to Python. + + :param class_name: Name of the class to import. + :returns: A pointer to the class, which can be used to + e.g. instantiate objects of that class. + + jstacktrace(exc) + Extract the Java-side stack trace from a Java exception. + + Example of usage: + + from scyjava import jimport + try: + Integer = jimport('java.lang.Integer') + nan = Integer.parseInt('not a number') + except Exception as exc: + print(jstacktrace(exc)) + + :param exc: The Java Throwable from which to extract the stack trace. + :returns: A multi-line string containing the stack trace, or empty string + if no stack trace could be extracted. + + jvm_started() + Return true iff a Java virtual machine (JVM) has been started. + + start_jvm(options=[]) + Explicitly connect to the Java virtual machine (JVM). Only one JVM can + be active; does nothing if the JVM has already been started. Calling + this function directly is typically not necessary, because the first + time a scyjava function needing a JVM is invoked, one is started on the + fly with the configuration specified via the scijava.config mechanism. + + :param options: List of options to pass to the JVM. For example: + ['-Djava.awt.headless=true', '-Xmx4g'] + to_java(data) Recursively convert a Python object to a Java object. :param data: The Python object to convert. @@ -177,9 +214,11 @@ FUNCTIONS :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. - to_python(data) + to_python(data, gentle=False) Recursively convert a Java object to a Python object. :param data: The Java object to convert. + :param gentle: If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. Supported types include: * String, Character -> str * Boolean -> bool @@ -192,5 +231,6 @@ FUNCTIONS * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :raises TypeError: if the argument is not one of the aforementioned types, + and the gentle flag is not set. ``` From 4e8d70e98a7475a8d3b334766db6ac199dbd17ad Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 20:55:47 -0600 Subject: [PATCH 061/505] Update authors list and contact email Philipp isn't active anymore, and I wrote most of the current code. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6053cc6d..025034c7 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,8 @@ python_requires='>=3', packages=find_packages(), version='1.0.0.dev0', - author='Philipp Hanslovsky, Curtis Rueden', - author_email='hanslovskyp@janelia.hhmi.org', + author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', + author_email='ctrueden@wisc.edu', description='scyjava', long_description=scyjava_long_description, long_description_content_type='text/markdown', From 6e69f45093ac857ec1406597e8b2fb58ec9abc93 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 21:02:50 -0600 Subject: [PATCH 062/505] Release version 1.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 025034c7..09f33a03 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.0.0.dev0', + version='1.0.0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 5fc5dd0c886242e2913890b6cecd8511baa0d9d1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 28 Nov 2020 21:05:02 -0600 Subject: [PATCH 063/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09f33a03..0d4d6f71 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.0.0', + version='1.0.1.dev0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 060b8dc1a0705b1d822f5777f9ee754db7c06be2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 12:01:26 -0600 Subject: [PATCH 064/505] README.md: fix issues with the examples --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6262ded8..0c3f9786 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ and [jgo](https://github.com/scijava/jgo). To pass parameters to the JVM, such as an increased max heap size: ```python ->>> from scyjava import jimport >>> import scyjava.config >>> scyjava.config.add_option('-Xmx6g') +>>> from scyjava import jimport >>> Runtime = jimport('java.lang.Runtime') >>> Runtime.getRuntime().maxMemory() / 2**30 5.33349609375 @@ -103,6 +103,7 @@ True Traceback (most recent call last): File "", line 1, in AttributeError: 'java.util.HashSet' object has no attribute 'isdisjoint' +>>> from scyjava import to_python as j2p >>> j2p(jbirds).isdisjoint(moves) False >>> j2p(jbirds).isdisjoint(fish) From 4cd2eebe933e10edb8b8d74bd5c4070427f936c8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 18:21:58 -0600 Subject: [PATCH 065/505] Get rid of redundant __all__ declaration All globals have a leading underscore. All public functions do not. Therefore, the default public API inference suffices. I am a fan of not declaring __all__ if at all possible, since it really a duplicate data structure and therefore a possible source of code skew. --- scyjava/config/__init__.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 05ef1229..c9255506 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -1,23 +1,3 @@ -__all__ = ( - 'maven_scijava_repository', - 'add_endpoints', - 'get_endpoints', - 'add_repositories', - 'get_repositories', - 'set_verbose', - 'get_verbose', - 'set_manage_deps', - 'get_manage_deps', - 'set_cache_dir', - 'get_cache_dir', - 'set_m2_repo', - 'get_m2_repo', - 'add_classpath', - 'get_classpath', - 'add_option', - 'add_options', - 'get_options') - import logging import pathlib import jpype From 7879162aa8134562fde02e9358be45ed9e760089 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 22:36:05 -0600 Subject: [PATCH 066/505] Make usage of scijava.config more succinct --- README.md | 17 +++++++---------- tests/test_convert.py | 7 +++---- tests/test_pandas.py | 7 +++---- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 0c3f9786..e73fcfb6 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ and [jgo](https://github.com/scijava/jgo). To pass parameters to the JVM, such as an increased max heap size: ```python ->>> import scyjava.config ->>> scyjava.config.add_option('-Xmx6g') ->>> from scyjava import jimport +>>> from scyjava import config, jimport +>>> config.add_option('-Xmx6g') >>> Runtime = jimport('java.lang.Runtime') >>> Runtime.getRuntime().maxMemory() / 2**30 5.33349609375 @@ -34,9 +33,8 @@ for all the gritty details on how this wrapping works. >>> import sys >>> sys.version_info sys.version_info(major=3, minor=8, micro=5, releaselevel='final', serial=0) ->>> import scyjava.config ->>> scyjava.config.add_endpoints('org.python:jython-slim:2.7.2') ->>> from scyjava import jimport +>>> from scyjava import config, jimport +>>> config.add_endpoints('org.python:jython-slim:2.7.2') >>> jython = jimport('org.python.util.jython') >>> jython.main([]) Jython 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:12:24) @@ -53,10 +51,9 @@ u'1.8.0_152-release' ### From other Maven repositories ```python ->>> import scyjava.config ->>> scyjava.config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) ->>> scyjava.config.add_endpoints('net.imagej:imagej:2.1.0') ->>> from scyjava import jimport +>>> from scyjava import config, jimport +>>> config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) +>>> config.add_endpoints('net.imagej:imagej:2.1.0') >>> ImageJ = jimport('net.imagej.ImageJ') >>> ij = ImageJ() >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" diff --git a/tests/test_convert.py b/tests/test_convert.py index 3e991f16..985cff84 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,9 +1,8 @@ -import scyjava.config import unittest -from scyjava import jclass, jimport, to_java, to_python +from scyjava import config, jclass, jimport, to_java, to_python -scyjava.config.add_endpoints('org.scijava:scijava-table') -scyjava.config.add_option('-Djava.awt.headless=true') +config.add_endpoints('org.scijava:scijava-table') +config.add_option('-Djava.awt.headless=true') def assert_same_table(table, df): assert len(table.toArray()) == df.shape[1] diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 95845d12..1b04421b 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,12 +1,11 @@ import numpy as np import numpy.testing as npt import pandas as pd -import scyjava.config import unittest -from scyjava import jimport, to_java +from scyjava import config, jimport, to_java -scyjava.config.add_endpoints('org.scijava:scijava-table') -scyjava.config.add_option('-Djava.awt.headless=true') +config.add_endpoints('org.scijava:scijava-table') +config.add_option('-Djava.awt.headless=true') def assert_same_table(table, df): From 160edbb90b76e800e311d11aa82f89f5c6484488 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 22:42:21 -0600 Subject: [PATCH 067/505] README: remove extra blank line --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e73fcfb6..12052e86 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,6 @@ True '[1, 2, 3]' ``` - ## Available functions ``` From 688e649a42a12ad2719fb9eabd03ef6e60d47fd4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 19:27:47 -0600 Subject: [PATCH 068/505] Add a mechanism for callbacks upon JVM start This lets downstream code perform initialization tasks as soon as the JVM starts, but not before. --- README.md | 8 ++++++++ scyjava/__init__.py | 29 +++++++++++++++++++++++++++-- setup.py | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12052e86..3b4142d2 100644 --- a/README.md +++ b/README.md @@ -230,4 +230,12 @@ FUNCTIONS :returns: A corresponding Python object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types, and the gentle flag is not set. + + when_jvm_starts(f) + Registers a function to be called when the JVM starts (or immediately). + This is useful to defer construction of Java-dependent data structures + until the JVM is known to be available. If the JVM has already been + started, the function executes immediately. + + :param f: Function to invoke when scyjava.start_jvm() is called. ``` diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 1461b3c0..e8b43c20 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -12,6 +12,9 @@ # -- JVM setup -- +_callbacks = [] + + def start_jvm(options=scyjava.config.get_options()): """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can @@ -49,10 +52,10 @@ def start_jvm(options=scyjava.config.get_options()): ) jpype.addClassPath(os.path.join(workspace, '*')) - # Initialize JPype JVM + # initialize JPype JVM jpype.startJVM(*options) - # Grab needed Java classes. + # grab needed Java classes global Boolean; Boolean = jimport('java.lang.Boolean') global Byte; Byte = jimport('java.lang.Byte') global Character; Character = jimport('java.lang.Character') @@ -76,12 +79,34 @@ def start_jvm(options=scyjava.config.get_options()): global Map; Map = jimport('java.util.Map') global Set; Set = jimport('java.util.Set') + # invoke registered callback functions + for callback in _callbacks: + callback() + def jvm_started(): """Return true iff a Java virtual machine (JVM) has been started.""" return jpype.isJVMStarted() +def when_jvm_starts(f): + """ + Registers a function to be called when the JVM starts (or immediately). + This is useful to defer construction of Java-dependent data structures + until the JVM is known to be available. If the JVM has already been + started, the function executes immediately. + + :param f: Function to invoke when scyjava.start_jvm() is called. + """ + if jvm_started(): + # JVM was already started; invoke callback function immediately. + f() + else: + # Add function to the list of callbacks to invoke upon start_jvm(). + global _callbacks + _callbacks.append(f) + + # -- Python to Java -- # Adapted from code posted by vslotman on GitHub: diff --git a/setup.py b/setup.py index 0d4d6f71..efd12194 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.0.1.dev0', + version='1.1.0.dev0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 40e13f59768ff09eb908ea21d13f6791c6befd49 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 23:31:57 -0600 Subject: [PATCH 069/505] Release version 1.1.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index efd12194..670f5136 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.1.0.dev0', + version='1.1.0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 89e1aa9142b71abe2a33aab170894555127ae1b7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 29 Nov 2020 23:33:04 -0600 Subject: [PATCH 070/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 670f5136..6cded234 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.1.0', + version='1.1.1.dev0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From cbd3812592a69e37f048e363d09b51c676b8c544 Mon Sep 17 00:00:00 2001 From: Jan Eglinger Date: Mon, 26 Apr 2021 13:49:08 +0200 Subject: [PATCH 071/505] config.add_options(): handle string arguments --- scyjava/config/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index c9255506..6ddb7ec6 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -105,7 +105,10 @@ def add_option(option): def add_options(options): global _options - _options.extend(options) + if isinstance(options, str): + _options.append(options) + else: + _options.extend(options) def get_options(): From f033a100a23fcf8766bde813eeec753c12aa2b1c Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 29 Jul 2021 10:02:55 -0500 Subject: [PATCH 072/505] Add utility functions Added get_version method to return Java class versions. Added compare_version method to return booleans for version checking. If the passed Java class has a higher version than specified, True is returned. If the Java class has a lower version than specified, False is returned. These methods are needed to check the imagej-common version to support faster rai_to_numpy operations. --- scyjava/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index e8b43c20..922cf014 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -106,6 +106,23 @@ def when_jvm_starts(f): global _callbacks _callbacks.append(f) +# -- Utility functions -- + +def get_version(java_class): + """Return the version of a Java class. """ + VersionUtils = jimport('org.scijava.util.VersionUtils') + version = VersionUtils.getVersion(java_class) + return version + +def compare_version(version, java_class_version): + """ + Return a boolean on a version comparison. True is returned + if the Java class version is higher than the specified version. False + is returned if the specified version is higher than the Java class version. + """ + VersionUtils = jimport('org.scijava.util.VersionUtils') + comparison = VersionUtils.compare(version, java_class_version) < 0 + return comparison # -- Python to Java -- From a188eb7899600eaca274fa0b57ff0e971d883b62 Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Sat, 31 Jul 2021 13:39:50 +0200 Subject: [PATCH 073/505] Add configurable shortcuts to pass to jgo.resolve_dependencies --- scyjava/__init__.py | 3 ++- scyjava/config/__init__.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 922cf014..80da688a 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -48,7 +48,8 @@ def start_jvm(options=scyjava.config.get_options()): cache_dir=scyjava.config.get_cache_dir(), manage_dependencies=scyjava.config.get_manage_deps(), repositories=repositories, - verbose=scyjava.config.get_verbose() + verbose=scyjava.config.get_verbose(), + shortcuts=scyjava.config.get_shortcuts(), ) jpype.addClassPath(os.path.join(workspace, '*')) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 6ddb7ec6..1233b6ef 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -11,7 +11,7 @@ _cache_dir = pathlib.Path.home() / '.jgo' _m2_repo = pathlib.Path.home() / '.m2' / 'repository' _options = [] - +_shortcuts = {} def maven_scijava_repository(): """ @@ -114,3 +114,11 @@ def add_options(options): def get_options(): global _options return _options + + +def add_shortcut(k, v): + _shortcuts[k] = v + + +def get_shortcuts(): + return _shortcuts From 26b9cc7dc1c6e7f9f134f55dfa6bc1b5222028c5 Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Sat, 31 Jul 2021 13:40:20 +0200 Subject: [PATCH 074/505] Reduce diff --- scyjava/config/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 1233b6ef..1a19d887 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -13,6 +13,7 @@ _options = [] _shortcuts = {} + def maven_scijava_repository(): """ :return: url for public scijava maven repo From af46f7725b1790242a9fd3cf560fb308cabef139 Mon Sep 17 00:00:00 2001 From: Charles Tapley Hoyt Date: Sat, 31 Jul 2021 13:41:30 +0200 Subject: [PATCH 075/505] Add global I don't think this is strictly necessary since it's not a primitive datatype, but I'm including this just to keep consistent --- scyjava/config/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 1a19d887..efb9d163 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -118,8 +118,10 @@ def get_options(): def add_shortcut(k, v): + global _shortcuts _shortcuts[k] = v def get_shortcuts(): + global _shortcuts return _shortcuts From 57559789a2a9a1527ba36a2ab038fdef26cfef00 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 15 Sep 2021 09:44:21 -0500 Subject: [PATCH 076/505] Fix type check on org.scijava.table.Table Scijava tables failed this type check when checking against the java class object. Instead the check should be done against the java class. Changing jclass to jimport resolves the error: TypeError: isinstance() arg 2 must be a type or tuple of types. Tables are then properly converted into a pandas DataFrame. --- scyjava/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 922cf014..043fcd3a 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -490,9 +490,9 @@ def to_python(data, gentle=False): return float(data.toString()) if isinstance(data, String): return str(data) - + try: - if isinstance(data, jclass('org.scijava.table.Table')): + if isinstance(data, jimport('org.scijava.table.Table')): return _table_to_pandas(data) except: # No worries if scijava-table is not available. @@ -527,6 +527,7 @@ def _import_pandas(): def _table_to_pandas(table): + breakpoint() pd = _import_pandas() data = [] From f2999494b56b124a91e0cfb63ea422302df31b63 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 15 Sep 2021 09:50:07 -0500 Subject: [PATCH 077/505] Remove debugging breakpoint Accidently left a breakpoint in scyjava! Oops! --- scyjava/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 043fcd3a..1c9c081c 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -527,7 +527,6 @@ def _import_pandas(): def _table_to_pandas(table): - breakpoint() pd = _import_pandas() data = [] From cb4973524e4a4bca4f9c444c8c564129c50f4ade Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 15 Sep 2021 10:06:40 -0500 Subject: [PATCH 078/505] Convert table header java strings to python strings When setting the pandas dataframe columns to the headers from the table, the java.lang.String headers are converted into individual chars like this: df.columns Index([('A', 'r', 'e', 'a'), ('M', 'e', 'a', 'n'), ('M', 'i', 'n'), ('M', 'a', 'x')], dtype='object') Converting them to python strings before adding them to the dataframe resolves this bug. --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 1c9c081c..6a36d90f 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -533,7 +533,7 @@ def _table_to_pandas(table): headers = [] for i, column in enumerate(table.toArray()): data.append(column.toArray()) - headers.append(table.getColumnHeader(i)) + headers.append(str(table.getColumnHeader(i))) df = pd.DataFrame(data).T df.columns = headers return df From a688dd529f96922c7d641abf87c29cc693352050 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 15 Sep 2021 16:00:42 -0500 Subject: [PATCH 079/505] Fix default repositories We want to use the public group, not the releases repo, by default. And eliminate redundant maven_scijava_repository function. It is already defined in jgo; we can just import it. --- scyjava/config/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 6ddb7ec6..30133766 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -1,11 +1,12 @@ import logging import pathlib import jpype +from jgo import maven_scijava_repository _logger = logging.getLogger(__name__) _endpoints = [] -_repositories = {1: 'https://maven.scijava.org/content/repositories/releases'} +_repositories = {'scijava.public': maven_scijava_repository()} _verbose = 0 _manage_deps = True _cache_dir = pathlib.Path.home() / '.jgo' @@ -13,13 +14,6 @@ _options = [] -def maven_scijava_repository(): - """ - :return: url for public scijava maven repo - """ - return 'https://maven.scijava.org/content/groups/public' - - def add_endpoints(*endpoints): global _endpoints _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) From f1430f14906fd9df3f902c790dd6c81258ffe9d2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 19 Sep 2021 21:55:08 -0500 Subject: [PATCH 080/505] Bump minor version digit New functions were added with commit f033a100a23fcf8766bde813eeec753c12aa2b1c. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6cded234..1ed5fbbc 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.1.1.dev0', + version='1.2.0.dev0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 4590a691e8d456d439292ce5f0e5c25e715c489d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 19 Sep 2021 21:55:57 -0500 Subject: [PATCH 081/505] Release version 1.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ed5fbbc..546b2a9f 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.2.0.dev0', + version='1.2.0', author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 0abacca90440316d0673c9059f14c20b9f176501 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 19 Sep 2021 21:56:02 -0500 Subject: [PATCH 082/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 546b2a9f..dc0e42dc 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version='1.2.0', + version="1.2.1.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 8a60294c4beb20c6a38ba11135d8b7a623c1f9e7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 19 Sep 2021 22:00:58 -0500 Subject: [PATCH 083/505] Tell git to ignore .eggs directory --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 38a533bc..03f85b89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -__pycache__/ +/.eggs/ /build/ /dist/ /scyjava.egg-info/ +__pycache__/ From 0f48a872f3220e785d1cf5892ef22bd2beb74e5a Mon Sep 17 00:00:00 2001 From: hinerm Date: Mon, 20 Sep 2021 12:30:02 -0500 Subject: [PATCH 084/505] Make scyjava.config.endpoint public Switched to public field naming convention and deprecated setter/getter. Bumps next version to 1.3.0. Closes #32 --- scyjava/config/__init__.py | 24 +++++++++++++++++------- setup.py | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index 30133766..cb9609d3 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -5,7 +5,7 @@ _logger = logging.getLogger(__name__) -_endpoints = [] +endpoints = [] _repositories = {'scijava.public': maven_scijava_repository()} _verbose = 0 _manage_deps = True @@ -14,15 +14,25 @@ _options = [] -def add_endpoints(*endpoints): - global _endpoints - _logger.debug('Adding endpoints %s to %s', endpoints, _endpoints) - _endpoints.extend(endpoints) +def add_endpoints(*new_endpoints): + """ + DEPRECATED since v1.2.1 + Please modify the endpoints field directly instead. + """ + _logger.warn('Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead.') + global endpoints + _logger.debug('Adding endpoints %s to %s', new_endpoints, endpoints) + endpoints.extend(new_endpoints) def get_endpoints(): - global _endpoints - return _endpoints + """ + DEPRECATED since v1.2.1 + Please access the endpoints field directly instead. + """ + _logger.warn('Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead.') + global endpoints + return endpoints def add_repositories(*args, **kwargs): diff --git a/setup.py b/setup.py index dc0e42dc..980455e7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.2.1.dev0", + version="1.3.0.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From 257df07ae2a3e9c4e9f99c6ce4636542cb0376db Mon Sep 17 00:00:00 2001 From: hinerm Date: Mon, 20 Sep 2021 14:19:24 -0500 Subject: [PATCH 085/505] Release version 1.3.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 980455e7..5021eb7b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.3.0.dev0", + version="1.3.0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From a02cbdd822c140435b68995f90f8378a93c6a490 Mon Sep 17 00:00:00 2001 From: hinerm Date: Mon, 20 Sep 2021 14:19:25 -0500 Subject: [PATCH 086/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5021eb7b..8fe5dbac 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.3.0", + version="1.3.1.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', author_email='ctrueden@wisc.edu', description='scyjava', From f85fe8282e5a75f7f8f146b09d9aa6dac2e97a7b Mon Sep 17 00:00:00 2001 From: hinerm Date: Mon, 20 Sep 2021 15:43:46 -0500 Subject: [PATCH 087/505] Add Mark Hiner as author --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8fe5dbac..5f8fbd19 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ python_requires='>=3', packages=find_packages(), version="1.3.1.dev0", - author='Curtis Rueden, Philipp Hanslovsky, Edward Evans', + author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', long_description=scyjava_long_description, From 642e89a167554b8effc15093dbd026e6cb4e4a3a Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 23 Sep 2021 11:21:42 -0500 Subject: [PATCH 088/505] Remove deprecated get_endpoints use --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 6a36d90f..c162e8fc 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -32,7 +32,7 @@ def start_jvm(options=scyjava.config.get_options()): return # retrieve endpoint and repositories from scyjava config - endpoints = scyjava.config.get_endpoints() + endpoints = scyjava.config.endpoints repositories = scyjava.config.get_repositories() # use the logger to notify user that endpoints are being added From dafb8d69e1f70b61816a29e2ef9bdb3f9a00953a Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 23 Sep 2021 11:22:08 -0500 Subject: [PATCH 089/505] Release version 1.3.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f8fbd19..64e6941f 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.3.1.dev0", + version="1.3.1", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From fcfa139d1ea550ad6bc789736795fdc72f3b4d0c Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 23 Sep 2021 11:22:08 -0500 Subject: [PATCH 090/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 64e6941f..01a01046 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.3.1", + version="1.3.2.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 0850abeaf94e82dd702ce14d902b69804a7bbb3d Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 09:00:58 -0600 Subject: [PATCH 091/505] Add GitHub Action CI --- .github/workflows/python-test-conda.yml | 38 +++++++++++++++++++++++++ environment-test.yml | 11 +++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/workflows/python-test-conda.yml create mode 100644 environment-test.yml diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml new file mode 100644 index 00000000..97cb190e --- /dev/null +++ b/.github/workflows/python-test-conda.yml @@ -0,0 +1,38 @@ +name: build + +on: + push: + branches: + - master + tags: + - "*-[0-9]+.*" + pull_request: + branches: + - master + +jobs: + build-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Add conda to system path + run: | + # $CONDA is an environment variable pointing to the root of the miniconda directory + echo $CONDA/bin >> $GITHUB_PATH + - name: Install mamba + run: | + conda install -c conda-forge mamba + - name: Install dependencies + run: | + mamba env update --file environment-test.yml --name base + - name: Install primary project + run: | + pip install -e . + - name: Install pytest + run: | + mamba install -c conda-forge pytest + - name: Test with pytest + run: | + pytest + diff --git a/environment-test.yml b/environment-test.yml new file mode 100644 index 00000000..cdaeb0b2 --- /dev/null +++ b/environment-test.yml @@ -0,0 +1,11 @@ +# Use this environment file when running the tests, when scyjava will be +# installed from source +name: scyjava +channels: + - conda-forge + - defaults +dependencies: + - jpype1 + - jgo + - numpy + - pandas From c0b67ca5401d78d3c6f10d8da2d945030aa80bcd Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 09:02:56 -0600 Subject: [PATCH 092/505] Add build status badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3b4142d2..67cda2b4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![build status](https://github.com/hinerm/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/hinerm/scyjava/actions/workflows/python-test-conda.yml) + Supercharged Java access from Python. Built on [JPype](https://jpype.readthedocs.io/en/latest/) From eb58398c21d4768463018fd7b38e5d59e9cfdc2f Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 09:05:14 -0600 Subject: [PATCH 093/505] Add environment.yml --- environment.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..30929762 --- /dev/null +++ b/environment.yml @@ -0,0 +1,8 @@ +name: scyjava +channels: + - conda-forge + - defaults +dependencies: + - jpype1 + - jgo + - scyjava From ecd7784ebe65b27cc5a9f72b9b08fab85d4e8305 Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 09:05:28 -0600 Subject: [PATCH 094/505] gitignore swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 03f85b89..8178177a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.swp /.eggs/ /build/ /dist/ From 5be04750462f9533a5abd2110da2f3241a3b6a58 Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 09:18:37 -0600 Subject: [PATCH 095/505] GH action config: tweak naming --- .github/workflows/python-test-conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index 97cb190e..2988e9e8 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | mamba env update --file environment-test.yml --name base - - name: Install primary project + - name: Install current project in dev mode run: | pip install -e . - name: Install pytest From f627dd95a51430a8bbbb028c87e2114d9b72e03c Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 18 Nov 2021 10:45:00 -0600 Subject: [PATCH 096/505] Fix build status badge URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67cda2b4..01043e00 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![build status](https://github.com/hinerm/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/hinerm/scyjava/actions/workflows/python-test-conda.yml) +[![build status](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml) Supercharged Java access from Python. From 61d000789fd9d4c76132e55375b38693fe6707c9 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 7 Dec 2021 12:11:34 -0600 Subject: [PATCH 097/505] Improve JVM shutdown We are overriding JPype's builtin exit handling with our own so that we can provide an extensible callback mechanism for downstream uses. In this way consumers can ensure that specific things happen before the JVM shutsdown. We also catch ^C keyboard interrupt, so termination occurs even with the interactive shell. --- scyjava/__init__.py | 52 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index c162e8fc..bf0006d6 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,9 +1,11 @@ +import atexit import collections.abc import jgo import jpype import logging import os import scyjava.config +import jpype.config from jpype.types import * from _jpype import _JObject @@ -12,7 +14,8 @@ # -- JVM setup -- -_callbacks = [] +_startup_callbacks = [] +_shutdown_callbacks = [] def start_jvm(options=scyjava.config.get_options()): @@ -53,7 +56,12 @@ def start_jvm(options=scyjava.config.get_options()): jpype.addClassPath(os.path.join(workspace, '*')) # initialize JPype JVM - jpype.startJVM(*options) + jpype.startJVM(*options, interrupt=True) + + # replace JPype/JVM shutdown handling with our own + jpype.config.onexit = False + jpype.config.free_resources = False + atexit.register(shutdown_jvm) # grab needed Java classes global Boolean; Boolean = jimport('java.lang.Boolean') @@ -80,9 +88,30 @@ def start_jvm(options=scyjava.config.get_options()): global Set; Set = jimport('java.util.Set') # invoke registered callback functions - for callback in _callbacks: + for callback in _startup_callbacks: callback() +def shutdown_jvm(): + """Shutdown the JVM. + + Shutdown the JVM. Set the jpype .config.destroy_jvm flag to true + to ask JPype to destory the JVM itself. Note that enabling + jpype.config.destroy_jvm can lead to delayed shutdown times while + the JVM is waiting for threads to finish. + """ + # invoke registered shutdown callback functions + for callback in _shutdown_callbacks: + try: + callback() + except Exception as e: + print(f"Exception during shutdown callback: {e}") + + + # okay to shutdown JVM + try: + jpype.shutdownJVM() + except Exception as e: + print(f"Exception during JVM shutdown: {e}") def jvm_started(): """Return true iff a Java virtual machine (JVM) has been started.""" @@ -103,8 +132,21 @@ def when_jvm_starts(f): f() else: # Add function to the list of callbacks to invoke upon start_jvm(). - global _callbacks - _callbacks.append(f) + global _startup_callbacks + _startup_callbacks.append(f) + +def when_jvm_stops(f): + """ + Registers a function to be called when the JVM starts (or immediately). + This is useful to defer construction of Java-dependent data structures + until the JVM is known to be available. If the JVM has already been + started, the function executes immediately. + + :param f: Function to invoke when scyjava.start_jvm() is called. + """ + global _shutdown_callbacks + _shutdown_callbacks.append(f) + # -- Utility functions -- From 132a650964a46dae1ce5f0cc13797dd7215ce88a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Dec 2021 12:30:06 -0600 Subject: [PATCH 098/505] Improve scyjava.config.add_classpath * Fix a bug when more than one classpath element is given. * Add a new find_jars function for recursive JAR discovery. --- scyjava/config/__init__.py | 44 +++++++++++++++++++++++++++++++++++++- setup.py | 2 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index cb9609d3..ce9bb954 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -1,4 +1,5 @@ import logging +import os import pathlib import jpype from jgo import maven_scijava_repository @@ -95,7 +96,48 @@ def get_m2_repo(): def add_classpath(*path): - jpype.addClassPath(*path) + """ + Add elements to the Java class path. + + See also find_jars, which can be combined with add_classpath to + add all the JARs beneath a given directory to the class path, a la: + + add_classpath(*find_jars('/path/to/folder-of-jars')) + + :param path: + One or more file paths to add to the Java class path. + + A valid Java class path element is typically either a .jar file or a + directory. When a class needs to be loaded, the Java runtime looks + beneath each class path element for the .class file, nested in a folder + structure matching the class's package name. For example, when loading + a class foo.bar.Fubar, if a directory /home/jdoe/classes is included as + a class path element, the class file at + /home/jdoe/classes/foo/bar/Fubar.class will be used. It works the same + for JAR files, except that the class files are loaded from the + directory structure inside the JAR; in this example, a JAR file + /home/jdoe/jars/fubar.jar on the class path containing file + foo/bar/Fubar.class inside would be another way to provide the class + foo.bar.Fubar. + """ + for p in path: + jpype.addClassPath(p) + + +def find_jars(directory): + """ + Find .jar files beneath a given directory. + + :param directory: the folder to be searched + :return: a list of JAR files + """ + jars = [] + for root, dirs, files in os.walk(directory): + for f in files: + if f.lower().endswith('.jar'): + path = os.path.join(root, f) + jars.append(path) + return jars def get_classpath(): diff --git a/setup.py b/setup.py index 01a01046..80c327b8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.3.2.dev0", + version="1.4.0.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 050ac3cb4e301baf3f57eeba78bc1530fd9ee77c Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 7 Dec 2021 12:22:36 -0600 Subject: [PATCH 099/505] Fix deprecated logging usages --- scyjava/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scyjava/config/__init__.py b/scyjava/config/__init__.py index ce9bb954..15e38efc 100644 --- a/scyjava/config/__init__.py +++ b/scyjava/config/__init__.py @@ -20,7 +20,7 @@ def add_endpoints(*new_endpoints): DEPRECATED since v1.2.1 Please modify the endpoints field directly instead. """ - _logger.warn('Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead.') + _logger.warning('Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead.') global endpoints _logger.debug('Adding endpoints %s to %s', new_endpoints, endpoints) endpoints.extend(new_endpoints) @@ -31,7 +31,7 @@ def get_endpoints(): DEPRECATED since v1.2.1 Please access the endpoints field directly instead. """ - _logger.warn('Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead.') + _logger.warning('Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead.') global endpoints return endpoints From 860517470fa1709d75f59a270332475e8004c141 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 7 Dec 2021 12:26:19 -0600 Subject: [PATCH 100/505] Fix deprecated add endpoints usage --- README.md | 4 ++-- tests/test_convert.py | 2 +- tests/test_pandas.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 01043e00..e8e7027d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ for all the gritty details on how this wrapping works. >>> sys.version_info sys.version_info(major=3, minor=8, micro=5, releaselevel='final', serial=0) >>> from scyjava import config, jimport ->>> config.add_endpoints('org.python:jython-slim:2.7.2') +>>> config.endpoints.append('org.python:jython-slim:2.7.2') >>> jython = jimport('org.python.util.jython') >>> jython.main([]) Jython 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:12:24) @@ -55,7 +55,7 @@ u'1.8.0_152-release' ```python >>> from scyjava import config, jimport >>> config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) ->>> config.add_endpoints('net.imagej:imagej:2.1.0') +>>> config.endpoints.append('net.imagej:imagej:2.1.0') >>> ImageJ = jimport('net.imagej.ImageJ') >>> ij = ImageJ() >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" diff --git a/tests/test_convert.py b/tests/test_convert.py index 985cff84..7091354c 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,7 +1,7 @@ import unittest from scyjava import config, jclass, jimport, to_java, to_python -config.add_endpoints('org.scijava:scijava-table') +config.endpoints.append('org.scijava:scijava-table') config.add_option('-Djava.awt.headless=true') def assert_same_table(table, df): diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 1b04421b..9c384805 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -4,7 +4,7 @@ import unittest from scyjava import config, jimport, to_java -config.add_endpoints('org.scijava:scijava-table') +config.endpoints.append('org.scijava:scijava-table') config.add_option('-Djava.awt.headless=true') From 59bd7f1e7974d52eacabd47e48c7b481ff28e5dc Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 7 Dec 2021 12:29:28 -0600 Subject: [PATCH 101/505] Release version 1.4.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 80c327b8..67194b29 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.4.0.dev0", + version="1.4.0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 687a1f31f1e7583e5a79c5f7c32be9aa877dba30 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 7 Dec 2021 12:46:39 -0600 Subject: [PATCH 102/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 67194b29..623c7dc8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.4.0", + version="1.4.1.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 7622dc31ca9d4acfd88687a500e42e9d6b6c8248 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 8 Dec 2021 16:03:03 -0600 Subject: [PATCH 103/505] Require jpype 1.3.0 or later The jpype.config logic was introduced in 1.3.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 623c7dc8..44d5b287 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ long_description_content_type='text/markdown', license='Public domain', url='https://github.com/scijava/scyjava', - install_requires=['jpype1', 'jgo'], + install_requires=['jpype1 >= 1.3.0', 'jgo'], ) From c5d8ac1a10e527db392d02a1ffef0f29fb5c021f Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 14 Dec 2021 09:58:47 -0600 Subject: [PATCH 104/505] Ensure remaining windows are cleaned up On other platforms (like Windows) ij.dispose() does not close all windows and some hidden windows remain which causes a delay in the JVM shutdown. --- scyjava/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index bf0006d6..90a5b634 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -106,6 +106,10 @@ def shutdown_jvm(): except Exception as e: print(f"Exception during shutdown callback: {e}") + # clean up remaining awt windows + Window = jimport('java.awt.Window') + for w in Window.getWindows(): + w.dispose() # okay to shutdown JVM try: From 0e46ae59630c54915d2839f9a33a9491914b5c8a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Feb 2022 08:46:43 -0600 Subject: [PATCH 105/505] Fix usage of dash-separated 'description-file' It is deprecated in favor of 'description_file'. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 12871ff0..b90f6ada 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file=README.md +description_file=README.md From 962e3111dcbb3d8df5821e8e4eeed29c549874f1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Feb 2022 08:49:47 -0600 Subject: [PATCH 106/505] Release version 1.4.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 44d5b287..53091d95 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.4.1.dev0", + version="1.4.1", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 886016d16012f30b32f9415e5daddb9cf6efb6d6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Feb 2022 08:56:02 -0600 Subject: [PATCH 107/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 53091d95..c715ed65 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3', packages=find_packages(), - version="1.4.1", + version="1.4.2.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From b733a4bea6041e19ccb3adec9b92ce8e7f16e1d9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Feb 2022 15:32:52 -0600 Subject: [PATCH 108/505] Require Python 3.6 or later --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c715ed65..d31318ea 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='scyjava', - python_requires='>=3', + python_requires='>=3.6', packages=find_packages(), version="1.4.2.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', From 0b60eac908562d18789d058c42f63fbd19665d5e Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 10 Feb 2022 13:21:34 -0600 Subject: [PATCH 109/505] Add converter infrastructure --- scyjava/__init__.py | 225 ++++++++++++++++++++++++++++++++---------- tests/test_convert.py | 36 +++++++ 2 files changed, 210 insertions(+), 51 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 90a5b634..2bf9c697 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,5 +1,8 @@ import atexit import collections.abc +import traceback +from typing import Any, Callable, NamedTuple +import typing import jgo import jpype import logging @@ -241,7 +244,157 @@ def jstacktrace(exc): return '' -def to_java(data): +class Converter(NamedTuple): + predicate: Callable[[Any], bool] + converter: Callable[[Any], Any] + priority: float + + +def _stock_java_converters() -> typing.List[Converter]: + return [ + # Other (Exceptional) converter + Converter( + predicate=lambda obj: True, + converter=_raise_type_exception, + priority=-10001 + ), + # NoneType converter + Converter( + predicate=lambda obj: obj is None, + converter=lambda obj: None, + priority=10001 + ), + # Java identity converter + Converter( + predicate=isjava, + converter=lambda obj: obj, + priority=10000 + ), + # String converter + Converter( + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: String(obj.encode('utf-8'), 'utf-8'), + priority=0 + ), + # Boolean converter + Converter( + predicate=lambda obj: isinstance(obj, bool), + converter=Boolean, + priority=0 + ), + # Integer converter + Converter( + predicate=lambda obj: isinstance(obj, int) and obj <= Integer.MAX_VALUE and obj >= Integer.MIN_VALUE, + converter= Integer, + priority=0 + ), + # Long converter + Converter( + predicate=lambda obj: isinstance(obj, int) and obj <= Long.MAX_VALUE, + converter=Long, + priority=-1 + ), + # BigInteger converter + Converter( + predicate=lambda obj: isinstance(obj, int), + converter=lambda obj: BigInteger(str(obj)), + priority=-2 + ), + # Float converter + Converter( + predicate=lambda obj: isinstance(obj, float) and obj <= Float.MAX_VALUE and obj >= Float.MIN_VALUE, + converter= Float, + priority=0 + ), + # Double converter + Converter( + predicate=lambda obj: isinstance(obj, float) and obj <= Double.MAX_VALUE and obj >= Float.MIN_VALUE, + converter=Double, + priority=-1 + ), + # BigDecimal converter + Converter( + predicate=lambda obj: isinstance(obj, float), + converter=lambda obj: BigDecimal(str(obj)), + priority=-2 + ), + # Pandas table converter + Converter( + predicate=lambda obj: type(obj).__name__ == 'DataFrame', + converter=_pandas_to_table, + priority=1 + ), + # Mapping converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Mapping), + converter=convertMap, + priority=0 + ), + # Set converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Set), + converter=convertSet, + priority=0 + ), + # Iterable converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Iterable), + converter=convertIterable, + priority=-1 + ), + ] + + +def _raise_type_exception(obj: Any): + raise TypeError('Unsupported type: ' + str(type(obj))) + + +def convertMap(obj: collections.abc.Mapping): + jmap = LinkedHashMap() + for k, v in obj.items(): + jk = to_java(k) + jv = to_java(v) + jmap.put(jk, jv) + return jmap + + +def convertSet(obj: collections.abc.Set): + jset = LinkedHashSet() + for item in obj: + jitem = to_java(item) + jset.add(jitem) + return jset + + +def convertIterable(obj: collections.abc.Iterable): + jlist = ArrayList() + for item in obj: + jitem = to_java(item) + jlist.add(jitem) + return jlist + + +def _stock_py_converters() -> typing.List[Converter]: + return [ + ## String converter + Converter( + lambda obj: isinstance(obj, String), + lambda obj: str(obj), + 0), + ] + +# TODO: Consider priority aliases? +java_converters : typing.List[Converter] = [] +when_jvm_starts( + lambda : [_add_converter(c, java_converters) for c in _stock_java_converters()] +) + +py_converters : typing.List[Converter] = [] +when_jvm_starts( + lambda : [_add_converter(c, py_converters) for c in _stock_py_converters()] +) + +def to_java(obj: Any): """ Recursively convert a Python object to a Java object. :param data: The Python object to convert. @@ -257,62 +410,32 @@ def to_java(data): :raises TypeError: if the argument is not one of the aforementioned types. """ start_jvm() + return _convert(obj, java_converters) - if data is None: - return None - if isjava(data): - return data +def convert_to_python(obj: Any): + start_jvm() + return _convert(obj, py_converters) - if isinstance(data, str): - return String(data.encode('utf-8'), 'utf-8') - if isinstance(data, bool): - return Boolean(data) +def _convert(obj: Any, converters: typing.List[Converter]) -> Any: + suitable_converters = filter(lambda c: c.predicate(obj), converters) + prioritized = max(suitable_converters, key = lambda c: c.priority) + return prioritized.converter(obj) - if isinstance(data, int): - if data <= Integer.MAX_VALUE: - return Integer(data) - elif data <= Long.MAX_VALUE: - return Long(data) - else: - return BigInteger(str(data)) - if isinstance(data, float): - if data <= Float.MAX_VALUE: - return Float(data) - elif data <= Double.MAX_VALUE: - return Double(data) - else: - return BigDecimal(str(data)) - - # Trying to get the type without importing Pandas. - if type(data).__name__ == 'DataFrame': - return _pandas_to_table(data) - - if isinstance(data, collections.abc.Mapping): - jmap = LinkedHashMap() - for k, v in data.items(): - jk = to_java(k) - jv = to_java(v) - jmap.put(jk, jv) - return jmap - - if isinstance(data, collections.abc.Set): - jset = LinkedHashSet() - for item in data: - jitem = to_java(item) - jset.add(jitem) - return jset - - if isinstance(data, collections.abc.Iterable): - jlist = ArrayList() - for item in data: - jitem = to_java(item) - jlist.add(jitem) - return jlist - - raise TypeError('Unsupported type: ' + str(type(data))) +def _add_converter(converter: Converter, converters: typing.List[Converter]): + converters.append(converter) + + +def add_java_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + c = Converter(predicate, converter, priority) + _add_converter(c, java_converters) + + +def add_py_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + c = Converter(predicate, converter, priority) + _add_converter(c, py_converters) # -- Java to Python -- diff --git a/tests/test_convert.py b/tests/test_convert.py index 7091354c..216f096f 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -76,6 +76,23 @@ def testBigInteger(self): self.assertEqual(bi, pbi) self.assertEqual(str(bi), str(pbi)) + def testFloat(self): + f = 5. + jf = to_java(f) + self.assertEqual(f, jf.floatValue()) + pf = to_python(jf) + self.assertEqual(f, pf) + self.assertEqual(str(f), str(pf)) + + def testDouble(self): + Float = jimport('java.lang.Float') + d = Float.MAX_VALUE * 2 + jd = to_java(d) + self.assertEqual(d, jd.doubleValue()) + pd = to_python(jd) + self.assertEqual(d, pd) + self.assertEqual(str(d), str(pd)) + def testString(self): s = 'Hello world!' js = to_java(s) @@ -208,5 +225,24 @@ def testStructureWithSomeUnsupportedItems(self): self.assertEqual(pdict['foo'], 'bar') + def test_conversion_priority(self): + # Add a converter prioritized over the default converter + String = jimport('java.lang.String') + invader = 'Not Hello World' + + from scyjava import add_java_converter + add_java_converter( + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: String(invader.encode('utf-8'), 'utf-8'), + priority=100 + ) + + # Ensure that the conversion uses our new converter + s = 'Hello world!' + js = to_java(s) + for e, a in zip(invader, js.toCharArray()): + self.assertEqual(e, a) + + if __name__ == '__main__': unittest.main() From 3e6d0e36d8d938ad677312b83f6006307e8a1eaa Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 10 Feb 2022 14:57:20 -0600 Subject: [PATCH 110/505] Replace to_python --- scyjava/__init__.py | 252 +++++++++++++++++++++++++++++++------------- 1 file changed, 176 insertions(+), 76 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 2bf9c697..3accb82c 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -345,6 +345,176 @@ def _stock_java_converters() -> typing.List[Converter]: ] +def _stock_py_converters() -> typing.List: + return [ + # Other (Exceptional) converter + Converter( + predicate=lambda obj: True, + converter=_raise_type_exception, + priority=-10001 + ), + # Java identity converter + Converter( + predicate=lambda obj: not isjava(obj), + converter=lambda obj: obj, + priority=10000 + ), + # JBoolean converter + Converter( + predicate=lambda obj: isinstance(obj, JBoolean), + converter=bool, + priority=1 + ), + # JInt/JLong/JShort converter + Converter( + predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), + converter=int, + priority=1 + ), + # JDouble/JFloat converter + Converter( + predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), + converter=float, + priority=1 + ), + # JChar converter + Converter( + predicate=lambda obj: isinstance(obj, JChar), + converter=str, + priority=1 + ), + # Boolean converter + Converter( + predicate=lambda obj: isinstance(obj, Boolean), + converter=lambda obj: obj.booleanValue(), + priority=0 + ), + # Byte converter + Converter( + predicate=lambda obj: isinstance(obj, Byte), + converter=lambda obj: obj.byteValue(), + priority=0 + ), + # Char converter + Converter( + predicate=lambda obj: isinstance(obj, Character), + converter=lambda obj: obj.toString(), + priority=0 + ), + # Double converter + Converter( + predicate=lambda obj: isinstance(obj, Double), + converter=lambda obj: obj.doubleValue(), + priority=0 + ), + # Float converter + Converter( + predicate=lambda obj: isinstance(obj, Float), + converter=lambda obj: obj.floatValue(), + priority=0 + ), + # Integer converter + Converter( + predicate=lambda obj: isinstance(obj, Integer), + converter=lambda obj: obj.intValue(), + priority=0 + ), + # Long converter + Converter( + predicate=lambda obj: isinstance(obj, Long), + converter=lambda obj: obj.longValue(), + priority=0 + ), + # Short converter + Converter( + predicate=lambda obj: isinstance(obj, Short), + converter=lambda obj: obj.shortValue(), + priority=0 + ), + # Void converter + Converter( + predicate=lambda obj: isinstance(obj, Void), + converter=lambda obj: None, + priority=0 + ), + # String converter + Converter( + predicate=lambda obj: isinstance(obj, String), + converter=lambda obj: str(obj), + priority=0 + ), + # BigInteger converter + Converter( + predicate=lambda obj: isinstance(obj, BigInteger), + converter=lambda obj: int(str(obj.toString())), + priority=0 + ), + # BigDecimal converter + Converter( + predicate=lambda obj: isinstance(obj, BigDecimal), + converter=lambda obj: float(obj.toString), + priority=0 + ), + # SciJava Table converter + Converter( + predicate=_is_table, + converter=_convert_table, + priority=0 + ), + # List converter + Converter( + predicate=lambda obj: isinstance(obj, List), + converter=JavaList, + priority=0 + ), + # Map converter + Converter( + predicate=lambda obj: isinstance(obj, Map), + converter=JavaMap, + priority=0 + ), + # Set converter + Converter( + predicate=lambda obj: isinstance(obj, Set), + converter=JavaSet, + priority=0 + ), + # Collection converter + Converter( + predicate=lambda obj: isinstance(obj, Collection), + converter=JavaCollection, + priority=-1 + ), + # Iterable converter + Converter( + predicate=lambda obj: isinstance(obj, Iterable), + converter=JavaIterable, + priority=-1 + ), + # Iterator converter + Converter( + predicate=lambda obj: isinstance(obj, Iterator), + converter=JavaIterator, + priority=-1 + ), + ] + + +def _is_table(obj: Any): + try: + return isinstance(obj, jimport('org.scijava.table.Table')) + except: + # No worries if scijava-table is not available. + pass + +def _convert_table(obj: Any): + try: + return _table_to_pandas(obj) + except: + # No worries if scijava-table is not available. + pass + + def _raise_type_exception(obj: Any): raise TypeError('Unsupported type: ' + str(type(obj))) @@ -374,15 +544,6 @@ def convertIterable(obj: collections.abc.Iterable): return jlist -def _stock_py_converters() -> typing.List[Converter]: - return [ - ## String converter - Converter( - lambda obj: isinstance(obj, String), - lambda obj: str(obj), - 0), - ] - # TODO: Consider priority aliases? java_converters : typing.List[Converter] = [] when_jvm_starts( @@ -394,7 +555,7 @@ def _stock_py_converters() -> typing.List[Converter]: lambda : [_add_converter(c, py_converters) for c in _stock_py_converters()] ) -def to_java(obj: Any): +def to_java(obj: Any) -> Any: """ Recursively convert a Python object to a Java object. :param data: The Python object to convert. @@ -413,11 +574,6 @@ def to_java(obj: Any): return _convert(obj, java_converters) -def convert_to_python(obj: Any): - start_jvm() - return _convert(obj, py_converters) - - def _convert(obj: Any, converters: typing.List[Converter]) -> Any: suitable_converters = filter(lambda c: c.predicate(obj), converters) prioritized = max(suitable_converters, key = lambda c: c.priority) @@ -599,7 +755,7 @@ def __str__(self): return '{' + ', '.join(_jstr(v) for v in self) + '}' -def to_python(data, gentle=False): +def to_python(data: Any, gentle: bool =False) -> Any: """ Recursively convert a Java object to a Python object. :param data: The Java object to convert. @@ -621,68 +777,12 @@ def to_python(data, gentle=False): and the gentle flag is not set. """ start_jvm() - - if not isjava(data): - return data - - if isinstance(data, JBoolean): - return bool(data) - if isinstance(data, JInt) or isinstance(data, JLong) or isinstance(data, JShort): - return int(data) - if isinstance(data, JDouble) or isinstance(data, JFloat): - return float(data) - if isinstance(data, JChar): - return str(data) - - if isinstance(data, Boolean): - return data.booleanValue() - if isinstance(data, Byte): - return data.byteValue() - if isinstance(data, Character): - return data.toString() - if isinstance(data, Double): - return data.doubleValue() - if isinstance(data, Float): - return data.floatValue() - if isinstance(data, Integer): - return data.intValue() - if isinstance(data, Long): - return data.longValue() - if isinstance(data, Short): - return data.shortValue() - if isinstance(data, Void): - return None - - if isinstance(data, BigInteger): - return int(str(data.toString())) - if isinstance(data, BigDecimal): - return float(data.toString()) - if isinstance(data, String): - return str(data) - try: - if isinstance(data, jimport('org.scijava.table.Table')): - return _table_to_pandas(data) - except: - # No worries if scijava-table is not available. - pass + return _convert(data, py_converters) + except TypeError as exc: + if gentle: return data + raise exc - if isinstance(data, List): - return JavaList(data) - if isinstance(data, Map): - return JavaMap(data) - if isinstance(data, Set): - return JavaSet(data) - if isinstance(data, Collection): - return JavaCollection(data) - if isinstance(data, Iterable): - return JavaIterable(data) - if isinstance(data, Iterator): - return JavaIterator(data) - - if gentle: - return data - raise TypeError('Unsupported data type: ' + str(type(data))) def _import_pandas(): From 8f3669101ff482d8809713aca8049a778ff306e2 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 10 Feb 2022 15:08:21 -0600 Subject: [PATCH 111/505] Do some cleaning up --- scyjava/__init__.py | 504 ++++++++++++++++++++++---------------------- 1 file changed, 256 insertions(+), 248 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 3accb82c..1c32ee05 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -173,6 +173,25 @@ def compare_version(version, java_class_version): comparison = VersionUtils.compare(version, java_class_version) < 0 return comparison +# -- Type Conversion Utilities -- + + +class Converter(NamedTuple): + predicate: Callable[[Any], bool] + converter: Callable[[Any], Any] + priority: float + + +def _convert(obj: Any, converters: typing.List[Converter]) -> Any: + suitable_converters = filter(lambda c: c.predicate(obj), converters) + prioritized = max(suitable_converters, key = lambda c: c.priority) + return prioritized.converter(obj) + + +def _add_converter(converter: Converter, converters: typing.List[Converter]): + converters.append(converter) + + # -- Python to Java -- # Adapted from code posted by vslotman on GitHub: @@ -244,10 +263,60 @@ def jstacktrace(exc): return '' -class Converter(NamedTuple): - predicate: Callable[[Any], bool] - converter: Callable[[Any], Any] - priority: float +def _raise_type_exception(obj: Any): + raise TypeError('Unsupported type: ' + str(type(obj))) + + +def convertMap(obj: collections.abc.Mapping): + jmap = LinkedHashMap() + for k, v in obj.items(): + jk = to_java(k) + jv = to_java(v) + jmap.put(jk, jv) + return jmap + + +def convertSet(obj: collections.abc.Set): + jset = LinkedHashSet() + for item in obj: + jitem = to_java(item) + jset.add(jitem) + return jset + + +def convertIterable(obj: collections.abc.Iterable): + jlist = ArrayList() + for item in obj: + jitem = to_java(item) + jlist.add(jitem) + return jlist + + +java_converters : typing.List[Converter] = [] + + +def add_java_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + c = Converter(predicate, converter, priority) + _add_converter(c, java_converters) + + +def to_java(obj: Any) -> Any: + """ + Recursively convert a Python object to a Java object. + :param data: The Python object to convert. + Supported types include: + * str -> String + * bool -> Boolean + * int -> Integer, Long or BigInteger as appropriate + * float -> Float, Double or BigDecimal as appropriate + * dict -> LinkedHashMap + * set -> LinkedHashSet + * list -> ArrayList + :returns: A corresponding Java object with the same contents. + :raises TypeError: if the argument is not one of the aforementioned types. + """ + start_jvm() + return _convert(obj, java_converters) def _stock_java_converters() -> typing.List[Converter]: @@ -345,254 +414,10 @@ def _stock_java_converters() -> typing.List[Converter]: ] -def _stock_py_converters() -> typing.List: - return [ - # Other (Exceptional) converter - Converter( - predicate=lambda obj: True, - converter=_raise_type_exception, - priority=-10001 - ), - # Java identity converter - Converter( - predicate=lambda obj: not isjava(obj), - converter=lambda obj: obj, - priority=10000 - ), - # JBoolean converter - Converter( - predicate=lambda obj: isinstance(obj, JBoolean), - converter=bool, - priority=1 - ), - # JInt/JLong/JShort converter - Converter( - predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), - converter=int, - priority=1 - ), - # JDouble/JFloat converter - Converter( - predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), - converter=float, - priority=1 - ), - # JChar converter - Converter( - predicate=lambda obj: isinstance(obj, JChar), - converter=str, - priority=1 - ), - # Boolean converter - Converter( - predicate=lambda obj: isinstance(obj, Boolean), - converter=lambda obj: obj.booleanValue(), - priority=0 - ), - # Byte converter - Converter( - predicate=lambda obj: isinstance(obj, Byte), - converter=lambda obj: obj.byteValue(), - priority=0 - ), - # Char converter - Converter( - predicate=lambda obj: isinstance(obj, Character), - converter=lambda obj: obj.toString(), - priority=0 - ), - # Double converter - Converter( - predicate=lambda obj: isinstance(obj, Double), - converter=lambda obj: obj.doubleValue(), - priority=0 - ), - # Float converter - Converter( - predicate=lambda obj: isinstance(obj, Float), - converter=lambda obj: obj.floatValue(), - priority=0 - ), - # Integer converter - Converter( - predicate=lambda obj: isinstance(obj, Integer), - converter=lambda obj: obj.intValue(), - priority=0 - ), - # Long converter - Converter( - predicate=lambda obj: isinstance(obj, Long), - converter=lambda obj: obj.longValue(), - priority=0 - ), - # Short converter - Converter( - predicate=lambda obj: isinstance(obj, Short), - converter=lambda obj: obj.shortValue(), - priority=0 - ), - # Void converter - Converter( - predicate=lambda obj: isinstance(obj, Void), - converter=lambda obj: None, - priority=0 - ), - # String converter - Converter( - predicate=lambda obj: isinstance(obj, String), - converter=lambda obj: str(obj), - priority=0 - ), - # BigInteger converter - Converter( - predicate=lambda obj: isinstance(obj, BigInteger), - converter=lambda obj: int(str(obj.toString())), - priority=0 - ), - # BigDecimal converter - Converter( - predicate=lambda obj: isinstance(obj, BigDecimal), - converter=lambda obj: float(obj.toString), - priority=0 - ), - # SciJava Table converter - Converter( - predicate=_is_table, - converter=_convert_table, - priority=0 - ), - # List converter - Converter( - predicate=lambda obj: isinstance(obj, List), - converter=JavaList, - priority=0 - ), - # Map converter - Converter( - predicate=lambda obj: isinstance(obj, Map), - converter=JavaMap, - priority=0 - ), - # Set converter - Converter( - predicate=lambda obj: isinstance(obj, Set), - converter=JavaSet, - priority=0 - ), - # Collection converter - Converter( - predicate=lambda obj: isinstance(obj, Collection), - converter=JavaCollection, - priority=-1 - ), - # Iterable converter - Converter( - predicate=lambda obj: isinstance(obj, Iterable), - converter=JavaIterable, - priority=-1 - ), - # Iterator converter - Converter( - predicate=lambda obj: isinstance(obj, Iterator), - converter=JavaIterator, - priority=-1 - ), - ] - - -def _is_table(obj: Any): - try: - return isinstance(obj, jimport('org.scijava.table.Table')) - except: - # No worries if scijava-table is not available. - pass - -def _convert_table(obj: Any): - try: - return _table_to_pandas(obj) - except: - # No worries if scijava-table is not available. - pass - - -def _raise_type_exception(obj: Any): - raise TypeError('Unsupported type: ' + str(type(obj))) - - -def convertMap(obj: collections.abc.Mapping): - jmap = LinkedHashMap() - for k, v in obj.items(): - jk = to_java(k) - jv = to_java(v) - jmap.put(jk, jv) - return jmap - - -def convertSet(obj: collections.abc.Set): - jset = LinkedHashSet() - for item in obj: - jitem = to_java(item) - jset.add(jitem) - return jset - - -def convertIterable(obj: collections.abc.Iterable): - jlist = ArrayList() - for item in obj: - jitem = to_java(item) - jlist.add(jitem) - return jlist - - -# TODO: Consider priority aliases? -java_converters : typing.List[Converter] = [] when_jvm_starts( lambda : [_add_converter(c, java_converters) for c in _stock_java_converters()] ) -py_converters : typing.List[Converter] = [] -when_jvm_starts( - lambda : [_add_converter(c, py_converters) for c in _stock_py_converters()] -) - -def to_java(obj: Any) -> Any: - """ - Recursively convert a Python object to a Java object. - :param data: The Python object to convert. - Supported types include: - * str -> String - * bool -> Boolean - * int -> Integer, Long or BigInteger as appropriate - * float -> Float, Double or BigDecimal as appropriate - * dict -> LinkedHashMap - * set -> LinkedHashSet - * list -> ArrayList - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. - """ - start_jvm() - return _convert(obj, java_converters) - - -def _convert(obj: Any, converters: typing.List[Converter]) -> Any: - suitable_converters = filter(lambda c: c.predicate(obj), converters) - prioritized = max(suitable_converters, key = lambda c: c.priority) - return prioritized.converter(obj) - - -def _add_converter(converter: Converter, converters: typing.List[Converter]): - converters.append(converter) - - -def add_java_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): - c = Converter(predicate, converter, priority) - _add_converter(c, java_converters) - - -def add_py_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): - c = Converter(predicate, converter, priority) - _add_converter(c, py_converters) - # -- Java to Python -- @@ -755,6 +580,14 @@ def __str__(self): return '{' + ', '.join(_jstr(v) for v in self) + '}' +py_converters : typing.List[Converter] = [] + + +def add_py_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + c = Converter(predicate, converter, priority) + _add_converter(c, py_converters) + + def to_python(data: Any, gentle: bool =False) -> Any: """ Recursively convert a Java object to a Python object. @@ -784,6 +617,181 @@ def to_python(data: Any, gentle: bool =False) -> Any: raise exc +def _stock_py_converters() -> typing.List: + return [ + # Other (Exceptional) converter + Converter( + predicate=lambda obj: True, + converter=_raise_type_exception, + priority=-10001 + ), + # Java identity converter + Converter( + predicate=lambda obj: not isjava(obj), + converter=lambda obj: obj, + priority=10000 + ), + # JBoolean converter + Converter( + predicate=lambda obj: isinstance(obj, JBoolean), + converter=bool, + priority=1 + ), + # JInt/JLong/JShort converter + Converter( + predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), + converter=int, + priority=1 + ), + # JDouble/JFloat converter + Converter( + predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), + converter=float, + priority=1 + ), + # JChar converter + Converter( + predicate=lambda obj: isinstance(obj, JChar), + converter=str, + priority=1 + ), + # Boolean converter + Converter( + predicate=lambda obj: isinstance(obj, Boolean), + converter=lambda obj: obj.booleanValue(), + priority=0 + ), + # Byte converter + Converter( + predicate=lambda obj: isinstance(obj, Byte), + converter=lambda obj: obj.byteValue(), + priority=0 + ), + # Char converter + Converter( + predicate=lambda obj: isinstance(obj, Character), + converter=lambda obj: obj.toString(), + priority=0 + ), + # Double converter + Converter( + predicate=lambda obj: isinstance(obj, Double), + converter=lambda obj: obj.doubleValue(), + priority=0 + ), + # Float converter + Converter( + predicate=lambda obj: isinstance(obj, Float), + converter=lambda obj: obj.floatValue(), + priority=0 + ), + # Integer converter + Converter( + predicate=lambda obj: isinstance(obj, Integer), + converter=lambda obj: obj.intValue(), + priority=0 + ), + # Long converter + Converter( + predicate=lambda obj: isinstance(obj, Long), + converter=lambda obj: obj.longValue(), + priority=0 + ), + # Short converter + Converter( + predicate=lambda obj: isinstance(obj, Short), + converter=lambda obj: obj.shortValue(), + priority=0 + ), + # Void converter + Converter( + predicate=lambda obj: isinstance(obj, Void), + converter=lambda obj: None, + priority=0 + ), + # String converter + Converter( + predicate=lambda obj: isinstance(obj, String), + converter=lambda obj: str(obj), + priority=0 + ), + # BigInteger converter + Converter( + predicate=lambda obj: isinstance(obj, BigInteger), + converter=lambda obj: int(str(obj.toString())), + priority=0 + ), + # BigDecimal converter + Converter( + predicate=lambda obj: isinstance(obj, BigDecimal), + converter=lambda obj: float(obj.toString), + priority=0 + ), + # SciJava Table converter + Converter( + predicate=_is_table, + converter=_convert_table, + priority=0 + ), + # List converter + Converter( + predicate=lambda obj: isinstance(obj, List), + converter=JavaList, + priority=0 + ), + # Map converter + Converter( + predicate=lambda obj: isinstance(obj, Map), + converter=JavaMap, + priority=0 + ), + # Set converter + Converter( + predicate=lambda obj: isinstance(obj, Set), + converter=JavaSet, + priority=0 + ), + # Collection converter + Converter( + predicate=lambda obj: isinstance(obj, Collection), + converter=JavaCollection, + priority=-1 + ), + # Iterable converter + Converter( + predicate=lambda obj: isinstance(obj, Iterable), + converter=JavaIterable, + priority=-1 + ), + # Iterator converter + Converter( + predicate=lambda obj: isinstance(obj, Iterator), + converter=JavaIterator, + priority=-1 + ), + ] + + +when_jvm_starts( + lambda : [_add_converter(c, py_converters) for c in _stock_py_converters()] +) + + +def _is_table(obj: Any): + try: + return isinstance(obj, jimport('org.scijava.table.Table')) + except: + # No worries if scijava-table is not available. + pass + + +def _convert_table(obj: Any): + try: + return _table_to_pandas(obj) + except: + # No worries if scijava-table is not available. + pass + def _import_pandas(): try: From 6282e8aea525bfda29e9f31db3bf354c48728794 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 10 Feb 2022 15:54:00 -0600 Subject: [PATCH 112/505] Add Priority constants It'd be cool if we could just use org.scijava.priority.Priority, but we can't do that without pulling in all of SJC. We could get some good use out of SciJava3 having a SciJava Priority module :) --- scyjava/__init__.py | 103 ++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 1c32ee05..820bc758 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -175,11 +175,25 @@ def compare_version(version, java_class_version): # -- Type Conversion Utilities -- +# TODO: It would be cool to just use org.scijava.priority.Priority. +# Unfortunately, we cannot do that without bringing in all of SJC. +# Once SciJava 3 is mainstream, we could use a SciJava Priority module :) +class Priority: + FIRST = 1E300 + EXTREMELY_HIGH = 1E6 + VERY_HIGH = 1E4 + HIGH = 1E2 + NORMAL = 0 + LOW = -1E2 + VERY_LOW = -1E4 + EXTREMELY_LOW = -1E6 + LAST = -1E300 class Converter(NamedTuple): predicate: Callable[[Any], bool] converter: Callable[[Any], Any] - priority: float + # Corresponds with Priority.NORMAL + priority: float = 0 def _convert(obj: Any, converters: typing.List[Converter]) -> Any: @@ -296,6 +310,13 @@ def convertIterable(obj: collections.abc.Iterable): def add_java_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + """ + Adds a converter to the list used by to_java + :param predicate: A Callable identifying suitable data types for this converter + :param converter: A Callable able to convert a set of types + :priority: + + """ c = Converter(predicate, converter, priority) _add_converter(c, java_converters) @@ -325,91 +346,91 @@ def _stock_java_converters() -> typing.List[Converter]: Converter( predicate=lambda obj: True, converter=_raise_type_exception, - priority=-10001 + priority=Priority.EXTREMELY_LOW - 1 ), # NoneType converter Converter( predicate=lambda obj: obj is None, converter=lambda obj: None, - priority=10001 + priority=Priority.EXTREMELY_HIGH + 1 ), # Java identity converter Converter( predicate=isjava, converter=lambda obj: obj, - priority=10000 + priority=Priority.EXTREMELY_HIGH ), # String converter Converter( predicate=lambda obj: isinstance(obj, str), converter=lambda obj: String(obj.encode('utf-8'), 'utf-8'), - priority=0 + priority=Priority.NORMAL ), # Boolean converter Converter( predicate=lambda obj: isinstance(obj, bool), converter=Boolean, - priority=0 + priority=Priority.NORMAL ), # Integer converter Converter( predicate=lambda obj: isinstance(obj, int) and obj <= Integer.MAX_VALUE and obj >= Integer.MIN_VALUE, converter= Integer, - priority=0 + priority=Priority.NORMAL ), # Long converter Converter( predicate=lambda obj: isinstance(obj, int) and obj <= Long.MAX_VALUE, converter=Long, - priority=-1 + priority=Priority.NORMAL - 1 ), # BigInteger converter Converter( predicate=lambda obj: isinstance(obj, int), converter=lambda obj: BigInteger(str(obj)), - priority=-2 + priority=Priority.NORMAL - 2 ), # Float converter Converter( predicate=lambda obj: isinstance(obj, float) and obj <= Float.MAX_VALUE and obj >= Float.MIN_VALUE, converter= Float, - priority=0 + priority=Priority.NORMAL ), # Double converter Converter( predicate=lambda obj: isinstance(obj, float) and obj <= Double.MAX_VALUE and obj >= Float.MIN_VALUE, converter=Double, - priority=-1 + priority=Priority.NORMAL - 1 ), # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, float), converter=lambda obj: BigDecimal(str(obj)), - priority=-2 + priority=Priority.NORMAL - 2 ), # Pandas table converter Converter( predicate=lambda obj: type(obj).__name__ == 'DataFrame', converter=_pandas_to_table, - priority=1 + priority=Priority.NORMAL + 1 ), # Mapping converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Mapping), converter=convertMap, - priority=0 + priority=Priority.NORMAL ), # Set converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Set), converter=convertSet, - priority=0 + priority=Priority.NORMAL ), # Iterable converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Iterable), converter=convertIterable, - priority=-1 + priority=Priority.NORMAL -1 ), ] @@ -623,151 +644,151 @@ def _stock_py_converters() -> typing.List: Converter( predicate=lambda obj: True, converter=_raise_type_exception, - priority=-10001 + priority=Priority.EXTREMELY_LOW - 1 ), # Java identity converter Converter( predicate=lambda obj: not isjava(obj), converter=lambda obj: obj, - priority=10000 + priority=Priority.EXTREMELY_HIGH ), # JBoolean converter Converter( predicate=lambda obj: isinstance(obj, JBoolean), converter=bool, - priority=1 + priority=Priority.NORMAL + 1 ), # JInt/JLong/JShort converter Converter( predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), converter=int, - priority=1 + priority=Priority.NORMAL + 1 ), # JDouble/JFloat converter Converter( predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), converter=float, - priority=1 + priority=Priority.NORMAL + 1 ), # JChar converter Converter( predicate=lambda obj: isinstance(obj, JChar), converter=str, - priority=1 + priority=Priority.NORMAL + 1 ), # Boolean converter Converter( predicate=lambda obj: isinstance(obj, Boolean), converter=lambda obj: obj.booleanValue(), - priority=0 + priority=Priority.NORMAL ), # Byte converter Converter( predicate=lambda obj: isinstance(obj, Byte), converter=lambda obj: obj.byteValue(), - priority=0 + priority=Priority.NORMAL ), # Char converter Converter( predicate=lambda obj: isinstance(obj, Character), converter=lambda obj: obj.toString(), - priority=0 + priority=Priority.NORMAL ), # Double converter Converter( predicate=lambda obj: isinstance(obj, Double), converter=lambda obj: obj.doubleValue(), - priority=0 + priority=Priority.NORMAL ), # Float converter Converter( predicate=lambda obj: isinstance(obj, Float), converter=lambda obj: obj.floatValue(), - priority=0 + priority=Priority.NORMAL ), # Integer converter Converter( predicate=lambda obj: isinstance(obj, Integer), converter=lambda obj: obj.intValue(), - priority=0 + priority=Priority.NORMAL ), # Long converter Converter( predicate=lambda obj: isinstance(obj, Long), converter=lambda obj: obj.longValue(), - priority=0 + priority=Priority.NORMAL ), # Short converter Converter( predicate=lambda obj: isinstance(obj, Short), converter=lambda obj: obj.shortValue(), - priority=0 + priority=Priority.NORMAL ), # Void converter Converter( predicate=lambda obj: isinstance(obj, Void), converter=lambda obj: None, - priority=0 + priority=Priority.NORMAL ), # String converter Converter( predicate=lambda obj: isinstance(obj, String), converter=lambda obj: str(obj), - priority=0 + priority=Priority.NORMAL ), # BigInteger converter Converter( predicate=lambda obj: isinstance(obj, BigInteger), converter=lambda obj: int(str(obj.toString())), - priority=0 + priority=Priority.NORMAL ), # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, BigDecimal), converter=lambda obj: float(obj.toString), - priority=0 + priority=Priority.NORMAL ), # SciJava Table converter Converter( predicate=_is_table, converter=_convert_table, - priority=0 + priority=Priority.NORMAL ), # List converter Converter( predicate=lambda obj: isinstance(obj, List), converter=JavaList, - priority=0 + priority=Priority.NORMAL ), # Map converter Converter( predicate=lambda obj: isinstance(obj, Map), converter=JavaMap, - priority=0 + priority=Priority.NORMAL ), # Set converter Converter( predicate=lambda obj: isinstance(obj, Set), converter=JavaSet, - priority=0 + priority=Priority.NORMAL ), # Collection converter Converter( predicate=lambda obj: isinstance(obj, Collection), converter=JavaCollection, - priority=-1 + priority=Priority.NORMAL -1 ), # Iterable converter Converter( predicate=lambda obj: isinstance(obj, Iterable), converter=JavaIterable, - priority=-1 + priority=Priority.NORMAL -1 ), # Iterator converter Converter( predicate=lambda obj: isinstance(obj, Iterator), converter=JavaIterator, - priority=-1 + priority=Priority.NORMAL - 1 ), ] From 107140a822714a899c602256b073950d5375f757 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 10 Feb 2022 15:55:21 -0600 Subject: [PATCH 113/505] More cleaning up --- scyjava/__init__.py | 58 +++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 820bc758..29abd7f8 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -192,8 +192,7 @@ class Priority: class Converter(NamedTuple): predicate: Callable[[Any], bool] converter: Callable[[Any], Any] - # Corresponds with Priority.NORMAL - priority: float = 0 + priority: float = Priority.NORMAL def _convert(obj: Any, converters: typing.List[Converter]) -> Any: @@ -281,7 +280,7 @@ def _raise_type_exception(obj: Any): raise TypeError('Unsupported type: ' + str(type(obj))) -def convertMap(obj: collections.abc.Mapping): +def _convertMap(obj: collections.abc.Mapping): jmap = LinkedHashMap() for k, v in obj.items(): jk = to_java(k) @@ -290,7 +289,7 @@ def convertMap(obj: collections.abc.Mapping): return jmap -def convertSet(obj: collections.abc.Set): +def _convertSet(obj: collections.abc.Set): jset = LinkedHashSet() for item in obj: jitem = to_java(item) @@ -298,7 +297,7 @@ def convertSet(obj: collections.abc.Set): return jset -def convertIterable(obj: collections.abc.Iterable): +def _convertIterable(obj: collections.abc.Iterable): jlist = ArrayList() for item in obj: jitem = to_java(item) @@ -341,6 +340,11 @@ def to_java(obj: Any) -> Any: def _stock_java_converters() -> typing.List[Converter]: + """ + Returns all python-to-java converters supported out of the box! + This should only be called after the JVM has been started! + :returns: A list of Converters + """ return [ # Other (Exceptional) converter Converter( @@ -364,19 +368,16 @@ def _stock_java_converters() -> typing.List[Converter]: Converter( predicate=lambda obj: isinstance(obj, str), converter=lambda obj: String(obj.encode('utf-8'), 'utf-8'), - priority=Priority.NORMAL ), # Boolean converter Converter( predicate=lambda obj: isinstance(obj, bool), converter=Boolean, - priority=Priority.NORMAL ), # Integer converter Converter( predicate=lambda obj: isinstance(obj, int) and obj <= Integer.MAX_VALUE and obj >= Integer.MIN_VALUE, converter= Integer, - priority=Priority.NORMAL ), # Long converter Converter( @@ -394,7 +395,6 @@ def _stock_java_converters() -> typing.List[Converter]: Converter( predicate=lambda obj: isinstance(obj, float) and obj <= Float.MAX_VALUE and obj >= Float.MIN_VALUE, converter= Float, - priority=Priority.NORMAL ), # Double converter Converter( @@ -417,19 +417,17 @@ def _stock_java_converters() -> typing.List[Converter]: # Mapping converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Mapping), - converter=convertMap, - priority=Priority.NORMAL + converter=_convertMap, ), # Set converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Set), - converter=convertSet, - priority=Priority.NORMAL + converter=_convertSet, ), # Iterable converter Converter( predicate=lambda obj: isinstance(obj, collections.abc.Iterable), - converter=convertIterable, + converter=_convertIterable, priority=Priority.NORMAL -1 ), ] @@ -605,6 +603,13 @@ def __str__(self): def add_py_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): + """ + Adds a converter to the list used by to_python + :param predicate: A Callable identifying suitable data types for this converter + :param converter: A Callable able to convert a set of types + :priority: + + """ c = Converter(predicate, converter, priority) _add_converter(c, py_converters) @@ -639,6 +644,11 @@ def to_python(data: Any, gentle: bool =False) -> Any: def _stock_py_converters() -> typing.List: + """ + Returns all java-to-python converters supported out of the box! + This should only be called after the JVM has been started! + :returns: A list of Converters + """ return [ # Other (Exceptional) converter Converter( @@ -680,97 +690,81 @@ def _stock_py_converters() -> typing.List: Converter( predicate=lambda obj: isinstance(obj, Boolean), converter=lambda obj: obj.booleanValue(), - priority=Priority.NORMAL ), # Byte converter Converter( predicate=lambda obj: isinstance(obj, Byte), converter=lambda obj: obj.byteValue(), - priority=Priority.NORMAL ), # Char converter Converter( predicate=lambda obj: isinstance(obj, Character), converter=lambda obj: obj.toString(), - priority=Priority.NORMAL ), # Double converter Converter( predicate=lambda obj: isinstance(obj, Double), converter=lambda obj: obj.doubleValue(), - priority=Priority.NORMAL ), # Float converter Converter( predicate=lambda obj: isinstance(obj, Float), converter=lambda obj: obj.floatValue(), - priority=Priority.NORMAL ), # Integer converter Converter( predicate=lambda obj: isinstance(obj, Integer), converter=lambda obj: obj.intValue(), - priority=Priority.NORMAL ), # Long converter Converter( predicate=lambda obj: isinstance(obj, Long), converter=lambda obj: obj.longValue(), - priority=Priority.NORMAL ), # Short converter Converter( predicate=lambda obj: isinstance(obj, Short), converter=lambda obj: obj.shortValue(), - priority=Priority.NORMAL ), # Void converter Converter( predicate=lambda obj: isinstance(obj, Void), converter=lambda obj: None, - priority=Priority.NORMAL ), # String converter Converter( predicate=lambda obj: isinstance(obj, String), converter=lambda obj: str(obj), - priority=Priority.NORMAL ), # BigInteger converter Converter( predicate=lambda obj: isinstance(obj, BigInteger), converter=lambda obj: int(str(obj.toString())), - priority=Priority.NORMAL ), # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, BigDecimal), converter=lambda obj: float(obj.toString), - priority=Priority.NORMAL ), # SciJava Table converter Converter( predicate=_is_table, converter=_convert_table, - priority=Priority.NORMAL ), # List converter Converter( predicate=lambda obj: isinstance(obj, List), converter=JavaList, - priority=Priority.NORMAL ), # Map converter Converter( predicate=lambda obj: isinstance(obj, Map), converter=JavaMap, - priority=Priority.NORMAL ), # Set converter Converter( predicate=lambda obj: isinstance(obj, Set), converter=JavaSet, - priority=Priority.NORMAL ), # Collection converter Converter( @@ -798,7 +792,8 @@ def _stock_py_converters() -> typing.List: ) -def _is_table(obj: Any): +def _is_table(obj: Any) -> bool: + """Checks if obj is a table""" try: return isinstance(obj, jimport('org.scijava.table.Table')) except: @@ -807,6 +802,7 @@ def _is_table(obj: Any): def _convert_table(obj: Any): + """Converts obj to a table.""" try: return _table_to_pandas(obj) except: From a8b8052dcd6a8764fe5bfbd292d75e57877b16e8 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 14 Feb 2022 11:11:14 -0600 Subject: [PATCH 114/505] Use Converter class instead of passing components If you are going to add a significant number of these Converters, you are going to want to use the Converter class. For that reason, we get rid of the API that takes all of the Converter components... --- scyjava/__init__.py | 20 ++++++-------------- tests/test_convert.py | 10 ++++++---- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 29abd7f8..4af46bea 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -308,16 +308,12 @@ def _convertIterable(obj: collections.abc.Iterable): java_converters : typing.List[Converter] = [] -def add_java_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): +def add_java_converter(converter: Converter): """ Adds a converter to the list used by to_java - :param predicate: A Callable identifying suitable data types for this converter - :param converter: A Callable able to convert a set of types - :priority: - + :param converter: A Converter going from python to java """ - c = Converter(predicate, converter, priority) - _add_converter(c, java_converters) + _add_converter(converter, java_converters) def to_java(obj: Any) -> Any: @@ -602,16 +598,12 @@ def __str__(self): py_converters : typing.List[Converter] = [] -def add_py_converter(predicate: Callable[[Any], bool], converter: Callable[[Any], Any], priority: float): +def add_py_converter(converter: Converter): """ Adds a converter to the list used by to_python - :param predicate: A Callable identifying suitable data types for this converter - :param converter: A Callable able to convert a set of types - :priority: - + :param converter: A Converter from java to python """ - c = Converter(predicate, converter, priority) - _add_converter(c, py_converters) + _add_converter(converter, py_converters) def to_python(data: Any, gentle: bool =False) -> Any: diff --git a/tests/test_convert.py b/tests/test_convert.py index 216f096f..65b3f0d4 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,5 +1,5 @@ import unittest -from scyjava import config, jclass, jimport, to_java, to_python +from scyjava import Converter, config, jclass, jimport, to_java, to_python config.endpoints.append('org.scijava:scijava-table') config.add_option('-Djava.awt.headless=true') @@ -232,9 +232,11 @@ def test_conversion_priority(self): from scyjava import add_java_converter add_java_converter( - predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: String(invader.encode('utf-8'), 'utf-8'), - priority=100 + Converter( + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: String(invader.encode('utf-8'), 'utf-8'), + priority=100 + ) ) # Ensure that the conversion uses our new converter From ada6142dfee3564395a9916c5b1244cebce116f0 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 15 Feb 2022 15:06:38 -0600 Subject: [PATCH 115/505] Add JArray->List conversion Note that we do not go in the other direction. This is because Python does not have an array type, preferring Lists (and we have a List -> List converter already) --- .vscode/settings.json | 8 ++++++++ scyjava/__init__.py | 6 ++++++ tests/test_convert.py | 12 +++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ff801d3f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.formatting.provider": "black" +} \ No newline at end of file diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 4af46bea..ef15e214 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -776,6 +776,12 @@ def _stock_py_converters() -> typing.List: converter=JavaIterator, priority=Priority.NORMAL - 1 ), + # JArray converter + Converter( + predicate=lambda obj: isinstance(obj, JArray), + converter=lambda obj:[to_python(o) for o in obj], + priority=Priority.VERY_LOW + ), ] diff --git a/tests/test_convert.py b/tests/test_convert.py index 65b3f0d4..2dbf20e6 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,5 +1,7 @@ import unittest -from scyjava import Converter, config, jclass, jimport, to_java, to_python + +from jpype import JArray, JInt, JLong +from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python config.endpoints.append('org.scijava:scijava-table') config.add_option('-Djava.awt.headless=true') @@ -124,6 +126,14 @@ def testSet(self): self.assertEqual(s, ps) self.assertEqual(str(s), str(ps)) + def testArray(self): + start_jvm() + arr = JArray(JInt)(4) + for i in range(len(arr)): + arr[i] = to_java(i) + py_arr = to_python(arr) + assert py_arr == [0, 1, 2, 3] + def testDict(self): d = { 'access_log': [ From 8ff843b1a5bac597d3f76181d6232608053558ae Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Feb 2022 17:29:06 -0600 Subject: [PATCH 116/505] Add a function to get the JVM version Even if the JVM is not running. This is an important feature, so that you can pass different flags to the JVM at startup depending on which version of Java it's going to be. --- scyjava/__init__.py | 61 ++++++++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- tests/test_jvm.py | 30 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/test_jvm.py diff --git a/scyjava/__init__.py b/scyjava/__init__.py index 90a5b634..de1e21e6 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -2,10 +2,12 @@ import collections.abc import jgo import jpype +import jpype.config import logging import os import scyjava.config -import jpype.config +import subprocess +from pathlib import Path from jpype.types import * from _jpype import _JObject @@ -18,6 +20,63 @@ _shutdown_callbacks = [] +def jvm_version(): + """ + Gets the version of the JVM as a tuple, with each dot-separated digit as one element. + Characters in the version string beyond only numbers and dots are ignored, in line + with the java.version system property. + + Examples: + * OpenJDK 17.0.1 -> [17, 0, 1] + * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] + * OpenJDK 1.8.0_312 -> [1, 8, 0] + + If the JVM is already started, this function should return the equivalent of: + jimport('java.lang.System').getProperty('java.version').split('.') + + In case the JVM is not started yet, a best effort is made to deduce the + version from the environment without actually starting up the JVM in-process. + """ + jvm_version = jpype.getJVMVersion() + if jvm_version and jvm_version[0]: + # JPype already knew the version. + # JVM is probably already started. + # Or JPype got smarter since 1.3.0. + return jvm_version + + # JPype was clueless, which means the JVM has probably not started yet. + # Let's look for a java executable, and ask it directly with 'java -version'. + + default_jvm_path = jpype.getDefaultJVMPath() + if not default_jvm_path: + raise RuntimeError("Cannot glean the default JVM path") + + p = Path(default_jvm_path) + if not p.is_dir(): + raise RuntimeError(f"Invalid default JVM path: {p}") + + java = None + for _ in range(3): # The bin folder is always <=3 levels up from libjvm. + p = p.parent + if p.name == 'lib': + java = p.parent / 'bin' / 'java' + if os.name == 'nt': + # Good ol' Windows! Nothing beats Windows. + java = java.with_suffix('.exe') + if not java.is_file(): + raise RuntimeError(f"No ../bin/java found at: {p}") + break + if java is None: + raise RuntimeError(f"No java executable found inside: {p}") + + version = subprocess.check_output([java, '-version'], stderr=subprocess.STDOUT).decode() + m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) + if not m: + raise RuntimeError(f"Inscrutable java command output:\n{version}") + + return tuple(map(int, m.group(1).split('.'))) + + def start_jvm(options=scyjava.config.get_options()): """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can diff --git a/setup.py b/setup.py index d31318ea..178cc374 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3.6', packages=find_packages(), - version="1.4.2.dev0", + version="1.5.0.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', diff --git a/tests/test_jvm.py b/tests/test_jvm.py new file mode 100644 index 00000000..cd1e7ff9 --- /dev/null +++ b/tests/test_jvm.py @@ -0,0 +1,30 @@ +import scyjava +import unittest + +class TestJVM(unittest.TestCase): + """ + Tests scyjava JVM management functions. + """ + + def test_jvm_version(self): + """ + Tests the jvm_version() function. + """ + + before_version = scyjava.jvm_version() + self.assertTrue(before_version is not None) + self.assertTrue(len(before_version) >= 3) + self.assertTrue(before_version[0] > 0) + + scyjava.start_jvm() + + after_version = scyjava.jvm_version() + self.assertTrue(after_version is not None) + self.assertTrue(len(after_version) >= 3) + self.assertTrue(after_version[0] > 0) + + self.assertEqual(before_version, after_version) + + +if __name__ == '__main__': + unittest.main() From 7dd2f7570444a6d6ff048da60ab248a1e4368a73 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Feb 2022 17:40:54 -0600 Subject: [PATCH 117/505] Fix bug in jvm_exists function The default JVM path will typically point to libjvm.so, which is, of course, not a directory. The thing I don't understand is: why was the unit test passing then? --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index de1e21e6..ec26e013 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -52,7 +52,7 @@ def jvm_version(): raise RuntimeError("Cannot glean the default JVM path") p = Path(default_jvm_path) - if not p.is_dir(): + if not p.exists(): raise RuntimeError(f"Invalid default JVM path: {p}") java = None From 73d758e453b86e8e4d895b0664d2d199143e044d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Feb 2022 17:42:59 -0600 Subject: [PATCH 118/505] Fix another bug in the jvm_version function The long code path must not be getting exercised with the unit test when run as part of pytest. Unfortunate. However, running the unit test standalone like: python tests/test_jvm.py did trigger the problem fixed by this commit. --- scyjava/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index ec26e013..c4789d7e 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -5,6 +5,7 @@ import jpype.config import logging import os +import re import scyjava.config import subprocess from pathlib import Path From c45519eb03e69f84c624cb5c65db35b97c2b0378 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Feb 2022 17:48:06 -0600 Subject: [PATCH 119/505] Run tests also individually in separate processes This is an attempt to catch problems relating to the JVM already being started, and thus altering the behavior of later tests. --- .github/workflows/python-test-conda.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index 2988e9e8..c8f0f5cf 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -35,4 +35,6 @@ jobs: - name: Test with pytest run: | pytest - + - name: Run tests individually in separate processes + run: | + for t in tests/*.py; do python $t; done From d9c49e526ee44037b96a5c36d0a37587b4851b55 Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 24 Feb 2022 14:20:01 -0600 Subject: [PATCH 120/505] Run tests cross-platform This should help us to catch platform-specific problems. Note that the individual tests are run explicitly instead of in a for-loop, due to Windows running in powershell on default and not having an easy consistent syntax that also ensures appropriate error code propagation. This is detrimental to extensiblilty and should be resolved if more tests are added. --- .github/workflows/python-test-conda.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index c8f0f5cf..7327276b 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -11,11 +11,19 @@ on: - master jobs: - build-linux: - runs-on: ubuntu-latest + build-cross-platform: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.8"] steps: - uses: actions/checkout@v2 + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} - name: Add conda to system path run: | # $CONDA is an environment variable pointing to the root of the miniconda directory @@ -35,6 +43,12 @@ jobs: - name: Test with pytest run: | pytest - - name: Run tests individually in separate processes + - name: Test Convert run: | - for t in tests/*.py; do python $t; done + python tests/test_convert.py + - name: Test JVM + run: | + python tests/test_jvm.py + - name: Test Pandas + run: | + python tests/test_pandas.py From 4b8cd8276cd48ef864760c176d26f7db644a3693 Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 24 Feb 2022 15:21:39 -0600 Subject: [PATCH 121/505] jvm_version: support bin subdir It is possible for the default_jvm_path to start in a /bin subdir on Windows, not just a /lib subdir. This change now supports both scenarios. --- scyjava/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index c4789d7e..ff68f80d 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -61,6 +61,10 @@ def jvm_version(): p = p.parent if p.name == 'lib': java = p.parent / 'bin' / 'java' + elif p.name == 'bin': + java = p / 'java' + + if java is not None: if os.name == 'nt': # Good ol' Windows! Nothing beats Windows. java = java.with_suffix('.exe') From d49eab2a1cc317470af3daf30a30233d6e15bca2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Feb 2022 15:46:11 -0600 Subject: [PATCH 122/505] jvm_version: note that RuntimeError may be raised --- scyjava/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index ff68f80d..e1d18123 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -37,6 +37,7 @@ def jvm_version(): In case the JVM is not started yet, a best effort is made to deduce the version from the environment without actually starting up the JVM in-process. + If the version cannot be deduced, a RuntimeError with the cause is raised. """ jvm_version = jpype.getJVMVersion() if jvm_version and jvm_version[0]: From 43c34dcd4862c02963860e66db0cabf41f0fde10 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Feb 2022 19:03:31 -0600 Subject: [PATCH 123/505] test_jvm: set headless mode Otherwise, this test screws up the macOS CI node. --- tests/test_jvm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_jvm.py b/tests/test_jvm.py index cd1e7ff9..a981fce8 100644 --- a/tests/test_jvm.py +++ b/tests/test_jvm.py @@ -16,6 +16,7 @@ def test_jvm_version(self): self.assertTrue(len(before_version) >= 3) self.assertTrue(before_version[0] > 0) + scyjava.config.add_option('-Djava.awt.headless=true') scyjava.start_jvm() after_version = scyjava.jvm_version() From 3f306860fed56841edba2c41ccbb1a6e14b36d5d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 8 Mar 2022 14:23:12 -0600 Subject: [PATCH 124/505] Release version 1.5.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 178cc374..f26614e7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3.6', packages=find_packages(), - version="1.5.0.dev0", + version="1.5.0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 62c254ed451a8278b7b07056e656e7f3a5d51293 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 8 Mar 2022 14:28:49 -0600 Subject: [PATCH 125/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f26614e7..0bff1eb8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3.6', packages=find_packages(), - version="1.5.0", + version="1.5.1.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From d646cce1fa8a47f5a27e2b89945dcba6dc41b156 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 11 Mar 2022 11:46:56 -0600 Subject: [PATCH 126/505] Fix java version check failure on Windows scyjava's version check fails with: TypeError: argument of type 'WindowsPath' is not iterable when on Windows and using Python <=3.7. Is seems that Pathlib.path only became iterable with version 3.8. Casting the 'WindowsPath' as a string resolves this issue. I tested this with Python versions 3.6.13, 3.7.11, 3.8.12 and 3.9.7. All tests pass on Windows and Linux for the same Python version range. --- scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index f0578716..de598a9a 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -78,7 +78,7 @@ def jvm_version(): if java is None: raise RuntimeError(f"No java executable found inside: {p}") - version = subprocess.check_output([java, '-version'], stderr=subprocess.STDOUT).decode() + version = subprocess.check_output([str(java), '-version'], stderr=subprocess.STDOUT).decode() m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) if not m: raise RuntimeError(f"Inscrutable java command output:\n{version}") From 0416a381249225221f1a947aa00547edfc5feace Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 11 Mar 2022 12:24:22 -0600 Subject: [PATCH 127/505] Release version 1.5.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0bff1eb8..621c08e5 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3.6', packages=find_packages(), - version="1.5.1.dev0", + version="1.5.1", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From 37d8b3ceaffa13ac94cd16ae878b939ab8a69442 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 11 Mar 2022 12:27:18 -0600 Subject: [PATCH 128/505] Bump to next development cycle --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 621c08e5..5eec539d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ name='scyjava', python_requires='>=3.6', packages=find_packages(), - version="1.5.1", + version="1.5.2.dev0", author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', author_email='ctrueden@wisc.edu', description='scyjava', From af02aaad23a76cf6b85b4b8039fd1e0ae51f3721 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 30 Mar 2022 11:08:13 -0400 Subject: [PATCH 129/505] Cache jimport results (#34) This may speed up the return for repeated imports. And ensures the exact same object is given back when importing the same class again. --- scyjava/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scyjava/__init__.py b/scyjava/__init__.py index de598a9a..53436634 100644 --- a/scyjava/__init__.py +++ b/scyjava/__init__.py @@ -1,5 +1,6 @@ import atexit import collections.abc +from functools import lru_cache import traceback from typing import Any, Callable, NamedTuple import typing @@ -302,6 +303,7 @@ def jclass(data): raise TypeError('Cannot glean class from data of type: ' + str(type(data))) +@lru_cache(maxsize=None) def jimport(class_name): """ Import a class from Java to Python. From 2ada7a0e415eb2bdd0ccd2b7abb3adc736bf4542 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 7 Apr 2022 17:08:41 -0500 Subject: [PATCH 130/505] Remove obsolete Travis CI configuration --- .travis.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6407bb44..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Adapted from: -# https://conda.io/docs/user-guide/tasks/use-conda-with-travis-ci.html -language: python -python: "3.6" -branches: - only: - - master -install: - - sudo apt-get update - - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh - - bash miniconda.sh -b -p $HOME/miniconda - - export PATH="$HOME/miniconda/bin:$PATH" - - hash -r - - conda config --set always_yes yes --set changeps1 no - - conda update -q conda - # Useful for debugging any issues with conda - - conda info -a - - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION - - source activate test-environment - - conda install -c conda-forge jpype1 jgo pandas numpy - -script: - - python -m unittest discover tests -v From 917cd36a12bf22d1734356ba8b1525953ee98cce Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 6 Apr 2022 15:48:15 -0500 Subject: [PATCH 131/505] Improve project building This solves the following goals: * Refactor the scijava directory into the src/scijava directory * Remove setup.py in favor of setup.cfg * Set up tox --- .gitignore | 15 +++++-- pyproject.toml | 6 +++ setup.cfg | 50 ++++++++++++++++++++- setup.py | 22 --------- {scyjava => src/scyjava}/__init__.py | 0 {scyjava => src/scyjava}/config/__init__.py | 0 tox.ini | 19 ++++++++ 7 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py rename {scyjava => src/scyjava}/__init__.py (100%) rename {scyjava => src/scyjava}/config/__init__.py (100%) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 8178177a..f3f038f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ -*.swp -/.eggs/ +# Byte-compiled / optimized / DLL files +*__pycache__/ +*.py[cod] + +# Distribution / packaging /build/ /dist/ -/scyjava.egg-info/ -__pycache__/ +/eggs/ +/.eggs/ +*egg-info/ + +# vi +*.swp diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..31434581 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +# Setuptools version constraint ensures PEP 517 compatibility +requires = [ + "setuptools >= 40.9.0" +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index b90f6ada..3641673b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,50 @@ [metadata] -description_file=README.md +name = scyjava +version = 1.5.2.dev0 +author = Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner +author_email = ctrueden@wisc.edu +description = Supercharged Java access from Python +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/scijava/scyjava +project_urls = + Bug Tracker = https://github.com/scijava/scyjava/issues +classifiers = + Intended Audience :: Developers + Intended Audience :: Education + Intended Audience :: Science/Research + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3 :: 3.6 + Programming Language :: Python :: 3 :: 3.7 + Programming Language :: Python :: 3 :: 3.8 + Programming Language :: Python :: 3 :: 3.9 + Programming Language :: Python :: 3 :: 3.10 + License :: OSI Approved :: The Unlicense (Unlicense) + Operating System :: Microsoft :: Windows + Operating System :: Unix + Operating System :: MacOS + Topic :: Scientific/Engineering + Topic :: Software Development :: Libraries :: Java Libraries + Topic :: Software Development :: Libraries :: Python Modules + Topic :: Utilities + +[options] +packages = find: +package_dir = + = src +# Ensure any changes to this list are also added to environment.yml AND environment-test.yml! +python_requires = >=3.6 +install_requires = + jpype1 >= 1.3.0 + jgo + +[options.packages.find] +where = src + +[options.extras_require] + +# Ensure any changes to this list are also added to environment-test.yml! +test = + pytest + numpy + pandas diff --git a/setup.py b/setup.py deleted file mode 100644 index 5eec539d..00000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from setuptools import setup, find_packages -from os import path - -here = path.abspath(path.dirname(__file__)) - -with open(path.join(here, 'README.md')) as f: - scyjava_long_description = f.read() - -setup( - name='scyjava', - python_requires='>=3.6', - packages=find_packages(), - version="1.5.2.dev0", - author='Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner', - author_email='ctrueden@wisc.edu', - description='scyjava', - long_description=scyjava_long_description, - long_description_content_type='text/markdown', - license='Public domain', - url='https://github.com/scijava/scyjava', - install_requires=['jpype1 >= 1.3.0', 'jgo'], -) diff --git a/scyjava/__init__.py b/src/scyjava/__init__.py similarity index 100% rename from scyjava/__init__.py rename to src/scyjava/__init__.py diff --git a/scyjava/config/__init__.py b/src/scyjava/config/__init__.py similarity index 100% rename from scyjava/config/__init__.py rename to src/scyjava/config/__init__.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..45e8305d --- /dev/null +++ b/tox.ini @@ -0,0 +1,19 @@ +[tox] +envlist = py{36, 37, 38, 39, 310}-{linux, macos, windows} +isolated_build = true +toxworkdir = /tmp/.tox + +[testenv] +platform = + macos: darwin + linux: linux + windows: win32 + +deps = + .[test] + +setenv = + PYTHONPATH={toxinidir} + +commands = + pytest From c91b8c8e7384566977a262ec997599e7a9d63963 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 6 Apr 2022 16:00:12 -0500 Subject: [PATCH 132/505] Format code using black --- src/scyjava/__init__.py | 260 +++++++++++++++++++-------------- src/scyjava/config/__init__.py | 32 ++-- tests/test_convert.py | 142 +++++++++--------- tests/test_jvm.py | 5 +- tests/test_pandas.py | 21 ++- 5 files changed, 255 insertions(+), 205 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index fb686afd..babab00a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -62,29 +62,31 @@ def jvm_version(): raise RuntimeError(f"Invalid default JVM path: {p}") java = None - for _ in range(3): # The bin folder is always <=3 levels up from libjvm. + for _ in range(3): # The bin folder is always <=3 levels up from libjvm. p = p.parent - if p.name == 'lib': - java = p.parent / 'bin' / 'java' - elif p.name == 'bin': - java = p / 'java' + if p.name == "lib": + java = p.parent / "bin" / "java" + elif p.name == "bin": + java = p / "java" if java is not None: - if os.name == 'nt': + if os.name == "nt": # Good ol' Windows! Nothing beats Windows. - java = java.with_suffix('.exe') + java = java.with_suffix(".exe") if not java.is_file(): raise RuntimeError(f"No ../bin/java found at: {p}") break if java is None: raise RuntimeError(f"No java executable found inside: {p}") - version = subprocess.check_output([str(java), '-version'], stderr=subprocess.STDOUT).decode() + version = subprocess.check_output( + [str(java), "-version"], stderr=subprocess.STDOUT + ).decode() m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) if not m: raise RuntimeError(f"Inscrutable java command output:\n{version}") - return tuple(map(int, m.group(1).split('.'))) + return tuple(map(int, m.group(1).split("."))) def start_jvm(options=scyjava.config.get_options()): @@ -100,7 +102,7 @@ def start_jvm(options=scyjava.config.get_options()): """ # if JVM is already running -- break if jvm_started(): - _logger.debug('The JVM is already running.') + _logger.debug("The JVM is already running.") return # retrieve endpoint and repositories from scyjava config @@ -108,14 +110,14 @@ def start_jvm(options=scyjava.config.get_options()): repositories = scyjava.config.get_repositories() # use the logger to notify user that endpoints are being added - _logger.debug('Adding jars from endpoints {0}'.format(endpoints)) + _logger.debug("Adding jars from endpoints {0}".format(endpoints)) # get endpoints and add to JPype class path if len(endpoints) > 0: endpoints = endpoints[:1] + sorted(endpoints[1:]) - _logger.debug('Using endpoints %s', endpoints) + _logger.debug("Using endpoints %s", endpoints) _, workspace = jgo.resolve_dependencies( - '+'.join(endpoints), + "+".join(endpoints), m2_repo=scyjava.config.get_m2_repo(), cache_dir=scyjava.config.get_cache_dir(), manage_dependencies=scyjava.config.get_manage_deps(), @@ -123,7 +125,7 @@ def start_jvm(options=scyjava.config.get_options()): verbose=scyjava.config.get_verbose(), shortcuts=scyjava.config.get_shortcuts(), ) - jpype.addClassPath(os.path.join(workspace, '*')) + jpype.addClassPath(os.path.join(workspace, "*")) # initialize JPype JVM jpype.startJVM(*options, interrupt=True) @@ -134,38 +136,61 @@ def start_jvm(options=scyjava.config.get_options()): atexit.register(shutdown_jvm) # grab needed Java classes - global Boolean; Boolean = jimport('java.lang.Boolean') - global Byte; Byte = jimport('java.lang.Byte') - global Character; Character = jimport('java.lang.Character') - global Double; Double = jimport('java.lang.Double') - global Float; Float = jimport('java.lang.Float') - global Integer; Integer = jimport('java.lang.Integer') - global Iterable; Iterable = jimport('java.lang.Iterable') - global Long; Long = jimport('java.lang.Long') - global Object; Object = jimport('java.lang.Object') - global Short; Short = jimport('java.lang.Short') - global String; String = jimport('java.lang.String') - global Void; Void = jimport('java.lang.Void') - global BigDecimal; BigDecimal = jimport('java.math.BigDecimal') - global BigInteger; BigInteger = jimport('java.math.BigInteger') - global ArrayList; ArrayList = jimport('java.util.ArrayList') - global Collection; Collection = jimport('java.util.Collection') - global Iterator; Iterator = jimport('java.util.Iterator') - global LinkedHashMap; LinkedHashMap = jimport('java.util.LinkedHashMap') - global LinkedHashSet; LinkedHashSet = jimport('java.util.LinkedHashSet') - global List; List = jimport('java.util.List') - global Map; Map = jimport('java.util.Map') - global Set; Set = jimport('java.util.Set') + global Boolean + Boolean = jimport("java.lang.Boolean") + global Byte + Byte = jimport("java.lang.Byte") + global Character + Character = jimport("java.lang.Character") + global Double + Double = jimport("java.lang.Double") + global Float + Float = jimport("java.lang.Float") + global Integer + Integer = jimport("java.lang.Integer") + global Iterable + Iterable = jimport("java.lang.Iterable") + global Long + Long = jimport("java.lang.Long") + global Object + Object = jimport("java.lang.Object") + global Short + Short = jimport("java.lang.Short") + global String + String = jimport("java.lang.String") + global Void + Void = jimport("java.lang.Void") + global BigDecimal + BigDecimal = jimport("java.math.BigDecimal") + global BigInteger + BigInteger = jimport("java.math.BigInteger") + global ArrayList + ArrayList = jimport("java.util.ArrayList") + global Collection + Collection = jimport("java.util.Collection") + global Iterator + Iterator = jimport("java.util.Iterator") + global LinkedHashMap + LinkedHashMap = jimport("java.util.LinkedHashMap") + global LinkedHashSet + LinkedHashSet = jimport("java.util.LinkedHashSet") + global List + List = jimport("java.util.List") + global Map + Map = jimport("java.util.Map") + global Set + Set = jimport("java.util.Set") # invoke registered callback functions for callback in _startup_callbacks: callback() + def shutdown_jvm(): """Shutdown the JVM. Shutdown the JVM. Set the jpype .config.destroy_jvm flag to true - to ask JPype to destory the JVM itself. Note that enabling + to ask JPype to destory the JVM itself. Note that enabling jpype.config.destroy_jvm can lead to delayed shutdown times while the JVM is waiting for threads to finish. """ @@ -175,9 +200,9 @@ def shutdown_jvm(): callback() except Exception as e: print(f"Exception during shutdown callback: {e}") - + # clean up remaining awt windows - Window = jimport('java.awt.Window') + Window = jimport("java.awt.Window") for w in Window.getWindows(): w.dispose() @@ -187,6 +212,7 @@ def shutdown_jvm(): except Exception as e: print(f"Exception during JVM shutdown: {e}") + def jvm_started(): """Return true iff a Java virtual machine (JVM) has been started.""" return jpype.isJVMStarted() @@ -209,6 +235,7 @@ def when_jvm_starts(f): global _startup_callbacks _startup_callbacks.append(f) + def when_jvm_stops(f): """ Registers a function to be called when the JVM starts (or immediately). @@ -224,37 +251,41 @@ def when_jvm_stops(f): # -- Utility functions -- + def get_version(java_class): - """Return the version of a Java class. """ - VersionUtils = jimport('org.scijava.util.VersionUtils') + """Return the version of a Java class.""" + VersionUtils = jimport("org.scijava.util.VersionUtils") version = VersionUtils.getVersion(java_class) return version + def compare_version(version, java_class_version): """ Return a boolean on a version comparison. True is returned if the Java class version is higher than the specified version. False is returned if the specified version is higher than the Java class version. """ - VersionUtils = jimport('org.scijava.util.VersionUtils') + VersionUtils = jimport("org.scijava.util.VersionUtils") comparison = VersionUtils.compare(version, java_class_version) < 0 return comparison + # -- Type Conversion Utilities -- # TODO: It would be cool to just use org.scijava.priority.Priority. # Unfortunately, we cannot do that without bringing in all of SJC. # Once SciJava 3 is mainstream, we could use a SciJava Priority module :) class Priority: - FIRST = 1E300 - EXTREMELY_HIGH = 1E6 - VERY_HIGH = 1E4 - HIGH = 1E2 + FIRST = 1e300 + EXTREMELY_HIGH = 1e6 + VERY_HIGH = 1e4 + HIGH = 1e2 NORMAL = 0 - LOW = -1E2 - VERY_LOW = -1E4 - EXTREMELY_LOW = -1E6 - LAST = -1E300 + LOW = -1e2 + VERY_LOW = -1e4 + EXTREMELY_LOW = -1e6 + LAST = -1e300 + class Converter(NamedTuple): predicate: Callable[[Any], bool] @@ -264,7 +295,7 @@ class Converter(NamedTuple): def _convert(obj: Any, converters: typing.List[Converter]) -> Any: suitable_converters = filter(lambda c: c.predicate(obj), converters) - prioritized = max(suitable_converters, key = lambda c: c.priority) + prioritized = max(suitable_converters, key=lambda c: c.priority) return prioritized.converter(obj) @@ -277,6 +308,7 @@ def _add_converter(converter: Converter, converters: typing.List[Converter]): # Adapted from code posted by vslotman on GitHub: # https://github.com/kivy/pyjnius/issues/217#issue-145981070 + def isjava(data): """Return whether the given data object is a Java object.""" return isinstance(data, jpype.JClass) or isinstance(data, _JObject) @@ -301,7 +333,7 @@ def jclass(data): return data.getClass() if isinstance(data, str): return jclass(jimport(data)) - raise TypeError('Cannot glean class from data of type: ' + str(type(data))) + raise TypeError("Cannot glean class from data of type: " + str(type(data))) @lru_cache(maxsize=None) @@ -335,18 +367,18 @@ def jstacktrace(exc): if no stack trace could be extracted. """ try: - StringWriter = jimport('java.io.StringWriter') - PrintWriter = jimport('java.io.PrintWriter') + StringWriter = jimport("java.io.StringWriter") + PrintWriter = jimport("java.io.PrintWriter") sw = StringWriter() exc.printStackTrace(PrintWriter(sw, True)) return sw.toString() except: - return '' + return "" def _raise_type_exception(obj: Any): - raise TypeError('Unsupported type: ' + str(type(obj))) - + raise TypeError("Unsupported type: " + str(type(obj))) + def _convertMap(obj: collections.abc.Mapping): jmap = LinkedHashMap() @@ -373,7 +405,7 @@ def _convertIterable(obj: collections.abc.Iterable): return jlist -java_converters : typing.List[Converter] = [] +java_converters: typing.List[Converter] = [] def add_java_converter(converter: Converter): @@ -414,24 +446,24 @@ def _stock_java_converters() -> typing.List[Converter]: Converter( predicate=lambda obj: True, converter=_raise_type_exception, - priority=Priority.EXTREMELY_LOW - 1 + priority=Priority.EXTREMELY_LOW - 1, ), # NoneType converter Converter( predicate=lambda obj: obj is None, converter=lambda obj: None, - priority=Priority.EXTREMELY_HIGH + 1 + priority=Priority.EXTREMELY_HIGH + 1, ), # Java identity converter Converter( predicate=isjava, converter=lambda obj: obj, - priority=Priority.EXTREMELY_HIGH + priority=Priority.EXTREMELY_HIGH, ), # String converter Converter( - predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: String(obj.encode('utf-8'), 'utf-8'), + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: String(obj.encode("utf-8"), "utf-8"), ), # Boolean converter Converter( @@ -440,43 +472,49 @@ def _stock_java_converters() -> typing.List[Converter]: ), # Integer converter Converter( - predicate=lambda obj: isinstance(obj, int) and obj <= Integer.MAX_VALUE and obj >= Integer.MIN_VALUE, - converter= Integer, + predicate=lambda obj: isinstance(obj, int) + and obj <= Integer.MAX_VALUE + and obj >= Integer.MIN_VALUE, + converter=Integer, ), # Long converter Converter( predicate=lambda obj: isinstance(obj, int) and obj <= Long.MAX_VALUE, converter=Long, - priority=Priority.NORMAL - 1 + priority=Priority.NORMAL - 1, ), # BigInteger converter Converter( predicate=lambda obj: isinstance(obj, int), converter=lambda obj: BigInteger(str(obj)), - priority=Priority.NORMAL - 2 + priority=Priority.NORMAL - 2, ), # Float converter Converter( - predicate=lambda obj: isinstance(obj, float) and obj <= Float.MAX_VALUE and obj >= Float.MIN_VALUE, - converter= Float, + predicate=lambda obj: isinstance(obj, float) + and obj <= Float.MAX_VALUE + and obj >= Float.MIN_VALUE, + converter=Float, ), # Double converter Converter( - predicate=lambda obj: isinstance(obj, float) and obj <= Double.MAX_VALUE and obj >= Float.MIN_VALUE, + predicate=lambda obj: isinstance(obj, float) + and obj <= Double.MAX_VALUE + and obj >= Float.MIN_VALUE, converter=Double, - priority=Priority.NORMAL - 1 + priority=Priority.NORMAL - 1, ), # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, float), converter=lambda obj: BigDecimal(str(obj)), - priority=Priority.NORMAL - 2 + priority=Priority.NORMAL - 2, ), # Pandas table converter Converter( - predicate=lambda obj: type(obj).__name__ == 'DataFrame', + predicate=lambda obj: type(obj).__name__ == "DataFrame", converter=_pandas_to_table, - priority=Priority.NORMAL + 1 + priority=Priority.NORMAL + 1, ), # Mapping converter Converter( @@ -492,13 +530,13 @@ def _stock_java_converters() -> typing.List[Converter]: Converter( predicate=lambda obj: isinstance(obj, collections.abc.Iterable), converter=_convertIterable, - priority=Priority.NORMAL -1 + priority=Priority.NORMAL - 1, ), ] when_jvm_starts( - lambda : [_add_converter(c, java_converters) for c in _stock_java_converters()] + lambda: [_add_converter(c, java_converters) for c in _stock_java_converters()] ) @@ -509,15 +547,17 @@ def _jstr(data): if isinstance(data, JavaObject): return str(data) # NB: We want Python strings to render in single quotes. - return '{!r}'.format(data) + return "{!r}".format(data) -class JavaObject(): +class JavaObject: def __init__(self, jobj, intended_class=None): if intended_class is None: intended_class = Object if not isinstance(jobj, intended_class): - raise TypeError('Not a ' + intended_class.getName() + ': ' + jclass(jobj).getName()) + raise TypeError( + "Not a " + intended_class.getName() + ": " + jclass(jobj).getName() + ) self.jobj = jobj def __str__(self): @@ -532,7 +572,7 @@ def __iter__(self): return to_python(self.jobj.iterator()) def __str__(self): - return '[' + ', '.join(_jstr(v) for v in self) + ']' + return "[" + ", ".join(_jstr(v) for v in self) + "]" class JavaCollection(JavaIterable, collections.abc.Collection): @@ -630,7 +670,9 @@ def __eq__(self, other): return False def __str__(self): - return '{' + ', '.join(_jstr(k) + ': ' + _jstr(v) for k,v in self.items()) + '}' + return ( + "{" + ", ".join(_jstr(k) + ": " + _jstr(v) for k, v in self.items()) + "}" + ) class JavaSet(JavaCollection, collections.abc.MutableSet): @@ -660,10 +702,10 @@ def __eq__(self, other): return False def __str__(self): - return '{' + ', '.join(_jstr(v) for v in self) + '}' + return "{" + ", ".join(_jstr(v) for v in self) + "}" -py_converters : typing.List[Converter] = [] +py_converters: typing.List[Converter] = [] def add_py_converter(converter: Converter): @@ -674,7 +716,7 @@ def add_py_converter(converter: Converter): _add_converter(converter, py_converters) -def to_python(data: Any, gentle: bool =False) -> Any: +def to_python(data: Any, gentle: bool = False) -> Any: """ Recursively convert a Java object to a Python object. :param data: The Java object to convert. @@ -699,7 +741,8 @@ def to_python(data: Any, gentle: bool =False) -> Any: try: return _convert(data, py_converters) except TypeError as exc: - if gentle: return data + if gentle: + return data raise exc @@ -714,37 +757,37 @@ def _stock_py_converters() -> typing.List: Converter( predicate=lambda obj: True, converter=_raise_type_exception, - priority=Priority.EXTREMELY_LOW - 1 + priority=Priority.EXTREMELY_LOW - 1, ), # Java identity converter Converter( predicate=lambda obj: not isjava(obj), converter=lambda obj: obj, - priority=Priority.EXTREMELY_HIGH + priority=Priority.EXTREMELY_HIGH, ), # JBoolean converter Converter( predicate=lambda obj: isinstance(obj, JBoolean), converter=bool, - priority=Priority.NORMAL + 1 + priority=Priority.NORMAL + 1, ), # JInt/JLong/JShort converter Converter( predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), converter=int, - priority=Priority.NORMAL + 1 + priority=Priority.NORMAL + 1, ), # JDouble/JFloat converter Converter( predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), converter=float, - priority=Priority.NORMAL + 1 + priority=Priority.NORMAL + 1, ), # JChar converter Converter( predicate=lambda obj: isinstance(obj, JChar), converter=str, - priority=Priority.NORMAL + 1 + priority=Priority.NORMAL + 1, ), # Boolean converter Converter( @@ -793,7 +836,7 @@ def _stock_py_converters() -> typing.List: ), # String converter Converter( - predicate=lambda obj: isinstance(obj, String), + predicate=lambda obj: isinstance(obj, String), converter=lambda obj: str(obj), ), # BigInteger converter @@ -830,38 +873,38 @@ def _stock_py_converters() -> typing.List: Converter( predicate=lambda obj: isinstance(obj, Collection), converter=JavaCollection, - priority=Priority.NORMAL -1 + priority=Priority.NORMAL - 1, ), # Iterable converter Converter( predicate=lambda obj: isinstance(obj, Iterable), converter=JavaIterable, - priority=Priority.NORMAL -1 + priority=Priority.NORMAL - 1, ), # Iterator converter Converter( predicate=lambda obj: isinstance(obj, Iterator), converter=JavaIterator, - priority=Priority.NORMAL - 1 + priority=Priority.NORMAL - 1, ), # JArray converter Converter( predicate=lambda obj: isinstance(obj, JArray), - converter=lambda obj:[to_python(o) for o in obj], - priority=Priority.VERY_LOW + converter=lambda obj: [to_python(o) for o in obj], + priority=Priority.VERY_LOW, ), ] when_jvm_starts( - lambda : [_add_converter(c, py_converters) for c in _stock_py_converters()] + lambda: [_add_converter(c, py_converters) for c in _stock_py_converters()] ) def _is_table(obj: Any) -> bool: """Checks if obj is a table""" try: - return isinstance(obj, jimport('org.scijava.table.Table')) + return isinstance(obj, jimport("org.scijava.table.Table")) except: # No worries if scijava-table is not available. pass @@ -870,7 +913,7 @@ def _is_table(obj: Any) -> bool: def _convert_table(obj: Any): """Converts obj to a table.""" try: - return _table_to_pandas(obj) + return _table_to_pandas(obj) except: # No worries if scijava-table is not available. pass @@ -879,6 +922,7 @@ def _convert_table(obj: Any): def _import_pandas(): try: import pandas as pd + return pd except ImportError: msg = "The Pandas library is missing (http://pandas.pydata.org/). " @@ -893,7 +937,7 @@ def _table_to_pandas(table): headers = [] for i, column in enumerate(table.toArray()): data.append(column.toArray()) - headers.append(str(table.getColumnHeader(i))) + headers.append(str(table.getColumnHeader(i))) df = pd.DataFrame(data).T df.columns = headers return df @@ -903,15 +947,15 @@ def _pandas_to_table(df): pd = _import_pandas() if len(df.dtypes.unique()) > 1: - TableClass = jimport('org.scijava.table.DefaultGenericTable') + TableClass = jimport("org.scijava.table.DefaultGenericTable") else: table_type = df.dtypes.unique()[0] - if table_type.name.startswith('float'): - TableClass = jimport('org.scijava.table.DefaultFloatTable') - elif table_type.name.startswith('int'): - TableClass = jimport('org.scijava.table.DefaultIntTable') - elif table_type.name.startswith('bool'): - TableClass = jimport('org.scijava.table.DefaultBoolTable') + if table_type.name.startswith("float"): + TableClass = jimport("org.scijava.table.DefaultFloatTable") + elif table_type.name.startswith("int"): + TableClass = jimport("org.scijava.table.DefaultIntTable") + elif table_type.name.startswith("bool"): + TableClass = jimport("org.scijava.table.DefaultBoolTable") else: msg = "The type '{}' is not supported.".format(table_type.name) raise Exception(msg) diff --git a/src/scyjava/config/__init__.py b/src/scyjava/config/__init__.py index bf450e09..1a3fbc74 100644 --- a/src/scyjava/config/__init__.py +++ b/src/scyjava/config/__init__.py @@ -7,11 +7,11 @@ _logger = logging.getLogger(__name__) endpoints = [] -_repositories = {'scijava.public': maven_scijava_repository()} +_repositories = {"scijava.public": maven_scijava_repository()} _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 = {} @@ -21,9 +21,11 @@ def add_endpoints(*new_endpoints): DEPRECATED since v1.2.1 Please modify the endpoints field directly instead. """ - _logger.warning('Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead.') + _logger.warning( + "Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead." + ) global endpoints - _logger.debug('Adding endpoints %s to %s', new_endpoints, endpoints) + _logger.debug("Adding endpoints %s to %s", new_endpoints, endpoints) endpoints.extend(new_endpoints) @@ -32,7 +34,9 @@ def get_endpoints(): DEPRECATED since v1.2.1 Please access the endpoints field directly instead. """ - _logger.warning('Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead.') + _logger.warning( + "Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead." + ) global endpoints return endpoints @@ -40,9 +44,9 @@ def get_endpoints(): def add_repositories(*args, **kwargs): global _repositories for arg in args: - _logger.debug('Adding repositories %s to %s', arg, _repositories) + _logger.debug("Adding repositories %s to %s", arg, _repositories) _repositories.update(arg) - _logger.debug('Adding repositories %s to %s', kwargs, _repositories) + _logger.debug("Adding repositories %s to %s", kwargs, _repositories) _repositories.update(kwargs) @@ -53,19 +57,19 @@ def get_repositories(): def set_verbose(level): global _verbose - _logger.debug('Setting verbose level to %d (was %d)', level, _verbose) + _logger.debug("Setting verbose level to %d (was %d)", level, _verbose) _verbose = level def get_verbose(): global _verbose - _logger.debug('Getting verbose level: %d', _verbose) + _logger.debug("Getting verbose level: %d", _verbose) return _verbose def set_manage_deps(manage): global _manage_deps - _logger.debug('Setting manage deps to %d (was %d)', manage, _manage_deps) + _logger.debug("Setting manage deps to %d (was %d)", manage, _manage_deps) _manage_deps = manage @@ -76,7 +80,7 @@ def get_manage_deps(): def set_cache_dir(dir): global _cache_dir - _logger.debug('Setting cache dir to %s (was %s)', dir, _cache_dir) + _logger.debug("Setting cache dir to %s (was %s)", dir, _cache_dir) _cache_dir = dir @@ -87,7 +91,7 @@ def get_cache_dir(): def set_m2_repo(dir): global _m2_repo - _logger.debug('Setting m2 repo dir to %s (was %s)', dir, _m2_repo) + _logger.debug("Setting m2 repo dir to %s (was %s)", dir, _m2_repo) _m2_repo = dir @@ -135,7 +139,7 @@ def find_jars(directory): jars = [] for root, dirs, files in os.walk(directory): for f in files: - if f.lower().endswith('.jar'): + if f.lower().endswith(".jar"): path = os.path.join(root, f) jars.append(path) return jars diff --git a/tests/test_convert.py b/tests/test_convert.py index 2dbf20e6..7e2dd4ed 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -3,8 +3,9 @@ from jpype import JArray, JInt, JLong from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python -config.endpoints.append('org.scijava:scijava-table') -config.add_option('-Djava.awt.headless=true') +config.endpoints.append("org.scijava:scijava-table") +config.add_option("-Djava.awt.headless=true") + def assert_same_table(table, df): assert len(table.toArray()) == df.shape[1] @@ -17,42 +18,41 @@ def assert_same_table(table, df): class TestConvert(unittest.TestCase): - def testClass(self): """ Tests class detection from Java objects. """ int_class = jclass(to_java(5)) - self.assertEqual('java.lang.Integer', int_class.getName()) + self.assertEqual("java.lang.Integer", int_class.getName()) long_class = jclass(to_java(4000000001)) - self.assertEqual('java.lang.Long', long_class.getName()) + self.assertEqual("java.lang.Long", long_class.getName()) bigint_class = jclass(to_java(9879999999999999789)) - self.assertEqual('java.math.BigInteger', bigint_class.getName()) + self.assertEqual("java.math.BigInteger", bigint_class.getName()) - string_class = jclass(to_java('foobar')) - self.assertEqual('java.lang.String', string_class.getName()) + string_class = jclass(to_java("foobar")) + self.assertEqual("java.lang.String", string_class.getName()) list_class = jclass(to_java([1, 2, 3])) - self.assertEqual('java.util.ArrayList', list_class.getName()) + self.assertEqual("java.util.ArrayList", list_class.getName()) - map_class = jclass(to_java({'a':'b'})) - self.assertEqual('java.util.LinkedHashMap', map_class.getName()) + map_class = jclass(to_java({"a": "b"})) + self.assertEqual("java.util.LinkedHashMap", map_class.getName()) - self.assertEqual('java.util.Map', jclass('java.util.Map').getName()) + self.assertEqual("java.util.Map", jclass("java.util.Map").getName()) def testBoolean(self): jt = to_java(True) self.assertEqual(True, jt.booleanValue()) pt = to_python(jt) self.assertEqual(True, pt) - self.assertEqual('True', str(pt)) + self.assertEqual("True", str(pt)) jf = to_java(False) self.assertEqual(False, jf.booleanValue()) pf = to_python(jf) self.assertEqual(False, pf) - self.assertEqual('False', str(pf)) + self.assertEqual("False", str(pf)) def testInteger(self): i = 5 @@ -79,7 +79,7 @@ def testBigInteger(self): self.assertEqual(str(bi), str(pbi)) def testFloat(self): - f = 5. + f = 5.0 jf = to_java(f) self.assertEqual(f, jf.floatValue()) pf = to_python(jf) @@ -87,7 +87,7 @@ def testFloat(self): self.assertEqual(str(f), str(pf)) def testDouble(self): - Float = jimport('java.lang.Float') + Float = jimport("java.lang.Float") d = Float.MAX_VALUE * 2 jd = to_java(d) self.assertEqual(d, jd.doubleValue()) @@ -96,7 +96,7 @@ def testDouble(self): self.assertEqual(str(d), str(pd)) def testString(self): - s = 'Hello world!' + s = "Hello world!" js = to_java(s) for e, a in zip(s, js.toCharArray()): self.assertEqual(e, a) @@ -105,19 +105,19 @@ def testString(self): self.assertEqual(str(s), str(ps)) def testList(self): - l = 'The quick brown fox jumps over the lazy dogs'.split() + l = "The quick brown fox jumps over the lazy dogs".split() jl = to_java(l) for e, a in zip(l, jl): self.assertEqual(e, to_python(a)) pl = to_python(jl) self.assertEqual(l, pl) self.assertEqual(str(l), str(pl)) - self.assertEqual(pl[1], 'quick') - pl[7] = 'silly' - self.assertEqual('The quick brown fox jumps over the silly dogs', ' '.join(pl)) + self.assertEqual(pl[1], "quick") + pl[7] = "silly" + self.assertEqual("The quick brown fox jumps over the silly dogs", " ".join(pl)) def testSet(self): - s = set(['orange', 'apple', 'pineapple', 'plum']) + s = set(["orange", "apple", "pineapple", "plum"]) js = to_java(s) self.assertEqual(len(s), js.size()) for e in s: @@ -136,21 +136,21 @@ def testArray(self): def testDict(self): d = { - 'access_log': [ - {'stored_proc': 'getsomething'}, - {'uses': [ - {'usedin': 'some->bread->crumb'}, - {'usedin': 'something else here'}, - {'stored_proc': 'anothersp'} - ]}, - {'uses': [ - {'usedin': 'blahblah'} - ]} + "access_log": [ + {"stored_proc": "getsomething"}, + { + "uses": [ + {"usedin": "some->bread->crumb"}, + {"usedin": "something else here"}, + {"stored_proc": "anothersp"}, + ] + }, + {"uses": [{"usedin": "blahblah"}]}, + ], + "reporting": [ + {"stored_proc": "reportingsp"}, + {"uses": [{"usedin": "breadcrumb"}]}, ], - 'reporting': [ - {'stored_proc': 'reportingsp'}, - {'uses': [{'usedin': 'breadcrumb'}]} - ] } jd = to_java(d) self.assertEqual(len(d), jd.size()) @@ -163,12 +163,12 @@ def testDict(self): self.assertEqual(str(d), str(pd)) def testMixed(self): - d = {'a':'b', 'c':'d'} - l = ['e', 'f', 'g', 'h'] - s = set(['i', 'j', 'k']) + d = {"a": "b", "c": "d"} + l = ["e", "f", "g", "h"] + s = set(["i", "j", "k"]) # mixed types in a dictionary - md = {'d': d, 'l': l, 's': s, 'str': 'hello'} + md = {"d": d, "l": l, "s": s, "str": "hello"} jmd = to_java(md) self.assertEqual(len(md), jmd.size()) for k, v in md.items(): @@ -180,7 +180,7 @@ def testMixed(self): self.assertEqual(str(md), str(pmd)) # mixed types in a list - ml = [d, l, s, 'hello'] + ml = [d, l, s, "hello"] jml = to_java(ml) for e, a in zip(ml, jml): self.assertEqual(e, to_python(a)) @@ -189,17 +189,17 @@ def testMixed(self): self.assertEqual(str(ml), str(pml)) def testNone(self): - d = {'key':None, None:'value', 'foo':'bar'} + d = {"key": None, None: "value", "foo": "bar"} jd = to_java(d) self.assertEqual(3, jd.size()) - self.assertEqual(None, jd.get('key')) - self.assertEqual('value', jd.get(None)) - self.assertEqual('bar', jd.get('foo')) + self.assertEqual(None, jd.get("key")) + self.assertEqual("value", jd.get(None)) + self.assertEqual("bar", jd.get("foo")) pd = to_python(jd) self.assertEqual(d, pd) def testGentle(self): - Object = jimport('java.lang.Object') + Object = jimport("java.lang.Object") unknown_thing = Object() converted_thing = to_python(unknown_thing, gentle=True) assert type(converted_thing) == Object @@ -213,48 +213,50 @@ def testGentle(self): def testStructureWithSomeUnsupportedItems(self): # Create Java data structure with some challenging items. - Object = jimport('java.lang.Object') - jmap = to_java({ - 'list': ['a', Object(), 1], - 'set': {'x', Object(), 2}, - 'object': Object(), - 'foo': 'bar' - }) - self.assertEqual('java.util.LinkedHashMap', jclass(jmap).getName()) + Object = jimport("java.lang.Object") + jmap = to_java( + { + "list": ["a", Object(), 1], + "set": {"x", Object(), 2}, + "object": Object(), + "foo": "bar", + } + ) + self.assertEqual("java.util.LinkedHashMap", jclass(jmap).getName()) # Convert it back to Python. pdict = to_python(jmap) - l = pdict['list'] - self.assertEqual(pdict['list'][0], 'a') - assert type(pdict['list'][1]) == Object - assert pdict['list'][2] == 1 - assert 'x' in pdict['set'] - assert 2 in pdict['set'] - assert len(pdict['set']) == 3 - assert type(pdict['object']) == Object - self.assertEqual(pdict['foo'], 'bar') - + l = pdict["list"] + self.assertEqual(pdict["list"][0], "a") + assert type(pdict["list"][1]) == Object + assert pdict["list"][2] == 1 + assert "x" in pdict["set"] + assert 2 in pdict["set"] + assert len(pdict["set"]) == 3 + assert type(pdict["object"]) == Object + self.assertEqual(pdict["foo"], "bar") def test_conversion_priority(self): # Add a converter prioritized over the default converter - String = jimport('java.lang.String') - invader = 'Not Hello World' + String = jimport("java.lang.String") + invader = "Not Hello World" from scyjava import add_java_converter + add_java_converter( Converter( predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: String(invader.encode('utf-8'), 'utf-8'), - priority=100 + converter=lambda obj: String(invader.encode("utf-8"), "utf-8"), + priority=100, ) ) # Ensure that the conversion uses our new converter - s = 'Hello world!' + s = "Hello world!" js = to_java(s) for e, a in zip(invader, js.toCharArray()): self.assertEqual(e, a) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_jvm.py b/tests/test_jvm.py index a981fce8..564e6757 100644 --- a/tests/test_jvm.py +++ b/tests/test_jvm.py @@ -1,6 +1,7 @@ import scyjava import unittest + class TestJVM(unittest.TestCase): """ Tests scyjava JVM management functions. @@ -16,7 +17,7 @@ def test_jvm_version(self): self.assertTrue(len(before_version) >= 3) self.assertTrue(before_version[0] > 0) - scyjava.config.add_option('-Djava.awt.headless=true') + scyjava.config.add_option("-Djava.awt.headless=true") scyjava.start_jvm() after_version = scyjava.jvm_version() @@ -27,5 +28,5 @@ def test_jvm_version(self): self.assertEqual(before_version, after_version) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 9c384805..f8e16f00 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -4,8 +4,8 @@ import unittest from scyjava import config, jimport, to_java -config.endpoints.append('org.scijava:scijava-table') -config.add_option('-Djava.awt.headless=true') +config.endpoints.append("org.scijava:scijava-table") +config.add_option("-Djava.awt.headless=true") def assert_same_table(table, df): @@ -19,7 +19,6 @@ def assert_same_table(table, df): class TestPandas(unittest.TestCase): - def testPandasToTable(self): # Float table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -29,18 +28,18 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport('org.scijava.table.DefaultFloatTable') + assert type(table) == jimport("org.scijava.table.DefaultFloatTable") # Int table. columns = ["header1", "header2", "header3", "header4", "header5"] array = np.random.random(size=(7, 5)) * 100 - array = array.astype('int') + array = array.astype("int") df = pd.DataFrame(array, columns=columns) table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport('org.scijava.table.DefaultIntTable') + assert type(table) == jimport("org.scijava.table.DefaultIntTable") # Bool table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -50,7 +49,7 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport('org.scijava.table.DefaultBoolTable') + assert type(table) == jimport("org.scijava.table.DefaultBoolTable") # Mixed table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -59,18 +58,18 @@ def testPandasToTable(self): df = pd.DataFrame(array, columns=columns) # Convert column 0 to integer - df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype('int') + df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype("int") # Convert column 1 to bool df.iloc[:, 1] = df.iloc[:, 1] > 0.5 # Convert column 2 to string - df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split('\n') + df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split("\n") table = to_java(df) # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) - assert type(table) == jimport('org.scijava.table.DefaultGenericTable') + assert type(table) == jimport("org.scijava.table.DefaultGenericTable") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 64dc8731dc0778f76c21c6bf3df7f80dbbc0e735 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 6 Apr 2022 16:11:11 -0500 Subject: [PATCH 133/505] Add Github action for linting with black --- .github/workflows/python-test-conda.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index 7327276b..d1aab09c 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -52,3 +52,8 @@ jobs: - name: Test Pandas run: | python tests/test_pandas.py + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: psf/black@stable From 2d5391103da6f99c56a7b1254e05467c2ba19385 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Wed, 6 Apr 2022 17:54:42 -0500 Subject: [PATCH 134/505] Use codecov TBD whether we adopt this tech. --- .github/workflows/python-test-conda.yml | 31 +++++++++++++++++++++++++ .gitignore | 7 +++++- codecov.yml | 2 ++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index d1aab09c..af3cc9d2 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -57,3 +57,34 @@ jobs: steps: - uses: actions/checkout@v2 - uses: psf/black@stable + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} + - name: Add conda to system path + run: | + # $CONDA is an environment variable pointing to the root of the miniconda directory + echo $CONDA/bin >> $GITHUB_PATH + - name: Install mamba + run: | + conda install -c conda-forge mamba + - name: Install dependencies + run: | + mamba env update --file environment-test.yml --name base + - name: Install current project in dev mode + run: | + pip install -e . + - name: Install pytest + run: | + mamba install -c conda-forge pytest-cov + - name: Generate Report + run: | + python -m pytest --cov-report=xml --cov=. + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v2 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index f3f038f3..24fb4994 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,10 @@ /.eggs/ *egg-info/ -# vi +# Vi *.swp + +# Unit test / coverage reports +.coverage +.coverage.* +coverage.xml \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..aa84c676 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "*/tests/*" From 9230bbe73f41fd2586522dfe78d3a7a50087011f Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 7 Apr 2022 10:23:51 -0500 Subject: [PATCH 135/505] Clean up github actions --- .github/workflows/python-test-conda.yml | 35 ++++++++++++------------- environment-test.yml | 2 ++ tox.ini | 24 ++++++++++++++++- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml index af3cc9d2..b8690e44 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-conda.yml @@ -12,11 +12,22 @@ on: jobs: build-cross-platform: + name: test ${{matrix.os}} - ${{matrix.python-version}} runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8"] + os: [ + ubuntu-latest, + windows-latest, + macos-latest + ] + python-version: [ + '3.6', + '3.7', + '3.8', + '3.9', + '3.10' + ] steps: - uses: actions/checkout@v2 @@ -36,22 +47,10 @@ jobs: mamba env update --file environment-test.yml --name base - name: Install current project in dev mode run: | - pip install -e . - - name: Install pytest - run: | - mamba install -c conda-forge pytest - - name: Test with pytest - run: | - pytest - - name: Test Convert - run: | - python tests/test_convert.py - - name: Test JVM - run: | - python tests/test_jvm.py - - name: Test Pandas - run: | - python tests/test_pandas.py + python -m pip install -e . + - name: Run test suite + run: python -m pytest -p no:faulthandler --color=yes + lint: runs-on: ubuntu-latest steps: diff --git a/environment-test.yml b/environment-test.yml index cdaeb0b2..49ac00d0 100644 --- a/environment-test.yml +++ b/environment-test.yml @@ -5,6 +5,8 @@ channels: - conda-forge - defaults dependencies: + - pytest + - pytest-cov - jpype1 - jgo - numpy diff --git a/tox.ini b/tox.ini index 45e8305d..6923a51f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,9 @@ envlist = py{36, 37, 38, 39, 310}-{linux, macos, windows} isolated_build = true toxworkdir = /tmp/.tox +requires= + tox-conda + tox-gh-actions [testenv] platform = @@ -12,8 +15,27 @@ platform = deps = .[test] +passenv = + CI + GITHUB_ACTIONS + JAVA_HOME + setenv = PYTHONPATH={toxinidir} commands = - pytest + python -m pytest -p no:faulthandler --color=yes + +[gh-actions] +python = + 3.6 = py36 + 3.7 = py37 + 3.8 = py38 + 3.9 = py39 + 3.10 = py310 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows \ No newline at end of file From 7dc3f4a77ada2a31bb876951f63de52ec8416c37 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 7 Apr 2022 12:50:27 -0500 Subject: [PATCH 136/505] Remove tox file There isn't a good use for this. --- tox.ini | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6923a51f..00000000 --- a/tox.ini +++ /dev/null @@ -1,41 +0,0 @@ -[tox] -envlist = py{36, 37, 38, 39, 310}-{linux, macos, windows} -isolated_build = true -toxworkdir = /tmp/.tox -requires= - tox-conda - tox-gh-actions - -[testenv] -platform = - macos: darwin - linux: linux - windows: win32 - -deps = - .[test] - -passenv = - CI - GITHUB_ACTIONS - JAVA_HOME - -setenv = - PYTHONPATH={toxinidir} - -commands = - python -m pytest -p no:faulthandler --color=yes - -[gh-actions] -python = - 3.6 = py36 - 3.7 = py37 - 3.8 = py38 - 3.9 = py39 - 3.10 = py310 - -[gh-actions:env] -PLATFORM = - ubuntu-latest: linux - macos-latest: macos - windows-latest: windows \ No newline at end of file From feb63d010362a5582cf74048193b8152477d2a2a Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 7 Apr 2022 15:57:36 -0500 Subject: [PATCH 137/505] Add myself as author --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3641673b..37a85102 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = scyjava version = 1.5.2.dev0 -author = Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner +author = Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner, Gabriel Selzer author_email = ctrueden@wisc.edu description = Supercharged Java access from Python long_description = file: README.md From a88be1c85a5c8876fd07d9a55572b445e54be18b Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 7 Apr 2022 16:05:14 -0500 Subject: [PATCH 138/505] Add additional project urls --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 37a85102..bc293cbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,8 @@ long_description_content_type = text/markdown url = https://github.com/scijava/scyjava project_urls = Bug Tracker = https://github.com/scijava/scyjava/issues + Documentation = https://github.com/scijava/scyjava/blob/master/README.md + Source Code = https://github.com/scijava/scyjava classifiers = Intended Audience :: Developers Intended Audience :: Education From c3022fcaeb575b9a66d0e91e265fc056d96ade8f Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 7 Apr 2022 16:28:50 -0500 Subject: [PATCH 139/505] Add license Before it was "Public Domain", but that isn't really a license. I'll write "The Unlicense" --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index bc293cbf..d84a1f01 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ author_email = ctrueden@wisc.edu description = Supercharged Java access from Python long_description = file: README.md long_description_content_type = text/markdown +license= The Unlicense url = https://github.com/scijava/scyjava project_urls = Bug Tracker = https://github.com/scijava/scyjava/issues From a14efe854237add3362aa7e3f67f67968025ee43 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 8 Apr 2022 09:47:10 -0500 Subject: [PATCH 140/505] Add tests folder to the distribution This commit adds a MANIFEST.in to the project, which seems to be the preferred way to specify such things. See https://docs.python.org/3/distutils/sourcedist.html#specifying-the-files-to-distribute --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..1ba94da8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +# include these directories +recursive-include tests *.py \ No newline at end of file From 5d0367df8d7bf52b14128f693de4bd621054626e Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 11 Apr 2022 09:14:00 -0500 Subject: [PATCH 141/505] Add codecov badge to README Forgot to add this in my PR :sweat_smile: --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8e7027d..fc9840b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![build status](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml) +[![build status](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml) [![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) Supercharged Java access from Python. From f60cdbe95b2837d6218e655ed8432514dba5f582 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 11 Apr 2022 09:24:56 -0500 Subject: [PATCH 142/505] setup.cfg: Add development status @ctrueden suggested that we use a Production/Stable tag, and after I remembered that we are post-1.0, I agree! --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index d84a1f01..117c58c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,7 @@ project_urls = Documentation = https://github.com/scijava/scyjava/blob/master/README.md Source Code = https://github.com/scijava/scyjava classifiers = + Development Status :: 5 - Production/Stable Intended Audience :: Developers Intended Audience :: Education Intended Audience :: Science/Research From 3b8847a6a8b02308849443d54ea9d9a38ab06e97 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 14 Apr 2022 15:19:34 -0500 Subject: [PATCH 143/505] Add debug message before JVM startup --- src/scyjava/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index babab00a..05dacea6 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -128,6 +128,7 @@ def start_jvm(options=scyjava.config.get_options()): jpype.addClassPath(os.path.join(workspace, "*")) # initialize JPype JVM + _logger.debug("Starting JVM") jpype.startJVM(*options, interrupt=True) # replace JPype/JVM shutdown handling with our own From 3d77efd51ee5b7f590b33a4628ab5c634add3333 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 18 Apr 2022 13:24:13 -0500 Subject: [PATCH 144/505] Try to detect JAVA_HOME if unset This lets you, for example, use PyImageJ with a conda environment that has not been activated, as long as you use that environment's Python executable. See imagej/pyimagej#176. --- src/scyjava/__init__.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 05dacea6..d5136810 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -12,6 +12,7 @@ import re import scyjava.config import subprocess +import sys from pathlib import Path from jpype.types import * from _jpype import _JObject @@ -127,6 +128,33 @@ def start_jvm(options=scyjava.config.get_options()): ) jpype.addClassPath(os.path.join(workspace, "*")) + # HACK: Try to set JAVA_HOME if it isn't already. + if 'JAVA_HOME' not in os.environ \ + or not os.environ['JAVA_HOME'] \ + or not os.path.isdir(os.environ['JAVA_HOME']): + + _logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.") + + libjvm_win = Path('Library') / 'jre' / 'bin' / 'server' / 'jvm.dll' + libjvm_macos = Path('lib') / 'server' / 'libjvm.dylib' + libjvm_linux = Path('lib') / 'server' / 'libjvm.so' + libjvm_paths = { + libjvm_win: Path('Library'), + libjvm_macos: Path(), + libjvm_linux: Path(), + } + for p in sys.path: + if not p.endswith('site-packages'): continue + # e.g. $CONDA_PREFIX/lib/python3.10/site-packages -> $CONDA_PREFIX + # But we want it to work outside of Conda as well, theoretically. + base = Path(p).parent.parent.parent + for libjvm_path, java_home_path in libjvm_paths.items(): + if (base / libjvm_path).exists(): + java_home = str((base / java_home_path).resolve()) + _logger.debug(f"Detected JAVA_HOME: %s", java_home) + os.environ['JAVA_HOME'] = java_home + break + # initialize JPype JVM _logger.debug("Starting JVM") jpype.startJVM(*options, interrupt=True) From 67762c8f7a4a5666a3bb06e0cfac9ba97dcbb3d4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 20 Apr 2022 15:55:42 -0500 Subject: [PATCH 145/505] Make black happy --- src/scyjava/__init__.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index d5136810..74fd124e 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -129,22 +129,25 @@ def start_jvm(options=scyjava.config.get_options()): jpype.addClassPath(os.path.join(workspace, "*")) # HACK: Try to set JAVA_HOME if it isn't already. - if 'JAVA_HOME' not in os.environ \ - or not os.environ['JAVA_HOME'] \ - or not os.path.isdir(os.environ['JAVA_HOME']): + if ( + "JAVA_HOME" not in os.environ + or not os.environ["JAVA_HOME"] + or not os.path.isdir(os.environ["JAVA_HOME"]) + ): _logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.") - libjvm_win = Path('Library') / 'jre' / 'bin' / 'server' / 'jvm.dll' - libjvm_macos = Path('lib') / 'server' / 'libjvm.dylib' - libjvm_linux = Path('lib') / 'server' / 'libjvm.so' + libjvm_win = Path("Library") / "jre" / "bin" / "server" / "jvm.dll" + libjvm_macos = Path("lib") / "server" / "libjvm.dylib" + libjvm_linux = Path("lib") / "server" / "libjvm.so" libjvm_paths = { - libjvm_win: Path('Library'), + libjvm_win: Path("Library"), libjvm_macos: Path(), libjvm_linux: Path(), } for p in sys.path: - if not p.endswith('site-packages'): continue + if not p.endswith("site-packages"): + continue # e.g. $CONDA_PREFIX/lib/python3.10/site-packages -> $CONDA_PREFIX # But we want it to work outside of Conda as well, theoretically. base = Path(p).parent.parent.parent @@ -152,7 +155,7 @@ def start_jvm(options=scyjava.config.get_options()): if (base / libjvm_path).exists(): java_home = str((base / java_home_path).resolve()) _logger.debug(f"Detected JAVA_HOME: %s", java_home) - os.environ['JAVA_HOME'] = java_home + os.environ["JAVA_HOME"] = java_home break # initialize JPype JVM From 26ad79bc023f0435db821458c270c7d562f445eb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 27 Apr 2022 08:08:20 -0500 Subject: [PATCH 146/505] Fix BigDecimal conversion bug Closes #39. --- src/scyjava/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 74fd124e..62ed81e8 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -879,7 +879,7 @@ def _stock_py_converters() -> typing.List: # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, BigDecimal), - converter=lambda obj: float(obj.toString), + converter=lambda obj: float(str(obj.toString())), ), # SciJava Table converter Converter( From 61bc020b694a376460c29f6923e979f73c61a2d1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 20 May 2022 16:11:18 -0500 Subject: [PATCH 147/505] Require JPype 1.4.0+ This fixes fatal problems on Windows with Python 3.10. It's not strictly required for other scenarios, but this proactive change will help people to avoid non-functional combinations that would be otherwise allowed by the dependency rules. --- environment-test.yml | 2 +- environment.yml | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment-test.yml b/environment-test.yml index 49ac00d0..d8f4f346 100644 --- a/environment-test.yml +++ b/environment-test.yml @@ -7,7 +7,7 @@ channels: dependencies: - pytest - pytest-cov - - jpype1 + - jpype1 >= 1.4.0 - jgo - numpy - pandas diff --git a/environment.yml b/environment.yml index 30929762..b131348c 100644 --- a/environment.yml +++ b/environment.yml @@ -3,6 +3,6 @@ channels: - conda-forge - defaults dependencies: - - jpype1 + - jpype1 >= 1.4.0 - jgo - scyjava diff --git a/setup.cfg b/setup.cfg index 117c58c3..979ecc0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ package_dir = # Ensure any changes to this list are also added to environment.yml AND environment-test.yml! python_requires = >=3.6 install_requires = - jpype1 >= 1.3.0 + jpype1 >= 1.4.0 jgo [options.packages.find] From b63a6640b89c19b32d5de4d223ebc26b8bf06688 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 26 May 2022 11:39:56 -0500 Subject: [PATCH 148/505] Revert to JPype 1.3.0+ This mostly reverts commit 61bc020b694a376460c29f6923e979f73c61a2d1. Unfortunately, there is a holdup getting JPype 1.4.0 onto conda-forge. So we cannot depend on 1.4.0 yet. See jpype-project/jpype1#1072. --- environment-test.yml | 2 +- environment.yml | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment-test.yml b/environment-test.yml index d8f4f346..8c8226fe 100644 --- a/environment-test.yml +++ b/environment-test.yml @@ -7,7 +7,7 @@ channels: dependencies: - pytest - pytest-cov - - jpype1 >= 1.4.0 + - jpype1 >= 1.3.0 - jgo - numpy - pandas diff --git a/environment.yml b/environment.yml index b131348c..302a9c20 100644 --- a/environment.yml +++ b/environment.yml @@ -3,6 +3,6 @@ channels: - conda-forge - defaults dependencies: - - jpype1 >= 1.4.0 + - jpype1 >= 1.3.0 - jgo - scyjava diff --git a/setup.cfg b/setup.cfg index 979ecc0c..117c58c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ package_dir = # Ensure any changes to this list are also added to environment.yml AND environment-test.yml! python_requires = >=3.6 install_requires = - jpype1 >= 1.4.0 + jpype1 >= 1.3.0 jgo [options.packages.find] From 07a6feaf68b0482caed9ca17ae258951eb781ac8 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 10:02:45 -0500 Subject: [PATCH 149/505] Clean up build process The github actions script now is much more similar to PyImageJ's. --- .../{python-test-conda.yml => build.yml} | 38 ++++++++++--------- dev-environment.yml | 31 +++++++++++++++ environment-test.yml | 13 ------- environment.yml | 18 ++++++++- setup.cfg | 5 ++- 5 files changed, 73 insertions(+), 32 deletions(-) rename .github/workflows/{python-test-conda.yml => build.yml} (69%) create mode 100644 dev-environment.yml delete mode 100644 environment-test.yml diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/build.yml similarity index 69% rename from .github/workflows/python-test-conda.yml rename to .github/workflows/build.yml index b8690e44..e8c79b4f 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/build.yml @@ -28,28 +28,32 @@ jobs: '3.9', '3.10' ] + # Breaks due to https://github.com/jpype-project/jpype/issues/1009 + # Should be included once the fix is released + exclude: + - os: windows-latest + python-version: "3.10" steps: - uses: actions/checkout@v2 - - uses: conda-incubator/setup-miniconda@v2 + + - uses: actions/setup-python@v3 with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - name: Install mamba - run: | - conda install -c conda-forge mamba - - name: Install dependencies + python-version: ${{matrix.python-version}} + + - uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'zulu' + + - name: Install ScyJava run: | - mamba env update --file environment-test.yml --name base - - name: Install current project in dev mode + python -m pip install --upgrade pip + python -m pip install -e '.[dev]' + + - name: Test ScyJava run: | - python -m pip install -e . - - name: Run test suite - run: python -m pytest -p no:faulthandler --color=yes + python -m pytest -p no:faulthandler --color=yes lint: runs-on: ubuntu-latest @@ -74,7 +78,7 @@ jobs: conda install -c conda-forge mamba - name: Install dependencies run: | - mamba env update --file environment-test.yml --name base + mamba env update --file dev-environment.yml --name base - name: Install current project in dev mode run: | pip install -e . diff --git a/dev-environment.yml b/dev-environment.yml new file mode 100644 index 00000000..9646b491 --- /dev/null +++ b/dev-environment.yml @@ -0,0 +1,31 @@ +# Use this file to construct an environment +# for developing scyjava from source. +# +# mamba env create -f dev-environment.yml +# conda activate scyjava-dev +# +# In addition to the dependencies needed for using scyjava, it includes tools +# for developer-related actions like running automated tests (pytest), +# linting the code (black), and generating the API documentation (sphinx). +# If you want an environment without these tools, use environment.yml. +name: scyjava-dev +channels: + - conda-forge + - defaults +dependencies: + # Project dependencies + - jpype1 >= 1.3.0 + - jgo + # Test dependencies + - numpy + - pandas + # Developer tools + - black + - build + - pytest + - pytest-cov + # Project from source + - pip + - pip: + - -e . + diff --git a/environment-test.yml b/environment-test.yml deleted file mode 100644 index 8c8226fe..00000000 --- a/environment-test.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Use this environment file when running the tests, when scyjava will be -# installed from source -name: scyjava -channels: - - conda-forge - - defaults -dependencies: - - pytest - - pytest-cov - - jpype1 >= 1.3.0 - - jgo - - numpy - - pandas diff --git a/environment.yml b/environment.yml index 302a9c20..3f3c0760 100644 --- a/environment.yml +++ b/environment.yml @@ -1,8 +1,24 @@ +# Use this file to construct an environment for working +# with scyjava in a runtime setting +# +# mamba env create +# conda activate scyjava +# +# It includes the dependencies needed for using scyjava but not tools +# for developer-related actions like running automated tests (pytest), +# linting the code (black), and generating the API documentation (sphinx). +# If you want an environment including these tools, use dev-environment.yml. + name: scyjava channels: - conda-forge - defaults dependencies: + # Project dependencies - jpype1 >= 1.3.0 - jgo - - scyjava + - openjdk=8 + # Project from source + - pip + - pip: + - -e . diff --git a/setup.cfg b/setup.cfg index 117c58c3..12f60533 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,10 @@ where = src [options.extras_require] # Ensure any changes to this list are also added to environment-test.yml! -test = +dev = + black + build pytest + pytest-cov numpy pandas From 1ad304e69e423a00a6b8a116086907acea4ab677 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 10:54:22 -0500 Subject: [PATCH 150/505] Improve conda test and code coverage --- .github/workflows/build.yml | 39 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8c79b4f..fac5ed5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,33 +61,32 @@ jobs: - uses: actions/checkout@v2 - uses: psf/black@stable - coverage: + conda-dev-test: + name: Conda Setup & Code Coverage runs-on: ubuntu-latest + defaults: + # Steps that rely on the activated environment must be run with this shell setup. + # See https://github.com/marketplace/actions/setup-miniconda#important + run: + shell: bash -l {0} steps: - uses: actions/checkout@v2 - uses: conda-incubator/setup-miniconda@v2 with: + # Create env with dev packages auto-update-conda: true - python-version: ${{ matrix.python-version }} - - name: Add conda to system path + python-version: 3.9 + environment-file: dev-environment.yml + # Activate imglyb-dev environment + activate-environment: imglyb-dev + auto-activate-base: false + # Use mamba for faster setup + use-mamba: true + mamba-version: "*" + - name: Test imglyb run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - name: Install mamba - run: | - conda install -c conda-forge mamba - - name: Install dependencies - run: | - mamba env update --file dev-environment.yml --name base - - name: Install current project in dev mode - run: | - pip install -e . - - name: Install pytest - run: | - mamba install -c conda-forge pytest-cov - - name: Generate Report - run: | - python -m pytest --cov-report=xml --cov=. + python -m pytest tests/ -p no:faulthandler --cov-report=xml --cov=. + # We could do this in its own action, but we'd have to setup the environment again. - name: Upload Coverage to Codecov uses: codecov/codecov-action@v2 \ No newline at end of file From ab21f2c4d3a8e920273c5aba5c6c683dfd653dd5 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 11:09:23 -0500 Subject: [PATCH 151/505] Bump minimum python version to 3.7 --- .github/workflows/build.yml | 1 - setup.cfg | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fac5ed5b..db91e38b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,6 @@ jobs: macos-latest ] python-version: [ - '3.6', '3.7', '3.8', '3.9', diff --git a/setup.cfg b/setup.cfg index 12f60533..671b3387 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ classifiers = Intended Audience :: Education Intended Audience :: Science/Research Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3 :: 3.6 Programming Language :: Python :: 3 :: 3.7 Programming Language :: Python :: 3 :: 3.8 Programming Language :: Python :: 3 :: 3.9 @@ -37,7 +36,7 @@ packages = find: package_dir = = src # Ensure any changes to this list are also added to environment.yml AND environment-test.yml! -python_requires = >=3.6 +python_requires = >=3.7 install_requires = jpype1 >= 1.3.0 jgo From 198703e922fce999c144cf8ec97ce76f93c9231e Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 12:50:05 -0500 Subject: [PATCH 152/505] Use setuptools_scm to determine version --- .gitignore | 3 ++ pyproject.toml | 11 +++++++- setup.cfg | 1 - src/scyjava/__init__.py | 61 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 24fb4994..59c41e56 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ /.eggs/ *egg-info/ +# setuptools_scm +src/scyjava/_version.py + # Vi *.swp diff --git a/pyproject.toml b/pyproject.toml index 31434581..6e84a8ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,15 @@ [build-system] # Setuptools version constraint ensures PEP 517 compatibility requires = [ - "setuptools >= 40.9.0" + "setuptools >= 45", + "wheel", + "setuptools_scm>=6.2", ] build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "src/scyjava/version.py" + +[tool.black] +# This file is autogenerated by setuptools_scm - it cannot be modified +--exclude = "src/scyjava/version.py" diff --git a/setup.cfg b/setup.cfg index 671b3387..03024c1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,5 @@ [metadata] name = scyjava -version = 1.5.2.dev0 author = Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner, Gabriel Selzer author_email = ctrueden@wisc.edu description = Supercharged Java access from Python diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 62ed81e8..ee785f64 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -14,11 +14,72 @@ import subprocess import sys from pathlib import Path +from typing import Dict from jpype.types import * from _jpype import _JObject _logger = logging.getLogger(__name__) +# Set of module properties +_MODULE_PROPERTIES: Dict[str, Callable] = {} + + +def module_property(func: Callable[[], Any]) -> Callable[[], Any]: + """ + Turns a function into a property of this module + Functions decorated with this property must have a + leading underscore! + :param func: The function to turn into a property + """ + if func.__name__[0] != "_": + raise ValueError( + f"""Function {func.__name__} must have + a leading underscore in its name + to become a module property!""" + ) + name = func.__name__[1:] + _MODULE_PROPERTIES[name] = func + return func + + +def __getattr__(name): + """ + Runs as a fallback when this module does not have an + attribute. + :param name: The name of the attribute being searched for. + """ + if name in _MODULE_PROPERTIES: + return _MODULE_PROPERTIES[name]() + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +@module_property +@lru_cache(maxsize=None) +def ___version__(): + # First pass: use the version output by setuptools_scm + try: + import scyjava.version + + return scyjava.version.version + except ImportError: + pass + # Second pass: use importlib.metadata + try: + from importlib.metadata import version, PackageNotFoundError + + return version("scyjava") + except ImportError or PackageNotFoundError: + pass + # Third pass: use pkg_resources + try: + from pkg_resources import get_distribution, DistributionNotFound + + return get_distribution("scyjava").version + except DistributionNotFound: + pass + # Fourth pass: Give up + return "Cannot determine version! Ensure pkg_resources is installed!" + # -- JVM setup -- From 02c113673befcf75c690e5db38da88188acad7f0 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 13:16:41 -0500 Subject: [PATCH 153/505] Remove traceback import It is not used --- src/scyjava/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ee785f64..34d8dd89 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1,7 +1,6 @@ import atexit import collections.abc from functools import lru_cache -import traceback from typing import Any, Callable, NamedTuple import typing import jgo From 18a07d634e6f5c507672849853a1ef45a9265d68 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 21 Apr 2022 13:16:51 -0500 Subject: [PATCH 154/505] Remove lru_cache in favor of cache param This makes it easier to read --- src/scyjava/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 34d8dd89..59b51740 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -23,7 +23,7 @@ _MODULE_PROPERTIES: Dict[str, Callable] = {} -def module_property(func: Callable[[], Any]) -> Callable[[], Any]: +def module_property(func: Callable[[], Any], cache=True) -> Callable[[], Any]: """ Turns a function into a property of this module Functions decorated with this property must have a @@ -37,6 +37,8 @@ def module_property(func: Callable[[], Any]) -> Callable[[], Any]: to become a module property!""" ) name = func.__name__[1:] + if cache: + func = (lru_cache(maxsize=None))(func) _MODULE_PROPERTIES[name] = func return func @@ -53,7 +55,6 @@ def __getattr__(name): @module_property -@lru_cache(maxsize=None) def ___version__(): # First pass: use the version output by setuptools_scm try: From e09cd9045573bb09c60be0916660e6b72228146d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 11:10:47 -0500 Subject: [PATCH 155/505] Switch to pytest We were already depending on it for code coverage, so let's just make the full switch --- .gitignore | 2 +- src/scyjava/__init__.py | 2 +- tests/test_convert.py | 134 +++++++++++++++++++--------------------- tests/test_jvm.py | 21 +++---- tests/test_pandas.py | 7 +-- 5 files changed, 75 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index 59c41e56..6f4ab9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ *egg-info/ # setuptools_scm -src/scyjava/_version.py +src/scyjava/version.py # Vi *.swp diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 59b51740..8552423b 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -75,7 +75,7 @@ def ___version__(): from pkg_resources import get_distribution, DistributionNotFound return get_distribution("scyjava").version - except DistributionNotFound: + except ImportError: pass # Fourth pass: Give up return "Cannot determine version! Ensure pkg_resources is installed!" diff --git a/tests/test_convert.py b/tests/test_convert.py index 7e2dd4ed..6f5c50f9 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,5 +1,3 @@ -import unittest - from jpype import JArray, JInt, JLong from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python @@ -17,114 +15,114 @@ def assert_same_table(table, df): assert table.getColumnHeader(i) == df.columns[i] -class TestConvert(unittest.TestCase): +class TestConvert(object): def testClass(self): """ Tests class detection from Java objects. """ int_class = jclass(to_java(5)) - self.assertEqual("java.lang.Integer", int_class.getName()) + "java.lang.Integer" == int_class.getName() long_class = jclass(to_java(4000000001)) - self.assertEqual("java.lang.Long", long_class.getName()) + "java.lang.Long" == long_class.getName() bigint_class = jclass(to_java(9879999999999999789)) - self.assertEqual("java.math.BigInteger", bigint_class.getName()) + "java.math.BigInteger" == bigint_class.getName() string_class = jclass(to_java("foobar")) - self.assertEqual("java.lang.String", string_class.getName()) + "java.lang.String" == string_class.getName() list_class = jclass(to_java([1, 2, 3])) - self.assertEqual("java.util.ArrayList", list_class.getName()) + assert "java.util.ArrayList" == list_class.getName() map_class = jclass(to_java({"a": "b"})) - self.assertEqual("java.util.LinkedHashMap", map_class.getName()) + "java.util.LinkedHashMap" == map_class.getName() - self.assertEqual("java.util.Map", jclass("java.util.Map").getName()) + "java.util.Map" == jclass("java.util.Map").getName() def testBoolean(self): jt = to_java(True) - self.assertEqual(True, jt.booleanValue()) + assert True == jt.booleanValue() pt = to_python(jt) - self.assertEqual(True, pt) - self.assertEqual("True", str(pt)) + assert True == pt + assert "True" == str(pt) jf = to_java(False) - self.assertEqual(False, jf.booleanValue()) + assert False == jf.booleanValue() pf = to_python(jf) - self.assertEqual(False, pf) - self.assertEqual("False", str(pf)) + assert False == pf + assert "False" == str(pf) def testInteger(self): i = 5 ji = to_java(i) - self.assertEqual(i, ji.intValue()) + assert i == ji.intValue() pi = to_python(ji) - self.assertEqual(i, pi) - self.assertEqual(str(i), str(pi)) + assert i == pi + assert str(i) == str(pi) def testLong(self): l = 4000000001 jl = to_java(l) - self.assertEqual(l, jl.longValue()) + assert l == jl.longValue() pl = to_python(jl) - self.assertEqual(l, pl) - self.assertEqual(str(l), str(pl)) + assert l == pl + assert str(l) == str(pl) def testBigInteger(self): bi = 9879999999999999789 jbi = to_java(bi) - self.assertEqual(bi, int(str(jbi.toString()))) + assert bi == int(str(jbi.toString())) pbi = to_python(jbi) - self.assertEqual(bi, pbi) - self.assertEqual(str(bi), str(pbi)) + assert bi == pbi + assert str(bi) == str(pbi) def testFloat(self): f = 5.0 jf = to_java(f) - self.assertEqual(f, jf.floatValue()) + assert f == jf.floatValue() pf = to_python(jf) - self.assertEqual(f, pf) - self.assertEqual(str(f), str(pf)) + assert f == pf + assert str(f) == str(pf) def testDouble(self): Float = jimport("java.lang.Float") d = Float.MAX_VALUE * 2 jd = to_java(d) - self.assertEqual(d, jd.doubleValue()) + assert d == jd.doubleValue() pd = to_python(jd) - self.assertEqual(d, pd) - self.assertEqual(str(d), str(pd)) + assert d == pd + assert str(d) == str(pd) def testString(self): s = "Hello world!" js = to_java(s) for e, a in zip(s, js.toCharArray()): - self.assertEqual(e, a) + assert e == a ps = to_python(js) - self.assertEqual(s, ps) - self.assertEqual(str(s), str(ps)) + assert s == ps + assert str(s) == str(ps) def testList(self): l = "The quick brown fox jumps over the lazy dogs".split() jl = to_java(l) for e, a in zip(l, jl): - self.assertEqual(e, to_python(a)) + assert e == to_python(a) pl = to_python(jl) - self.assertEqual(l, pl) - self.assertEqual(str(l), str(pl)) - self.assertEqual(pl[1], "quick") + assert l == pl + assert str(l) == str(pl) + assert pl[1] == "quick" pl[7] = "silly" - self.assertEqual("The quick brown fox jumps over the silly dogs", " ".join(pl)) + assert "The quick brown fox jumps over the silly dogs" == " ".join(pl) def testSet(self): s = set(["orange", "apple", "pineapple", "plum"]) js = to_java(s) - self.assertEqual(len(s), js.size()) + assert len(s) == js.size() for e in s: - self.assertTrue(js.contains(to_java(e))) + assert js.contains(to_java(e)) ps = to_python(js) - self.assertEqual(s, ps) - self.assertEqual(str(s), str(ps)) + assert s == ps + assert str(s) == str(ps) def testArray(self): start_jvm() @@ -153,14 +151,14 @@ def testDict(self): ], } jd = to_java(d) - self.assertEqual(len(d), jd.size()) + assert len(d) == jd.size() for k, v in d.items(): jk = to_java(k) - self.assertTrue(jd.containsKey(jk)) - self.assertEqual(v, to_python(jd.get(jk))) + jd.containsKey(jk) + assert v == to_python(jd.get(jk)) pd = to_python(jd) - self.assertEqual(d, pd) - self.assertEqual(str(d), str(pd)) + assert d == pd + assert str(d) == str(pd) def testMixed(self): d = {"a": "b", "c": "d"} @@ -170,33 +168,33 @@ def testMixed(self): # mixed types in a dictionary md = {"d": d, "l": l, "s": s, "str": "hello"} jmd = to_java(md) - self.assertEqual(len(md), jmd.size()) + assert len(md) == jmd.size() for k, v in md.items(): jk = to_java(k) - self.assertTrue(jmd.containsKey(jk)) - self.assertEqual(v, to_python(jmd.get(jk))) + jmd.containsKey(jk) + assert v == to_python(jmd.get(jk)) pmd = to_python(jmd) - self.assertEqual(md, pmd) - self.assertEqual(str(md), str(pmd)) + assert md == pmd + assert str(md) == str(pmd) # mixed types in a list ml = [d, l, s, "hello"] jml = to_java(ml) for e, a in zip(ml, jml): - self.assertEqual(e, to_python(a)) + assert e == to_python(a) pml = to_python(jml) - self.assertEqual(ml, pml) - self.assertEqual(str(ml), str(pml)) + assert ml == pml + assert str(ml) == str(pml) def testNone(self): d = {"key": None, None: "value", "foo": "bar"} jd = to_java(d) - self.assertEqual(3, jd.size()) - self.assertEqual(None, jd.get("key")) - self.assertEqual("value", jd.get(None)) - self.assertEqual("bar", jd.get("foo")) + assert 3 == jd.size() + assert None == jd.get("key") + assert "value" == jd.get(None) + assert "bar" == jd.get("foo") pd = to_python(jd) - self.assertEqual(d, pd) + assert d == pd def testGentle(self): Object = jimport("java.lang.Object") @@ -209,7 +207,7 @@ def testGentle(self): except: # NB: Failure is expected here. pass - self.assertIsNone(bad_conversion) + assert bad_conversion is None def testStructureWithSomeUnsupportedItems(self): # Create Java data structure with some challenging items. @@ -222,19 +220,19 @@ def testStructureWithSomeUnsupportedItems(self): "foo": "bar", } ) - self.assertEqual("java.util.LinkedHashMap", jclass(jmap).getName()) + assert "java.util.LinkedHashMap" == jclass(jmap).getName() # Convert it back to Python. pdict = to_python(jmap) l = pdict["list"] - self.assertEqual(pdict["list"][0], "a") + assert pdict["list"][0] == "a" assert type(pdict["list"][1]) == Object assert pdict["list"][2] == 1 assert "x" in pdict["set"] assert 2 in pdict["set"] assert len(pdict["set"]) == 3 assert type(pdict["object"]) == Object - self.assertEqual(pdict["foo"], "bar") + assert pdict["foo"] == "bar" def test_conversion_priority(self): # Add a converter prioritized over the default converter @@ -255,8 +253,4 @@ def test_conversion_priority(self): s = "Hello world!" js = to_java(s) for e, a in zip(invader, js.toCharArray()): - self.assertEqual(e, a) - - -if __name__ == "__main__": - unittest.main() + assert e == a diff --git a/tests/test_jvm.py b/tests/test_jvm.py index 564e6757..cc429c8e 100644 --- a/tests/test_jvm.py +++ b/tests/test_jvm.py @@ -1,8 +1,7 @@ import scyjava -import unittest -class TestJVM(unittest.TestCase): +class TestJVM(object): """ Tests scyjava JVM management functions. """ @@ -13,20 +12,16 @@ def test_jvm_version(self): """ before_version = scyjava.jvm_version() - self.assertTrue(before_version is not None) - self.assertTrue(len(before_version) >= 3) - self.assertTrue(before_version[0] > 0) + assert before_version is not None + assert len(before_version) >= 3 + assert before_version[0] > 0 scyjava.config.add_option("-Djava.awt.headless=true") scyjava.start_jvm() after_version = scyjava.jvm_version() - self.assertTrue(after_version is not None) - self.assertTrue(len(after_version) >= 3) - self.assertTrue(after_version[0] > 0) + assert after_version is not None + assert len(after_version) >= 3 + assert after_version[0] > 0 - self.assertEqual(before_version, after_version) - - -if __name__ == "__main__": - unittest.main() + assert before_version == after_version diff --git a/tests/test_pandas.py b/tests/test_pandas.py index f8e16f00..7895eb72 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,7 +1,6 @@ import numpy as np import numpy.testing as npt import pandas as pd -import unittest from scyjava import config, jimport, to_java config.endpoints.append("org.scijava:scijava-table") @@ -18,7 +17,7 @@ def assert_same_table(table, df): assert table.getColumnHeader(i) == df.columns[i] -class TestPandas(unittest.TestCase): +class TestPandas(object): def testPandasToTable(self): # Float table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -69,7 +68,3 @@ def testPandasToTable(self): # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) assert type(table) == jimport("org.scijava.table.DefaultGenericTable") - - -if __name__ == "__main__": - unittest.main() From 2e4554e2e51b84872e2845d4470c9966bee29b45 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 13:35:48 -0500 Subject: [PATCH 156/505] Add tests for scyjava.__version__ --- dev-environment.yml | 1 + setup.cfg | 1 + tests/test_version.py | 84 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/test_version.py diff --git a/dev-environment.yml b/dev-environment.yml index 9646b491..816e9a7b 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -24,6 +24,7 @@ dependencies: - build - pytest - pytest-cov + - setuptools-scm >= 6.2 # Project from source - pip - pip: diff --git a/setup.cfg b/setup.cfg index 03024c1d..c830ef06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,3 +53,4 @@ dev = pytest-cov numpy pandas + setuptools-scm >= 6.2 diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..dc0f5300 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,84 @@ +import os +import pytest +import sys + +setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "version.py") + + +def _scyjava_version(): + """ + Get ScyJava's version. + """ + import scyjava + + # It's important that we clear the cache here, + # so that we can test different behaviors. + scyjava.___version__.cache_clear() + # Get the version + return scyjava.__version__ + + +def test_version_file(): + """Ensures that, ideally, the version from setuptools_scm is used""" + # Get the version from setuptools_scm + from setuptools_scm import get_version + + setuptools_version = get_version(write_to="src/scyjava/version.py") + # Ensure that the version was written to file + assert os.path.isfile(setuptools_file) + # Ensure that scyjava.__version__ matches this. + assert _scyjava_version() == setuptools_version + # Cleanup - remove file + os.remove(setuptools_file) + assert not os.path.isfile(setuptools_file) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") +def test_version_importlib(): + """ + Ensures that, with scyjava.version.version unavailable, + importlib.metadata is used next WITH python 3.8+ + """ + # Remove scyjava.version + sys.modules["scyjava.version"] = None + # Ensure scyjava.__version__ matches importlib.metadata.version() + from importlib.metadata import version + + assert _scyjava_version() == version("scyjava") + + +@pytest.mark.skipif( + sys.version_info >= (3, 8), reason="importlib used instead for Python 3.8+" +) +def test_version_pkg_resources(): + """ + Ensures that, with scyjava.version.version AND + importlib.metadata unavailable, + pkg_resources is used next. + """ + # Remove scyjava.version + sys.modules["scyjava.version"] = None + # Remove importlib.metadata + sys.modules["importlib.metadata"] = None + # Ensure scyjava.__version__ matches pkg_resources.get_distribution().version + from pkg_resources import get_distribution + + assert _scyjava_version() == get_distribution("scyjava").version + + +def test_version_unvailable(): + """ + Ensures that no version is returned if none of these + strategies works. + """ + # Remove scyjava.version + sys.modules["scyjava.version"] = None + # Remove importlib.metadata + sys.modules["importlib.metadata"] = None + # Remove pkg_resources + sys.modules["pkg_resources"] = None + # Ensure scyjava.__version__ is an error message. + assert ( + _scyjava_version() + == "Cannot determine version! Ensure pkg_resources is installed!" + ) From e440e51a5751e6a16882987509e575186c9f5113 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 14:02:17 -0500 Subject: [PATCH 157/505] Remove .vscode file from repository --- .vscode/settings.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ff801d3f..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.formatting.provider": "black" -} \ No newline at end of file From 0276845018dea2ea4ce133f9bfbadac21fc1cca3 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 14:02:39 -0500 Subject: [PATCH 158/505] .gitignore: ignore .vscode --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6f4ab9a6..e3227377 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,10 @@ src/scyjava/version.py # Vi *.swp +# VSCode +.vscode + # Unit test / coverage reports .coverage .coverage.* -coverage.xml \ No newline at end of file +coverage.xml From 62e2bce874d68ac1d2e59e5c51f063a7e9d23e94 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 14:07:34 -0500 Subject: [PATCH 159/505] Remove MANIFEST.in We instead rely on setuptools_scm to manage the files added to the sdist. The discussion on whether test and documentation files belong in the sdist is an ongoing one (see https://discuss.python.org/t/should-sdists-include-docs-and-tests/14578?page=5 ), and it's my impression that the participants in the dicussion want to leave this up to the tool building the sdist. So we will do that! --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1ba94da8..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -# include these directories -recursive-include tests *.py \ No newline at end of file From c8c8100a52db460e53ace05161846a2baaa9ce90 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 16:44:03 -0500 Subject: [PATCH 160/505] flake code --- setup.cfg | 5 ++ src/scyjava/__init__.py | 86 ++++++++++++++++--------- src/scyjava/config/__init__.py | 6 +- tests/test_convert.py | 112 +++++++++++++++------------------ tests/test_pandas.py | 13 ++-- tests/test_version.py | 6 +- 6 files changed, 129 insertions(+), 99 deletions(-) diff --git a/setup.cfg b/setup.cfg index c830ef06..49b351b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,3 +54,8 @@ dev = numpy pandas setuptools-scm >= 6.2 + +[flake8] +# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 +max-line-length = 88 +extend-ignore = E203 diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 8552423b..863ad4d8 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -14,7 +14,16 @@ import sys from pathlib import Path from typing import Dict -from jpype.types import * +from jpype.types import ( + JArray, + JBoolean, + JChar, + JDouble, + JFloat, + JInt, + JLong, + JShort, +) from _jpype import _JObject _logger = logging.getLogger(__name__) @@ -72,7 +81,7 @@ def ___version__(): pass # Third pass: use pkg_resources try: - from pkg_resources import get_distribution, DistributionNotFound + from pkg_resources import get_distribution return get_distribution("scyjava").version except ImportError: @@ -89,8 +98,10 @@ def ___version__(): def jvm_version(): """ - Gets the version of the JVM as a tuple, with each dot-separated digit as one element. - Characters in the version string beyond only numbers and dots are ignored, in line + Gets the version of the JVM as a tuple, + with each dot-separated digit as one element. + Characters in the version string beyond only + numbers and dots are ignored, in line with the java.version system property. Examples: @@ -98,12 +109,16 @@ def jvm_version(): * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] * OpenJDK 1.8.0_312 -> [1, 8, 0] - If the JVM is already started, this function should return the equivalent of: - jimport('java.lang.System').getProperty('java.version').split('.') + If the JVM is already started, + this function should return the equivalent of: + jimport('java.lang.System') + .getProperty('java.version') + .split('.') - In case the JVM is not started yet, a best effort is made to deduce the - version from the environment without actually starting up the JVM in-process. - If the version cannot be deduced, a RuntimeError with the cause is raised. + In case the JVM is not started yet,a best effort is made to deduce + the version from the environment without actually starting up the + JVM in-process. If the version cannot be deduced, a RuntimeError + with the cause is raised. """ jvm_version = jpype.getJVMVersion() if jvm_version and jvm_version[0]: @@ -113,7 +128,8 @@ def jvm_version(): return jvm_version # JPype was clueless, which means the JVM has probably not started yet. - # Let's look for a java executable, and ask it directly with 'java -version'. + # Let's look for a java executable, and ask it directly with 'java + # -version'. default_jvm_path = jpype.getDefaultJVMPath() if not default_jvm_path: @@ -215,7 +231,7 @@ def start_jvm(options=scyjava.config.get_options()): for libjvm_path, java_home_path in libjvm_paths.items(): if (base / libjvm_path).exists(): java_home = str((base / java_home_path).resolve()) - _logger.debug(f"Detected JAVA_HOME: %s", java_home) + _logger.debug(f"Detected JAVA_HOME: {java_home}") os.environ["JAVA_HOME"] = java_home break @@ -465,7 +481,7 @@ def jstacktrace(exc): sw = StringWriter() exc.printStackTrace(PrintWriter(sw, True)) return sw.toString() - except: + except BaseException: return "" @@ -528,6 +544,10 @@ def to_java(obj: Any) -> Any: return _convert(obj, java_converters) +def _is_a_long(obj) -> bool: + return isinstance(obj, int) and obj <= Long.MAX_VALUE + + def _stock_java_converters() -> typing.List[Converter]: """ Returns all python-to-java converters supported out of the box! @@ -572,7 +592,7 @@ def _stock_java_converters() -> typing.List[Converter]: ), # Long converter Converter( - predicate=lambda obj: isinstance(obj, int) and obj <= Long.MAX_VALUE, + predicate=_is_a_long, converter=Long, priority=Priority.NORMAL - 1, ), @@ -628,9 +648,12 @@ def _stock_java_converters() -> typing.List[Converter]: ] -when_jvm_starts( - lambda: [_add_converter(c, java_converters) for c in _stock_java_converters()] -) +def _initialize_converters(): + for converter in _stock_java_converters(): + _add_converter(converter, java_converters) + + +when_jvm_starts(_initialize_converters) # -- Java to Python -- @@ -649,7 +672,7 @@ def __init__(self, jobj, intended_class=None): intended_class = Object if not isinstance(jobj, intended_class): raise TypeError( - "Not a " + intended_class.getName() + ": " + jclass(jobj).getName() + f"Not a {intended_class.getName()}: {jclass(jobj).getName()}" ) self.jobj = jobj @@ -713,7 +736,8 @@ def __getitem__(self, key): return to_python(self.jobj.get(key), gentle=True) def __setitem__(self, key, value): - # NB: List.set(int, Object) returns inserted element, so be gentle here. + # NB: List.set(int, Object) returns inserted element, so be gentle + # here. return to_python(self.jobj.set(key, to_java(value)), gentle=True) def __delitem__(self, key): @@ -721,7 +745,8 @@ def __delitem__(self, key): return to_python(self.jobj.remove(to_java(key))) def insert(self, index, object): - # NB: List.set(int, Object) returns inserted element, so be gentle here. + # NB: List.set(int, Object) returns inserted element, so be gentle + # here. return to_python(self.jobj.set(index, to_java(object)), gentle=True) @@ -735,8 +760,10 @@ def __getitem__(self, key): return to_python(self.jobj.get(to_java(key)), gentle=True) def __setitem__(self, key, value): - # NB: Map.put(Object, Object) returns inserted value, so be gentle here. - return to_python(self.jobj.put(to_java(key), to_java(value)), gentle=True) + # NB: Map.put(Object, Object) returns inserted value, so be gentle + # here. + put_return: bool = self.jobj.put(to_java(key), to_java(value)) + return to_python(put_return, gentle=True) def __delitem__(self, key): # NB: Map.remove(Object) returns the removed key, so be gentle here. @@ -756,16 +783,17 @@ def __eq__(self, other): if len(self) != len(other): return False for k in self: - if not k in other or self[k] != other[k]: + if k not in other or self[k] != other[k]: return False return True except TypeError: return False def __str__(self): - return ( - "{" + ", ".join(_jstr(k) + ": " + _jstr(v) for k, v in self.items()) + "}" - ) + def item_str(k, v): + return _jstr(k) + ": " + _jstr(v) + + return "{" + ", ".join(item_str(k, v) for k, v in self.items()) + "}" class JavaSet(JavaCollection, collections.abc.MutableSet): @@ -788,7 +816,7 @@ def __eq__(self, other): if len(self) != len(other): return False for k in self: - if not k in other: + if k not in other: return False return True except TypeError: @@ -998,7 +1026,7 @@ def _is_table(obj: Any) -> bool: """Checks if obj is a table""" try: return isinstance(obj, jimport("org.scijava.table.Table")) - except: + except BaseException: # No worries if scijava-table is not available. pass @@ -1007,7 +1035,7 @@ def _convert_table(obj: Any): """Converts obj to a table.""" try: return _table_to_pandas(obj) - except: + except BaseException: # No worries if scijava-table is not available. pass @@ -1037,8 +1065,6 @@ def _table_to_pandas(table): def _pandas_to_table(df): - pd = _import_pandas() - if len(df.dtypes.unique()) > 1: TableClass = jimport("org.scijava.table.DefaultGenericTable") else: diff --git a/src/scyjava/config/__init__.py b/src/scyjava/config/__init__.py index 1a3fbc74..12d38a45 100644 --- a/src/scyjava/config/__init__.py +++ b/src/scyjava/config/__init__.py @@ -22,7 +22,8 @@ def add_endpoints(*new_endpoints): Please modify the endpoints field directly instead. """ _logger.warning( - "Deprecated method call: scyjava.config.add_endpoints(). Please modify scyjava.config.endpoints directly instead." + "Deprecated method call: scyjava.config.add_endpoints(). " + "Please modify scyjava.config.endpoints directly instead." ) global endpoints _logger.debug("Adding endpoints %s to %s", new_endpoints, endpoints) @@ -35,7 +36,8 @@ def get_endpoints(): Please access the endpoints field directly instead. """ _logger.warning( - "Deprecated method call: scyjava.config.get_endpoints(). Please access scyjava.config.endpoints directly instead." + "Deprecated method call: scyjava.config.get_endpoints(). " + "Please access scyjava.config.endpoints directly instead." ) global endpoints return endpoints diff --git a/tests/test_convert.py b/tests/test_convert.py index 6f5c50f9..6faa2d48 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,55 +1,46 @@ -from jpype import JArray, JInt, JLong +from jpype import JArray, JInt + from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") -def assert_same_table(table, df): - assert len(table.toArray()) == df.shape[1] - assert len(table.toArray()[0].toArray()) == df.shape[0] - - for i, column in enumerate(table.toArray()): - npt.assert_array_almost_equal(df.iloc[:, i].values, column.toArray()) - - assert table.getColumnHeader(i) == df.columns[i] - - class TestConvert(object): def testClass(self): """ Tests class detection from Java objects. """ int_class = jclass(to_java(5)) - "java.lang.Integer" == int_class.getName() + assert "java.lang.Integer" == int_class.getName() long_class = jclass(to_java(4000000001)) - "java.lang.Long" == long_class.getName() + assert "java.lang.Long" == long_class.getName() bigint_class = jclass(to_java(9879999999999999789)) - "java.math.BigInteger" == bigint_class.getName() + assert "java.math.BigInteger" == bigint_class.getName() string_class = jclass(to_java("foobar")) - "java.lang.String" == string_class.getName() + assert "java.lang.String" == string_class.getName() list_class = jclass(to_java([1, 2, 3])) assert "java.util.ArrayList" == list_class.getName() map_class = jclass(to_java({"a": "b"})) - "java.util.LinkedHashMap" == map_class.getName() + assert "java.util.LinkedHashMap" == map_class.getName() - "java.util.Map" == jclass("java.util.Map").getName() + assert "java.util.Map" == jclass("java.util.Map").getName() def testBoolean(self): jt = to_java(True) - assert True == jt.booleanValue() + assert jt.booleanValue() pt = to_python(jt) - assert True == pt + assert pt assert "True" == str(pt) jf = to_java(False) - assert False == jf.booleanValue() + assert not jf.booleanValue() pf = to_python(jf) - assert False == pf + assert not pf assert "False" == str(pf) def testInteger(self): @@ -61,12 +52,12 @@ def testInteger(self): assert str(i) == str(pi) def testLong(self): - l = 4000000001 - jl = to_java(l) - assert l == jl.longValue() - pl = to_python(jl) - assert l == pl - assert str(l) == str(pl) + long = 4000000001 + jlong = to_java(long) + assert long == jlong.longValue() + plong = to_python(jlong) + assert long == plong + assert str(long) == str(plong) def testBigInteger(self): bi = 9879999999999999789 @@ -103,16 +94,16 @@ def testString(self): assert str(s) == str(ps) def testList(self): - l = "The quick brown fox jumps over the lazy dogs".split() - jl = to_java(l) - for e, a in zip(l, jl): + list = "The quick brown fox jumps over the lazy dogs".split() + jlist = to_java(list) + for e, a in zip(list, jlist): assert e == to_python(a) - pl = to_python(jl) - assert l == pl - assert str(l) == str(pl) - assert pl[1] == "quick" - pl[7] = "silly" - assert "The quick brown fox jumps over the silly dogs" == " ".join(pl) + plist = to_python(jlist) + assert list == plist + assert str(list) == str(plist) + assert plist[1] == "quick" + plist[7] = "silly" + assert "The quick brown fox jumps over the silly dogs" == " ".join(plist) def testSet(self): s = set(["orange", "apple", "pineapple", "plum"]) @@ -161,36 +152,36 @@ def testDict(self): assert str(d) == str(pd) def testMixed(self): - d = {"a": "b", "c": "d"} - l = ["e", "f", "g", "h"] - s = set(["i", "j", "k"]) + test_dict = {"a": "b", "c": "d"} + test_list = ["e", "f", "g", "h"] + test_set = set(["i", "j", "k"]) # mixed types in a dictionary - md = {"d": d, "l": l, "s": s, "str": "hello"} - jmd = to_java(md) - assert len(md) == jmd.size() - for k, v in md.items(): - jk = to_java(k) - jmd.containsKey(jk) - assert v == to_python(jmd.get(jk)) - pmd = to_python(jmd) - assert md == pmd - assert str(md) == str(pmd) + mixed_dict = {"d": test_dict, "l": test_list, "s": test_set, "str": "hello"} + j_mixed_dict = to_java(mixed_dict) + assert len(mixed_dict) == j_mixed_dict.size() + for k, v in mixed_dict.items(): + j_k = to_java(k) + j_mixed_dict.containsKey(j_k) + assert v == to_python(j_mixed_dict.get(j_k)) + p_mixed_dict = to_python(j_mixed_dict) + assert mixed_dict == p_mixed_dict + assert str(mixed_dict) == str(p_mixed_dict) # mixed types in a list - ml = [d, l, s, "hello"] - jml = to_java(ml) - for e, a in zip(ml, jml): + mixed_list = [test_dict, test_list, test_set, "hello"] + j_mixed_list = to_java(mixed_list) + for e, a in zip(mixed_list, j_mixed_list): assert e == to_python(a) - pml = to_python(jml) - assert ml == pml - assert str(ml) == str(pml) + p_mixed_list = to_python(j_mixed_list) + assert mixed_list == p_mixed_list + assert str(mixed_list) == str(p_mixed_list) def testNone(self): d = {"key": None, None: "value", "foo": "bar"} jd = to_java(d) assert 3 == jd.size() - assert None == jd.get("key") + assert None is jd.get("key") assert "value" == jd.get(None) assert "bar" == jd.get("foo") pd = to_python(jd) @@ -200,11 +191,11 @@ def testGentle(self): Object = jimport("java.lang.Object") unknown_thing = Object() converted_thing = to_python(unknown_thing, gentle=True) - assert type(converted_thing) == Object + assert isinstance(converted_thing, Object) bad_conversion = None try: bad_conversion = to_python(unknown_thing) - except: + except BaseException: # NB: Failure is expected here. pass assert bad_conversion is None @@ -224,14 +215,13 @@ def testStructureWithSomeUnsupportedItems(self): # Convert it back to Python. pdict = to_python(jmap) - l = pdict["list"] assert pdict["list"][0] == "a" - assert type(pdict["list"][1]) == Object + assert isinstance(pdict["list"][1], Object) assert pdict["list"][2] == 1 assert "x" in pdict["set"] assert 2 in pdict["set"] assert len(pdict["set"]) == 3 - assert type(pdict["object"]) == Object + assert isinstance(pdict["object"], Object) assert pdict["foo"] == "bar" def test_conversion_priority(self): diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 7895eb72..45a068a4 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,6 +1,7 @@ import numpy as np import numpy.testing as npt import pandas as pd + from scyjava import config, jimport, to_java config.endpoints.append("org.scijava:scijava-table") @@ -27,7 +28,8 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport("org.scijava.table.DefaultFloatTable") + DefaultFloatTable = jimport("org.scijava.table.DefaultFloatTable") + assert isinstance(table, DefaultFloatTable) # Int table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -38,7 +40,8 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport("org.scijava.table.DefaultIntTable") + DefaultIntTable = jimport("org.scijava.table.DefaultIntTable") + assert isinstance(table, DefaultIntTable) # Bool table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -48,7 +51,8 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - assert type(table) == jimport("org.scijava.table.DefaultBoolTable") + DefaultBoolTable = jimport("org.scijava.table.DefaultBoolTable") + assert isinstance(table, DefaultBoolTable) # Mixed table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -67,4 +71,5 @@ def testPandasToTable(self): # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) - assert type(table) == jimport("org.scijava.table.DefaultGenericTable") + DefaultGenericTable = jimport("org.scijava.table.DefaultGenericTable") + assert isinstance(table, DefaultGenericTable) diff --git a/tests/test_version.py b/tests/test_version.py index dc0f5300..f3780d81 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,7 +1,8 @@ import os -import pytest import sys +import pytest + setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "version.py") @@ -60,7 +61,8 @@ def test_version_pkg_resources(): sys.modules["scyjava.version"] = None # Remove importlib.metadata sys.modules["importlib.metadata"] = None - # Ensure scyjava.__version__ matches pkg_resources.get_distribution().version + # Ensure scyjava.__version__ matches + # pkg_resources.get_distribution().version from pkg_resources import get_distribution assert _scyjava_version() == get_distribution("scyjava").version From 1a9d69058c94c5429ee451db396caac9c25d5127 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 28 Apr 2022 10:45:04 -0500 Subject: [PATCH 161/505] Rename module_property -> constant Since we cache by default, this decorator was intended to establish constants. Of course, in the wild west that is Python, someone could just "overwrite" our constants, but it helps to declare our intentions with the functions being decorated --- src/scyjava/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 863ad4d8..39befe20 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -29,10 +29,10 @@ _logger = logging.getLogger(__name__) # Set of module properties -_MODULE_PROPERTIES: Dict[str, Callable] = {} +_CONSTANTS: Dict[str, Callable] = {} -def module_property(func: Callable[[], Any], cache=True) -> Callable[[], Any]: +def constant(func: Callable[[], Any], cache=True) -> Callable[[], Any]: """ Turns a function into a property of this module Functions decorated with this property must have a @@ -48,7 +48,7 @@ def module_property(func: Callable[[], Any], cache=True) -> Callable[[], Any]: name = func.__name__[1:] if cache: func = (lru_cache(maxsize=None))(func) - _MODULE_PROPERTIES[name] = func + _CONSTANTS[name] = func return func @@ -58,12 +58,12 @@ def __getattr__(name): attribute. :param name: The name of the attribute being searched for. """ - if name in _MODULE_PROPERTIES: - return _MODULE_PROPERTIES[name]() + if name in _CONSTANTS: + return _CONSTANTS[name]() raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -@module_property +@constant def ___version__(): # First pass: use the version output by setuptools_scm try: From d4418c3ea8964f3a1d1b6df6af84b32eb8532d57 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 28 Apr 2022 11:09:23 -0500 Subject: [PATCH 162/505] Export func. default to module property Function calls as default arguments are really misleading. Default arguments are evaulated at function definition time, NOT when the function is called. While in this particular case, we are accessing a global variable, leading to a case where no harm is done, we should avoid this generally speaking. --- src/scyjava/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 39befe20..d2ae0852 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -167,7 +167,10 @@ def jvm_version(): return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=scyjava.config.get_options()): +_config_options = scyjava.config.get_options() + + +def start_jvm(options=_config_options): """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From 4bb938400a9eae15f8064577495a7da8fb582879 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 28 Apr 2022 11:23:02 -0500 Subject: [PATCH 163/505] Use underscore for unused loop vars This is a common convention in Python to indicate that variables are deliberately discarded. See https://stackoverflow.com/a/5893946 --- src/scyjava/__init__.py | 2 +- src/scyjava/config/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index d2ae0852..8b68b100 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1087,7 +1087,7 @@ def _pandas_to_table(df): for c, column_name in enumerate(df.columns): table.setColumnHeader(c, column_name) - for i, (index, row) in enumerate(df.iterrows()): + for i, (_, row) in enumerate(df.iterrows()): for c, value in enumerate(row): header = df.columns[c] table.set(header, i, to_java(value)) diff --git a/src/scyjava/config/__init__.py b/src/scyjava/config/__init__.py index 12d38a45..7d6bc20c 100644 --- a/src/scyjava/config/__init__.py +++ b/src/scyjava/config/__init__.py @@ -139,7 +139,7 @@ def find_jars(directory): :return: a list of JAR files """ jars = [] - for root, dirs, 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) From 596c06c1e27823d4624f997f90db48953e298b4c Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 26 Apr 2022 16:37:33 -0500 Subject: [PATCH 164/505] Add flake8 job --- .github/workflows/build.yml | 15 +++++++++++++++ dev-environment.yml | 2 ++ setup.cfg | 2 ++ 3 files changed, 19 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db91e38b..3f9cb388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,6 +60,21 @@ jobs: - uses: actions/checkout@v2 - uses: psf/black@stable + flake: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v3 + - name: flake src code + run: | + python -m pip install flake8 + python -m flake8 src + - name: flake test code + run: | + python -m flake8 tests + + + conda-dev-test: name: Conda Setup & Code Coverage runs-on: ubuntu-latest diff --git a/dev-environment.yml b/dev-environment.yml index 816e9a7b..0ba23551 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -20,8 +20,10 @@ dependencies: - numpy - pandas # Developer tools + - autopep8 - black - build + - flake8 - pytest - pytest-cov - setuptools-scm >= 6.2 diff --git a/setup.cfg b/setup.cfg index 49b351b8..1565ec45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,8 +47,10 @@ where = src # Ensure any changes to this list are also added to environment-test.yml! dev = + autopep8 black build + flake8 pytest pytest-cov numpy From 1f10886f1b0eeceb8866b952ee7d61bb9efd18dd Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 13:25:19 -0500 Subject: [PATCH 165/505] CI: fix name of conda environment --- .github/workflows/build.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f9cb388..f93bbcf9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,8 +73,6 @@ jobs: run: | python -m flake8 tests - - conda-dev-test: name: Conda Setup & Code Coverage runs-on: ubuntu-latest @@ -91,13 +89,13 @@ jobs: auto-update-conda: true python-version: 3.9 environment-file: dev-environment.yml - # Activate imglyb-dev environment - activate-environment: imglyb-dev + # Activate scyjava-dev environment + activate-environment: scyjava-dev auto-activate-base: false # Use mamba for faster setup use-mamba: true mamba-version: "*" - - name: Test imglyb + - name: Test scyjava run: | python -m pytest tests/ -p no:faulthandler --cov-report=xml --cov=. # We could do this in its own action, but we'd have to setup the environment again. From 83e9cc7da8e2f29233205f427dea115dfe3a94a6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 13:39:41 -0500 Subject: [PATCH 166/505] Split test execution into a separate script Because "python -m pytest tests/ -p no:faulthandler" is too complex to be typing manually from the command line all the time. --- .github/workflows/build.yml | 3 +-- test.sh | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100755 test.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f93bbcf9..d14e2f77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,8 +97,7 @@ jobs: mamba-version: "*" - name: Test scyjava run: | - python -m pytest tests/ -p no:faulthandler --cov-report=xml --cov=. + ./test.sh --cov-report=xml --cov=. # We could do this in its own action, but we'd have to setup the environment again. - name: Upload Coverage to Codecov uses: codecov/codecov-action@v2 - \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..cfb8596d --- /dev/null +++ b/test.sh @@ -0,0 +1,2 @@ +#!/bin/sh +python -m pytest tests/ -p no:faulthandler $@ From a6b7c82d8600cc48d65dc770da4b6f80fdd4997b Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Thu, 26 May 2022 11:47:34 -0500 Subject: [PATCH 167/505] Tell git to ignore IntelliJ IDEA project metadata --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e3227377..e6a53771 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ src/scyjava/version.py .coverage .coverage.* coverage.xml + +# IDEA +.idea/ +*.iml From 89f6bc1aff2a4c66b9de3bb1d320faef372dabfb Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Thu, 26 May 2022 11:52:36 -0500 Subject: [PATCH 168/505] Move Java class imports to a separate function This will make it feasible to perform these imports independently from the start_jvm routine. Co-authored-by: Curtis Rueden --- src/scyjava/__init__.py | 97 ++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 8b68b100..6f3bf307 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -247,51 +247,7 @@ def start_jvm(options=_config_options): jpype.config.free_resources = False atexit.register(shutdown_jvm) - # grab needed Java classes - global Boolean - Boolean = jimport("java.lang.Boolean") - global Byte - Byte = jimport("java.lang.Byte") - global Character - Character = jimport("java.lang.Character") - global Double - Double = jimport("java.lang.Double") - global Float - Float = jimport("java.lang.Float") - global Integer - Integer = jimport("java.lang.Integer") - global Iterable - Iterable = jimport("java.lang.Iterable") - global Long - Long = jimport("java.lang.Long") - global Object - Object = jimport("java.lang.Object") - global Short - Short = jimport("java.lang.Short") - global String - String = jimport("java.lang.String") - global Void - Void = jimport("java.lang.Void") - global BigDecimal - BigDecimal = jimport("java.math.BigDecimal") - global BigInteger - BigInteger = jimport("java.math.BigInteger") - global ArrayList - ArrayList = jimport("java.util.ArrayList") - global Collection - Collection = jimport("java.util.Collection") - global Iterator - Iterator = jimport("java.util.Iterator") - global LinkedHashMap - LinkedHashMap = jimport("java.util.LinkedHashMap") - global LinkedHashSet - LinkedHashSet = jimport("java.util.LinkedHashSet") - global List - List = jimport("java.util.List") - global Map - Map = jimport("java.util.Map") - global Set - Set = jimport("java.util.Set") + _import_java_classes() # invoke registered callback functions for callback in _startup_callbacks: @@ -1043,6 +999,57 @@ def _convert_table(obj: Any): pass +def _import_java_classes(): + global Boolean + global Byte + global Character + global Double + global Float + global Integer + global Iterable + global Long + global Object + global Short + global String + global Void + global BigDecimal + global BigInteger + global ArrayList + global Collection + global Iterator + global LinkedHashMap + global LinkedHashSet + global List + global Map + global Set + + _logger.debug('Importing Java classes...') + + # grab needed Java classes + Boolean = jimport("java.lang.Boolean") + Byte = jimport("java.lang.Byte") + Character = jimport("java.lang.Character") + Double = jimport("java.lang.Double") + Float = jimport("java.lang.Float") + Integer = jimport("java.lang.Integer") + Iterable = jimport("java.lang.Iterable") + Long = jimport("java.lang.Long") + Object = jimport("java.lang.Object") + Short = jimport("java.lang.Short") + String = jimport("java.lang.String") + Void = jimport("java.lang.Void") + BigDecimal = jimport("java.math.BigDecimal") + BigInteger = jimport("java.math.BigInteger") + ArrayList = jimport("java.util.ArrayList") + Collection = jimport("java.util.Collection") + Iterator = jimport("java.util.Iterator") + LinkedHashMap = jimport("java.util.LinkedHashMap") + LinkedHashSet = jimport("java.util.LinkedHashSet") + List = jimport("java.util.List") + Map = jimport("java.util.Map") + Set = jimport("java.util.Set") + + def _import_pandas(): try: import pandas as pd From b7c0b26f60efddf665ddd558128a2b89ab34d420 Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Thu, 26 May 2022 16:16:46 -0500 Subject: [PATCH 169/505] Use Python's multi-compare syntax Co-authored-by: Curtis Rueden --- src/scyjava/__init__.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 6f3bf307..e97ce201 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -503,10 +503,6 @@ def to_java(obj: Any) -> Any: return _convert(obj, java_converters) -def _is_a_long(obj) -> bool: - return isinstance(obj, int) and obj <= Long.MAX_VALUE - - def _stock_java_converters() -> typing.List[Converter]: """ Returns all python-to-java converters supported out of the box! @@ -545,13 +541,13 @@ def _stock_java_converters() -> typing.List[Converter]: # Integer converter Converter( predicate=lambda obj: isinstance(obj, int) - and obj <= Integer.MAX_VALUE - and obj >= Integer.MIN_VALUE, + and Integer.MIN_VALUE <= obj <= Integer.MAX_VALUE, converter=Integer, ), # Long converter Converter( - predicate=_is_a_long, + predicate=lambda obj: isinstance(obj, int) + and Long.MIN_VALUE <= obj <= Long.MAX_VALUE, converter=Long, priority=Priority.NORMAL - 1, ), @@ -564,15 +560,13 @@ def _stock_java_converters() -> typing.List[Converter]: # Float converter Converter( predicate=lambda obj: isinstance(obj, float) - and obj <= Float.MAX_VALUE - and obj >= Float.MIN_VALUE, + and Float.MIN_VALUE <= obj <= Float.MAX_VALUE, converter=Float, ), # Double converter Converter( predicate=lambda obj: isinstance(obj, float) - and obj <= Double.MAX_VALUE - and obj >= Float.MIN_VALUE, + and Double.MAX_VALUE <= obj <= Double.MAX_VALUE, converter=Double, priority=Priority.NORMAL - 1, ), From 07cbf68a4e9a1bcae01212765f48dcac34a01d56 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 11:49:20 -0500 Subject: [PATCH 170/505] Revise note about the Python-side Priority class We'll probably never be able to use Java code for this. --- src/scyjava/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e97ce201..d3940bcf 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -340,9 +340,9 @@ def compare_version(version, java_class_version): # -- Type Conversion Utilities -- -# TODO: It would be cool to just use org.scijava.priority.Priority. -# Unfortunately, we cannot do that without bringing in all of SJC. -# Once SciJava 3 is mainstream, we could use a SciJava Priority module :) +# NB: We cannot use org.scijava.priority.Priority or other Java-side class +# here because we don't want to impose Java-side dependencies, and we don't +# want to require Java to be started before reasoning about priorities. class Priority: FIRST = 1e300 EXTREMELY_HIGH = 1e6 From a464d68dc87a46006c460bf0a4b785b851bfb2f6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 11:49:58 -0500 Subject: [PATCH 171/505] Move the JVM startup callbacks to the end This is a necessary refactoring for Jep mode. Co-authored-by: Amandine Tournay (Kitwaii) --- src/scyjava/__init__.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index d3940bcf..64a9ed2a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -601,14 +601,6 @@ def _stock_java_converters() -> typing.List[Converter]: ] -def _initialize_converters(): - for converter in _stock_java_converters(): - _add_converter(converter, java_converters) - - -when_jvm_starts(_initialize_converters) - - # -- Java to Python -- @@ -970,11 +962,6 @@ def _stock_py_converters() -> typing.List: ] -when_jvm_starts( - lambda: [_add_converter(c, py_converters) for c in _stock_py_converters()] -) - - def _is_table(obj: Any) -> bool: """Checks if obj is a table""" try: @@ -1094,3 +1081,21 @@ def _pandas_to_table(df): table.set(header, i, to_java(value)) return table + + +# -- JVM startup callbacks -- + +# NB: These must be performed last, because if this class is imported after the +# JVM is already running -- for example, if we are running in Jep mode, where +# Python is started from inside the JVM -- then these functions execute the +# callbacks immediately, which means the involved functions must be defined and +# functional at this point. + +def _initialize_converters(): + for converter in _stock_java_converters(): + _add_converter(converter, java_converters) + for converter in _stock_py_converters(): + _add_converter(converter, py_converters) + + +when_jvm_starts(_initialize_converters) From a3f7b86f7a150a86940f81de90f3a0c3ad229400 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 13:53:18 -0500 Subject: [PATCH 172/505] Make black happy --- src/scyjava/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 64a9ed2a..b5ec7dbe 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1004,7 +1004,7 @@ def _import_java_classes(): global Map global Set - _logger.debug('Importing Java classes...') + _logger.debug("Importing Java classes...") # grab needed Java classes Boolean = jimport("java.lang.Boolean") @@ -1091,6 +1091,7 @@ def _pandas_to_table(df): # callbacks immediately, which means the involved functions must be defined and # functional at this point. + def _initialize_converters(): for converter in _stock_java_converters(): _add_converter(converter, java_converters) From 2a7a13d7b5dc3baf48e7418b42bfc279c491ec41 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 14 Jun 2022 16:31:23 -0500 Subject: [PATCH 173/505] CI: call test.sh for running the tests So that we know we test the same thing on CI as manually. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d14e2f77..5eb9cc49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - name: Test ScyJava run: | - python -m pytest -p no:faulthandler --color=yes + ./test.sh --color=yes lint: runs-on: ubuntu-latest From c88918b81197590baab1c5bd0066c8019bef4484 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 17 Jun 2022 11:35:21 -0500 Subject: [PATCH 174/505] Add openjdk=8 dependency for dev environment --- dev-environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-environment.yml b/dev-environment.yml index 0ba23551..871819e0 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -16,6 +16,7 @@ dependencies: # Project dependencies - jpype1 >= 1.3.0 - jgo + - openjdk=8 # Test dependencies - numpy - pandas From 433c88494fb706ac422c969c6a67b89dac1ccaa6 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 17 Jun 2022 15:29:39 -0500 Subject: [PATCH 175/505] Remove Python3.10 exclusion on Windows --- .github/workflows/build.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5eb9cc49..6274cee0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,11 +27,6 @@ jobs: '3.9', '3.10' ] - # Breaks due to https://github.com/jpype-project/jpype/issues/1009 - # Should be included once the fix is released - exclude: - - os: windows-latest - python-version: "3.10" steps: - uses: actions/checkout@v2 From 5213402aeed048eb002501dd5f402cf18ca9ec85 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 27 Jun 2022 14:25:26 -0500 Subject: [PATCH 176/505] Rename version.py to _version.py As per setuptools_scm convention: https://github.com/pypa/setuptools_scm#pyprojecttoml-usage And as discussed on Zulip: https://imagesc.zulipchat.com/#narrow/stream/327236-ImageJ2/topic/_version.2Epy.20in.20PyImageJ --- .gitignore | 2 +- pyproject.toml | 4 ++-- tests/test_version.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e6a53771..fbcac6bc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ *egg-info/ # setuptools_scm -src/scyjava/version.py +/src/*/_version.py # Vi *.swp diff --git a/pyproject.toml b/pyproject.toml index 6e84a8ad..42853180 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,8 @@ requires = [ build-backend = "setuptools.build_meta" [tool.setuptools_scm] -write_to = "src/scyjava/version.py" +write_to = "src/scyjava/_version.py" [tool.black] # This file is autogenerated by setuptools_scm - it cannot be modified ---exclude = "src/scyjava/version.py" +--exclude = "src/scyjava/_version.py" diff --git a/tests/test_version.py b/tests/test_version.py index f3780d81..b8a03860 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -3,7 +3,7 @@ import pytest -setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "version.py") +setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "_version.py") def _scyjava_version(): @@ -24,7 +24,7 @@ def test_version_file(): # Get the version from setuptools_scm from setuptools_scm import get_version - setuptools_version = get_version(write_to="src/scyjava/version.py") + setuptools_version = get_version(write_to="src/scyjava/_version.py") # Ensure that the version was written to file assert os.path.isfile(setuptools_file) # Ensure that scyjava.__version__ matches this. From 2652e292aedf3fd4ff56aa3061d3fa429b8cb21a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 27 Jun 2022 18:59:00 -0500 Subject: [PATCH 177/505] README: fix CI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fc9840b8..2c263914 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![build status](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/python-test-conda.yml) [![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) +[![build status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) Supercharged Java access from Python. From 134540ad430ddb28c9510297bfdb5fdb18817cd0 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 19 Jul 2022 15:38:25 -0500 Subject: [PATCH 178/505] Add support for JByte -> int conversion We don't go the other way because there isn't a great return type. We convert Integers to ints and ints to Integers, and I don't think it is smart to convert all ints that are small enough into Bytes. --- src/scyjava/__init__.py | 3 ++- tests/test_convert.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index b5ec7dbe..ac411ba4 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -17,6 +17,7 @@ from jpype.types import ( JArray, JBoolean, + JByte, JChar, JDouble, JFloat, @@ -839,7 +840,7 @@ def _stock_py_converters() -> typing.List: ), # JInt/JLong/JShort converter Converter( - predicate=lambda obj: isinstance(obj, (JInt, JLong, JShort)), + predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), converter=int, priority=Priority.NORMAL + 1, ), diff --git a/tests/test_convert.py b/tests/test_convert.py index 6faa2d48..42124035 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,4 +1,4 @@ -from jpype import JArray, JInt +from jpype import JArray, JByte, JInt from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python @@ -43,6 +43,15 @@ def testBoolean(self): assert not pf assert "False" == str(pf) + def testByte(self): + # NB we can't (yet) convert TO Bytes, since there is not (yet) + # a great type to convert FROM. We convert python ints to Integers + i = 5 + ji = JByte(i) + pi = to_python(ji) + assert i == pi + assert str(i) == str(pi) + def testInteger(self): i = 5 ji = to_java(i) From c715b9350f12d6e610fbe582b58e14e836470b92 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 21 Jul 2022 11:54:40 -0500 Subject: [PATCH 179/505] Fix mangled text data columns When creating a pandas DataFrame form the extracted org.scijava.table.Table contents, the resulting pandas DataFrame containes char arrays of the string/text column entries. The issue is caused when converting the List of java objects into the pandas DataFrame. Converting the contents of the the List to Python with 'to_python' before creating the DataFrame fixes this bug. --- src/scyjava/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ac411ba4..7ab8b2bc 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1051,6 +1051,8 @@ def _table_to_pandas(table): for i, column in enumerate(table.toArray()): data.append(column.toArray()) headers.append(str(table.getColumnHeader(i))) + for j in range(len(data)): + data[j] = to_python(data[j]) df = pd.DataFrame(data).T df.columns = headers return df From bb31182eb42c12d68e84ec77961514a3a7f8858e Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 21 Jul 2022 12:11:00 -0500 Subject: [PATCH 180/505] Apply isort imports reordering --- src/scyjava/__init__.py | 32 ++++++++++++-------------------- src/scyjava/config/__init__.py | 1 + tests/test_convert.py | 3 ++- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 7ab8b2bc..7b94874a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1,31 +1,23 @@ import atexit import collections.abc -from functools import lru_cache -from typing import Any, Callable, NamedTuple -import typing -import jgo -import jpype -import jpype.config import logging import os import re -import scyjava.config import subprocess import sys +import typing +from functools import lru_cache from pathlib import Path -from typing import Dict -from jpype.types import ( - JArray, - JBoolean, - JByte, - JChar, - JDouble, - JFloat, - JInt, - JLong, - JShort, -) +from typing import Any, Callable, Dict, NamedTuple + +import jgo +import jpype +import jpype.config from _jpype import _JObject +from jpype.types import (JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, + JLong, JShort) + +import scyjava.config _logger = logging.getLogger(__name__) @@ -75,7 +67,7 @@ def ___version__(): pass # Second pass: use importlib.metadata try: - from importlib.metadata import version, PackageNotFoundError + from importlib.metadata import PackageNotFoundError, version return version("scyjava") except ImportError or PackageNotFoundError: diff --git a/src/scyjava/config/__init__.py b/src/scyjava/config/__init__.py index 7d6bc20c..a6ef2754 100644 --- a/src/scyjava/config/__init__.py +++ b/src/scyjava/config/__init__.py @@ -1,6 +1,7 @@ import logging import os import pathlib + import jpype from jgo import maven_scijava_repository diff --git a/tests/test_convert.py b/tests/test_convert.py index 42124035..501e16b0 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,6 +1,7 @@ from jpype import JArray, JByte, JInt -from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python +from scyjava import (Converter, config, jclass, jimport, start_jvm, to_java, + to_python) config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") From d68efb40ca90966c46cea20fe223b2fded73bd9c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 25 Jul 2022 15:38:11 -0500 Subject: [PATCH 181/505] Format with black --- src/scyjava/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 7b94874a..db6a815f 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -14,8 +14,17 @@ import jpype import jpype.config from _jpype import _JObject -from jpype.types import (JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, - JLong, JShort) +from jpype.types import ( + JArray, + JBoolean, + JByte, + JChar, + JDouble, + JFloat, + JInt, + JLong, + JShort, +) import scyjava.config From d078a400f7305fa2dfc4412a42c3c855d9efc2ae Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 25 Jul 2022 15:41:48 -0500 Subject: [PATCH 182/505] Introduce is_version_at_least function And deprecate compare_version. The name compare_version is unclear, and the behavior is surprising (to me): the given version must be *strictly greater than* some stated minimum. But our primary use case for this query is "I need to know if library X is new enough to call function Y, so I'll check whether library X is at least version Z, which was the first version to include function Y." The new function name and behavior is_version_at_least is more intuitive for that. If you need to know strictly greater than, you can check for is_version_at_least together with inequality. --- src/scyjava/__init__.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index db6a815f..5ef7a427 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -323,21 +323,45 @@ def when_jvm_stops(f): def get_version(java_class): - """Return the version of a Java class.""" + """ + Return the version of a Java class. + Requires org.scijava:scijava-common on the classpath. + + The version string is extracted from the given class's associated JAR + artifact (if any), either the embedded Maven POM if the project was built + with Maven, or the JAR manifest's Specification-Version value if it exists. + + See org.scijava.VersionUtils.getVersion(Class) for further details. + """ VersionUtils = jimport("org.scijava.util.VersionUtils") version = VersionUtils.getVersion(java_class) return version -def compare_version(version, java_class_version): +def is_version_at_least(actual_version, minimum_version): """ - Return a boolean on a version comparison. True is returned - if the Java class version is higher than the specified version. False - is returned if the specified version is higher than the Java class version. + Return a boolean on a version comparison. + Requires org.scijava:scijava-common on the classpath. + + Returns True if the given actual version is greater than or + equal to the specified minimum version, or False otherwise. + + See org.scijava.VersionUtils.compare(String, String) for further details. """ VersionUtils = jimport("org.scijava.util.VersionUtils") - comparison = VersionUtils.compare(version, java_class_version) < 0 - return comparison + return VersionUtils.compare(actual_version, minimum_version) >= 0 + + +def compare_version(version, java_class_version): + """ + This function is deprecated. Use is_version_at_least instead. + """ + _logger.warning( + "The compare_version function is deprecated. Use is_version_at_least instead." + ) + return version != java_class_version and is_version_at_least( + java_class_version, version + ) # -- Type Conversion Utilities -- From ad39e0c7d9d773cab0fadbca9d5d4cca25e83ffd Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 25 Jul 2022 16:04:34 -0500 Subject: [PATCH 183/505] Run black on tests --- tests/test_convert.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index 501e16b0..42124035 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,7 +1,6 @@ from jpype import JArray, JByte, JInt -from scyjava import (Converter, config, jclass, jimport, start_jvm, to_java, - to_python) +from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") From 042387bd8709b7007245cf6bb4143471b1fa497a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 13:21:13 -0500 Subject: [PATCH 184/505] Move test.sh script into bin folder --- .github/workflows/build.yml | 4 ++-- test.sh => bin/test.sh | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename test.sh => bin/test.sh (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6274cee0..cee0aada 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: - name: Test ScyJava run: | - ./test.sh --color=yes + bin/test.sh --color=yes lint: runs-on: ubuntu-latest @@ -92,7 +92,7 @@ jobs: mamba-version: "*" - name: Test scyjava run: | - ./test.sh --cov-report=xml --cov=. + bin/test.sh --cov-report=xml --cov=. # We could do this in its own action, but we'd have to setup the environment again. - name: Upload Coverage to Codecov uses: codecov/codecov-action@v2 diff --git a/test.sh b/bin/test.sh similarity index 100% rename from test.sh rename to bin/test.sh From a02459bdeaec6b1c4d821b8a5dd3b065b3755c5c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 13:24:31 -0500 Subject: [PATCH 185/505] Add shortcuts for common dev tasks --- Makefile | 29 +++++++++++++++++++++++++++++ bin/check.sh | 10 ++++++++++ bin/clean.sh | 9 +++++++++ bin/lint.sh | 7 +++++++ bin/setup.sh | 6 ++++++ bin/test.sh | 4 ++++ 6 files changed, 65 insertions(+) create mode 100644 Makefile create mode 100755 bin/check.sh create mode 100755 bin/clean.sh create mode 100755 bin/lint.sh create mode 100755 bin/setup.sh diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f17ff365 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +help: + @echo "Available targets:\n\ + clean - remove build files and directories\n\ + setup - create mamba developer environment\n\ + lint - run code formatters and linters\n\ + test - run automated test suite\n\ + dist - generate release archives\n\ + \n\ + Remember to 'mamba activate scyjava-dev' first!" + +clean: + bin/clean.sh + +setup: + bin/setup.sh + +check: + @bin/check.sh + +lint: check + bin/lint.sh + +test: check + bin/test.sh + +dist: check + python -m build + +.PHONY: test diff --git a/bin/check.sh b/bin/check.sh new file mode 100755 index 00000000..4a9db614 --- /dev/null +++ b/bin/check.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +case "$CONDA_PREFIX" in + */scyjava-dev) + ;; + *) + echo "Please run 'make setup' and then 'mamba activate scyjava-dev' first." + exit 1 + ;; +esac diff --git a/bin/clean.sh b/bin/clean.sh new file mode 100755 index 00000000..739d6ec3 --- /dev/null +++ b/bin/clean.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +dir=$(dirname "$0") +cd "$dir/.." + +find . -name __pycache__ -type d | while read d + do rm -rf "$d" +done +rm -rf .pytest_cache build dist src/*.egg-info diff --git a/bin/lint.sh b/bin/lint.sh new file mode 100755 index 00000000..2a93272d --- /dev/null +++ b/bin/lint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +dir=$(dirname "$0") +cd "$dir/.." + +black src tests +python -m flake8 src tests diff --git a/bin/setup.sh b/bin/setup.sh new file mode 100755 index 00000000..3c711c75 --- /dev/null +++ b/bin/setup.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +dir=$(dirname "$0") +cd "$dir/.." + +mamba env create -f dev-environment.yml diff --git a/bin/test.sh b/bin/test.sh index cfb8596d..87171e60 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -1,2 +1,6 @@ #!/bin/sh + +dir=$(dirname "$0") +cd "$dir/.." + python -m pytest tests/ -p no:faulthandler $@ From 27251c582c9fcdb9f40bc04f2b7bae99c8278e19 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 14:54:56 -0500 Subject: [PATCH 186/505] README: add hint about AWT + macOS --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 2c263914..e240e856 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ u'1.8.0_152-release' ```python >>> from scyjava import config, jimport +>>> config.add_option('-Djava.awt.headless=true') >>> config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) >>> config.endpoints.append('net.imagej:imagej:2.1.0') >>> ImageJ = jimport('net.imagej.ImageJ') @@ -241,3 +242,35 @@ FUNCTIONS :param f: Function to invoke when scyjava.start_jvm() is called. ``` + +## Troubleshooting + +On macOS, attempting to use AWT/Swing from Python will cause a hang, +unless you do one of two things: + +1. Start Java in headless mode: + + ```python + from scyjava import config, jimport + config.add_option('-Djava.awt.headless=true') + ``` + + In which case, you'll get `java.awt.HeadlessException` instead of a + hang when you attempt to do something graphical, e.g. create a window. + +2. Or install [PyObjC](https://pyobjc.readthedocs.io/), specifically the + `pyobjc-core` and `pyobjc-framework-cocoa` packages from conda-forge, + or `pyobjc` from PyPI; and then do your AWT-related things inside of + a `jpype.setupGuiEnvironment` call on the main Python thread: + + ```python + import jpype, scyjava + scyjava.start_jvm() + def hello(): + JOptionPane = scyjava.jimport('javax.swing.JOptionPane') + JOptionPane.showMessageDialog(None, "Hello world") + jpype.setupGuiEnvironment(hello) + ``` + + In which case, the `setupGuiEnvironment` call will block the main Python + thread forever. From 759418603f97e7c23585daf2ca56c298ffd37330 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 15:35:57 -0500 Subject: [PATCH 187/505] README: tweak badge whitespace One per line is nicer. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e240e856..56d77e67 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![build status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) +[![build status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) Supercharged Java access from Python. From 4c53743386c297bab6e7939f956dd7fa3c28b248 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 16:44:12 -0500 Subject: [PATCH 188/505] Fix when_jvm_stops docstring --- src/scyjava/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 5ef7a427..06c64056 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -308,12 +308,10 @@ def when_jvm_starts(f): def when_jvm_stops(f): """ - Registers a function to be called when the JVM starts (or immediately). - This is useful to defer construction of Java-dependent data structures - until the JVM is known to be available. If the JVM has already been - started, the function executes immediately. + Registers a function to be called just before the JVM shuts down. + This is useful to perform cleanup of Java-dependent data structures. - :param f: Function to invoke when scyjava.start_jvm() is called. + :param f: Function to invoke when scyjava.shutdown_jvm() is called. """ global _shutdown_callbacks _shutdown_callbacks.append(f) From 5f9c826f7b6886bd80a8e782e4a82851b427a056 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 16:48:29 -0500 Subject: [PATCH 189/505] Improve shutdown_jvm docstring --- src/scyjava/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 06c64056..ea1c3e70 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -259,10 +259,18 @@ def start_jvm(options=_config_options): def shutdown_jvm(): """Shutdown the JVM. - Shutdown the JVM. Set the jpype .config.destroy_jvm flag to true - to ask JPype to destory the JVM itself. Note that enabling - jpype.config.destroy_jvm can lead to delayed shutdown times while - the JVM is waiting for threads to finish. + This function makes a best effort to clean up Java resources first. + In particular, shutdown hooks registered with scyjava.when_jvm_stops + are sequentially invoked. + + Then, all AWT windows (as identified + by the java.awt.Window.getWindows() method) are disposed to reduce the + risk of GUI resources delaying JVM shutdown. + + Finally, the jpype.shutdownJVM() function is called. Note that you can + set the jpype.config.destroy_jvm flag to request JPype to destroy the + JVM explicitly, although setting this flag can lead to delayed shutdown + times while the JVM is waiting for threads to finish. """ # invoke registered shutdown callback functions for callback in _shutdown_callbacks: From 2f2ed95d90ce090aa046ddc4f7473679fb6adc76 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 16:49:04 -0500 Subject: [PATCH 190/505] Add is_jvm_headless function --- src/scyjava/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ea1c3e70..f219cd2c 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -296,6 +296,19 @@ def jvm_started(): return jpype.isJVMStarted() +def is_jvm_headless(): + """ + Return true iff Java is running in headless mode. + + :raises RuntimeError: If the JVM has not started yet. + """ + if not jvm_started(): + raise RuntimeError("JVM has not started yet!") + + GraphicsEnvironment = scyjava.jimport("java.awt.GraphicsEnvironment") + return GraphicsEnvironment.isHeadless() + + def when_jvm_starts(f): """ Registers a function to be called when the JVM starts (or immediately). From b41a346bfb99e90aefe36ab51d5df8a283e46b95 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 16:49:35 -0500 Subject: [PATCH 191/505] Make shutdown_jvm a no-op when JVM is not running It's dumb to start up the JVM just to immediately shut it down. --- src/scyjava/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index f219cd2c..ee47618b 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -271,7 +271,13 @@ def shutdown_jvm(): set the jpype.config.destroy_jvm flag to request JPype to destroy the JVM explicitly, although setting this flag can lead to delayed shutdown times while the JVM is waiting for threads to finish. + + Note that if the JVM is not already running, then this function does + nothing! In particular, shutdown hooks are skipped in this situation. """ + if not jvm_started(): + return + # invoke registered shutdown callback functions for callback in _shutdown_callbacks: try: @@ -332,6 +338,9 @@ def when_jvm_stops(f): Registers a function to be called just before the JVM shuts down. This is useful to perform cleanup of Java-dependent data structures. + Note that if the JVM is not already running when shutdown_jvm is + called, then these registered callback functions will be skipped! + :param f: Function to invoke when scyjava.shutdown_jvm() is called. """ global _shutdown_callbacks From 0e370194540e9eae83f9fbb6a7dcbdb7787e0bd2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 16:50:09 -0500 Subject: [PATCH 192/505] Only dispose AWT windows if AWT has started On macOS, even jimport-ing an AWT-related class can trigger the startup of Java's AWT subsystem, which leads to a deadlock when not running headless or using setupGuiEnvironment. To avoid this conundrum, we use a heuristic to check for AWT: are there any running threads with the AWT- prefix? If not, don't bother trying to clean up any AWT things. At least on my macOS system, this change fixes deadlocks on shutdown, so I'm hopeful that it will fix some scenarios on macOS CI as well. --- src/scyjava/__init__.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ee47618b..c9eb3fb7 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -263,7 +263,7 @@ def shutdown_jvm(): In particular, shutdown hooks registered with scyjava.when_jvm_stops are sequentially invoked. - Then, all AWT windows (as identified + Then, if the AWT subsystem has started, all AWT windows (as identified by the java.awt.Window.getWindows() method) are disposed to reduce the risk of GUI resources delaying JVM shutdown. @@ -285,10 +285,11 @@ def shutdown_jvm(): except Exception as e: print(f"Exception during shutdown callback: {e}") - # clean up remaining awt windows - Window = jimport("java.awt.Window") - for w in Window.getWindows(): - w.dispose() + # dispose AWT resources if applicable + if is_awt_initialized(): + Window = jimport("java.awt.Window") + for w in Window.getWindows(): + w.dispose() # okay to shutdown JVM try: @@ -315,6 +316,24 @@ def is_jvm_headless(): return GraphicsEnvironment.isHeadless() +def is_awt_initialized(): + """ + Return true iff the AWT subsystem has been initialized. + + Java starts up its AWT subsystem automatically and implicitly, as + soon as an action is performed requiring it -- for example, if you + jimport a java.awt or javax.swing class. This can lead to deadlocks + on macOS if you are not running in headless mode and did not invoke + those actions via the jpype.setupGuiEnvironment wrapper function; + see the Troubleshooting section of the scyjava README for details. + """ + if not jvm_started(): + return False + Thread = scyjava.jimport("java.lang.Thread") + threads = Thread.getAllStackTraces().keySet() + return any(t.getName().startsWith("AWT-") for t in threads) + + def when_jvm_starts(f): """ Registers a function to be called when the JVM starts (or immediately). From f285a00ed9837d968c31ca9406e5478ca44edbf7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 29 Aug 2022 15:36:16 -0500 Subject: [PATCH 193/505] README: update available functions section --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 56d77e67..fac7a42e 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,54 @@ True >>> help(scyjava) ... FUNCTIONS + add_java_converter(converter: scyjava.Converter) + Adds a converter to the list used by to_java + :param converter: A Converter going from python to java + + add_py_converter(converter: scyjava.Converter) + Adds a converter to the list used by to_python + :param converter: A Converter from java to python + + constant(func: Callable[[], Any], cache=True) -> Callable[[], Any] + Turns a function into a property of this module + Functions decorated with this property must have a + leading underscore! + :param func: The function to turn into a property + + get_version(java_class) + Return the version of a Java class. + Requires org.scijava:scijava-common on the classpath. + + The version string is extracted from the given class's associated JAR + artifact (if any), either the embedded Maven POM if the project was built + with Maven, or the JAR manifest's Specification-Version value if it exists. + + See org.scijava.VersionUtils.getVersion(Class) for further details. + + is_awt_initialized() + Return true iff the AWT subsystem has been initialized. + + Java starts up its AWT subsystem automatically and implicitly, as + soon as an action is performed requiring it -- for example, if you + jimport a java.awt or javax.swing class. This can lead to deadlocks + on macOS if you are not running in headless mode and did not invoke + those actions via the jpype.setupGuiEnvironment wrapper function; + see the Troubleshooting section below for details. + + is_jvm_headless() + Return true iff Java is running in headless mode. + + :raises RuntimeException: If the JVM has not started yet. + + is_version_at_least(actual_version, minimum_version) + Return a boolean on a version comparison. + Requires org.scijava:scijava-common on the classpath. + + Returns True if the given actual version is greater than or + equal to the specified minimum version, or False otherwise. + + See org.scijava.VersionUtils.compare(String, String) for further details. + isjava(data) Return whether the given data object is a Java object. @@ -177,7 +225,7 @@ FUNCTIONS Example of usage: - from scyjava import jimport + from scyjava import jimport, jstacktrace try: Integer = jimport('java.lang.Integer') nan = Integer.parseInt('not a number') @@ -191,6 +239,48 @@ FUNCTIONS jvm_started() Return true iff a Java virtual machine (JVM) has been started. + jvm_version() + Gets the version of the JVM as a tuple, + with each dot-separated digit as one element. + Characters in the version string beyond only + numbers and dots are ignored, in line + with the java.version system property. + + Examples: + * OpenJDK 17.0.1 -> [17, 0, 1] + * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] + * OpenJDK 1.8.0_312 -> [1, 8, 0] + + If the JVM is already started, + this function should return the equivalent of: + jimport('java.lang.System') + .getProperty('java.version') + .split('.') + + In case the JVM is not started yet,a best effort is made to deduce + the version from the environment without actually starting up the + JVM in-process. If the version cannot be deduced, a RuntimeError + with the cause is raised. + + shutdown_jvm() + Shutdown the JVM. + + This function makes a best effort to clean up Java resources first. + In particular, shutdown hooks registered with scyjava.when_jvm_stops + are sequentially invoked. + + Then, if the AWT subsystem has started, all AWT windows (as identified + by the java.awt.Window.getWindows() method) are disposed to reduce the + risk of GUI resources delaying JVM shutdown. + + Finally, the jpype.shutdownJVM() function is called. Note that you can + set the jpype.config.destroy_jvm flag to request JPype to destroy the + JVM explicitly, although setting this flag can lead to delayed shutdown + times while the JVM is waiting for threads to finish. + + Note that if the JVM is not already running, then this function does + nothing! In particular, shutdown hooks are skipped in this situation. + start_jvm(options=[]) Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling @@ -201,7 +291,7 @@ FUNCTIONS :param options: List of options to pass to the JVM. For example: ['-Djava.awt.headless=true', '-Xmx4g'] - to_java(data) + to_java(obj: Any) -> Any Recursively convert a Python object to a Java object. :param data: The Python object to convert. Supported types include: @@ -215,7 +305,7 @@ FUNCTIONS :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. - to_python(data, gentle=False) + to_python(data: Any, gentle: bool = False) -> Any Recursively convert a Java object to a Python object. :param data: The Java object to convert. :param gentle: If set, and the type cannot be converted, leaves @@ -242,6 +332,15 @@ FUNCTIONS started, the function executes immediately. :param f: Function to invoke when scyjava.start_jvm() is called. + + when_jvm_stops(f) + Registers a function to be called just before the JVM shuts down. + This is useful to perform cleanup of Java-dependent data structures. + + Note that if the JVM is not already running when shutdown_jvm is + called, then these registered callback functions will be skipped! + + :param f: Function to invoke when scyjava.shutdown_jvm() is called. ``` ## Troubleshooting From d26f817c69717397daa0d30aefb2cf1e0f2e0e73 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Wed, 7 Sep 2022 16:43:14 +0300 Subject: [PATCH 194/505] Fixed Trove classifiers into valid ones. --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1565ec45..3162e043 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,10 +17,10 @@ classifiers = Intended Audience :: Education Intended Audience :: Science/Research Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3 :: 3.7 - Programming Language :: Python :: 3 :: 3.8 - Programming Language :: Python :: 3 :: 3.9 - Programming Language :: Python :: 3 :: 3.10 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 License :: OSI Approved :: The Unlicense (Unlicense) Operating System :: Microsoft :: Windows Operating System :: Unix From ab53314e9ceab6fd52ef32da6d59a683f34acd99 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Wed, 7 Sep 2022 16:38:54 +0300 Subject: [PATCH 195/505] Migrated the metadata into `PEP-621`-compliant `pyproject.toml`. --- pyproject.toml | 83 ++++++++++++++++++++++++++++++++++++++++++++------ setup.cfg | 63 -------------------------------------- 2 files changed, 73 insertions(+), 73 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 42853180..4c6289fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,78 @@ [build-system] -# Setuptools version constraint ensures PEP 517 compatibility -requires = [ - "setuptools >= 45", - "wheel", - "setuptools_scm>=6.2", -] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.3"] build-backend = "setuptools.build_meta" +[project] +name = "scyjava" +authors = [ + {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, + {name = "Philipp Hanslovsky"}, + {name = "Edward Evans"}, + {name = "Mark Hiner"}, + {name = "Gabriel Selzer"}, +] +description = "Supercharged Java access from Python" +readme = "README.md" +license = {text = "The Unlicense"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: The Unlicense (Unlicense)", + "Operating System :: Microsoft :: Windows", + "Operating System :: Unix", + "Operating System :: MacOS", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Java Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", +] +requires-python = ">=3.7" +dependencies = [ + "jpype1 >= 1.3.0", + "jgo", +] +dynamic = ["version"] + + +[project.urls] +Homepage = "https://github.com/scijava/scyjava" +"Bug Tracker" = "https://github.com/scijava/scyjava/issues" +Documentation = "https://github.com/scijava/scyjava/blob/master/README.md" +"Source Code" = "https://github.com/scijava/scyjava" + +[project.optional-dependencies] +# Ensure any changes to this list are also added to environment-test.yml! +dev = [ + "autopep8", + "black", + "build", + "flake8", + "pytest", + "pytest-cov", + "numpy", + "pandas", + "setuptools-scm >= 6.2", +] + +[tool.setuptools] +package-dir = {"" = "src"} +# Ensure any changes to this list are also added to environment.yml AND environment-test.yml! +include-package-data = false + +[tool.setuptools.packages.find] +where = ["src"] +namespaces = false + [tool.setuptools_scm] -write_to = "src/scyjava/_version.py" -[tool.black] -# This file is autogenerated by setuptools_scm - it cannot be modified ---exclude = "src/scyjava/_version.py" +[tool.flake8] +# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 +max-line-length = "88" +extend-ignore = "E203" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3162e043..00000000 --- a/setup.cfg +++ /dev/null @@ -1,63 +0,0 @@ -[metadata] -name = scyjava -author = Curtis Rueden, Philipp Hanslovsky, Edward Evans, Mark Hiner, Gabriel Selzer -author_email = ctrueden@wisc.edu -description = Supercharged Java access from Python -long_description = file: README.md -long_description_content_type = text/markdown -license= The Unlicense -url = https://github.com/scijava/scyjava -project_urls = - Bug Tracker = https://github.com/scijava/scyjava/issues - Documentation = https://github.com/scijava/scyjava/blob/master/README.md - Source Code = https://github.com/scijava/scyjava -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Intended Audience :: Education - Intended Audience :: Science/Research - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - License :: OSI Approved :: The Unlicense (Unlicense) - Operating System :: Microsoft :: Windows - Operating System :: Unix - Operating System :: MacOS - Topic :: Scientific/Engineering - Topic :: Software Development :: Libraries :: Java Libraries - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Utilities - -[options] -packages = find: -package_dir = - = src -# Ensure any changes to this list are also added to environment.yml AND environment-test.yml! -python_requires = >=3.7 -install_requires = - jpype1 >= 1.3.0 - jgo - -[options.packages.find] -where = src - -[options.extras_require] - -# Ensure any changes to this list are also added to environment-test.yml! -dev = - autopep8 - black - build - flake8 - pytest - pytest-cov - numpy - pandas - setuptools-scm >= 6.2 - -[flake8] -# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 -max-line-length = 88 -extend-ignore = E203 From 2bd24f0afca896f77496c9895970861658c6c3ac Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 22 Sep 2022 11:36:54 -0500 Subject: [PATCH 196/505] Re-insert flake8 config into setup.cfg Flake8 does not yet support configuration via pyproject.toml. See https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations --- pyproject.toml | 5 ----- setup.cfg | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 4c6289fd..b8ea7251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,3 @@ where = ["src"] namespaces = false [tool.setuptools_scm] - -[tool.flake8] -# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 -max-line-length = "88" -extend-ignore = "E203" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..e87259ee --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +# TODO: Move all configuration into pyproject.toml + +[flake8] +# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 +max-line-length = 88 +extend-ignore = E203 From c535dfc426d17de4418c109a4fbde51fd717f55d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 22 Sep 2022 13:14:57 -0500 Subject: [PATCH 197/505] Use static versioning over setuptools_scm --- .gitignore | 3 --- dev-environment.yml | 2 +- pyproject.toml | 7 ++---- src/scyjava/__init__.py | 32 +++++---------------------- src/scyjava/_version.py | 16 ++++++++++++++ tests/test_version.py | 48 +++++++++++++---------------------------- 6 files changed, 39 insertions(+), 69 deletions(-) create mode 100644 src/scyjava/_version.py diff --git a/.gitignore b/.gitignore index fbcac6bc..7fdf6cba 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,6 @@ /.eggs/ *egg-info/ -# setuptools_scm -/src/*/_version.py - # Vi *.swp diff --git a/dev-environment.yml b/dev-environment.yml index 871819e0..02d70dde 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -27,7 +27,7 @@ dependencies: - flake8 - pytest - pytest-cov - - setuptools-scm >= 6.2 + - toml # Project from source - pip - pip: diff --git a/pyproject.toml b/pyproject.toml index b8ea7251..dff67d12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" +version = "1.5.2.dev0" authors = [ {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, {name = "Philipp Hanslovsky"}, @@ -38,8 +39,6 @@ dependencies = [ "jpype1 >= 1.3.0", "jgo", ] -dynamic = ["version"] - [project.urls] Homepage = "https://github.com/scijava/scyjava" @@ -58,7 +57,7 @@ dev = [ "pytest-cov", "numpy", "pandas", - "setuptools-scm >= 6.2", + "toml", ] [tool.setuptools] @@ -69,5 +68,3 @@ include-package-data = false [tool.setuptools.packages.find] where = ["src"] namespaces = false - -[tool.setuptools_scm] diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c9eb3fb7..0bca244a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -25,9 +25,14 @@ JLong, JShort, ) +from scyjava._version import _find_version import scyjava.config + +__version__ = _find_version() + + _logger = logging.getLogger(__name__) # Set of module properties @@ -65,33 +70,6 @@ def __getattr__(name): raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -@constant -def ___version__(): - # First pass: use the version output by setuptools_scm - try: - import scyjava.version - - return scyjava.version.version - except ImportError: - pass - # Second pass: use importlib.metadata - try: - from importlib.metadata import PackageNotFoundError, version - - return version("scyjava") - except ImportError or PackageNotFoundError: - pass - # Third pass: use pkg_resources - try: - from pkg_resources import get_distribution - - return get_distribution("scyjava").version - except ImportError: - pass - # Fourth pass: Give up - return "Cannot determine version! Ensure pkg_resources is installed!" - - # -- JVM setup -- _startup_callbacks = [] diff --git a/src/scyjava/_version.py b/src/scyjava/_version.py new file mode 100644 index 00000000..ce45f429 --- /dev/null +++ b/src/scyjava/_version.py @@ -0,0 +1,16 @@ +from importlib.util import find_spec + + +def _find_version(): + # First pass: use importlib.metadata + if find_spec("importlib.metadata"): + from importlib.metadata import version + + return version("scyjava") + + if find_spec("pkg_resources"): + from pkg_resources import get_distribution + + return get_distribution("scyjava").version + # Fourth pass: Give up + return "Cannot determine version! Ensure pkg_resources is installed!" diff --git a/tests/test_version.py b/tests/test_version.py index b8a03860..71af4545 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,37 +1,25 @@ -import os import sys +import toml +from pathlib import Path import pytest - -setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "_version.py") +import scyjava +from scyjava._version import _find_version def _scyjava_version(): """ Get ScyJava's version. """ - import scyjava - - # It's important that we clear the cache here, - # so that we can test different behaviors. - scyjava.___version__.cache_clear() - # Get the version - return scyjava.__version__ - + pyproject = toml.load(Path(__file__).parents[1] / "pyproject.toml") + return pyproject["project"]["version"] -def test_version_file(): - """Ensures that, ideally, the version from setuptools_scm is used""" - # Get the version from setuptools_scm - from setuptools_scm import get_version - setuptools_version = get_version(write_to="src/scyjava/_version.py") - # Ensure that the version was written to file - assert os.path.isfile(setuptools_file) - # Ensure that scyjava.__version__ matches this. - assert _scyjava_version() == setuptools_version - # Cleanup - remove file - os.remove(setuptools_file) - assert not os.path.isfile(setuptools_file) +def test_version_dunder(): + """ + Ensures that the dunder variable matches _scyjava_version + """ + assert scyjava.__version__ == _scyjava_version() @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") @@ -43,9 +31,8 @@ def test_version_importlib(): # Remove scyjava.version sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - from importlib.metadata import version - assert _scyjava_version() == version("scyjava") + assert _scyjava_version() == _find_version() @pytest.mark.skipif( @@ -57,15 +44,12 @@ def test_version_pkg_resources(): importlib.metadata unavailable, pkg_resources is used next. """ - # Remove scyjava.version - sys.modules["scyjava.version"] = None # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - from pkg_resources import get_distribution - assert _scyjava_version() == get_distribution("scyjava").version + assert _scyjava_version() == _find_version() def test_version_unvailable(): @@ -73,14 +57,12 @@ def test_version_unvailable(): Ensures that no version is returned if none of these strategies works. """ - # Remove scyjava.version - sys.modules["scyjava.version"] = None # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Remove pkg_resources sys.modules["pkg_resources"] = None # Ensure scyjava.__version__ is an error message. assert ( - _scyjava_version() - == "Cannot determine version! Ensure pkg_resources is installed!" + "Cannot determine version! Ensure pkg_resources is installed!" + == _find_version() ) From 1d608ff71775283f0903a1813374825df5c6de98 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 13:49:22 -0500 Subject: [PATCH 198/505] test_version: do some minor cleanups * Rename _scyjava_version to _expected_version, for clarity. * Use imperative tense for docstrings, as per Python convention. * Terminate each docstring sentence with a period. --- tests/test_version.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index 71af4545..2c7a8d1a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,9 +7,9 @@ from scyjava._version import _find_version -def _scyjava_version(): +def _expected_version(): """ - Get ScyJava's version. + Get the project version from pyproject.toml. """ pyproject = toml.load(Path(__file__).parents[1] / "pyproject.toml") return pyproject["project"]["version"] @@ -17,22 +17,22 @@ def _scyjava_version(): def test_version_dunder(): """ - Ensures that the dunder variable matches _scyjava_version + Ensure that the dunder variable matches _expected_version. """ - assert scyjava.__version__ == _scyjava_version() + assert _expected_version() == scyjava.__version__ @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") def test_version_importlib(): """ - Ensures that, with scyjava.version.version unavailable, - importlib.metadata is used next WITH python 3.8+ + Ensure that, with scyjava.version.version unavailable, + importlib.metadata is used next WITH python 3.8+. """ # Remove scyjava.version sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - assert _scyjava_version() == _find_version() + assert _expected_version() == _find_version() @pytest.mark.skipif( @@ -40,7 +40,7 @@ def test_version_importlib(): ) def test_version_pkg_resources(): """ - Ensures that, with scyjava.version.version AND + Ensure that, with scyjava.version.version AND importlib.metadata unavailable, pkg_resources is used next. """ @@ -49,12 +49,12 @@ def test_version_pkg_resources(): # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - assert _scyjava_version() == _find_version() + assert _expected_version() == _find_version() -def test_version_unvailable(): +def test_version_unavailable(): """ - Ensures that no version is returned if none of these + Ensure that no version is returned if none of these strategies works. """ # Remove importlib.metadata From b2c61a69f2483e6529b1e39e3753ebcbab2a322b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 13:41:55 -0500 Subject: [PATCH 199/505] Unify Python module version logic into get_version In this way, it can be used easily by a downstream component to extract its version into its own __version__ dunder as well. And change get_version to raise an exception if extraction fails. --- README.md | 14 +++++++++----- src/scyjava/__init__.py | 43 ++++++++++++++++++++++++++++++----------- src/scyjava/_version.py | 16 --------------- tests/test_version.py | 18 ++++++++--------- 4 files changed, 50 insertions(+), 41 deletions(-) delete mode 100644 src/scyjava/_version.py diff --git a/README.md b/README.md index fac7a42e..4d29a330 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,13 @@ FUNCTIONS leading underscore! :param func: The function to turn into a property - get_version(java_class) - Return the version of a Java class. - Requires org.scijava:scijava-common on the classpath. + get_version(java_class_or_python_package) + Return the version of a Java class or Python package. + + For Python packages, uses importlib.metadata.version if available + (Python 3.8+), with pkg_resources.get_distribution as a fallback. + + For Java classes, requires org.scijava:scijava-common on the classpath. The version string is extracted from the given class's associated JAR artifact (if any), either the embedded Maven POM if the project was built @@ -182,12 +186,12 @@ FUNCTIONS jimport a java.awt or javax.swing class. This can lead to deadlocks on macOS if you are not running in headless mode and did not invoke those actions via the jpype.setupGuiEnvironment wrapper function; - see the Troubleshooting section below for details. + see the Troubleshooting section of the scyjava README for details. is_jvm_headless() Return true iff Java is running in headless mode. - :raises RuntimeException: If the JVM has not started yet. + :raises RuntimeError: If the JVM has not started yet. is_version_at_least(actual_version, minimum_version) Return a boolean on a version comparison. diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 0bca244a..10d21a43 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -7,6 +7,7 @@ import sys import typing from functools import lru_cache +from importlib.util import find_spec from pathlib import Path from typing import Any, Callable, Dict, NamedTuple @@ -25,14 +26,9 @@ JLong, JShort, ) -from scyjava._version import _find_version - import scyjava.config -__version__ = _find_version() - - _logger = logging.getLogger(__name__) # Set of module properties @@ -347,10 +343,14 @@ def when_jvm_stops(f): # -- Utility functions -- -def get_version(java_class): +def get_version(java_class_or_python_package): """ - Return the version of a Java class. - Requires org.scijava:scijava-common on the classpath. + 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 Java classes, requires org.scijava:scijava-common on the classpath. The version string is extracted from the given class's associated JAR artifact (if any), either the embedded Maven POM if the project was built @@ -358,9 +358,27 @@ def get_version(java_class): See org.scijava.VersionUtils.getVersion(Class) for further details. """ - VersionUtils = jimport("org.scijava.util.VersionUtils") - version = VersionUtils.getVersion(java_class) - return version + + if isjava(java_class_or_python_package): + # Assume we were given a Java class object. + VersionUtils = jimport("org.scijava.util.VersionUtils") + return str(VersionUtils.getVersion(java_class_or_python_package)) + + # Assume we were given a Python package name. + + if find_spec("importlib.metadata"): + # Fastest, but requires Python 3.8+. + from importlib.metadata import version + + return version(java_class_or_python_package) + + if find_spec("pkg_resources"): + # Slower, but works on Python 3.7. + from pkg_resources import get_distribution + + return get_distribution(java_class_or_python_package).version + + raise RuntimeError("Cannot determine version! Is pkg_resources installed?") def is_version_at_least(actual_version, minimum_version): @@ -1136,6 +1154,9 @@ def _pandas_to_table(df): return table +__version__ = get_version("scyjava") + + # -- JVM startup callbacks -- # NB: These must be performed last, because if this class is imported after the diff --git a/src/scyjava/_version.py b/src/scyjava/_version.py deleted file mode 100644 index ce45f429..00000000 --- a/src/scyjava/_version.py +++ /dev/null @@ -1,16 +0,0 @@ -from importlib.util import find_spec - - -def _find_version(): - # First pass: use importlib.metadata - if find_spec("importlib.metadata"): - from importlib.metadata import version - - return version("scyjava") - - if find_spec("pkg_resources"): - from pkg_resources import get_distribution - - return get_distribution("scyjava").version - # Fourth pass: Give up - return "Cannot determine version! Ensure pkg_resources is installed!" diff --git a/tests/test_version.py b/tests/test_version.py index 2c7a8d1a..7d62db18 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest import scyjava -from scyjava._version import _find_version +from scyjava import get_version def _expected_version(): @@ -32,7 +32,7 @@ def test_version_importlib(): sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - assert _expected_version() == _find_version() + assert _expected_version() == get_version("scyjava") @pytest.mark.skipif( @@ -49,20 +49,20 @@ def test_version_pkg_resources(): # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - assert _expected_version() == _find_version() + assert _expected_version() == get_version("scyjava") def test_version_unavailable(): """ - Ensure that no version is returned if none of these - strategies works. + Ensure that an exception is raised if none of these strategies works. """ # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Remove pkg_resources sys.modules["pkg_resources"] = None - # Ensure scyjava.__version__ is an error message. + # Ensure scyjava.__version__ raises an exception. + with pytest.raises(RuntimeError) as e_info: + get_version("scyjava") assert ( - "Cannot determine version! Ensure pkg_resources is installed!" - == _find_version() - ) + "RuntimeError: Cannot determine version! Is pkg_resources installed?" + ) == e_info.exconly() From c487c77fa4db63bfe2667338de0ed877ad8005e2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 15:15:55 -0500 Subject: [PATCH 200/505] Fix name of dev environment --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dff67d12..6961ee56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ Documentation = "https://github.com/scijava/scyjava/blob/master/README.md" "Source Code" = "https://github.com/scijava/scyjava" [project.optional-dependencies] -# Ensure any changes to this list are also added to environment-test.yml! +# Ensure any changes to this list are also added to dev-environment.yml! dev = [ "autopep8", "black", @@ -62,7 +62,7 @@ dev = [ [tool.setuptools] package-dir = {"" = "src"} -# Ensure any changes to this list are also added to environment.yml AND environment-test.yml! +# Ensure any changes to this list are also added to environment.yml AND dev-environment.yml! include-package-data = false [tool.setuptools.packages.find] From 01b71173822be4dc3cdf751f23f83720d94ff81c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 15:16:29 -0500 Subject: [PATCH 201/505] Bump to next minor version There is new API, e.g. is_version_at_least and is_jvm_headless. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6961ee56..10e16930 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.5.2.dev0" +version = "1.6.0.dev0" authors = [ {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, {name = "Philipp Hanslovsky"}, From 0382b1d2d857916c55072de4aa7ab7d55acb394c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 15:21:30 -0500 Subject: [PATCH 202/505] Release version 1.6.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 10e16930..57c489ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.6.0.dev0" +version = "1.6.0" authors = [ {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, {name = "Philipp Hanslovsky"}, From 8e06702266e055eca762a81c8bbd1b107be49a2f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 15:31:52 -0500 Subject: [PATCH 203/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 57c489ca..16d25b3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.6.0" +version = "1.6.1.dev0" authors = [ {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, {name = "Philipp Hanslovsky"}, From 4229d3cb3917b302a8807d430cb0b32915cc4115 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 17:49:21 -0500 Subject: [PATCH 204/505] Remove remnants of setuptools_scm --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 16d25b3c..e81553ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.3"] +requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [project] From 4990a8508676131ae00c1ba5c06db5097b45df62 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 11:59:25 -0500 Subject: [PATCH 205/505] Add type reasoning utility methods for arraylikes These are mostly useful for NumPy-related activities. But they do not introduce any numpy dependency, so we include them as utility methods here for convenience. --- src/scyjava/__init__.py | 55 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 10d21a43..7be85474 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -340,7 +340,7 @@ def when_jvm_stops(f): _shutdown_callbacks.append(f) -# -- Utility functions -- +# -- Version reasoning -- def get_version(java_class_or_python_package): @@ -407,7 +407,58 @@ def compare_version(version, java_class_version): ) -# -- Type Conversion Utilities -- +# -- Type reasoning -- + + +def is_arraylike(arr): + """ + Return True iff the object is arraylike: possessing + .shape, .dtype, .__array__, and .ndim attributes. + + :param arr: The object to check for arraylike properties + :return: True iff the object is arraylike + """ + return ( + hasattr(arr, "shape") + and hasattr(arr, "dtype") + and hasattr(arr, "__array__") + and hasattr(arr, "ndim") + ) + + +def is_memoryarraylike(arr): + """ + Return True iff the object is memoryarraylike: + an arraylike object whose .data type is memoryview. + + :param arr: The object to check for memoryarraylike properties + :return: True iff the object is memoryarraylike + """ + return ( + is_arraylike(arr) + and hasattr(arr, "data") + and type(arr.data).__name__ == "memoryview" + ) + + +def is_xarraylike(xarr): + """ + Return True iff the object is xarraylike: + possessing .values, .dims, and .coords attributes, + and whose .values are arraylike. + + :param arr: The object to check for xarraylike properties + :return: True iff the object is xarraylike + """ + return ( + hasattr(xarr, "values") + and hasattr(xarr, "dims") + and hasattr(xarr, "coords") + and is_arraylike(xarr.values) + ) + + +# -- Type conversion -- # NB: We cannot use org.scijava.priority.Priority or other Java-side class # here because we don't want to impose Java-side dependencies, and we don't From 7471b5637148d33ed37b8db8d71c205255ea9de9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 13:18:05 -0500 Subject: [PATCH 206/505] Add isort to the linting process --- .github/workflows/build.yml | 24 ++++++++++++------------ .pre-commit-config.yaml | 23 +++++++++++++++++++++++ bin/lint.sh | 1 + dev-environment.yml | 1 + pyproject.toml | 4 ++++ 5 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cee0aada..d47a6c81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,24 +49,24 @@ jobs: run: | bin/test.sh --color=yes - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: psf/black@stable - - flake: + ensure-clean-code: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v3 - - name: flake src code + + - name: Lint code + uses: psf/black@stable + + - name: Flake code run: | python -m pip install flake8 - python -m flake8 src - - name: flake test code - run: | - python -m flake8 tests + python -m flake8 src tests + + - name: Check import ordering + uses: isort/isort-action@master + with: + configuration: --check-only conda-dev-test: name: Conda Setup & Code Coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..82261083 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: + # First, autoflake the code to avoid issues that could be solved quickly + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: ["--in-place", "--remove-all-unused-imports"] + # Then, flake + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: [flake8-typing-imports==1.7.0] + # Next, sort imports + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + # Finally, lint + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black diff --git a/bin/lint.sh b/bin/lint.sh index 2a93272d..c1dbebd1 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,4 +4,5 @@ dir=$(dirname "$0") cd "$dir/.." black src tests +isort src tests python -m flake8 src tests diff --git a/dev-environment.yml b/dev-environment.yml index 02d70dde..6a29853a 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -25,6 +25,7 @@ dependencies: - black - build - flake8 + - isort - pytest - pytest-cov - toml diff --git a/pyproject.toml b/pyproject.toml index e81553ee..0b51accb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dev = [ "black", "build", "flake8", + "isort", "pytest", "pytest-cov", "numpy", @@ -68,3 +69,6 @@ include-package-data = false [tool.setuptools.packages.find] where = ["src"] namespaces = false + +[tool.isort] +profile = "black" From f5a9c5ecd5e680f4b25053d0668b7eea9122862a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 13:18:33 -0500 Subject: [PATCH 207/505] Run isort on the project --- src/scyjava/__init__.py | 2 +- tests/test_version.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 7be85474..efd66cb5 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -26,8 +26,8 @@ JLong, JShort, ) -import scyjava.config +import scyjava.config _logger = logging.getLogger(__name__) diff --git a/tests/test_version.py b/tests/test_version.py index 7d62db18..8fb1587f 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,8 +1,9 @@ import sys -import toml - from pathlib import Path + import pytest +import toml + import scyjava from scyjava import get_version From edee25f99f650609f34f4bd24ffe7bca1fc9abcf Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 13:27:07 -0500 Subject: [PATCH 208/505] Use public jpype.JObject, not private _JObject I don't know why we were using the private version, but I tested all the functions with the public jpype.JObject instead, and it works just fine. --- src/scyjava/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index efd66cb5..f167cdcf 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -14,7 +14,6 @@ import jgo import jpype import jpype.config -from _jpype import _JObject from jpype.types import ( JArray, JBoolean, @@ -499,7 +498,7 @@ def _add_converter(converter: Converter, converters: typing.List[Converter]): def isjava(data): """Return whether the given data object is a Java object.""" - return isinstance(data, jpype.JClass) or isinstance(data, _JObject) + return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) def jclass(data): @@ -511,13 +510,13 @@ def jclass(data): A. Name of a class to look up, analogous to Class.forName("java.lang.String"); B. A jpype.JClass object analogous to String.class; - C. A _jpype._JObject instance analogous to o.getClass(). + C. A jpype.JObject instance analogous to o.getClass(). :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. """ if isinstance(data, jpype.JClass): return data.class_ - if isinstance(data, _JObject): + if isinstance(data, jpype.JObject): return data.getClass() if isinstance(data, str): return jclass(jimport(data)) From 5c4ac3c32ccb7cfec6d8f239ae8928cb9a95240a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 14:10:48 -0500 Subject: [PATCH 209/505] Move JVM-oriented functions to their own file --- src/scyjava/__init__.py | 545 ++++++++-------------------------------- src/scyjava/_java.py | 410 ++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+), 440 deletions(-) create mode 100644 src/scyjava/_java.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index f167cdcf..b23e5fe1 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1,19 +1,10 @@ -import atexit import collections.abc import logging -import os -import re -import subprocess -import sys import typing from functools import lru_cache from importlib.util import find_spec -from pathlib import Path from typing import Any, Callable, Dict, NamedTuple -import jgo -import jpype -import jpype.config from jpype.types import ( JArray, JBoolean, @@ -26,7 +17,21 @@ JShort, ) -import scyjava.config +from scyjava._java import ( # noqa: F401 + JavaClasses, + is_awt_initialized, + is_jvm_headless, + isjava, + jclass, + jimport, + jstacktrace, + jvm_started, + jvm_version, + shutdown_jvm, + start_jvm, + when_jvm_starts, + when_jvm_stops, +) _logger = logging.getLogger(__name__) @@ -65,280 +70,6 @@ def __getattr__(name): raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -# -- JVM setup -- - -_startup_callbacks = [] -_shutdown_callbacks = [] - - -def jvm_version(): - """ - Gets the version of the JVM as a tuple, - with each dot-separated digit as one element. - Characters in the version string beyond only - numbers and dots are ignored, in line - with the java.version system property. - - Examples: - * OpenJDK 17.0.1 -> [17, 0, 1] - * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] - * OpenJDK 1.8.0_312 -> [1, 8, 0] - - If the JVM is already started, - this function should return the equivalent of: - jimport('java.lang.System') - .getProperty('java.version') - .split('.') - - In case the JVM is not started yet,a best effort is made to deduce - the version from the environment without actually starting up the - JVM in-process. If the version cannot be deduced, a RuntimeError - with the cause is raised. - """ - jvm_version = jpype.getJVMVersion() - if jvm_version and jvm_version[0]: - # JPype already knew the version. - # JVM is probably already started. - # Or JPype got smarter since 1.3.0. - return jvm_version - - # JPype was clueless, which means the JVM has probably not started yet. - # Let's look for a java executable, and ask it directly with 'java - # -version'. - - default_jvm_path = jpype.getDefaultJVMPath() - if not default_jvm_path: - raise RuntimeError("Cannot glean the default JVM path") - - p = Path(default_jvm_path) - if not p.exists(): - raise RuntimeError(f"Invalid default JVM path: {p}") - - java = None - for _ in range(3): # The bin folder is always <=3 levels up from libjvm. - p = p.parent - if p.name == "lib": - java = p.parent / "bin" / "java" - elif p.name == "bin": - java = p / "java" - - if java is not None: - if os.name == "nt": - # Good ol' Windows! Nothing beats Windows. - java = java.with_suffix(".exe") - if not java.is_file(): - raise RuntimeError(f"No ../bin/java found at: {p}") - break - if java is None: - raise RuntimeError(f"No java executable found inside: {p}") - - version = subprocess.check_output( - [str(java), "-version"], stderr=subprocess.STDOUT - ).decode() - m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) - if not m: - raise RuntimeError(f"Inscrutable java command output:\n{version}") - - return tuple(map(int, m.group(1).split("."))) - - -_config_options = scyjava.config.get_options() - - -def start_jvm(options=_config_options): - """ - Explicitly connect to the Java virtual machine (JVM). Only one JVM can - be active; does nothing if the JVM has already been started. Calling - this function directly is typically not necessary, because the first - time a scyjava function needing a JVM is invoked, one is started on the - fly with the configuration specified via the scijava.config mechanism. - - :param options: List of options to pass to the JVM. For example: - ['-Djava.awt.headless=true', '-Xmx4g'] - """ - # if JVM is already running -- break - if jvm_started(): - _logger.debug("The JVM is already running.") - return - - # retrieve endpoint and repositories from scyjava config - endpoints = scyjava.config.endpoints - repositories = scyjava.config.get_repositories() - - # use the logger to notify user that endpoints are being added - _logger.debug("Adding jars from endpoints {0}".format(endpoints)) - - # get endpoints and add to JPype class path - if len(endpoints) > 0: - endpoints = endpoints[:1] + sorted(endpoints[1:]) - _logger.debug("Using endpoints %s", endpoints) - _, workspace = jgo.resolve_dependencies( - "+".join(endpoints), - m2_repo=scyjava.config.get_m2_repo(), - cache_dir=scyjava.config.get_cache_dir(), - manage_dependencies=scyjava.config.get_manage_deps(), - repositories=repositories, - verbose=scyjava.config.get_verbose(), - shortcuts=scyjava.config.get_shortcuts(), - ) - jpype.addClassPath(os.path.join(workspace, "*")) - - # HACK: Try to set JAVA_HOME if it isn't already. - if ( - "JAVA_HOME" not in os.environ - or not os.environ["JAVA_HOME"] - or not os.path.isdir(os.environ["JAVA_HOME"]) - ): - - _logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.") - - libjvm_win = Path("Library") / "jre" / "bin" / "server" / "jvm.dll" - libjvm_macos = Path("lib") / "server" / "libjvm.dylib" - libjvm_linux = Path("lib") / "server" / "libjvm.so" - libjvm_paths = { - libjvm_win: Path("Library"), - libjvm_macos: Path(), - libjvm_linux: Path(), - } - for p in sys.path: - if not p.endswith("site-packages"): - continue - # e.g. $CONDA_PREFIX/lib/python3.10/site-packages -> $CONDA_PREFIX - # But we want it to work outside of Conda as well, theoretically. - base = Path(p).parent.parent.parent - for libjvm_path, java_home_path in libjvm_paths.items(): - if (base / libjvm_path).exists(): - java_home = str((base / java_home_path).resolve()) - _logger.debug(f"Detected JAVA_HOME: {java_home}") - os.environ["JAVA_HOME"] = java_home - break - - # initialize JPype JVM - _logger.debug("Starting JVM") - jpype.startJVM(*options, interrupt=True) - - # replace JPype/JVM shutdown handling with our own - jpype.config.onexit = False - jpype.config.free_resources = False - atexit.register(shutdown_jvm) - - _import_java_classes() - - # invoke registered callback functions - for callback in _startup_callbacks: - callback() - - -def shutdown_jvm(): - """Shutdown the JVM. - - This function makes a best effort to clean up Java resources first. - In particular, shutdown hooks registered with scyjava.when_jvm_stops - are sequentially invoked. - - Then, if the AWT subsystem has started, all AWT windows (as identified - by the java.awt.Window.getWindows() method) are disposed to reduce the - risk of GUI resources delaying JVM shutdown. - - Finally, the jpype.shutdownJVM() function is called. Note that you can - set the jpype.config.destroy_jvm flag to request JPype to destroy the - JVM explicitly, although setting this flag can lead to delayed shutdown - times while the JVM is waiting for threads to finish. - - Note that if the JVM is not already running, then this function does - nothing! In particular, shutdown hooks are skipped in this situation. - """ - if not jvm_started(): - return - - # invoke registered shutdown callback functions - for callback in _shutdown_callbacks: - try: - callback() - except Exception as e: - print(f"Exception during shutdown callback: {e}") - - # dispose AWT resources if applicable - if is_awt_initialized(): - Window = jimport("java.awt.Window") - for w in Window.getWindows(): - w.dispose() - - # okay to shutdown JVM - try: - jpype.shutdownJVM() - except Exception as e: - print(f"Exception during JVM shutdown: {e}") - - -def jvm_started(): - """Return true iff a Java virtual machine (JVM) has been started.""" - return jpype.isJVMStarted() - - -def is_jvm_headless(): - """ - Return true iff Java is running in headless mode. - - :raises RuntimeError: If the JVM has not started yet. - """ - if not jvm_started(): - raise RuntimeError("JVM has not started yet!") - - GraphicsEnvironment = scyjava.jimport("java.awt.GraphicsEnvironment") - return GraphicsEnvironment.isHeadless() - - -def is_awt_initialized(): - """ - Return true iff the AWT subsystem has been initialized. - - Java starts up its AWT subsystem automatically and implicitly, as - soon as an action is performed requiring it -- for example, if you - jimport a java.awt or javax.swing class. This can lead to deadlocks - on macOS if you are not running in headless mode and did not invoke - those actions via the jpype.setupGuiEnvironment wrapper function; - see the Troubleshooting section of the scyjava README for details. - """ - if not jvm_started(): - return False - Thread = scyjava.jimport("java.lang.Thread") - threads = Thread.getAllStackTraces().keySet() - return any(t.getName().startsWith("AWT-") for t in threads) - - -def when_jvm_starts(f): - """ - Registers a function to be called when the JVM starts (or immediately). - This is useful to defer construction of Java-dependent data structures - until the JVM is known to be available. If the JVM has already been - started, the function executes immediately. - - :param f: Function to invoke when scyjava.start_jvm() is called. - """ - if jvm_started(): - # JVM was already started; invoke callback function immediately. - f() - else: - # Add function to the list of callbacks to invoke upon start_jvm(). - global _startup_callbacks - _startup_callbacks.append(f) - - -def when_jvm_stops(f): - """ - Registers a function to be called just before the JVM shuts down. - This is useful to perform cleanup of Java-dependent data structures. - - Note that if the JVM is not already running when shutdown_jvm is - called, then these registered callback functions will be skipped! - - :param f: Function to invoke when scyjava.shutdown_jvm() is called. - """ - global _shutdown_callbacks - _shutdown_callbacks.append(f) - - # -- Version reasoning -- @@ -496,79 +227,12 @@ def _add_converter(converter: Converter, converters: typing.List[Converter]): # https://github.com/kivy/pyjnius/issues/217#issue-145981070 -def isjava(data): - """Return whether the given data object is a Java object.""" - return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) - - -def jclass(data): - """ - Obtain a Java class object. - - :param data: The object from which to glean the class. - Supported types include: - A. Name of a class to look up, analogous to - Class.forName("java.lang.String"); - B. A jpype.JClass object analogous to String.class; - C. A jpype.JObject instance analogous to o.getClass(). - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. - """ - if isinstance(data, jpype.JClass): - return data.class_ - if isinstance(data, jpype.JObject): - return data.getClass() - if isinstance(data, str): - return jclass(jimport(data)) - raise TypeError("Cannot glean class from data of type: " + str(type(data))) - - -@lru_cache(maxsize=None) -def jimport(class_name): - """ - Import a class from Java to Python. - - :param class_name: Name of the class to import. - :returns: A pointer to the class, which can be used to - e.g. instantiate objects of that class. - """ - start_jvm() - return jpype.JClass(class_name) - - -def jstacktrace(exc): - """ - Extract the Java-side stack trace from a Java exception. - - Example of usage: - - from scyjava import jimport, jstacktrace - try: - Integer = jimport('java.lang.Integer') - nan = Integer.parseInt('not a number') - except Exception as exc: - print(jstacktrace(exc)) - - :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string - if no stack trace could be extracted. - """ - try: - StringWriter = jimport("java.io.StringWriter") - PrintWriter = jimport("java.io.PrintWriter") - sw = StringWriter() - exc.printStackTrace(PrintWriter(sw, True)) - return sw.toString() - except BaseException: - return "" - - def _raise_type_exception(obj: Any): raise TypeError("Unsupported type: " + str(type(obj))) def _convertMap(obj: collections.abc.Mapping): - jmap = LinkedHashMap() + jmap = _jc.LinkedHashMap() for k, v in obj.items(): jk = to_java(k) jv = to_java(v) @@ -577,7 +241,7 @@ def _convertMap(obj: collections.abc.Mapping): def _convertSet(obj: collections.abc.Set): - jset = LinkedHashSet() + jset = _jc.LinkedHashSet() for item in obj: jitem = to_java(item) jset.add(jitem) @@ -585,7 +249,7 @@ def _convertSet(obj: collections.abc.Set): def _convertIterable(obj: collections.abc.Iterable): - jlist = ArrayList() + jlist = _jc.ArrayList() for item in obj: jitem = to_java(item) jlist.add(jitem) @@ -650,49 +314,49 @@ def _stock_java_converters() -> typing.List[Converter]: # String converter Converter( predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: String(obj.encode("utf-8"), "utf-8"), + converter=lambda obj: _jc.String(obj.encode("utf-8"), "utf-8"), ), # Boolean converter Converter( predicate=lambda obj: isinstance(obj, bool), - converter=Boolean, + converter=_jc.Boolean, ), # Integer converter Converter( predicate=lambda obj: isinstance(obj, int) - and Integer.MIN_VALUE <= obj <= Integer.MAX_VALUE, - converter=Integer, + and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, + converter=_jc.Integer, ), # Long converter Converter( predicate=lambda obj: isinstance(obj, int) - and Long.MIN_VALUE <= obj <= Long.MAX_VALUE, - converter=Long, + and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, + converter=_jc.Long, priority=Priority.NORMAL - 1, ), # BigInteger converter Converter( predicate=lambda obj: isinstance(obj, int), - converter=lambda obj: BigInteger(str(obj)), + converter=lambda obj: _jc.BigInteger(str(obj)), priority=Priority.NORMAL - 2, ), # Float converter Converter( predicate=lambda obj: isinstance(obj, float) - and Float.MIN_VALUE <= obj <= Float.MAX_VALUE, - converter=Float, + and _jc.Float.MIN_VALUE <= obj <= _jc.Float.MAX_VALUE, + converter=_jc.Float, ), # Double converter Converter( predicate=lambda obj: isinstance(obj, float) - and Double.MAX_VALUE <= obj <= Double.MAX_VALUE, - converter=Double, + and _jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE, + converter=_jc.Double, priority=Priority.NORMAL - 1, ), # BigDecimal converter Converter( predicate=lambda obj: isinstance(obj, float), - converter=lambda obj: BigDecimal(str(obj)), + converter=lambda obj: _jc.BigDecimal(str(obj)), priority=Priority.NORMAL - 2, ), # Pandas table converter @@ -733,7 +397,7 @@ def _jstr(data): class JavaObject: def __init__(self, jobj, intended_class=None): if intended_class is None: - intended_class = Object + intended_class = _jc.Object if not isinstance(jobj, intended_class): raise TypeError( f"Not a {intended_class.getName()}: {jclass(jobj).getName()}" @@ -746,7 +410,7 @@ def __str__(self): class JavaIterable(JavaObject, collections.abc.Iterable): def __init__(self, jobj): - JavaObject.__init__(self, jobj, Iterable) + JavaObject.__init__(self, jobj, _jc.Iterable) def __iter__(self): return to_python(self.jobj.iterator()) @@ -757,7 +421,7 @@ def __str__(self): class JavaCollection(JavaIterable, collections.abc.Collection): def __init__(self, jobj): - JavaObject.__init__(self, jobj, Collection) + JavaObject.__init__(self, jobj, _jc.Collection) def __contains__(self, item): # NB: Collection.contains returns boolean, so no need for gentleness. @@ -780,7 +444,7 @@ def __eq__(self, other): class JavaIterator(JavaObject, collections.abc.Iterator): def __init__(self, jobj): - JavaObject.__init__(self, jobj, Iterator) + JavaObject.__init__(self, jobj, _jc.Iterator) def __next__(self): if self.jobj.hasNext(): @@ -792,7 +456,7 @@ def __next__(self): class JavaList(JavaCollection, collections.abc.MutableSequence): def __init__(self, jobj): - JavaObject.__init__(self, jobj, List) + JavaObject.__init__(self, jobj, _jc.List) def __getitem__(self, key): # NB: Even if an element cannot be converted, @@ -816,7 +480,7 @@ def insert(self, index, object): class JavaMap(JavaObject, collections.abc.MutableMapping): def __init__(self, jobj): - JavaObject.__init__(self, jobj, Map) + JavaObject.__init__(self, jobj, _jc.Map) def __getitem__(self, key): # NB: Even if an element cannot be converted, @@ -862,7 +526,7 @@ def item_str(k, v): class JavaSet(JavaCollection, collections.abc.MutableSet): def __init__(self, jobj): - JavaObject.__init__(self, jobj, Set) + JavaObject.__init__(self, jobj, _jc.Set) def add(self, item): # NB: Set.add returns boolean, so no need for gentleness. @@ -976,62 +640,62 @@ def _stock_py_converters() -> typing.List: ), # Boolean converter Converter( - predicate=lambda obj: isinstance(obj, Boolean), + predicate=lambda obj: isinstance(obj, _jc.Boolean), converter=lambda obj: obj.booleanValue(), ), # Byte converter Converter( - predicate=lambda obj: isinstance(obj, Byte), + predicate=lambda obj: isinstance(obj, _jc.Byte), converter=lambda obj: obj.byteValue(), ), # Char converter Converter( - predicate=lambda obj: isinstance(obj, Character), + predicate=lambda obj: isinstance(obj, _jc.Character), converter=lambda obj: obj.toString(), ), # Double converter Converter( - predicate=lambda obj: isinstance(obj, Double), + predicate=lambda obj: isinstance(obj, _jc.Double), converter=lambda obj: obj.doubleValue(), ), # Float converter Converter( - predicate=lambda obj: isinstance(obj, Float), + predicate=lambda obj: isinstance(obj, _jc.Float), converter=lambda obj: obj.floatValue(), ), # Integer converter Converter( - predicate=lambda obj: isinstance(obj, Integer), + predicate=lambda obj: isinstance(obj, _jc.Integer), converter=lambda obj: obj.intValue(), ), # Long converter Converter( - predicate=lambda obj: isinstance(obj, Long), + predicate=lambda obj: isinstance(obj, _jc.Long), converter=lambda obj: obj.longValue(), ), # Short converter Converter( - predicate=lambda obj: isinstance(obj, Short), + predicate=lambda obj: isinstance(obj, _jc.Short), converter=lambda obj: obj.shortValue(), ), # Void converter Converter( - predicate=lambda obj: isinstance(obj, Void), + predicate=lambda obj: isinstance(obj, _jc.Void), converter=lambda obj: None, ), # String converter Converter( - predicate=lambda obj: isinstance(obj, String), + predicate=lambda obj: isinstance(obj, _jc.String), converter=lambda obj: str(obj), ), # BigInteger converter Converter( - predicate=lambda obj: isinstance(obj, BigInteger), + predicate=lambda obj: isinstance(obj, _jc.BigInteger), converter=lambda obj: int(str(obj.toString())), ), # BigDecimal converter Converter( - predicate=lambda obj: isinstance(obj, BigDecimal), + predicate=lambda obj: isinstance(obj, _jc.BigDecimal), converter=lambda obj: float(str(obj.toString())), ), # SciJava Table converter @@ -1041,34 +705,34 @@ def _stock_py_converters() -> typing.List: ), # List converter Converter( - predicate=lambda obj: isinstance(obj, List), + predicate=lambda obj: isinstance(obj, _jc.List), converter=JavaList, ), # Map converter Converter( - predicate=lambda obj: isinstance(obj, Map), + predicate=lambda obj: isinstance(obj, _jc.Map), converter=JavaMap, ), # Set converter Converter( - predicate=lambda obj: isinstance(obj, Set), + predicate=lambda obj: isinstance(obj, _jc.Set), converter=JavaSet, ), # Collection converter Converter( - predicate=lambda obj: isinstance(obj, Collection), + predicate=lambda obj: isinstance(obj, _jc.Collection), converter=JavaCollection, priority=Priority.NORMAL - 1, ), # Iterable converter Converter( - predicate=lambda obj: isinstance(obj, Iterable), + predicate=lambda obj: isinstance(obj, _jc.Iterable), converter=JavaIterable, priority=Priority.NORMAL - 1, ), # Iterator converter Converter( - predicate=lambda obj: isinstance(obj, Iterator), + predicate=lambda obj: isinstance(obj, _jc.Iterator), converter=JavaIterator, priority=Priority.NORMAL - 1, ), @@ -1099,55 +763,56 @@ def _convert_table(obj: Any): pass -def _import_java_classes(): - global Boolean - global Byte - global Character - global Double - global Float - global Integer - global Iterable - global Long - global Object - global Short - global String - global Void - global BigDecimal - global BigInteger - global ArrayList - global Collection - global Iterator - global LinkedHashMap - global LinkedHashSet - global List - global Map - global Set - - _logger.debug("Importing Java classes...") - - # grab needed Java classes - Boolean = jimport("java.lang.Boolean") - Byte = jimport("java.lang.Byte") - Character = jimport("java.lang.Character") - Double = jimport("java.lang.Double") - Float = jimport("java.lang.Float") - Integer = jimport("java.lang.Integer") - Iterable = jimport("java.lang.Iterable") - Long = jimport("java.lang.Long") - Object = jimport("java.lang.Object") - Short = jimport("java.lang.Short") - String = jimport("java.lang.String") - Void = jimport("java.lang.Void") - BigDecimal = jimport("java.math.BigDecimal") - BigInteger = jimport("java.math.BigInteger") - ArrayList = jimport("java.util.ArrayList") - Collection = jimport("java.util.Collection") - Iterator = jimport("java.util.Iterator") - LinkedHashMap = jimport("java.util.LinkedHashMap") - LinkedHashSet = jimport("java.util.LinkedHashSet") - List = jimport("java.util.List") - Map = jimport("java.util.Map") - Set = jimport("java.util.Set") +# fmt: off +class _JavaClasses(JavaClasses): + @JavaClasses.java_import + def Boolean(self): return "java.lang.Boolean" # noqa: E272 + @JavaClasses.java_import + def Byte(self): return "java.lang.Byte" # noqa: E272 + @JavaClasses.java_import + def Character(self): return "java.lang.Character" # noqa: E272 + @JavaClasses.java_import + def Double(self): return "java.lang.Double" # noqa: E272 + @JavaClasses.java_import + def Float(self): return "java.lang.Float" # noqa: E272 + @JavaClasses.java_import + def Integer(self): return "java.lang.Integer" # noqa: E272 + @JavaClasses.java_import + def Iterable(self): return "java.lang.Iterable" # noqa: E272 + @JavaClasses.java_import + def Long(self): return "java.lang.Long" # noqa: E272 + @JavaClasses.java_import + def Object(self): return "java.lang.Object" # noqa: E272 + @JavaClasses.java_import + def Short(self): return "java.lang.Short" # noqa: E272 + @JavaClasses.java_import + def String(self): return "java.lang.String" # noqa: E272 + @JavaClasses.java_import + def Void(self): return "java.lang.Void" # noqa: E272 + @JavaClasses.java_import + def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 + @JavaClasses.java_import + def BigInteger(self): return "java.math.BigInteger" # noqa: E272 + @JavaClasses.java_import + def ArrayList(self): return "java.util.ArrayList" # noqa: E272 + @JavaClasses.java_import + def Collection(self): return "java.util.Collection" # noqa: E272 + @JavaClasses.java_import + def Iterator(self): return "java.util.Iterator" # noqa: E272 + @JavaClasses.java_import + def LinkedHashMap(self): return "java.util.LinkedHashMap" # noqa: E272 + @JavaClasses.java_import + def LinkedHashSet(self): return "java.util.LinkedHashSet" # noqa: E272 + @JavaClasses.java_import + def List(self): return "java.util.List" # noqa: E272 + @JavaClasses.java_import + def Map(self): return "java.util.Map" # noqa: E272 + @JavaClasses.java_import + def Set(self): return "java.util.Set" # noqa: E272 +# fmt: on + + +_jc = _JavaClasses() def _import_pandas(): diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py new file mode 100644 index 00000000..dbf5cad0 --- /dev/null +++ b/src/scyjava/_java.py @@ -0,0 +1,410 @@ +""" +Utility functions for working with the Java and JVM. +""" +import atexit +import logging +import os +import re +import subprocess +import sys +from functools import lru_cache +from pathlib import Path +from typing import Callable + +import jpype +import jpype.config +from jgo import jgo + +import scyjava.config + +_logger = logging.getLogger(__name__) + +_startup_callbacks = [] +_shutdown_callbacks = [] + + +class JavaClasses: + """ + Utility class used to make importing frequently-used Java classes + significantly easier and more readable. + + Benefits: + * Minimal boilerplate + * Lazy evaluation + * Usable within type hints + + Example: + + from scyjava import JavaClasses + + class MyJavaClasses(JavaClasses): + @JavaClasses.java_class + def String(self): return "java.lang.String" + @JavaClasses.java_class + def Integer(self): return "java.lang.Integer" + # ... and many more ... + + jc = MyJavaClasses() + + def parse_number_with_java(s: "jc.String") -> "jc.Integer": + return jc.Integer.parseInt(s) + """ + + def java_import(func: Callable[[], str]) -> Callable[[], jpype.JClass]: + """ + A decorator used to lazily evaluate a java import. + func is a function of a Python class that takes no arguments and + returns a string identifying a Java class by name. + + Using that function, this decorator creates a property + that when called, imports the class identified by the function. + """ + + @property + def inner(self): + if not jvm_started(): + raise Exception() + try: + return jimport(func(self)) + except TypeError: + return None + + return inner + + +# -- JVM functions -- + + +def jvm_version(): + """ + Gets the version of the JVM as a tuple, + with each dot-separated digit as one element. + Characters in the version string beyond only + numbers and dots are ignored, in line + with the java.version system property. + + Examples: + * OpenJDK 17.0.1 -> [17, 0, 1] + * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] + * OpenJDK 1.8.0_312 -> [1, 8, 0] + + If the JVM is already started, + this function should return the equivalent of: + jimport('java.lang.System') + .getProperty('java.version') + .split('.') + + In case the JVM is not started yet,a best effort is made to deduce + the version from the environment without actually starting up the + JVM in-process. If the version cannot be deduced, a RuntimeError + with the cause is raised. + """ + jvm_version = jpype.getJVMVersion() + if jvm_version and jvm_version[0]: + # JPype already knew the version. + # JVM is probably already started. + # Or JPype got smarter since 1.3.0. + return jvm_version + + # JPype was clueless, which means the JVM has probably not started yet. + # Let's look for a java executable, and ask it directly with 'java + # -version'. + + default_jvm_path = jpype.getDefaultJVMPath() + if not default_jvm_path: + raise RuntimeError("Cannot glean the default JVM path") + + p = Path(default_jvm_path) + if not p.exists(): + raise RuntimeError(f"Invalid default JVM path: {p}") + + java = None + for _ in range(3): # The bin folder is always <=3 levels up from libjvm. + p = p.parent + if p.name == "lib": + java = p.parent / "bin" / "java" + elif p.name == "bin": + java = p / "java" + + if java is not None: + if os.name == "nt": + # Good ol' Windows! Nothing beats Windows. + java = java.with_suffix(".exe") + if not java.is_file(): + raise RuntimeError(f"No ../bin/java found at: {p}") + break + if java is None: + raise RuntimeError(f"No java executable found inside: {p}") + + version = subprocess.check_output( + [str(java), "-version"], stderr=subprocess.STDOUT + ).decode() + m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) + if not m: + raise RuntimeError(f"Inscrutable java command output:\n{version}") + + return tuple(map(int, m.group(1).split("."))) + + +def start_jvm(options=None): + """ + Explicitly connect to the Java virtual machine (JVM). Only one JVM can + be active; does nothing if the JVM has already been started. Calling + this function directly is typically not necessary, because the first + time a scyjava function needing a JVM is invoked, one is started on the + fly with the configuration specified via the scijava.config mechanism. + + :param options: List of options to pass to the JVM. For example: + ['-Djava.awt.headless=true', '-Xmx4g'] + """ + # if JVM is already running -- break + if jvm_started(): + _logger.debug("The JVM is already running.") + return + + # retrieve endpoint and repositories from scyjava config + endpoints = scyjava.config.endpoints + repositories = scyjava.config.get_repositories() + + # use the logger to notify user that endpoints are being added + _logger.debug("Adding jars from endpoints {0}".format(endpoints)) + + # get endpoints and add to JPype class path + if len(endpoints) > 0: + endpoints = endpoints[:1] + sorted(endpoints[1:]) + _logger.debug("Using endpoints %s", endpoints) + _, workspace = jgo.resolve_dependencies( + "+".join(endpoints), + m2_repo=scyjava.config.get_m2_repo(), + cache_dir=scyjava.config.get_cache_dir(), + manage_dependencies=scyjava.config.get_manage_deps(), + repositories=repositories, + verbose=scyjava.config.get_verbose(), + shortcuts=scyjava.config.get_shortcuts(), + ) + jpype.addClassPath(os.path.join(workspace, "*")) + + # HACK: Try to set JAVA_HOME if it isn't already. + if ( + "JAVA_HOME" not in os.environ + or not os.environ["JAVA_HOME"] + or not os.path.isdir(os.environ["JAVA_HOME"]) + ): + + _logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.") + + libjvm_win = Path("Library") / "jre" / "bin" / "server" / "jvm.dll" + libjvm_macos = Path("lib") / "server" / "libjvm.dylib" + libjvm_linux = Path("lib") / "server" / "libjvm.so" + libjvm_paths = { + libjvm_win: Path("Library"), + libjvm_macos: Path(), + libjvm_linux: Path(), + } + for p in sys.path: + if not p.endswith("site-packages"): + continue + # e.g. $CONDA_PREFIX/lib/python3.10/site-packages -> $CONDA_PREFIX + # But we want it to work outside of Conda as well, theoretically. + base = Path(p).parent.parent.parent + for libjvm_path, java_home_path in libjvm_paths.items(): + if (base / libjvm_path).exists(): + java_home = str((base / java_home_path).resolve()) + _logger.debug(f"Detected JAVA_HOME: {java_home}") + os.environ["JAVA_HOME"] = java_home + break + + # initialize JPype JVM + _logger.debug("Starting JVM") + if options is None: + options = scyjava.config.get_options() + jpype.startJVM(*options, interrupt=True) + + # replace JPype/JVM shutdown handling with our own + jpype.config.onexit = False + jpype.config.free_resources = False + atexit.register(shutdown_jvm) + + # invoke registered callback functions + for callback in _startup_callbacks: + callback() + + +def shutdown_jvm(): + """Shutdown the JVM. + + This function makes a best effort to clean up Java resources first. + In particular, shutdown hooks registered with scyjava.when_jvm_stops + are sequentially invoked. + + Then, if the AWT subsystem has started, all AWT windows (as identified + by the java.awt.Window.getWindows() method) are disposed to reduce the + risk of GUI resources delaying JVM shutdown. + + Finally, the jpype.shutdownJVM() function is called. Note that you can + set the jpype.config.destroy_jvm flag to request JPype to destroy the + JVM explicitly, although setting this flag can lead to delayed shutdown + times while the JVM is waiting for threads to finish. + + Note that if the JVM is not already running, then this function does + nothing! In particular, shutdown hooks are skipped in this situation. + """ + if not jvm_started(): + return + + # invoke registered shutdown callback functions + for callback in _shutdown_callbacks: + try: + callback() + except Exception as e: + print(f"Exception during shutdown callback: {e}") + + # dispose AWT resources if applicable + if is_awt_initialized(): + Window = jimport("java.awt.Window") + for w in Window.getWindows(): + w.dispose() + + # okay to shutdown JVM + try: + jpype.shutdownJVM() + except Exception as e: + print(f"Exception during JVM shutdown: {e}") + + +def jvm_started(): + """Return true iff a Java virtual machine (JVM) has been started.""" + return jpype.isJVMStarted() + + +def is_jvm_headless(): + """ + Return true iff Java is running in headless mode. + + :raises RuntimeError: If the JVM has not started yet. + """ + if not jvm_started(): + raise RuntimeError("JVM has not started yet!") + + GraphicsEnvironment = scyjava.jimport("java.awt.GraphicsEnvironment") + return GraphicsEnvironment.isHeadless() + + +def is_awt_initialized(): + """ + Return true iff the AWT subsystem has been initialized. + + Java starts up its AWT subsystem automatically and implicitly, as + soon as an action is performed requiring it -- for example, if you + jimport a java.awt or javax.swing class. This can lead to deadlocks + on macOS if you are not running in headless mode and did not invoke + those actions via the jpype.setupGuiEnvironment wrapper function; + see the Troubleshooting section of the scyjava README for details. + """ + if not jvm_started(): + return False + Thread = scyjava.jimport("java.lang.Thread") + threads = Thread.getAllStackTraces().keySet() + return any(t.getName().startsWith("AWT-") for t in threads) + + +def when_jvm_starts(f): + """ + Registers a function to be called when the JVM starts (or immediately). + This is useful to defer construction of Java-dependent data structures + until the JVM is known to be available. If the JVM has already been + started, the function executes immediately. + + :param f: Function to invoke when scyjava.start_jvm() is called. + """ + if jvm_started(): + # JVM was already started; invoke callback function immediately. + f() + else: + # Add function to the list of callbacks to invoke upon start_jvm(). + global _startup_callbacks + _startup_callbacks.append(f) + + +def when_jvm_stops(f): + """ + Registers a function to be called just before the JVM shuts down. + This is useful to perform cleanup of Java-dependent data structures. + + Note that if the JVM is not already running when shutdown_jvm is + called, then these registered callback functions will be skipped! + + :param f: Function to invoke when scyjava.shutdown_jvm() is called. + """ + global _shutdown_callbacks + _shutdown_callbacks.append(f) + + +# -- Java functions -- + + +def isjava(data): + """Return whether the given data object is a Java object.""" + return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) + + +@lru_cache(maxsize=None) +def jimport(class_name): + """ + Import a class from Java to Python. + + :param class_name: Name of the class to import. + :returns: A pointer to the class, which can be used to + e.g. instantiate objects of that class. + """ + start_jvm() + return jpype.JClass(class_name) + + +def jclass(data): + """ + Obtain a Java class object. + + :param data: The object from which to glean the class. + Supported types include: + A. Name of a class to look up, analogous to + Class.forName("java.lang.String"); + B. A jpype.JClass object analogous to String.class; + C. A jpype.JObject instance analogous to o.getClass(). + :returns: A java.lang.Class object, suitable for use with reflection. + :raises TypeError: if the argument is not one of the aforementioned types. + """ + if isinstance(data, jpype.JClass): + return data.class_ + if isinstance(data, jpype.JObject): + return data.getClass() + if isinstance(data, str): + return jclass(jimport(data)) + raise TypeError("Cannot glean class from data of type: " + str(type(data))) + + +def jstacktrace(exc): + """ + Extract the Java-side stack trace from a Java exception. + + Example of usage: + + from scyjava import jimport, jstacktrace + try: + Integer = jimport('java.lang.Integer') + nan = Integer.parseInt('not a number') + except Exception as exc: + print(jstacktrace(exc)) + + :param exc: The Java Throwable from which to extract the stack trace. + :returns: A multi-line string containing the stack trace, or empty string + if no stack trace could be extracted. + """ + try: + StringWriter = jimport("java.io.StringWriter") + PrintWriter = jimport("java.io.PrintWriter") + sw = StringWriter() + exc.printStackTrace(PrintWriter(sw, True)) + return sw.toString() + except BaseException: + return "" From e075eed384ff8d4c833cf1ed02c073dc7989fc4b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 14:14:51 -0500 Subject: [PATCH 210/505] Move array-oriented functions to their own file --- src/scyjava/__init__.py | 56 ++++------------------------------------- src/scyjava/_arrays.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 src/scyjava/_arrays.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index b23e5fe1..cd7219d7 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -17,6 +17,11 @@ JShort, ) +from scyjava._arrays import ( # noqa: F401 + is_arraylike, + is_memoryarraylike, + is_xarraylike, +) from scyjava._java import ( # noqa: F401 JavaClasses, is_awt_initialized, @@ -137,57 +142,6 @@ def compare_version(version, java_class_version): ) -# -- Type reasoning -- - - -def is_arraylike(arr): - """ - Return True iff the object is arraylike: possessing - .shape, .dtype, .__array__, and .ndim attributes. - - :param arr: The object to check for arraylike properties - :return: True iff the object is arraylike - """ - return ( - hasattr(arr, "shape") - and hasattr(arr, "dtype") - and hasattr(arr, "__array__") - and hasattr(arr, "ndim") - ) - - -def is_memoryarraylike(arr): - """ - Return True iff the object is memoryarraylike: - an arraylike object whose .data type is memoryview. - - :param arr: The object to check for memoryarraylike properties - :return: True iff the object is memoryarraylike - """ - return ( - is_arraylike(arr) - and hasattr(arr, "data") - and type(arr.data).__name__ == "memoryview" - ) - - -def is_xarraylike(xarr): - """ - Return True iff the object is xarraylike: - possessing .values, .dims, and .coords attributes, - and whose .values are arraylike. - - :param arr: The object to check for xarraylike properties - :return: True iff the object is xarraylike - """ - return ( - hasattr(xarr, "values") - and hasattr(xarr, "dims") - and hasattr(xarr, "coords") - and is_arraylike(xarr.values) - ) - - # -- Type conversion -- # NB: We cannot use org.scijava.priority.Priority or other Java-side class diff --git a/src/scyjava/_arrays.py b/src/scyjava/_arrays.py new file mode 100644 index 00000000..fdb99cea --- /dev/null +++ b/src/scyjava/_arrays.py @@ -0,0 +1,46 @@ +def is_arraylike(arr): + """ + Return True iff the object is arraylike: possessing + .shape, .dtype, .__array__, and .ndim attributes. + + :param arr: The object to check for arraylike properties + :return: True iff the object is arraylike + """ + return ( + hasattr(arr, "shape") + and hasattr(arr, "dtype") + and hasattr(arr, "__array__") + and hasattr(arr, "ndim") + ) + + +def is_memoryarraylike(arr): + """ + Return True iff the object is memoryarraylike: + an arraylike object whose .data type is memoryview. + + :param arr: The object to check for memoryarraylike properties + :return: True iff the object is memoryarraylike + """ + return ( + is_arraylike(arr) + and hasattr(arr, "data") + and type(arr.data).__name__ == "memoryview" + ) + + +def is_xarraylike(xarr): + """ + Return True iff the object is xarraylike: + possessing .values, .dims, and .coords attributes, + and whose .values are arraylike. + + :param arr: The object to check for xarraylike properties + :return: True iff the object is xarraylike + """ + return ( + hasattr(xarr, "values") + and hasattr(xarr, "dims") + and hasattr(xarr, "coords") + and is_arraylike(xarr.values) + ) From b1cfa8de2fc9d81f0bb02eb4b27ebac6c93e93ec Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 16:08:24 -0500 Subject: [PATCH 211/505] Move version-oriented functions to their own file --- src/scyjava/__init__.py | 73 +++------------------------------------- src/scyjava/_versions.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 68 deletions(-) create mode 100644 src/scyjava/_versions.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index cd7219d7..3d78e744 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -2,7 +2,6 @@ import logging import typing from functools import lru_cache -from importlib.util import find_spec from typing import Any, Callable, Dict, NamedTuple from jpype.types import ( @@ -37,6 +36,11 @@ when_jvm_starts, when_jvm_stops, ) +from scyjava._versions import ( # noqa: F401 + compare_version, + get_version, + is_version_at_least, +) _logger = logging.getLogger(__name__) @@ -75,73 +79,6 @@ def __getattr__(name): raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -# -- Version reasoning -- - - -def get_version(java_class_or_python_package): - """ - 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 Java classes, requires org.scijava:scijava-common on the classpath. - - The version string is extracted from the given class's associated JAR - artifact (if any), either the embedded Maven POM if the project was built - with Maven, or the JAR manifest's Specification-Version value if it exists. - - See org.scijava.VersionUtils.getVersion(Class) for further details. - """ - - if isjava(java_class_or_python_package): - # Assume we were given a Java class object. - VersionUtils = jimport("org.scijava.util.VersionUtils") - return str(VersionUtils.getVersion(java_class_or_python_package)) - - # Assume we were given a Python package name. - - if find_spec("importlib.metadata"): - # Fastest, but requires Python 3.8+. - from importlib.metadata import version - - return version(java_class_or_python_package) - - if find_spec("pkg_resources"): - # Slower, but works on Python 3.7. - from pkg_resources import get_distribution - - return get_distribution(java_class_or_python_package).version - - raise RuntimeError("Cannot determine version! Is pkg_resources installed?") - - -def is_version_at_least(actual_version, minimum_version): - """ - Return a boolean on a version comparison. - Requires org.scijava:scijava-common on the classpath. - - Returns True if the given actual version is greater than or - equal to the specified minimum version, or False otherwise. - - See org.scijava.VersionUtils.compare(String, String) for further details. - """ - VersionUtils = jimport("org.scijava.util.VersionUtils") - return VersionUtils.compare(actual_version, minimum_version) >= 0 - - -def compare_version(version, java_class_version): - """ - This function is deprecated. Use is_version_at_least instead. - """ - _logger.warning( - "The compare_version function is deprecated. Use is_version_at_least instead." - ) - return version != java_class_version and is_version_at_least( - java_class_version, version - ) - - # -- Type conversion -- # NB: We cannot use org.scijava.priority.Priority or other Java-side class diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py new file mode 100644 index 00000000..c5522c23 --- /dev/null +++ b/src/scyjava/_versions.py @@ -0,0 +1,70 @@ +import logging +from importlib.util import find_spec + +from ._java import isjava, jimport + +_logger = logging.getLogger(__name__) + + +def get_version(java_class_or_python_package): + """ + 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 Java classes, requires org.scijava:scijava-common on the classpath. + + The version string is extracted from the given class's associated JAR + artifact (if any), either the embedded Maven POM if the project was built + with Maven, or the JAR manifest's Specification-Version value if it exists. + + See org.scijava.VersionUtils.getVersion(Class) for further details. + """ + + if isjava(java_class_or_python_package): + # Assume we were given a Java class object. + VersionUtils = jimport("org.scijava.util.VersionUtils") + return str(VersionUtils.getVersion(java_class_or_python_package)) + + # Assume we were given a Python package name. + + if find_spec("importlib.metadata"): + # Fastest, but requires Python 3.8+. + from importlib.metadata import version + + return version(java_class_or_python_package) + + if find_spec("pkg_resources"): + # Slower, but works on Python 3.7. + from pkg_resources import get_distribution + + return get_distribution(java_class_or_python_package).version + + raise RuntimeError("Cannot determine version! Is pkg_resources installed?") + + +def is_version_at_least(actual_version, minimum_version): + """ + Return a boolean on a version comparison. + Requires org.scijava:scijava-common on the classpath. + + Returns True if the given actual version is greater than or + equal to the specified minimum version, or False otherwise. + + See org.scijava.VersionUtils.compare(String, String) for further details. + """ + VersionUtils = jimport("org.scijava.util.VersionUtils") + return VersionUtils.compare(actual_version, minimum_version) >= 0 + + +def compare_version(version, java_class_version): + """ + This function is deprecated. Use is_version_at_least instead. + """ + _logger.warning( + "The compare_version function is deprecated. Use is_version_at_least instead." + ) + return version != java_class_version and is_version_at_least( + java_class_version, version + ) From 7fe220cc951b4202ed56edf656af6bbaac9cbc7b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 16:13:46 -0500 Subject: [PATCH 212/505] Eliminate internal _add_converter method --- src/scyjava/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 3d78e744..3f067ea4 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -108,10 +108,6 @@ def _convert(obj: Any, converters: typing.List[Converter]) -> Any: return prioritized.converter(obj) -def _add_converter(converter: Converter, converters: typing.List[Converter]): - converters.append(converter) - - # -- Python to Java -- # Adapted from code posted by vslotman on GitHub: @@ -155,7 +151,7 @@ def add_java_converter(converter: Converter): Adds a converter to the list used by to_java :param converter: A Converter going from python to java """ - _add_converter(converter, java_converters) + java_converters.append(converter) def to_java(obj: Any) -> Any: @@ -453,7 +449,7 @@ def add_py_converter(converter: Converter): Adds a converter to the list used by to_python :param converter: A Converter from java to python """ - _add_converter(converter, py_converters) + py_converters.append(converter) def to_python(data: Any, gentle: bool = False) -> Any: @@ -774,9 +770,9 @@ def _pandas_to_table(df): def _initialize_converters(): for converter in _stock_java_converters(): - _add_converter(converter, java_converters) + add_java_converter(converter) for converter in _stock_py_converters(): - _add_converter(converter, py_converters) + add_py_converter(converter) when_jvm_starts(_initialize_converters) From b808a7ecc05471697bf5d79cc375b72e5456d0b2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 16:29:43 -0500 Subject: [PATCH 213/505] Remove Void-to-None converter It is impossible to have an instance of java.lang.Void. So this converter's predicate could never be triggered. --- src/scyjava/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 3f067ea4..c541078d 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -565,11 +565,6 @@ def _stock_py_converters() -> typing.List: predicate=lambda obj: isinstance(obj, _jc.Short), converter=lambda obj: obj.shortValue(), ), - # Void converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Void), - converter=lambda obj: None, - ), # String converter Converter( predicate=lambda obj: isinstance(obj, _jc.String), @@ -675,8 +670,6 @@ def Short(self): return "java.lang.Short" # noqa: E272 @JavaClasses.java_import def String(self): return "java.lang.String" # noqa: E272 @JavaClasses.java_import - def Void(self): return "java.lang.Void" # noqa: E272 - @JavaClasses.java_import def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 @JavaClasses.java_import def BigInteger(self): return "java.math.BigInteger" # noqa: E272 From 96b50075ea852f40dec39772557809ff5ac21843 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 16:43:34 -0500 Subject: [PATCH 214/505] Improve to_java and to_python docstrings Add some whitespace, to make them easier to read. --- src/scyjava/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c541078d..6cc61506 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -157,7 +157,7 @@ def add_java_converter(converter: Converter): def to_java(obj: Any) -> Any: """ Recursively convert a Python object to a Java object. - :param data: The Python object to convert. + Supported types include: * str -> String * bool -> Boolean @@ -166,6 +166,8 @@ def to_java(obj: Any) -> Any: * dict -> LinkedHashMap * set -> LinkedHashSet * list -> ArrayList + + :param data: The Python object to convert. :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ @@ -455,9 +457,7 @@ def add_py_converter(converter: Converter): def to_python(data: Any, gentle: bool = False) -> Any: """ Recursively convert a Java object to a Python object. - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. + Supported types include: * String, Character -> str * Boolean -> bool @@ -469,6 +469,10 @@ def to_python(data: Any, gentle: bool = False) -> Any: * Collection -> collections.abc.Collection * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator + + :param data: The Java object to convert. + :param gentle: If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. :returns: A corresponding Python object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types, and the gentle flag is not set. From 4de4bbf19eafbff3cf346af010c5791ff1718eed Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 16:56:30 -0500 Subject: [PATCH 215/505] Move conversion logic to its own file --- src/scyjava/__init__.py | 715 ++-------------------------------------- src/scyjava/_convert.py | 683 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 706 insertions(+), 692 deletions(-) create mode 100644 src/scyjava/_convert.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 6cc61506..46d46dfd 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1,26 +1,31 @@ -import collections.abc import logging -import typing from functools import lru_cache -from typing import Any, Callable, Dict, NamedTuple - -from jpype.types import ( - JArray, - JBoolean, - JByte, - JChar, - JDouble, - JFloat, - JInt, - JLong, - JShort, -) +from typing import Any, Callable, Dict from scyjava._arrays import ( # noqa: F401 is_arraylike, is_memoryarraylike, is_xarraylike, ) +from scyjava._convert import ( # noqa: F401 + Converter, + JavaCollection, + JavaIterable, + JavaIterator, + JavaList, + JavaMap, + JavaObject, + JavaSet, + Priority, + _stock_java_converters, + _stock_py_converters, + add_java_converter, + add_py_converter, + java_converters, + py_converters, + to_java, + to_python, +) from scyjava._java import ( # noqa: F401 JavaClasses, is_awt_initialized, @@ -42,6 +47,9 @@ is_version_at_least, ) +__version__ = get_version("scyjava") + + _logger = logging.getLogger(__name__) # Set of module properties @@ -79,683 +87,6 @@ def __getattr__(name): raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -# -- Type conversion -- - -# NB: We cannot use org.scijava.priority.Priority or other Java-side class -# here because we don't want to impose Java-side dependencies, and we don't -# want to require Java to be started before reasoning about priorities. -class Priority: - FIRST = 1e300 - EXTREMELY_HIGH = 1e6 - VERY_HIGH = 1e4 - HIGH = 1e2 - NORMAL = 0 - LOW = -1e2 - VERY_LOW = -1e4 - EXTREMELY_LOW = -1e6 - LAST = -1e300 - - -class Converter(NamedTuple): - predicate: Callable[[Any], bool] - converter: Callable[[Any], Any] - priority: float = Priority.NORMAL - - -def _convert(obj: Any, converters: typing.List[Converter]) -> Any: - suitable_converters = filter(lambda c: c.predicate(obj), converters) - prioritized = max(suitable_converters, key=lambda c: c.priority) - return prioritized.converter(obj) - - -# -- Python to Java -- - -# Adapted from code posted by vslotman on GitHub: -# https://github.com/kivy/pyjnius/issues/217#issue-145981070 - - -def _raise_type_exception(obj: Any): - raise TypeError("Unsupported type: " + str(type(obj))) - - -def _convertMap(obj: collections.abc.Mapping): - jmap = _jc.LinkedHashMap() - for k, v in obj.items(): - jk = to_java(k) - jv = to_java(v) - jmap.put(jk, jv) - return jmap - - -def _convertSet(obj: collections.abc.Set): - jset = _jc.LinkedHashSet() - for item in obj: - jitem = to_java(item) - jset.add(jitem) - return jset - - -def _convertIterable(obj: collections.abc.Iterable): - jlist = _jc.ArrayList() - for item in obj: - jitem = to_java(item) - jlist.add(jitem) - return jlist - - -java_converters: typing.List[Converter] = [] - - -def add_java_converter(converter: Converter): - """ - Adds a converter to the list used by to_java - :param converter: A Converter going from python to java - """ - java_converters.append(converter) - - -def to_java(obj: Any) -> Any: - """ - Recursively convert a Python object to a Java object. - - Supported types include: - * str -> String - * bool -> Boolean - * int -> Integer, Long or BigInteger as appropriate - * float -> Float, Double or BigDecimal as appropriate - * dict -> LinkedHashMap - * set -> LinkedHashSet - * list -> ArrayList - - :param data: The Python object to convert. - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. - """ - start_jvm() - return _convert(obj, java_converters) - - -def _stock_java_converters() -> typing.List[Converter]: - """ - Returns all python-to-java converters supported out of the box! - This should only be called after the JVM has been started! - :returns: A list of Converters - """ - return [ - # Other (Exceptional) converter - Converter( - predicate=lambda obj: True, - converter=_raise_type_exception, - priority=Priority.EXTREMELY_LOW - 1, - ), - # NoneType converter - Converter( - predicate=lambda obj: obj is None, - converter=lambda obj: None, - priority=Priority.EXTREMELY_HIGH + 1, - ), - # Java identity converter - Converter( - predicate=isjava, - converter=lambda obj: obj, - priority=Priority.EXTREMELY_HIGH, - ), - # String converter - Converter( - predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: _jc.String(obj.encode("utf-8"), "utf-8"), - ), - # Boolean converter - Converter( - predicate=lambda obj: isinstance(obj, bool), - converter=_jc.Boolean, - ), - # Integer converter - Converter( - predicate=lambda obj: isinstance(obj, int) - and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, - converter=_jc.Integer, - ), - # Long converter - Converter( - predicate=lambda obj: isinstance(obj, int) - and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, - converter=_jc.Long, - priority=Priority.NORMAL - 1, - ), - # BigInteger converter - Converter( - predicate=lambda obj: isinstance(obj, int), - converter=lambda obj: _jc.BigInteger(str(obj)), - priority=Priority.NORMAL - 2, - ), - # Float converter - Converter( - predicate=lambda obj: isinstance(obj, float) - and _jc.Float.MIN_VALUE <= obj <= _jc.Float.MAX_VALUE, - converter=_jc.Float, - ), - # Double converter - Converter( - predicate=lambda obj: isinstance(obj, float) - and _jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE, - converter=_jc.Double, - priority=Priority.NORMAL - 1, - ), - # BigDecimal converter - Converter( - predicate=lambda obj: isinstance(obj, float), - converter=lambda obj: _jc.BigDecimal(str(obj)), - priority=Priority.NORMAL - 2, - ), - # Pandas table converter - Converter( - predicate=lambda obj: type(obj).__name__ == "DataFrame", - converter=_pandas_to_table, - priority=Priority.NORMAL + 1, - ), - # Mapping converter - Converter( - predicate=lambda obj: isinstance(obj, collections.abc.Mapping), - converter=_convertMap, - ), - # Set converter - Converter( - predicate=lambda obj: isinstance(obj, collections.abc.Set), - converter=_convertSet, - ), - # Iterable converter - Converter( - predicate=lambda obj: isinstance(obj, collections.abc.Iterable), - converter=_convertIterable, - priority=Priority.NORMAL - 1, - ), - ] - - -# -- Java to Python -- - - -def _jstr(data): - if isinstance(data, JavaObject): - return str(data) - # NB: We want Python strings to render in single quotes. - return "{!r}".format(data) - - -class JavaObject: - def __init__(self, jobj, intended_class=None): - if intended_class is None: - intended_class = _jc.Object - if not isinstance(jobj, intended_class): - raise TypeError( - f"Not a {intended_class.getName()}: {jclass(jobj).getName()}" - ) - self.jobj = jobj - - def __str__(self): - return _jstr(self.jobj) - - -class JavaIterable(JavaObject, collections.abc.Iterable): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.Iterable) - - def __iter__(self): - return to_python(self.jobj.iterator()) - - def __str__(self): - return "[" + ", ".join(_jstr(v) for v in self) + "]" - - -class JavaCollection(JavaIterable, collections.abc.Collection): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.Collection) - - def __contains__(self, item): - # NB: Collection.contains returns boolean, so no need for gentleness. - return to_python(self.jobj.contains(to_java(item))) - - def __len__(self): - return to_python(self.jobj.size()) - - def __eq__(self, other): - try: - if len(self) != len(other): - return False - for e1, e2 in zip(self, other): - if e1 != e2: - return False - return True - except TypeError: - return False - - -class JavaIterator(JavaObject, collections.abc.Iterator): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.Iterator) - - def __next__(self): - if self.jobj.hasNext(): - # NB: Even if an element cannot be converted, - # we still want to support Pythonic iteration. - return to_python(self.jobj.next(), gentle=True) - raise StopIteration - - -class JavaList(JavaCollection, collections.abc.MutableSequence): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.List) - - def __getitem__(self, key): - # NB: Even if an element cannot be converted, - # we still want Pythonic access to elements. - return to_python(self.jobj.get(key), gentle=True) - - def __setitem__(self, key, value): - # NB: List.set(int, Object) returns inserted element, so be gentle - # here. - return to_python(self.jobj.set(key, to_java(value)), gentle=True) - - def __delitem__(self, key): - # NB: List.remove(Object) returns boolean, so no need for gentleness. - return to_python(self.jobj.remove(to_java(key))) - - def insert(self, index, object): - # NB: List.set(int, Object) returns inserted element, so be gentle - # here. - return to_python(self.jobj.set(index, to_java(object)), gentle=True) - - -class JavaMap(JavaObject, collections.abc.MutableMapping): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.Map) - - def __getitem__(self, key): - # NB: Even if an element cannot be converted, - # we still want Pythonic access to elements. - return to_python(self.jobj.get(to_java(key)), gentle=True) - - def __setitem__(self, key, value): - # NB: Map.put(Object, Object) returns inserted value, so be gentle - # here. - put_return: bool = self.jobj.put(to_java(key), to_java(value)) - return to_python(put_return, gentle=True) - - def __delitem__(self, key): - # NB: Map.remove(Object) returns the removed key, so be gentle here. - return to_python(self.jobj.remove(to_java(key)), gentle=True) - - def keys(self): - return to_python(self.jobj.keySet()) - - def __iter__(self): - return self.keys().__iter__() - - def __len__(self): - return to_python(self.jobj.size()) - - def __eq__(self, other): - try: - if len(self) != len(other): - return False - for k in self: - if k not in other or self[k] != other[k]: - return False - return True - except TypeError: - return False - - def __str__(self): - def item_str(k, v): - return _jstr(k) + ": " + _jstr(v) - - return "{" + ", ".join(item_str(k, v) for k, v in self.items()) + "}" - - -class JavaSet(JavaCollection, collections.abc.MutableSet): - def __init__(self, jobj): - JavaObject.__init__(self, jobj, _jc.Set) - - def add(self, item): - # NB: Set.add returns boolean, so no need for gentleness. - return to_python(self.jobj.add(to_java(item))) - - def discard(self, item): - # NB: Set.remove returns boolean, so no need for gentleness. - return to_python(self.jobj.remove(to_java(item))) - - def __iter__(self): - return to_python(self.jobj.iterator()) - - def __eq__(self, other): - try: - if len(self) != len(other): - return False - for k in self: - if k not in other: - return False - return True - except TypeError: - return False - - def __str__(self): - return "{" + ", ".join(_jstr(v) for v in self) + "}" - - -py_converters: typing.List[Converter] = [] - - -def add_py_converter(converter: Converter): - """ - Adds a converter to the list used by to_python - :param converter: A Converter from java to python - """ - py_converters.append(converter) - - -def to_python(data: Any, gentle: bool = False) -> Any: - """ - Recursively convert a Java object to a Python object. - - Supported types include: - * String, Character -> str - * Boolean -> bool - * Byte, Short, Integer, Long, BigInteger -> int - * Float, Double, BigDecimal -> float - * Map -> collections.abc.MutableMapping (dict-like) - * Set -> collections.abc.MutableSet (set-like) - * List -> collections.abc.MutableSequence (list-like) - * Collection -> collections.abc.Collection - * Iterable -> collections.abc.Iterable - * Iterator -> collections.abc.Iterator - - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. - :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types, - and the gentle flag is not set. - """ - start_jvm() - try: - return _convert(data, py_converters) - except TypeError as exc: - if gentle: - return data - raise exc - - -def _stock_py_converters() -> typing.List: - """ - Returns all java-to-python converters supported out of the box! - This should only be called after the JVM has been started! - :returns: A list of Converters - """ - return [ - # Other (Exceptional) converter - Converter( - predicate=lambda obj: True, - converter=_raise_type_exception, - priority=Priority.EXTREMELY_LOW - 1, - ), - # Java identity converter - Converter( - predicate=lambda obj: not isjava(obj), - converter=lambda obj: obj, - priority=Priority.EXTREMELY_HIGH, - ), - # JBoolean converter - Converter( - predicate=lambda obj: isinstance(obj, JBoolean), - converter=bool, - priority=Priority.NORMAL + 1, - ), - # JInt/JLong/JShort converter - Converter( - predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), - converter=int, - priority=Priority.NORMAL + 1, - ), - # JDouble/JFloat converter - Converter( - predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), - converter=float, - priority=Priority.NORMAL + 1, - ), - # JChar converter - Converter( - predicate=lambda obj: isinstance(obj, JChar), - converter=str, - priority=Priority.NORMAL + 1, - ), - # Boolean converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Boolean), - converter=lambda obj: obj.booleanValue(), - ), - # Byte converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Byte), - converter=lambda obj: obj.byteValue(), - ), - # Char converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Character), - converter=lambda obj: obj.toString(), - ), - # Double converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Double), - converter=lambda obj: obj.doubleValue(), - ), - # Float converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Float), - converter=lambda obj: obj.floatValue(), - ), - # Integer converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Integer), - converter=lambda obj: obj.intValue(), - ), - # Long converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Long), - converter=lambda obj: obj.longValue(), - ), - # Short converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Short), - converter=lambda obj: obj.shortValue(), - ), - # String converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.String), - converter=lambda obj: str(obj), - ), - # BigInteger converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.BigInteger), - converter=lambda obj: int(str(obj.toString())), - ), - # BigDecimal converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.BigDecimal), - converter=lambda obj: float(str(obj.toString())), - ), - # SciJava Table converter - Converter( - predicate=_is_table, - converter=_convert_table, - ), - # List converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.List), - converter=JavaList, - ), - # Map converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Map), - converter=JavaMap, - ), - # Set converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Set), - converter=JavaSet, - ), - # Collection converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Collection), - converter=JavaCollection, - priority=Priority.NORMAL - 1, - ), - # Iterable converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Iterable), - converter=JavaIterable, - priority=Priority.NORMAL - 1, - ), - # Iterator converter - Converter( - predicate=lambda obj: isinstance(obj, _jc.Iterator), - converter=JavaIterator, - priority=Priority.NORMAL - 1, - ), - # JArray converter - Converter( - predicate=lambda obj: isinstance(obj, JArray), - converter=lambda obj: [to_python(o) for o in obj], - priority=Priority.VERY_LOW, - ), - ] - - -def _is_table(obj: Any) -> bool: - """Checks if obj is a table""" - try: - return isinstance(obj, jimport("org.scijava.table.Table")) - except BaseException: - # No worries if scijava-table is not available. - pass - - -def _convert_table(obj: Any): - """Converts obj to a table.""" - try: - return _table_to_pandas(obj) - except BaseException: - # No worries if scijava-table is not available. - pass - - -# fmt: off -class _JavaClasses(JavaClasses): - @JavaClasses.java_import - def Boolean(self): return "java.lang.Boolean" # noqa: E272 - @JavaClasses.java_import - def Byte(self): return "java.lang.Byte" # noqa: E272 - @JavaClasses.java_import - def Character(self): return "java.lang.Character" # noqa: E272 - @JavaClasses.java_import - def Double(self): return "java.lang.Double" # noqa: E272 - @JavaClasses.java_import - def Float(self): return "java.lang.Float" # noqa: E272 - @JavaClasses.java_import - def Integer(self): return "java.lang.Integer" # noqa: E272 - @JavaClasses.java_import - def Iterable(self): return "java.lang.Iterable" # noqa: E272 - @JavaClasses.java_import - def Long(self): return "java.lang.Long" # noqa: E272 - @JavaClasses.java_import - def Object(self): return "java.lang.Object" # noqa: E272 - @JavaClasses.java_import - def Short(self): return "java.lang.Short" # noqa: E272 - @JavaClasses.java_import - def String(self): return "java.lang.String" # noqa: E272 - @JavaClasses.java_import - def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 - @JavaClasses.java_import - def BigInteger(self): return "java.math.BigInteger" # noqa: E272 - @JavaClasses.java_import - def ArrayList(self): return "java.util.ArrayList" # noqa: E272 - @JavaClasses.java_import - def Collection(self): return "java.util.Collection" # noqa: E272 - @JavaClasses.java_import - def Iterator(self): return "java.util.Iterator" # noqa: E272 - @JavaClasses.java_import - def LinkedHashMap(self): return "java.util.LinkedHashMap" # noqa: E272 - @JavaClasses.java_import - def LinkedHashSet(self): return "java.util.LinkedHashSet" # noqa: E272 - @JavaClasses.java_import - def List(self): return "java.util.List" # noqa: E272 - @JavaClasses.java_import - def Map(self): return "java.util.Map" # noqa: E272 - @JavaClasses.java_import - def Set(self): return "java.util.Set" # noqa: E272 -# fmt: on - - -_jc = _JavaClasses() - - -def _import_pandas(): - try: - import pandas as pd - - return pd - except ImportError: - msg = "The Pandas library is missing (http://pandas.pydata.org/). " - msg += "Please install it before using this function." - raise Exception(msg) - - -def _table_to_pandas(table): - pd = _import_pandas() - - data = [] - headers = [] - for i, column in enumerate(table.toArray()): - data.append(column.toArray()) - headers.append(str(table.getColumnHeader(i))) - for j in range(len(data)): - data[j] = to_python(data[j]) - df = pd.DataFrame(data).T - df.columns = headers - return df - - -def _pandas_to_table(df): - if len(df.dtypes.unique()) > 1: - TableClass = jimport("org.scijava.table.DefaultGenericTable") - else: - table_type = df.dtypes.unique()[0] - if table_type.name.startswith("float"): - TableClass = jimport("org.scijava.table.DefaultFloatTable") - elif table_type.name.startswith("int"): - TableClass = jimport("org.scijava.table.DefaultIntTable") - elif table_type.name.startswith("bool"): - TableClass = jimport("org.scijava.table.DefaultBoolTable") - else: - msg = "The type '{}' is not supported.".format(table_type.name) - raise Exception(msg) - - table = TableClass(*df.shape[::-1]) - - for c, column_name in enumerate(df.columns): - table.setColumnHeader(c, column_name) - - for i, (_, row) in enumerate(df.iterrows()): - for c, value in enumerate(row): - header = df.columns[c] - table.set(header, i, to_java(value)) - - return table - - -__version__ = get_version("scyjava") - - # -- JVM startup callbacks -- # NB: These must be performed last, because if this class is imported after the diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py new file mode 100644 index 00000000..4479a822 --- /dev/null +++ b/src/scyjava/_convert.py @@ -0,0 +1,683 @@ +""" +The scyjava conversion subsystem, and built-in conversion functions. +""" + +import collections +import typing +from typing import Any, Callable, NamedTuple + +from jpype import JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort + +from ._java import JavaClasses, isjava, jclass, jimport, start_jvm + + +# NB: We cannot use org.scijava.priority.Priority or other Java-side class +# here because we don't want to impose Java-side dependencies, and we don't +# want to require Java to be started before reasoning about priorities. +class Priority: + FIRST = 1e300 + EXTREMELY_HIGH = 1e6 + VERY_HIGH = 1e4 + HIGH = 1e2 + NORMAL = 0 + LOW = -1e2 + VERY_LOW = -1e4 + EXTREMELY_LOW = -1e6 + LAST = -1e300 + + +class Converter(NamedTuple): + predicate: Callable[[Any], bool] + converter: Callable[[Any], Any] + priority: float = Priority.NORMAL + + +def _convert(obj: Any, converters: typing.List[Converter]) -> Any: + suitable_converters = filter(lambda c: c.predicate(obj), converters) + prioritized = max(suitable_converters, key=lambda c: c.priority) + return prioritized.converter(obj) + + +# -- Python to Java -- + +# Adapted from code posted by vslotman on GitHub: +# https://github.com/kivy/pyjnius/issues/217#issue-145981070 + + +def _raise_type_exception(obj: Any): + raise TypeError("Unsupported type: " + str(type(obj))) + + +def _convertMap(obj: collections.abc.Mapping): + jmap = _jc.LinkedHashMap() + for k, v in obj.items(): + jk = to_java(k) + jv = to_java(v) + jmap.put(jk, jv) + return jmap + + +def _convertSet(obj: collections.abc.Set): + jset = _jc.LinkedHashSet() + for item in obj: + jitem = to_java(item) + jset.add(jitem) + return jset + + +def _convertIterable(obj: collections.abc.Iterable): + jlist = _jc.ArrayList() + for item in obj: + jitem = to_java(item) + jlist.add(jitem) + return jlist + + +java_converters: typing.List[Converter] = [] + + +def add_java_converter(converter: Converter): + """ + Adds a converter to the list used by to_java + :param converter: A Converter going from python to java + """ + java_converters.append(converter) + + +def to_java(obj: Any) -> Any: + """ + Recursively convert a Python object to a Java object. + + Supported types include: + * str -> String + * bool -> Boolean + * int -> Integer, Long or BigInteger as appropriate + * float -> Float, Double or BigDecimal as appropriate + * dict -> LinkedHashMap + * set -> LinkedHashSet + * list -> ArrayList + + :param data: The Python object to convert. + :returns: A corresponding Java object with the same contents. + :raises TypeError: if the argument is not one of the aforementioned types. + """ + start_jvm() + return _convert(obj, java_converters) + + +def _stock_java_converters() -> typing.List[Converter]: + """ + Returns all python-to-java converters supported out of the box! + This should only be called after the JVM has been started! + :returns: A list of Converters + """ + return [ + # Other (Exceptional) converter + Converter( + predicate=lambda obj: True, + converter=_raise_type_exception, + priority=Priority.EXTREMELY_LOW - 1, + ), + # NoneType converter + Converter( + predicate=lambda obj: obj is None, + converter=lambda obj: None, + priority=Priority.EXTREMELY_HIGH + 1, + ), + # Java identity converter + Converter( + predicate=isjava, + converter=lambda obj: obj, + priority=Priority.EXTREMELY_HIGH, + ), + # String converter + Converter( + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: _jc.String(obj.encode("utf-8"), "utf-8"), + ), + # Boolean converter + Converter( + predicate=lambda obj: isinstance(obj, bool), + converter=_jc.Boolean, + ), + # Integer converter + Converter( + predicate=lambda obj: isinstance(obj, int) + and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, + converter=_jc.Integer, + ), + # Long converter + Converter( + predicate=lambda obj: isinstance(obj, int) + and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, + converter=_jc.Long, + priority=Priority.NORMAL - 1, + ), + # BigInteger converter + Converter( + predicate=lambda obj: isinstance(obj, int), + converter=lambda obj: _jc.BigInteger(str(obj)), + priority=Priority.NORMAL - 2, + ), + # Float converter + Converter( + predicate=lambda obj: isinstance(obj, float) + and _jc.Float.MIN_VALUE <= obj <= _jc.Float.MAX_VALUE, + converter=_jc.Float, + ), + # Double converter + Converter( + predicate=lambda obj: isinstance(obj, float) + and _jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE, + converter=_jc.Double, + priority=Priority.NORMAL - 1, + ), + # BigDecimal converter + Converter( + predicate=lambda obj: isinstance(obj, float), + converter=lambda obj: _jc.BigDecimal(str(obj)), + priority=Priority.NORMAL - 2, + ), + # Pandas table converter + Converter( + predicate=lambda obj: type(obj).__name__ == "DataFrame", + converter=_pandas_to_table, + priority=Priority.NORMAL + 1, + ), + # Mapping converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Mapping), + converter=_convertMap, + ), + # Set converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Set), + converter=_convertSet, + ), + # Iterable converter + Converter( + predicate=lambda obj: isinstance(obj, collections.abc.Iterable), + converter=_convertIterable, + priority=Priority.NORMAL - 1, + ), + ] + + +# -- Java to Python -- + + +def _jstr(data): + if isinstance(data, JavaObject): + return str(data) + # NB: We want Python strings to render in single quotes. + return "{!r}".format(data) + + +class JavaObject: + def __init__(self, jobj, intended_class=None): + if intended_class is None: + intended_class = _jc.Object + if not isinstance(jobj, intended_class): + raise TypeError( + f"Not a {intended_class.getName()}: {jclass(jobj).getName()}" + ) + self.jobj = jobj + + def __str__(self): + return _jstr(self.jobj) + + +class JavaIterable(JavaObject, collections.abc.Iterable): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.Iterable) + + def __iter__(self): + return to_python(self.jobj.iterator()) + + def __str__(self): + return "[" + ", ".join(_jstr(v) for v in self) + "]" + + +class JavaCollection(JavaIterable, collections.abc.Collection): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.Collection) + + def __contains__(self, item): + # NB: Collection.contains returns boolean, so no need for gentleness. + return to_python(self.jobj.contains(to_java(item))) + + def __len__(self): + return to_python(self.jobj.size()) + + def __eq__(self, other): + try: + if len(self) != len(other): + return False + for e1, e2 in zip(self, other): + if e1 != e2: + return False + return True + except TypeError: + return False + + +class JavaIterator(JavaObject, collections.abc.Iterator): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.Iterator) + + def __next__(self): + if self.jobj.hasNext(): + # NB: Even if an element cannot be converted, + # we still want to support Pythonic iteration. + return to_python(self.jobj.next(), gentle=True) + raise StopIteration + + +class JavaList(JavaCollection, collections.abc.MutableSequence): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.List) + + def __getitem__(self, key): + # NB: Even if an element cannot be converted, + # we still want Pythonic access to elements. + return to_python(self.jobj.get(key), gentle=True) + + def __setitem__(self, key, value): + # NB: List.set(int, Object) returns inserted element, so be gentle + # here. + return to_python(self.jobj.set(key, to_java(value)), gentle=True) + + def __delitem__(self, key): + # NB: List.remove(Object) returns boolean, so no need for gentleness. + return to_python(self.jobj.remove(to_java(key))) + + def insert(self, index, object): + # NB: List.set(int, Object) returns inserted element, so be gentle + # here. + return to_python(self.jobj.set(index, to_java(object)), gentle=True) + + +class JavaMap(JavaObject, collections.abc.MutableMapping): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.Map) + + def __getitem__(self, key): + # NB: Even if an element cannot be converted, + # we still want Pythonic access to elements. + return to_python(self.jobj.get(to_java(key)), gentle=True) + + def __setitem__(self, key, value): + # NB: Map.put(Object, Object) returns inserted value, so be gentle + # here. + put_return: bool = self.jobj.put(to_java(key), to_java(value)) + return to_python(put_return, gentle=True) + + def __delitem__(self, key): + # NB: Map.remove(Object) returns the removed key, so be gentle here. + return to_python(self.jobj.remove(to_java(key)), gentle=True) + + def keys(self): + return to_python(self.jobj.keySet()) + + def __iter__(self): + return self.keys().__iter__() + + def __len__(self): + return to_python(self.jobj.size()) + + def __eq__(self, other): + try: + if len(self) != len(other): + return False + for k in self: + if k not in other or self[k] != other[k]: + return False + return True + except TypeError: + return False + + def __str__(self): + def item_str(k, v): + return _jstr(k) + ": " + _jstr(v) + + return "{" + ", ".join(item_str(k, v) for k, v in self.items()) + "}" + + +class JavaSet(JavaCollection, collections.abc.MutableSet): + def __init__(self, jobj): + JavaObject.__init__(self, jobj, _jc.Set) + + def add(self, item): + # NB: Set.add returns boolean, so no need for gentleness. + return to_python(self.jobj.add(to_java(item))) + + def discard(self, item): + # NB: Set.remove returns boolean, so no need for gentleness. + return to_python(self.jobj.remove(to_java(item))) + + def __iter__(self): + return to_python(self.jobj.iterator()) + + def __eq__(self, other): + try: + if len(self) != len(other): + return False + for k in self: + if k not in other: + return False + return True + except TypeError: + return False + + def __str__(self): + return "{" + ", ".join(_jstr(v) for v in self) + "}" + + +py_converters: typing.List[Converter] = [] + + +def add_py_converter(converter: Converter): + """ + Adds a converter to the list used by to_python + :param converter: A Converter from java to python + """ + py_converters.append(converter) + + +def to_python(data: Any, gentle: bool = False) -> Any: + """ + Recursively convert a Java object to a Python object. + + Supported types include: + * String, Character -> str + * Boolean -> bool + * Byte, Short, Integer, Long, BigInteger -> int + * Float, Double, BigDecimal -> float + * Map -> collections.abc.MutableMapping (dict-like) + * Set -> collections.abc.MutableSet (set-like) + * List -> collections.abc.MutableSequence (list-like) + * Collection -> collections.abc.Collection + * Iterable -> collections.abc.Iterable + * Iterator -> collections.abc.Iterator + + :param data: The Java object to convert. + :param gentle: If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. + :returns: A corresponding Python object with the same contents. + :raises TypeError: if the argument is not one of the aforementioned types, + and the gentle flag is not set. + """ + start_jvm() + try: + return _convert(data, py_converters) + except TypeError as exc: + if gentle: + return data + raise exc + + +def _stock_py_converters() -> typing.List: + """ + Returns all java-to-python converters supported out of the box! + This should only be called after the JVM has been started! + :returns: A list of Converters + """ + return [ + # Other (Exceptional) converter + Converter( + predicate=lambda obj: True, + converter=_raise_type_exception, + priority=Priority.EXTREMELY_LOW - 1, + ), + # Java identity converter + Converter( + predicate=lambda obj: not isjava(obj), + converter=lambda obj: obj, + priority=Priority.EXTREMELY_HIGH, + ), + # JBoolean converter + Converter( + predicate=lambda obj: isinstance(obj, JBoolean), + converter=bool, + priority=Priority.NORMAL + 1, + ), + # JInt/JLong/JShort converter + Converter( + predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), + converter=int, + priority=Priority.NORMAL + 1, + ), + # JDouble/JFloat converter + Converter( + predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), + converter=float, + priority=Priority.NORMAL + 1, + ), + # JChar converter + Converter( + predicate=lambda obj: isinstance(obj, JChar), + converter=str, + priority=Priority.NORMAL + 1, + ), + # Boolean converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Boolean), + converter=lambda obj: obj.booleanValue(), + ), + # Byte converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Byte), + converter=lambda obj: obj.byteValue(), + ), + # Char converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Character), + converter=lambda obj: obj.toString(), + ), + # Double converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Double), + converter=lambda obj: obj.doubleValue(), + ), + # Float converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Float), + converter=lambda obj: obj.floatValue(), + ), + # Integer converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Integer), + converter=lambda obj: obj.intValue(), + ), + # Long converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Long), + converter=lambda obj: obj.longValue(), + ), + # Short converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Short), + converter=lambda obj: obj.shortValue(), + ), + # String converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.String), + converter=lambda obj: str(obj), + ), + # BigInteger converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.BigInteger), + converter=lambda obj: int(str(obj.toString())), + ), + # BigDecimal converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.BigDecimal), + converter=lambda obj: float(str(obj.toString())), + ), + # SciJava Table converter + Converter( + predicate=_is_table, + converter=_convert_table, + ), + # List converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.List), + converter=JavaList, + ), + # Map converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Map), + converter=JavaMap, + ), + # Set converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Set), + converter=JavaSet, + ), + # Collection converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Collection), + converter=JavaCollection, + priority=Priority.NORMAL - 1, + ), + # Iterable converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Iterable), + converter=JavaIterable, + priority=Priority.NORMAL - 1, + ), + # Iterator converter + Converter( + predicate=lambda obj: isinstance(obj, _jc.Iterator), + converter=JavaIterator, + priority=Priority.NORMAL - 1, + ), + # JArray converter + Converter( + predicate=lambda obj: isinstance(obj, JArray), + converter=lambda obj: [to_python(o) for o in obj], + priority=Priority.VERY_LOW, + ), + ] + + +def _is_table(obj: Any) -> bool: + """Checks if obj is a table""" + try: + return isinstance(obj, jimport("org.scijava.table.Table")) + except BaseException: + # No worries if scijava-table is not available. + pass + + +def _convert_table(obj: Any): + """Converts obj to a table.""" + try: + return _table_to_pandas(obj) + except BaseException: + # No worries if scijava-table is not available. + pass + + +def _import_pandas(): + try: + import pandas as pd + + return pd + except ImportError: + msg = "The Pandas library is missing (http://pandas.pydata.org/). " + msg += "Please install it before using this function." + raise Exception(msg) + + +def _table_to_pandas(table): + pd = _import_pandas() + + data = [] + headers = [] + for i, column in enumerate(table.toArray()): + data.append(column.toArray()) + headers.append(str(table.getColumnHeader(i))) + for j in range(len(data)): + data[j] = to_python(data[j]) + df = pd.DataFrame(data).T + df.columns = headers + return df + + +def _pandas_to_table(df): + if len(df.dtypes.unique()) > 1: + TableClass = jimport("org.scijava.table.DefaultGenericTable") + else: + table_type = df.dtypes.unique()[0] + if table_type.name.startswith("float"): + TableClass = jimport("org.scijava.table.DefaultFloatTable") + elif table_type.name.startswith("int"): + TableClass = jimport("org.scijava.table.DefaultIntTable") + elif table_type.name.startswith("bool"): + TableClass = jimport("org.scijava.table.DefaultBoolTable") + else: + msg = "The type '{}' is not supported.".format(table_type.name) + raise Exception(msg) + + table = TableClass(*df.shape[::-1]) + + for c, column_name in enumerate(df.columns): + table.setColumnHeader(c, column_name) + + for i, (_, row) in enumerate(df.iterrows()): + for c, value in enumerate(row): + header = df.columns[c] + table.set(header, i, to_java(value)) + + return table + + +# fmt: off +class _JavaClasses(JavaClasses): + @JavaClasses.java_import + def Boolean(self): return "java.lang.Boolean" # noqa: E272 + @JavaClasses.java_import + def Byte(self): return "java.lang.Byte" # noqa: E272 + @JavaClasses.java_import + def Character(self): return "java.lang.Character" # noqa: E272 + @JavaClasses.java_import + def Double(self): return "java.lang.Double" # noqa: E272 + @JavaClasses.java_import + def Float(self): return "java.lang.Float" # noqa: E272 + @JavaClasses.java_import + def Integer(self): return "java.lang.Integer" # noqa: E272 + @JavaClasses.java_import + def Iterable(self): return "java.lang.Iterable" # noqa: E272 + @JavaClasses.java_import + def Long(self): return "java.lang.Long" # noqa: E272 + @JavaClasses.java_import + def Object(self): return "java.lang.Object" # noqa: E272 + @JavaClasses.java_import + def Short(self): return "java.lang.Short" # noqa: E272 + @JavaClasses.java_import + def String(self): return "java.lang.String" # noqa: E272 + @JavaClasses.java_import + def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 + @JavaClasses.java_import + def BigInteger(self): return "java.math.BigInteger" # noqa: E272 + @JavaClasses.java_import + def ArrayList(self): return "java.util.ArrayList" # noqa: E272 + @JavaClasses.java_import + def Collection(self): return "java.util.Collection" # noqa: E272 + @JavaClasses.java_import + def Iterator(self): return "java.util.Iterator" # noqa: E272 + @JavaClasses.java_import + def LinkedHashMap(self): return "java.util.LinkedHashMap" # noqa: E272 + @JavaClasses.java_import + def LinkedHashSet(self): return "java.util.LinkedHashSet" # noqa: E272 + @JavaClasses.java_import + def List(self): return "java.util.List" # noqa: E272 + @JavaClasses.java_import + def Map(self): return "java.util.Map" # noqa: E272 + @JavaClasses.java_import + def Set(self): return "java.util.Set" # noqa: E272 +# fmt: on + + +_jc = _JavaClasses() From e27c0abe97cf6af79d9c5ae65e75296db8de6f91 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 17:04:49 -0500 Subject: [PATCH 216/505] Add more docstrings --- src/scyjava/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++ src/scyjava/_arrays.py | 5 +++ src/scyjava/_java.py | 1 + src/scyjava/_versions.py | 4 +++ 4 files changed, 78 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 46d46dfd..fa328972 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -1,3 +1,71 @@ +""" +Supercharged Java access from Python, built on JPype and jgo. + +Use Java classes from Python: + + >>> from scyjava import jimport + >>> System = jimport('java.lang.System') + >>> System.getProperty('java.version') + '1.8.0_252' + +Use Maven artifacts from remote repositories: + + >>> from scyjava import config, jimport + >>> config.add_option('-Djava.awt.headless=true') + >>> config.add_repositories({ + ... 'scijava.public': 'https://maven.scijava.org/content/groups/public', + ... }) + >>> config.endpoints.append('net.imagej:imagej:2.1.0') + >>> ImageJ = jimport('net.imagej.ImageJ') + >>> ij = ImageJ() + >>> formula = "10 * (Math.cos(0.3*p[0]) + Math.sin(0.3*p[1]))" + >>> ArrayImgs = jimport('net.imglib2.img.array.ArrayImgs') + >>> blank = ArrayImgs.floats(64, 16) + >>> sinusoid = ij.op().image().equation(blank, formula) + >>> print(ij.op().image().ascii(sinusoid)) + ,,,--+oo******oo+--,,,,,--+oo******o++--,,,,,--+oo******o++--,,, + ...,--+ooo**oo++--,....,,--+ooo**oo++-,,....,,--+ooo**oo++-,,... + ...,--++oooo++--,... ...,--++oooo++--,... ...,--++oooo++-,,... + ..,--++++++--,.. ..,--++o+++--,.. .,,--++o+++--,.. + ..,,-++++++-,,. ..,,-++++++-,,. ..,--++++++-,,. + .,,--++++--,,. .,,--++++--,,. .,,--++++--,.. + .,,--++++--,,. .,,-+++++--,,. .,,-+++++--,,. + ..,--++++++--,.. ..,--++++++--,.. ..,--++++++-,,.. + ..,,-++oooo++-,,.. ..,,-++oooo++-,,.. ..,,-++ooo+++-,,.. + ...,,-++oooooo++-,,.....,,-++oooooo++-,,.....,,-++oooooo+--,,... + .,,,-++oo****oo++-,,,.,,,-++oo****oo+--,,,.,,,-++oo****oo+--,,,. + ,,--++o***OO**oo++-,,,,--++o***OO**oo+--,,,,--++o***OO**oo+--,,, + ---++o**OOOOOO**o++-----++o**OOOOOO*oo++-----++o**OOOOOO*oo++--- + --++oo*OO####OO*oo++---++oo*OO####OO*oo++---++o**OO####OO*oo++-- + +++oo*OO######O**oo+++++oo*OO######O**oo+++++oo*OO######O**oo+++ + +++oo*OO######OO*oo+++++oo*OO######OO*oo+++++oo*OO######OO*oo+++ + +Convert Java collections to Python: + + >>> from scyjava import jimport + >>> HashSet = jimport('java.util.HashSet') + >>> moves = set(('jump', 'duck', 'dodge')) + >>> fish = set(('walleye', 'pike', 'trout')) + >>> jbirds = HashSet() + >>> for bird in ('duck', 'goose', 'swan'): jbirds.add(bird) + >>> from scyjava import to_python as j2p + >>> j2p(jbirds).isdisjoint(moves) + False + >>> j2p(jbirds).isdisjoint(fish) + True + +Convert Python collections to Java: + + >>> from scyjava import jimport + >>> HashSet = jimport('java.util.HashSet') + >>> jset = HashSet() + >>> pset = set((1, 2, 3)) + >>> from scyjava import to_java as p2j + >>> jset.addAll(p2j(pset)) + True + >>> jset.toString() + '[1, 2, 3]' +""" import logging from functools import lru_cache from typing import Any, Callable, Dict diff --git a/src/scyjava/_arrays.py b/src/scyjava/_arrays.py index fdb99cea..306568b2 100644 --- a/src/scyjava/_arrays.py +++ b/src/scyjava/_arrays.py @@ -1,3 +1,8 @@ +""" +Utility functions for working with and reasoning about arrays. +""" + + def is_arraylike(arr): """ Return True iff the object is arraylike: possessing diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index dbf5cad0..eb055084 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -1,6 +1,7 @@ """ Utility functions for working with the Java and JVM. """ + import atexit import logging import os diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index c5522c23..441e45db 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -1,3 +1,7 @@ +""" +Utility functions for working with and reasoning about software component versions. +""" + import logging from importlib.util import find_spec From cfdb6a046c92be8fe3033af996b5f2df7dc6be26 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 17:34:31 -0500 Subject: [PATCH 217/505] Eliminate unnecessary config dir nesting --- src/scyjava/{config/__init__.py => config.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/scyjava/{config/__init__.py => config.py} (100%) diff --git a/src/scyjava/config/__init__.py b/src/scyjava/config.py similarity index 100% rename from src/scyjava/config/__init__.py rename to src/scyjava/config.py From 0381932374e78148638c1d57269ba5631936dc60 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 19:05:25 -0500 Subject: [PATCH 218/505] Use set literals instead of explicit set ctors --- README.md | 6 +++--- src/scyjava/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d29a330..aba757d4 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,8 @@ See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven ```python >>> from scyjava import jimport >>> HashSet = jimport('java.util.HashSet') ->>> moves = set(('jump', 'duck', 'dodge')) ->>> fish = set(('walleye', 'pike', 'trout')) +>>> moves = {'jump', 'duck', 'dodge'} +>>> fish = {'walleye', 'pike', 'trout'} >>> jbirds = HashSet() >>> for bird in ('duck', 'goose', 'swan'): jbirds.add(bird) ... @@ -130,7 +130,7 @@ AttributeError: 'list' object has no attribute 'stream' >>> from scyjava import jimport >>> HashSet = jimport('java.util.HashSet') >>> jset = HashSet() ->>> pset = set((1, 2, 3)) +>>> pset = {1, 2, 3} >>> jset.addAll(pset) Traceback (most recent call last): File "", line 1, in diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index fa328972..0cd7d971 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -44,8 +44,8 @@ >>> from scyjava import jimport >>> HashSet = jimport('java.util.HashSet') - >>> moves = set(('jump', 'duck', 'dodge')) - >>> fish = set(('walleye', 'pike', 'trout')) + >>> moves = {'jump', 'duck', 'dodge'} + >>> fish = {'walleye', 'pike', 'trout'} >>> jbirds = HashSet() >>> for bird in ('duck', 'goose', 'swan'): jbirds.add(bird) >>> from scyjava import to_python as j2p @@ -59,7 +59,7 @@ >>> from scyjava import jimport >>> HashSet = jimport('java.util.HashSet') >>> jset = HashSet() - >>> pset = set((1, 2, 3)) + >>> pset = {1, 2, 3} >>> from scyjava import to_java as p2j >>> jset.addAll(p2j(pset)) True From 5ee1b625ed4f7967b4da35200b7fa8df054958f5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 19:06:02 -0500 Subject: [PATCH 219/505] Tweak __getattr__ docstring --- src/scyjava/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 0cd7d971..c13a2ed6 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -146,8 +146,7 @@ def constant(func: Callable[[], Any], cache=True) -> Callable[[], Any]: def __getattr__(name): """ - Runs as a fallback when this module does not have an - attribute. + Runs as a fallback when this module does not have an attribute. :param name: The name of the attribute being searched for. """ if name in _CONSTANTS: From 1be8979229c7988fe09154a48897a2aaadd6432f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 20:51:37 -0500 Subject: [PATCH 220/505] Fix parameter name --- src/scyjava/_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4479a822..00c20919 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -97,7 +97,7 @@ def to_java(obj: Any) -> Any: * set -> LinkedHashSet * list -> ArrayList - :param data: The Python object to convert. + :param obj: The Python object to convert. :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ From a73acffd303b1288a1e6e426b1ac57abe5605d2d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 20:51:50 -0500 Subject: [PATCH 221/505] Add converter from table only if Pandas available --- src/scyjava/_convert.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 00c20919..4f954a36 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -422,7 +422,8 @@ def _stock_py_converters() -> typing.List: This should only be called after the JVM has been started! :returns: A list of Converters """ - return [ + + converters = [ # Other (Exceptional) converter Converter( predicate=lambda obj: True, @@ -514,11 +515,6 @@ def _stock_py_converters() -> typing.List: predicate=lambda obj: isinstance(obj, _jc.BigDecimal), converter=lambda obj: float(str(obj.toString())), ), - # SciJava Table converter - Converter( - predicate=_is_table, - converter=_convert_table, - ), # List converter Converter( predicate=lambda obj: isinstance(obj, _jc.List), @@ -560,6 +556,17 @@ def _stock_py_converters() -> typing.List: ), ] + if _import_pandas(): + # SciJava Table converter + converters.append( + Converter( + predicate=_is_table, + converter=_convert_table, + ) + ) + + return converters + def _is_table(obj: Any) -> bool: """Checks if obj is a table""" From 9fe7d565b217aad1c91f9f489edcf01eb8e8297a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 21:39:51 -0500 Subject: [PATCH 222/505] Fix docstring verb tenses --- src/scyjava/_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4f954a36..9e8e46a1 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -569,7 +569,7 @@ def _stock_py_converters() -> typing.List: def _is_table(obj: Any) -> bool: - """Checks if obj is a table""" + """Check if obj is a table.""" try: return isinstance(obj, jimport("org.scijava.table.Table")) except BaseException: @@ -578,7 +578,7 @@ def _is_table(obj: Any) -> bool: def _convert_table(obj: Any): - """Converts obj to a table.""" + """Convert obj to a table.""" try: return _table_to_pandas(obj) except BaseException: From 8efa5f54a7d72057f00706a01edcb0add77feb63 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 21:40:19 -0500 Subject: [PATCH 223/505] Use exception chaining for pandas failure --- src/scyjava/_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 9e8e46a1..f1680f5d 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -591,10 +591,10 @@ def _import_pandas(): import pandas as pd return pd - except ImportError: + except ImportError as e: msg = "The Pandas library is missing (http://pandas.pydata.org/). " msg += "Please install it before using this function." - raise Exception(msg) + raise RuntimeError(msg) from e def _table_to_pandas(table): From 99b1c26c66818f88c7079554a5a7c57ed1614de9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 21:40:45 -0500 Subject: [PATCH 224/505] Add jarray function for creating Java arrays --- src/scyjava/__init__.py | 1 + src/scyjava/_java.py | 52 +++++++++++++++++++++++++++++++++++++++++ tests/test_convert.py | 27 ++++++++++++++++----- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c13a2ed6..819e466c 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -99,6 +99,7 @@ is_awt_initialized, is_jvm_headless, isjava, + jarray, jclass, jimport, jstacktrace, diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index eb055084..06663bef 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -409,3 +409,55 @@ def jstacktrace(exc): return sw.toString() except BaseException: return "" + + +def jarray(kind, lengths): + """ + Create a new n-dimensional Java array. + + :param kind: The type of array to create. This can either be a particular + type of object as obtained from jimport, or else a special code for one of + the eight primitive array types: + * 'b' for byte + * 'c' for char + * 'd' for double + * 'f' for float + * 'i' for int + * 'j' for long + * 's' for short + * 'z' for boolean + :param lengths: List of lengths for the array. For example: + `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. + You can pass a single integer to make a 1-dimensional array of that length. + :returns: The newly allocated array + """ + if isinstance(kind, str): + kind = kind.lower() + if isinstance(lengths, int): + lengths = [lengths] + arraytype = kind + + start_jvm() + + # build up the array type + kinds = { + "b": jpype.JByte, + "c": jpype.JChar, + "d": jpype.JDouble, + "f": jpype.JFloat, + "i": jpype.JInt, + "j": jpype.JLong, + "s": jpype.JShort, + "z": jpype.JBoolean, + } + if arraytype in kinds: + arraytype = kinds[arraytype] + for _ in range(len(lengths)): + arraytype = jpype.JArray(arraytype) + # instantiate the n-dimensional array + arr = arraytype(lengths[0]) + + if len(lengths) > 1: + for i in range(len(arr)): + arr[i] = jarray(kind, lengths[1:]) + return arr diff --git a/tests/test_convert.py b/tests/test_convert.py index 42124035..a6c9de0d 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,6 +1,6 @@ -from jpype import JArray, JByte, JInt +from jpype import JByte -from scyjava import Converter, config, jclass, jimport, start_jvm, to_java, to_python +from scyjava import Converter, config, jarray, jclass, jimport, to_java, to_python config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -124,14 +124,29 @@ def testSet(self): assert s == ps assert str(s) == str(ps) - def testArray(self): - start_jvm() - arr = JArray(JInt)(4) + def testPrimitiveIntArray(self): + arr = jarray("i", 4) for i in range(len(arr)): - arr[i] = to_java(i) + arr[i] = i # NB: assign Python int into Java int! py_arr = to_python(arr) + assert isinstance(py_arr, list) assert py_arr == [0, 1, 2, 3] + def test2DStringArray(self): + String = jimport("java.lang.String") + arr = jarray(String, [3, 5]) + for i in range(len(arr)): + for j in range(len(arr[i])): + s = f"{i}, {j}" + arr[i][j] = s # NB: assign Python str to Java String! + py_arr = to_python(arr) + assert isinstance(py_arr, list) + assert py_arr == [ + ["0, 0", "0, 1", "0, 2", "0, 3", "0, 4"], + ["1, 0", "1, 1", "1, 2", "1, 3", "1, 4"], + ["2, 0", "2, 1", "2, 2", "2, 3", "2, 4"], + ] + def testDict(self): d = { "access_log": [ From 377c323dbfab687934ae2ee62e79f4986880b5e8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Oct 2022 21:40:45 -0500 Subject: [PATCH 225/505] Convert Java primitive arrays to NumPy ndarrays When NumPy is available in the environment. See "Buffer transfer" section of https://jpype.readthedocs.io/en/latest/userguide.html#array-classes Thanks, JPype! --- src/scyjava/_convert.py | 89 +++++++++++++++++++++++++++++++++++++++++ tests/test_arrays.py | 52 ++++++++++++++++++++++++ tests/test_convert.py | 5 ++- 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 tests/test_arrays.py diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index f1680f5d..d7fd7f15 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -556,6 +556,15 @@ def _stock_py_converters() -> typing.List: ), ] + if _import_numpy(): + # Primitive Java array to NumPy converter + converters.append( + Converter( + predicate=_supports_jarray_to_ndarray, + converter=_jarray_to_ndarray, + ) + ) + if _import_pandas(): # SciJava Table converter converters.append( @@ -568,6 +577,86 @@ def _stock_py_converters() -> typing.List: return converters +############################### +# Java array -> NumPy ndarray # +############################### + + +def _jarray_to_ndarray(jarr): + """ + Convert the given Java primitive array into a NumPy ndarray. + + :param jarr: The Java primitive array + :return: The converted NumPy ndarray + """ + np = _import_numpy() + assert _supports_jarray_to_ndarray(jarr) + element_type = _jarray_element_type(jarr) + # fmt: off + jarraytype_map = { + JBoolean: np.bool8, + JByte: np.int8, + # JChar: np.???, + JDouble: np.float64, + JFloat: np.float32, + JInt: np.int32, + JLong: np.int64, + JShort: np.int16, + } + # fmt: on + dtype = jarraytype_map[element_type] + ndarray = np.frombuffer(memoryview(jarr), dtype=dtype) + return ndarray.reshape(_jarray_shape(jarr)) + + +def _supports_jarray_to_ndarray(obj): + """ + Return True iff the given object is convertible to a NumPy ndarray + via the _jarray_to_ndarray function. + + :param obj: The object to check for convertibility + :return: True iff conversion to a NumPy ndarray is possible + """ + element_type = _jarray_element_type(obj) + return element_type in (JBoolean, JByte, JDouble, JFloat, JInt, JLong, JShort) + + +def _jarray_element_type(jarr): + if not isinstance(jarr, JArray): + return None + element = jarr + while isinstance(element, JArray): + element = element[0] + return type(element) + + +def _jarray_shape(jarr): + if not isinstance(jarr, JArray): + return None + shape = [] + element = jarr + while isinstance(element, JArray): + shape.append(len(element)) + element = element[0] + return shape + + +def _import_numpy(): + try: + import numpy as np + + return np + except ImportError as e: + msg = "The NumPy library is missing (https://numpy.org/). " + msg += "Please install it before using this function." + raise RuntimeError(msg) from e + + +###################################### +# SciJava table <-> pandas DataFrame # +###################################### + + def _is_table(obj: Any) -> bool: """Check if obj is a table.""" try: diff --git a/tests/test_arrays.py b/tests/test_arrays.py new file mode 100644 index 00000000..53c99373 --- /dev/null +++ b/tests/test_arrays.py @@ -0,0 +1,52 @@ +import numpy as np +from jpype import JArray, JDouble, JInt + +from scyjava import jarray, to_python + + +class TestArrays(object): + def test_non_primitive_jarray(self): + pass + + def test_jarray_to_ndarray_1d(self): + nums = [11, 6, 2, 15, 5] + jints = jarray("i", len(nums)) + for i in range(len(nums)): + jints[i] = nums[i] + + assert isinstance(jints, JArray(JInt)) + assert len(nums) == len(jints) + for i in range(len(nums)): + assert nums[i] == jints[i] + + pints = to_python(jints) + assert isinstance(pints, np.ndarray) + assert np.int32 == pints.dtype + assert (5,) == pints.shape + for i in range(len(nums)): + assert nums[i] == pints[i] + + def test_jarray_to_ndarray_2d(self): + nums = [ + [1.2, 3.4, 5.6], + [7.8, 9.1, 2.3], + [4.5, 6.7, 8.9], + [0.2, 4.6, 8.0], + [1.3, 5.7, 9.1], + ] + jdoubles = jarray("d", [len(nums), len(nums[0])]) + for i in range(len(nums)): + for j in range(len(nums[i])): + jdoubles[i][j] = nums[i][j] + + assert isinstance(jdoubles, JArray(JArray(JDouble))) + assert 5 == len(jdoubles) + assert 3 == len(jdoubles[0]) + + pdoubles = to_python(jdoubles) + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape + for i in range(len(nums)): + for j in range(len(nums[i])): + assert nums[i][j] == pdoubles[i][j] diff --git a/tests/test_convert.py b/tests/test_convert.py index a6c9de0d..20b55870 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -129,8 +129,9 @@ def testPrimitiveIntArray(self): for i in range(len(arr)): arr[i] = i # NB: assign Python int into Java int! py_arr = to_python(arr) - assert isinstance(py_arr, list) - assert py_arr == [0, 1, 2, 3] + assert type(py_arr).__name__ == "ndarray" + # NB: Comparing ndarray vs list results in a list of bools. + assert all(py_arr == [0, 1, 2, 3]) def test2DStringArray(self): String = jimport("java.lang.String") From b93ac1618b43e5a3df2fa369988086d47a9a4d0e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 13:32:29 -0500 Subject: [PATCH 226/505] Document converters more precisely, and fix bugs When converting from Java primitive wrapper types, the returned type would be e.g. JByte or JShort instead of int. These need to be casted. When stringifying a Java object, the call to .toString() is redundant, at least for the types in play here (Character, BigInteger, BigDecimal). --- src/scyjava/_convert.py | 62 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index d7fd7f15..9cf470de 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -436,119 +436,119 @@ def _stock_py_converters() -> typing.List: converter=lambda obj: obj, priority=Priority.EXTREMELY_HIGH, ), - # JBoolean converter + # JBoolean -> bool Converter( predicate=lambda obj: isinstance(obj, JBoolean), converter=bool, priority=Priority.NORMAL + 1, ), - # JInt/JLong/JShort converter + # JByte/JInt/JLong/JShort -> int Converter( predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), converter=int, priority=Priority.NORMAL + 1, ), - # JDouble/JFloat converter + # JDouble/JFloat -> float Converter( predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), converter=float, priority=Priority.NORMAL + 1, ), - # JChar converter + # JChar -> str Converter( predicate=lambda obj: isinstance(obj, JChar), converter=str, priority=Priority.NORMAL + 1, ), - # Boolean converter + # java.lang.Boolean -> bool Converter( predicate=lambda obj: isinstance(obj, _jc.Boolean), converter=lambda obj: obj.booleanValue(), ), - # Byte converter + # java.lang.Byte -> int Converter( predicate=lambda obj: isinstance(obj, _jc.Byte), - converter=lambda obj: obj.byteValue(), + converter=lambda obj: int(obj.byteValue()), ), - # Char converter + # java.lang.Character -> str Converter( predicate=lambda obj: isinstance(obj, _jc.Character), - converter=lambda obj: obj.toString(), + converter=lambda obj: str, ), - # Double converter + # java.lang.Double -> float Converter( predicate=lambda obj: isinstance(obj, _jc.Double), - converter=lambda obj: obj.doubleValue(), + converter=lambda obj: float(obj.doubleValue()), ), - # Float converter + # java.lang.Float -> float Converter( predicate=lambda obj: isinstance(obj, _jc.Float), - converter=lambda obj: obj.floatValue(), + converter=lambda obj: float(obj.floatValue()), ), - # Integer converter + # java.lang.Integer -> int Converter( predicate=lambda obj: isinstance(obj, _jc.Integer), - converter=lambda obj: obj.intValue(), + converter=lambda obj: int(obj.intValue()), ), - # Long converter + # java.lang.Long -> int Converter( predicate=lambda obj: isinstance(obj, _jc.Long), - converter=lambda obj: obj.longValue(), + converter=lambda obj: int(obj.longValue()), ), - # Short converter + # java.lang.Short -> int Converter( predicate=lambda obj: isinstance(obj, _jc.Short), - converter=lambda obj: obj.shortValue(), + converter=lambda obj: int(obj.shortValue()), ), - # String converter + # java.lang.String -> str Converter( predicate=lambda obj: isinstance(obj, _jc.String), converter=lambda obj: str(obj), ), - # BigInteger converter + # java.math.BigInteger -> int Converter( predicate=lambda obj: isinstance(obj, _jc.BigInteger), - converter=lambda obj: int(str(obj.toString())), + converter=lambda obj: int(str(obj)), ), - # BigDecimal converter + # java.math.BigDecimal -> float Converter( predicate=lambda obj: isinstance(obj, _jc.BigDecimal), - converter=lambda obj: float(str(obj.toString())), + converter=lambda obj: float(str(obj)), ), - # List converter + # java.util.List -> scyjava.JavaList (list-like) Converter( predicate=lambda obj: isinstance(obj, _jc.List), converter=JavaList, ), - # Map converter + # java.util.Map -> scyjava.JavaMap (dict-like) Converter( predicate=lambda obj: isinstance(obj, _jc.Map), converter=JavaMap, ), - # Set converter + # java.util.Set -> scyjava.JavaSet (set-like) Converter( predicate=lambda obj: isinstance(obj, _jc.Set), converter=JavaSet, ), - # Collection converter + # java.util.Collection -> scyjava.JavaCollection (collections.abc.Collection) Converter( predicate=lambda obj: isinstance(obj, _jc.Collection), converter=JavaCollection, priority=Priority.NORMAL - 1, ), - # Iterable converter + # java.lang.Iterable -> scyjava.JavaIterable (collections.abc.Iterable) Converter( predicate=lambda obj: isinstance(obj, _jc.Iterable), converter=JavaIterable, priority=Priority.NORMAL - 1, ), - # Iterator converter + # java.util.Iterator -> scyjava.JavaIterator (collections.abc.Iterator) Converter( predicate=lambda obj: isinstance(obj, _jc.Iterator), converter=JavaIterator, priority=Priority.NORMAL - 1, ), - # JArray converter + # JArray -> list Converter( predicate=lambda obj: isinstance(obj, JArray), converter=lambda obj: [to_python(o) for o in obj], From c02c0e6ff8de381cecafca34b1b3f411fc12194a Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 4 Oct 2022 14:55:38 -0500 Subject: [PATCH 227/505] Add Path conversion support --- src/scyjava/_convert.py | 17 +++++++++++++++++ tests/test_convert.py | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 9cf470de..0094bf85 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -4,6 +4,7 @@ import collections import typing +from pathlib import Path from typing import Any, Callable, NamedTuple from jpype import JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort @@ -178,6 +179,12 @@ def _stock_java_converters() -> typing.List[Converter]: converter=lambda obj: _jc.BigDecimal(str(obj)), priority=Priority.NORMAL - 2, ), + # pathlib.Path -> java.nio.file.Path + Converter( + predicate=lambda obj: isinstance(obj, Path), + converter=lambda obj: _jc.Paths.get(str(obj)), + priority=Priority.NORMAL + 1, + ), # Pandas table converter Converter( predicate=lambda obj: type(obj).__name__ == "DataFrame", @@ -548,6 +555,12 @@ def _stock_py_converters() -> typing.List: converter=JavaIterator, priority=Priority.NORMAL - 1, ), + # java.nio.file.Path -> pathlib.Path + Converter( + predicate=lambda obj: isinstance(obj, _jc.Path), + converter=lambda obj: Path(str(obj)), + priority=Priority.NORMAL + 1, + ), # JArray -> list Converter( predicate=lambda obj: isinstance(obj, JArray), @@ -758,6 +771,10 @@ def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 @JavaClasses.java_import def BigInteger(self): return "java.math.BigInteger" # noqa: E272 @JavaClasses.java_import + def Path(self): return "java.nio.file.Path" # noqa: E272 + @JavaClasses.java_import + def Paths(self): return "java.nio.file.Paths" # noqa: E272 + @JavaClasses.java_import def ArrayList(self): return "java.util.ArrayList" # noqa: E272 @JavaClasses.java_import def Collection(self): return "java.util.Collection" # noqa: E272 diff --git a/tests/test_convert.py b/tests/test_convert.py index 20b55870..9d2048aa 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,3 +1,6 @@ +from os import getcwd +from pathlib import Path + from jpype import JByte from scyjava import Converter, config, jarray, jclass, jimport, to_java, to_python @@ -176,6 +179,15 @@ def testDict(self): assert d == pd assert str(d) == str(pd) + def testPath(self): + py_path = Path(getcwd()) + j_path = to_java(py_path) + assert isinstance(j_path, jimport("java.nio.file.Path")) + assert str(j_path) == str(py_path) + + actual = to_python(j_path) + assert actual == py_path + def testMixed(self): test_dict = {"a": "b", "c": "d"} test_list = ["e", "f", "g", "h"] From 8087ad295c622b6d7884f302ab96a2a2c25ca47a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 14:14:54 -0500 Subject: [PATCH 228/505] Document more converters more precisely This is a follow-on to b93ac1618b43e5a3df2fa369988086d47a9a4d0e, improving the remaining comment descriptions. --- src/scyjava/_convert.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 0094bf85..ef541c55 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -119,61 +119,61 @@ def _stock_java_converters() -> typing.List[Converter]: converter=_raise_type_exception, priority=Priority.EXTREMELY_LOW - 1, ), - # NoneType converter + # None -> None Converter( predicate=lambda obj: obj is None, converter=lambda obj: None, priority=Priority.EXTREMELY_HIGH + 1, ), - # Java identity converter + # Java object identity Converter( predicate=isjava, converter=lambda obj: obj, priority=Priority.EXTREMELY_HIGH, ), - # String converter + # str -> java.lang.String Converter( predicate=lambda obj: isinstance(obj, str), converter=lambda obj: _jc.String(obj.encode("utf-8"), "utf-8"), ), - # Boolean converter + # bool -> java.lang.Boolean Converter( predicate=lambda obj: isinstance(obj, bool), converter=_jc.Boolean, ), - # Integer converter + # int -> java.lang.Integer Converter( predicate=lambda obj: isinstance(obj, int) and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, converter=_jc.Integer, ), - # Long converter + # int -> java.lang.Long Converter( predicate=lambda obj: isinstance(obj, int) and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, converter=_jc.Long, priority=Priority.NORMAL - 1, ), - # BigInteger converter + # int -> java.math.BigInteger Converter( predicate=lambda obj: isinstance(obj, int), converter=lambda obj: _jc.BigInteger(str(obj)), priority=Priority.NORMAL - 2, ), - # Float converter + # float -> java.lang.Float Converter( predicate=lambda obj: isinstance(obj, float) and _jc.Float.MIN_VALUE <= obj <= _jc.Float.MAX_VALUE, converter=_jc.Float, ), - # Double converter + # float -> java.lang.Double Converter( predicate=lambda obj: isinstance(obj, float) and _jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE, converter=_jc.Double, priority=Priority.NORMAL - 1, ), - # BigDecimal converter + # float -> java.math.BigDecimal Converter( predicate=lambda obj: isinstance(obj, float), converter=lambda obj: _jc.BigDecimal(str(obj)), @@ -185,23 +185,23 @@ def _stock_java_converters() -> typing.List[Converter]: converter=lambda obj: _jc.Paths.get(str(obj)), priority=Priority.NORMAL + 1, ), - # Pandas table converter + # pandas.DataFrame -> org.scijava.table.Table Converter( predicate=lambda obj: type(obj).__name__ == "DataFrame", converter=_pandas_to_table, priority=Priority.NORMAL + 1, ), - # Mapping converter + # collections.abc.Mapping -> java.util.Map Converter( predicate=lambda obj: isinstance(obj, collections.abc.Mapping), converter=_convertMap, ), - # Set converter + # collections.abc.Set -> java.util.Set Converter( predicate=lambda obj: isinstance(obj, collections.abc.Set), converter=_convertSet, ), - # Iterable converter + # collections.abc.Iterable -> java.util.Iterable Converter( predicate=lambda obj: isinstance(obj, collections.abc.Iterable), converter=_convertIterable, @@ -437,7 +437,7 @@ def _stock_py_converters() -> typing.List: converter=_raise_type_exception, priority=Priority.EXTREMELY_LOW - 1, ), - # Java identity converter + # Python object identity Converter( predicate=lambda obj: not isjava(obj), converter=lambda obj: obj, @@ -570,7 +570,7 @@ def _stock_py_converters() -> typing.List: ] if _import_numpy(): - # Primitive Java array to NumPy converter + # primitive array -> numpy.ndarray converters.append( Converter( predicate=_supports_jarray_to_ndarray, @@ -579,7 +579,7 @@ def _stock_py_converters() -> typing.List: ) if _import_pandas(): - # SciJava Table converter + # org.scijava.table.Table -> pandas.DataFrame converters.append( Converter( predicate=_is_table, From ae39f13ddcd889c95f09d15468912417f59a5a06 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 14 Oct 2022 10:38:27 -0500 Subject: [PATCH 229/505] bin/clean.sh: report what gets removed --- bin/clean.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/clean.sh b/bin/clean.sh index 739d6ec3..485ed5ef 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -4,6 +4,6 @@ dir=$(dirname "$0") cd "$dir/.." find . -name __pycache__ -type d | while read d - do rm -rf "$d" + do rm -rfv "$d" done -rm -rf .pytest_cache build dist src/*.egg-info +rm -rfv .pytest_cache build dist src/*.egg-info From b3d940e97a42057a2ec57a4340a852d29bdd287d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 14 Oct 2022 11:16:43 -0500 Subject: [PATCH 230/505] Tweak authors label This aligns it with copyright blurbs in SciJava's Java-based components. --- pyproject.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b51accb..edd1a529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" version = "1.6.1.dev0" -authors = [ - {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, - {name = "Philipp Hanslovsky"}, - {name = "Edward Evans"}, - {name = "Mark Hiner"}, - {name = "Gabriel Selzer"}, -] +authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] description = "Supercharged Java access from Python" readme = "README.md" license = {text = "The Unlicense"} From 60961ea36646b0b635829556de33583d5e3dce26 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 14 Oct 2022 16:00:02 -0500 Subject: [PATCH 231/505] Tidy up pyproject.toml See scijava/jgo@5cf04c13229e645686d5eca66989d6ef6dd913fe for details. --- pyproject.toml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index edd1a529..8b8893cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,11 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" version = "1.6.1.dev0" -authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] description = "Supercharged Java access from Python" -readme = "README.md" license = {text = "The Unlicense"} +authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] +readme = "README.md" +keywords = ["java", "maven", "cross-language"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -28,20 +29,16 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", ] + +# NB: Keep this in sync with environment.yml AND dev-environment.yml! requires-python = ">=3.7" dependencies = [ "jpype1 >= 1.3.0", "jgo", ] -[project.urls] -Homepage = "https://github.com/scijava/scyjava" -"Bug Tracker" = "https://github.com/scijava/scyjava/issues" -Documentation = "https://github.com/scijava/scyjava/blob/master/README.md" -"Source Code" = "https://github.com/scijava/scyjava" - [project.optional-dependencies] -# Ensure any changes to this list are also added to dev-environment.yml! +# NB: Keep this in sync with dev-environment.yml! dev = [ "autopep8", "black", @@ -55,9 +52,15 @@ dev = [ "toml", ] +[project.urls] +homepage = "https://github.com/scijava/scyjava" +documentation = "https://github.com/scijava/scyjava/blob/master/README.md" +source = "https://github.com/scijava/scyjava" +download = "https://pypi.org/project/scyjava/" +tracker = "https://github.com/scijava/scyjava/issues" + [tool.setuptools] package-dir = {"" = "src"} -# Ensure any changes to this list are also added to environment.yml AND dev-environment.yml! include-package-data = false [tool.setuptools.packages.find] From b4e7b6ca7474edf681cc5afc4e81b105580f3de4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 14 Oct 2022 16:01:37 -0500 Subject: [PATCH 232/505] Add validate-pyproject to linting process --- .github/workflows/build.yml | 3 +++ .pre-commit-config.yaml | 4 ++++ bin/lint.sh | 1 + dev-environment.yml | 2 +- pyproject.toml | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d47a6c81..eceb5f43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,6 +68,9 @@ jobs: with: configuration: --check-only + - name: Validate pyproject.toml + run: python -m validate_pyproject pyproject.toml + conda-dev-test: name: Conda Setup & Code Coverage runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82261083..c77da0ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,3 +21,7 @@ repos: rev: 22.3.0 hooks: - id: black + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.10.1 + hooks: + - id: validate-pyproject diff --git a/bin/lint.sh b/bin/lint.sh index c1dbebd1..e2b6ddba 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -6,3 +6,4 @@ cd "$dir/.." black src tests isort src tests python -m flake8 src tests +validate-pyproject pyproject.toml diff --git a/dev-environment.yml b/dev-environment.yml index 6a29853a..e6a68abe 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -32,5 +32,5 @@ dependencies: # Project from source - pip - pip: + - validate-pyproject[all] - -e . - diff --git a/pyproject.toml b/pyproject.toml index 8b8893cb..62cec892 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dev = [ "numpy", "pandas", "toml", + "validate-pyproject[all]", ] [project.urls] From 533685d39cb57f884de8473590c3202995acbd71 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 14 Oct 2022 18:22:15 -0500 Subject: [PATCH 233/505] CI: fix validate-pyproject setup --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eceb5f43..f896b6b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,9 @@ jobs: configuration: --check-only - name: Validate pyproject.toml - run: python -m validate_pyproject pyproject.toml + run: | + python -m pip install validate-pyproject[all] + python -m validate_pyproject pyproject.toml conda-dev-test: name: Conda Setup & Code Coverage From 3ae2e31ef22cef4d93b3edc33e98aa99154ecb05 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 18 Oct 2022 09:48:54 -0500 Subject: [PATCH 234/505] Release version 1.7.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 62cec892..3ad90cc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.6.1.dev0" +version = "1.7.0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 4a6587725a27b75d08401ed5e37cd32f4276ab74 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 18 Oct 2022 09:57:26 -0500 Subject: [PATCH 235/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ad90cc1..abf31eb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.7.0" +version = "1.7.1.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From ce6c8aadea5919ae239daa171cb1d2b09a7f4f42 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 18 Oct 2022 09:55:46 -0500 Subject: [PATCH 236/505] Makefile: run clean before dist --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f17ff365..a2eaae67 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ lint: check test: check bin/test.sh -dist: check +dist: check clean python -m build .PHONY: test From a62ec75b1b8ef6f6766587142765d8d856d9113a Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 24 Oct 2022 11:31:44 -0500 Subject: [PATCH 237/505] Fix locked Java buffer copy for array conversions `memoryview` of Java arrays (jarr) exposes the jarr's buffer protocol and copies the buffer into a page which then is referenced locked by `memoryview` and things that are created from it (e.g. numpy.frombuffer). Changing the underlying jarr's memory and requesting a new `memoryview` does not return an updated view with the jarr's data change as we expect, but instead the initial `memoryview` data state. This is bad. In order to get `memoryview` to reflect the correct jarr data changes you need to dereference (e.g. del or set to None) the `memoryview` and objects that depend on it (e.g. a numpy array created from the view). This patch resolves this issue by obtaining the buffer that `memoryview` is getting and wrapping the buffer with numpy as we want. We can then safely release the buffer (preserving memory) and return a copy to the user. If _jarray_to_ndarray is called again on the same Java array a new buffer is obtained, wrapped with numpy and released. The end result is an updated view of the Java array upon request. --- src/scyjava/_convert.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index ef541c55..5b33cd64 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -618,7 +618,9 @@ def _jarray_to_ndarray(jarr): } # fmt: on dtype = jarraytype_map[element_type] - ndarray = np.frombuffer(memoryview(jarr), dtype=dtype) + bb = bytes(jarr) + ndarray = np.frombuffer(bb, dtype=dtype) + del bb # release the buffer return ndarray.reshape(_jarray_shape(jarr)) From eae0bba9db10de3905a7f642d2a30c2ba529699d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 24 Oct 2022 12:03:41 -0500 Subject: [PATCH 238/505] Add test for jarray to ndarray updates Changing an existing Java array and calling to_python() should return a numpy array with the latest buffer copy of the Java array. --- tests/test_arrays.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 53c99373..da252782 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -26,6 +26,33 @@ def test_jarray_to_ndarray_1d(self): for i in range(len(nums)): assert nums[i] == pints[i] + def test_jarray_to_ndarray_1d_updates(self): + nums_init = [11, 6, 2, 15, 5] + nums_delta = [4, 100, 36, 133, 3] + jints = jarray("i", len(nums_init)) + for i in range(len(nums_init)): + jints[i] = nums_init[i] + + # assert narr initial state + pints = to_python(jints) + assert isinstance(pints, np.ndarray) + assert np.int32 == pints.dtype + assert (5,) == pints.shape + for i in range(len(nums_init)): + assert nums_init[i] == pints[i] + + # change jint data state + for i in range(len(nums_delta)): + jints[i] = nums_delta[i] + + # assert narr delta state + pints = to_python(jints) + assert isinstance(pints, np.ndarray) + assert np.int32 == pints.dtype + assert (5,) == pints.shape + for i in range(len(nums_delta)): + assert nums_delta[i] == pints[i] + def test_jarray_to_ndarray_2d(self): nums = [ [1.2, 3.4, 5.6], @@ -50,3 +77,46 @@ def test_jarray_to_ndarray_2d(self): for i in range(len(nums)): for j in range(len(nums[i])): assert nums[i][j] == pdoubles[i][j] + + def test_jarray_to_ndarray_2d_updates(self): + nums_init = [ + [1.2, 3.4, 5.6], + [7.8, 9.1, 2.3], + [4.5, 6.7, 8.9], + [0.2, 4.6, 8.0], + [1.3, 5.7, 9.1], + ] + nums_delta = [ + [15.3, 3.4, 5.6], + [7.8, 9.1, 22.3], + [90.5, 0.7, 8.9], + [80.2, 3.6, 59.0], + [1.5, 95.4, 9.1], + ] + jdoubles = jarray("d", [len(nums_init), len(nums_init[0])]) + for i in range(len(nums_init)): + for j in range(len(nums_init[i])): + jdoubles[i][j] = nums_init[i][j] + + # assert narr initial state + pdoubles = to_python(jdoubles) + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape + for i in range(len(nums_init)): + for j in range(len(nums_init[i])): + assert nums_init[i][j] == pdoubles[i][j] + + # change jdoubles data state + for i in range(len(nums_delta)): + for j in range(len(nums_delta[i])): + jdoubles[i][j] = nums_delta[i][j] + + # assert narr delta state + pdoubles = to_python(jdoubles) + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape + for i in range(len(nums_delta)): + for j in range(len(nums_delta[i])): + assert nums_delta[i][j] == pdoubles[i][j] \ No newline at end of file From 864dd68d4db486438d51bfe77e3970a6734f1212 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 24 Oct 2022 12:07:55 -0500 Subject: [PATCH 239/505] Make flake8 happy -- add new line --- tests/test_arrays.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index da252782..52131371 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -119,4 +119,4 @@ def test_jarray_to_ndarray_2d_updates(self): assert (5, 3) == pdoubles.shape for i in range(len(nums_delta)): for j in range(len(nums_delta[i])): - assert nums_delta[i][j] == pdoubles[i][j] \ No newline at end of file + assert nums_delta[i][j] == pdoubles[i][j] From 0f943d86a8433e6a861d87ca0c33d477cc231539 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 17:31:17 -0500 Subject: [PATCH 240/505] Avoid ugly comment line wrapping --- src/scyjava/_convert.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 5b33cd64..45d2dd43 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -290,8 +290,7 @@ def __getitem__(self, key): return to_python(self.jobj.get(key), gentle=True) def __setitem__(self, key, value): - # NB: List.set(int, Object) returns inserted element, so be gentle - # here. + # NB: List.set(int, Object) returns inserted element; be gentle here. return to_python(self.jobj.set(key, to_java(value)), gentle=True) def __delitem__(self, key): @@ -299,8 +298,7 @@ def __delitem__(self, key): return to_python(self.jobj.remove(to_java(key))) def insert(self, index, object): - # NB: List.set(int, Object) returns inserted element, so be gentle - # here. + # NB: List.set(int, Object) returns inserted element; be gentle here. return to_python(self.jobj.set(index, to_java(object)), gentle=True) @@ -314,13 +312,12 @@ def __getitem__(self, key): return to_python(self.jobj.get(to_java(key)), gentle=True) def __setitem__(self, key, value): - # NB: Map.put(Object, Object) returns inserted value, so be gentle - # here. + # NB: Map.put(Object, Object) returns inserted value; be gentle here. put_return: bool = self.jobj.put(to_java(key), to_java(value)) return to_python(put_return, gentle=True) def __delitem__(self, key): - # NB: Map.remove(Object) returns the removed key, so be gentle here. + # NB: Map.remove(Object) returns the removed key; be gentle here. return to_python(self.jobj.remove(to_java(key)), gentle=True) def keys(self): From d98bd8a18facb7e5e0e7b606746b4ed5877cda46 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Oct 2022 13:34:05 -0500 Subject: [PATCH 241/505] Fix up conda environment files --- dev-environment.yml | 17 ++++++++++++----- environment.yml | 15 +++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index e6a68abe..7094f17f 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -1,22 +1,29 @@ # Use this file to construct an environment # for developing scyjava from source. # +# First, install mambaforge: +# +# https://github.com/conda-forge/miniforge#mambaforge +# +# Then run: +# # mamba env create -f dev-environment.yml # conda activate scyjava-dev # -# In addition to the dependencies needed for using scyjava, it includes tools -# for developer-related actions like running automated tests (pytest), -# linting the code (black), and generating the API documentation (sphinx). -# If you want an environment without these tools, use environment.yml. +# In addition to the dependencies needed for using scyjava, it +# includes tools for developer-related actions like running +# automated tests (pytest) and linting the code (black). If you +# want an environment without these tools, use environment.yml. name: scyjava-dev channels: - conda-forge - defaults dependencies: + - python >= 3.7 # Project dependencies - jpype1 >= 1.3.0 - jgo - - openjdk=8 + - openjdk # Test dependencies - numpy - pandas diff --git a/environment.yml b/environment.yml index 3f3c0760..19955ef6 100644 --- a/environment.yml +++ b/environment.yml @@ -1,10 +1,16 @@ -# Use this file to construct an environment for working -# with scyjava in a runtime setting +# Use this file to construct an environment for +# working with scyjava in a runtime setting. +# +# First, install mambaforge: +# +# https://github.com/conda-forge/miniforge#mambaforge +# +# Then run: # # mamba env create -# conda activate scyjava +# mamba activate scyjava # -# It includes the dependencies needed for using scyjava but not tools +# It includes the dependencies needed for using scyjava, but not tools # for developer-related actions like running automated tests (pytest), # linting the code (black), and generating the API documentation (sphinx). # If you want an environment including these tools, use dev-environment.yml. @@ -14,6 +20,7 @@ channels: - conda-forge - defaults dependencies: + - python >= 3.7 # Project dependencies - jpype1 >= 1.3.0 - jgo From 2e3d38edb62d0619033c703b11c93d1727cd50d6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Oct 2022 14:07:24 -0500 Subject: [PATCH 242/505] Fix the public API When refactoring the codebase to multiple files (4de4bbf1, b1cfa8de, e075eed3, 5c4ac3c3), automatic inference of the scyjava public API was lost, causing the help(scyjava) invocation to stop working properly. The fix is to explicitly declare the list attribute __all__ with elements matching the API intended for public consumption. The help function makes use of this attribute when generating its output. --- src/scyjava/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 819e466c..42dacb16 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -117,7 +117,13 @@ ) __version__ = get_version("scyjava") - +__all__ = [ + k + for k, v in globals().items() + if not k.startswith("_") + and hasattr(v, "__module__") + and v.__module__.startswith("scyjava.") +] _logger = logging.getLogger(__name__) From cb7371662cfddf72ebfddb92d5274ed6585fcd6a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 25 Oct 2022 14:48:36 -0500 Subject: [PATCH 243/505] README: update functions section for latest API --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aba757d4..bd1ad095 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ FUNCTIONS get_version(java_class_or_python_package) Return the version of a Java class or Python package. - For Python packages, uses importlib.metadata.version if available + For Python package, uses importlib.metadata.version if available (Python 3.8+), with pkg_resources.get_distribution as a fallback. For Java classes, requires org.scijava:scijava-common on the classpath. @@ -178,6 +178,13 @@ FUNCTIONS See org.scijava.VersionUtils.getVersion(Class) for further details. + is_arraylike(arr) + Return True iff the object is arraylike: possessing + .shape, .dtype, .__array__, and .ndim attributes. + + :param arr: The object to check for arraylike properties + :return: True iff the object is arraylike + is_awt_initialized() Return true iff the AWT subsystem has been initialized. @@ -193,6 +200,13 @@ FUNCTIONS :raises RuntimeError: If the JVM has not started yet. + is_memoryarraylike(arr) + Return True iff the object is memoryarraylike: + an arraylike object whose .data type is memoryview. + + :param arr: The object to check for memoryarraylike properties + :return: True iff the object is memoryarraylike + is_version_at_least(actual_version, minimum_version) Return a boolean on a version comparison. Requires org.scijava:scijava-common on the classpath. @@ -202,9 +216,36 @@ FUNCTIONS See org.scijava.VersionUtils.compare(String, String) for further details. + is_xarraylike(xarr) + Return True iff the object is xarraylike: + possessing .values, .dims, and .coords attributes, + and whose .values are arraylike. + + :param arr: The object to check for xarraylike properties + :return: True iff the object is xarraylike + isjava(data) Return whether the given data object is a Java object. + jarray(kind, lengths) + Create a new n-dimensional Java array. + + :param kind: The type of array to create. This can either be a particular + type of object as obtained from jimport, or else a special code for one of + the eight primitive array types: + * 'b' for byte + * 'c' for char + * 'd' for double + * 'f' for float + * 'i' for int + * 'j' for long + * 's' for short + * 'z' for boolean + :param lengths: List of lengths for the array. For example: + `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. + You can pass a single integer to make a 1-dimensional array of that length. + :returns: The newly allocated array + jclass(data) Obtain a Java class object. @@ -213,7 +254,7 @@ FUNCTIONS A. Name of a class to look up, analogous to Class.forName("java.lang.String"); B. A jpype.JClass object analogous to String.class; - C. A _jpype._JObject instance analogous to o.getClass(). + C. A jpype.JObject instance analogous to o.getClass(). :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. @@ -285,7 +326,7 @@ FUNCTIONS Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - start_jvm(options=[]) + start_jvm(options=None) Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling this function directly is typically not necessary, because the first @@ -297,7 +338,7 @@ FUNCTIONS to_java(obj: Any) -> Any Recursively convert a Python object to a Java object. - :param data: The Python object to convert. + Supported types include: * str -> String * bool -> Boolean @@ -306,14 +347,14 @@ FUNCTIONS * dict -> LinkedHashMap * set -> LinkedHashSet * list -> ArrayList + + :param obj: The Python object to convert. :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. to_python(data: Any, gentle: bool = False) -> Any Recursively convert a Java object to a Python object. - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. + Supported types include: * String, Character -> str * Boolean -> bool @@ -325,6 +366,10 @@ FUNCTIONS * Collection -> collections.abc.Collection * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator + + :param data: The Java object to convert. + :param gentle: If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. :returns: A corresponding Python object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types, and the gentle flag is not set. From f06fa5d0ce8eb691d45887f952334edcc601ec14 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Thu, 27 Oct 2022 10:17:15 -0500 Subject: [PATCH 244/505] Use bytearray instead of bytes Use a bytearray instead of bytes for the Java array data buffer. A bytearray is mutable, thus wrapping a bytearray with np.frombuffer() returns a numpy array with read/write access. --- src/scyjava/_convert.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 45d2dd43..bd6fec63 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -615,7 +615,16 @@ def _jarray_to_ndarray(jarr): } # fmt: on dtype = jarraytype_map[element_type] - bb = bytes(jarr) + # Use a bytearray instead of memoryview for np.frombuffer. + # Casting memoryview() on a Java array copies the array's content + # into a buffer which does not get released when a new view is + # requested. If the Java array's data changes a new memoryview will + # contain the old buffer data. The view and any object created with + # it must be deleted (del or =None) to release the buffer before + # requesting a new view. Instead of utilizing the buffer + # memoryview creates of the Java array, we obtain the buffer ouselves + # as a mutable bytearray. + bb = bytearray(jarr) ndarray = np.frombuffer(bb, dtype=dtype) del bb # release the buffer return ndarray.reshape(_jarray_shape(jarr)) From 93ca2ad26531976dff378993aa0d52d9a03cb0b5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Nov 2022 22:30:13 -0500 Subject: [PATCH 245/505] conda: set openjdk version ranges more carefully Certainly we require at least OpenJDK 8, although on conda-forge, 8.0.112 is the oldest anyway, so this rule has no effect in practice. For the developer environment, for now we restrict to OpenJDK 11 maximum, not OpenJDK 17, because we want CI to use OpenJDK 11 (the scijava-tables library has a bug with OpenJDK 17 as of this writing). In the long term, restricting OpenJDK in this way is the wrong thing to do, but it's tolerable for now to get tests passing. --- dev-environment.yml | 2 +- environment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index 7094f17f..bb4e4780 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -23,7 +23,7 @@ dependencies: # Project dependencies - jpype1 >= 1.3.0 - jgo - - openjdk + - openjdk >= 8, < 12 # Test dependencies - numpy - pandas diff --git a/environment.yml b/environment.yml index 19955ef6..1ec5d04c 100644 --- a/environment.yml +++ b/environment.yml @@ -24,7 +24,7 @@ dependencies: # Project dependencies - jpype1 >= 1.3.0 - jgo - - openjdk=8 + - openjdk >= 8 # Project from source - pip - pip: From 128517365358692c6344228be327ac4ba2770fd7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Nov 2022 11:21:28 -0500 Subject: [PATCH 246/505] Fix bug in optional dependency import There were two conflicting assumptions in the codebase: 1. If the library is not found, a falsy value is returned; and 2. If the library is not found, an exception is raised. When initializing converters, it's looking for the falsy value, i.e. just a simple test of whether the dependency is available. But when importing the dependency in a function that actually uses it, an exception is preferred, to ensure the import never ends up as None. This commit fulfills both scenarios by adding a boolean required flag to the respective import functions. If a gentle check for availability is preferred, one can pass required=False to obtain that behavior now. --- src/scyjava/_convert.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index bd6fec63..65e28de0 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -566,7 +566,7 @@ def _stock_py_converters() -> typing.List: ), ] - if _import_numpy(): + if _import_numpy(required=False): # primitive array -> numpy.ndarray converters.append( Converter( @@ -575,7 +575,7 @@ def _stock_py_converters() -> typing.List: ) ) - if _import_pandas(): + if _import_pandas(required=False): # org.scijava.table.Table -> pandas.DataFrame converters.append( Converter( @@ -662,15 +662,16 @@ def _jarray_shape(jarr): return shape -def _import_numpy(): +def _import_numpy(required=True): try: import numpy as np return np except ImportError as e: - msg = "The NumPy library is missing (https://numpy.org/). " - msg += "Please install it before using this function." - raise RuntimeError(msg) from e + if required: + msg = "The NumPy library is missing (https://numpy.org/). " + msg += "Please install it before using this function." + raise RuntimeError(msg) from e ###################################### @@ -696,15 +697,16 @@ def _convert_table(obj: Any): pass -def _import_pandas(): +def _import_pandas(required=True): try: import pandas as pd return pd except ImportError as e: - msg = "The Pandas library is missing (http://pandas.pydata.org/). " - msg += "Please install it before using this function." - raise RuntimeError(msg) from e + if required: + msg = "The Pandas library is missing (http://pandas.pydata.org/). " + msg += "Please install it before using this function." + raise RuntimeError(msg) from e def _table_to_pandas(table): From 33258b1d08191870aaa068bc5781a8f83b1257a3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Nov 2022 10:44:40 -0500 Subject: [PATCH 247/505] bin/test.sh: handle arguments better --- bin/test.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/test.sh b/bin/test.sh index 87171e60..82cd2203 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -3,4 +3,9 @@ dir=$(dirname "$0") cd "$dir/.." -python -m pytest tests/ -p no:faulthandler $@ +if [ $# -gt 0 ] +then + python -m pytest -p no:faulthandler $@ +else + python -m pytest -p no:faulthandler tests/ +fi From d8c54adb5431e873ce036bc6afd4bfddcb836beb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 13:38:00 -0500 Subject: [PATCH 248/505] Import List from typing This is more consistent with the other imports from typing. We didn't do it before because previously, List was a property for the jimported java.util.List class. But now all imported Java classes are inside the JavaClasses struct, so we're OK. --- src/scyjava/_convert.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 65e28de0..59d3e853 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -3,9 +3,8 @@ """ import collections -import typing from pathlib import Path -from typing import Any, Callable, NamedTuple +from typing import Any, Callable, List, NamedTuple from jpype import JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort @@ -33,7 +32,7 @@ class Converter(NamedTuple): priority: float = Priority.NORMAL -def _convert(obj: Any, converters: typing.List[Converter]) -> Any: +def _convert(obj: Any, converters: List[Converter]) -> Any: suitable_converters = filter(lambda c: c.predicate(obj), converters) prioritized = max(suitable_converters, key=lambda c: c.priority) return prioritized.converter(obj) @@ -74,7 +73,7 @@ def _convertIterable(obj: collections.abc.Iterable): return jlist -java_converters: typing.List[Converter] = [] +java_converters: List[Converter] = [] def add_java_converter(converter: Converter): @@ -106,7 +105,7 @@ def to_java(obj: Any) -> Any: return _convert(obj, java_converters) -def _stock_java_converters() -> typing.List[Converter]: +def _stock_java_converters() -> List[Converter]: """ Returns all python-to-java converters supported out of the box! This should only be called after the JVM has been started! @@ -377,7 +376,7 @@ def __str__(self): return "{" + ", ".join(_jstr(v) for v in self) + "}" -py_converters: typing.List[Converter] = [] +py_converters: List[Converter] = [] def add_py_converter(converter: Converter): @@ -420,7 +419,7 @@ def to_python(data: Any, gentle: bool = False) -> Any: raise exc -def _stock_py_converters() -> typing.List: +def _stock_py_converters() -> List: """ Returns all java-to-python converters supported out of the box! This should only be called after the JVM has been started! From bc8be7659c7a4dae2989b540fc5d1a061778c582 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 15:25:24 -0500 Subject: [PATCH 249/505] Add a jinstance method for checking Java type --- src/scyjava/__init__.py | 1 + src/scyjava/_convert.py | 4 ++-- src/scyjava/_java.py | 14 ++++++++++++++ tests/test_convert.py | 13 +++++++++++-- tests/test_pandas.py | 14 +++++--------- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 42dacb16..5f86ca86 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -102,6 +102,7 @@ jarray, jclass, jimport, + jinstance, jstacktrace, jvm_started, jvm_version, diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 59d3e853..19600122 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -8,7 +8,7 @@ from jpype import JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from ._java import JavaClasses, isjava, jclass, jimport, start_jvm +from ._java import JavaClasses, isjava, jclass, jimport, jinstance, start_jvm # NB: We cannot use org.scijava.priority.Priority or other Java-side class @@ -681,7 +681,7 @@ def _import_numpy(required=True): def _is_table(obj: Any) -> bool: """Check if obj is a table.""" try: - return isinstance(obj, jimport("org.scijava.table.Table")) + return jinstance(obj, "org.scijava.table.Table") except BaseException: # No worries if scijava-table is not available. pass diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 06663bef..463460f3 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -384,6 +384,20 @@ def jclass(data): raise TypeError("Cannot glean class from data of type: " + str(type(data))) +def jinstance(obj, jtype): + """ + Test if the given object is an instance of a particular Java type. + + :param obj: The object to check. + :param jtype: The Java type, as either a jimported class or as a string. + :returns: True iff the object is an instance of that Java type. + """ + if isinstance(jtype, str): + jtype = jimport(jtype) + + return isinstance(obj, jtype) + + def jstacktrace(exc): """ Extract the Java-side stack trace from a Java exception. diff --git a/tests/test_convert.py b/tests/test_convert.py index 9d2048aa..2086c5c1 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -3,7 +3,16 @@ from jpype import JByte -from scyjava import Converter, config, jarray, jclass, jimport, to_java, to_python +from scyjava import ( + Converter, + config, + jarray, + jclass, + jimport, + jinstance, + to_java, + to_python, +) config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -182,7 +191,7 @@ def testDict(self): def testPath(self): py_path = Path(getcwd()) j_path = to_java(py_path) - assert isinstance(j_path, jimport("java.nio.file.Path")) + assert jinstance(j_path, "java.nio.file.Path") assert str(j_path) == str(py_path) actual = to_python(j_path) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 45a068a4..505eaf26 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -2,7 +2,7 @@ import numpy.testing as npt import pandas as pd -from scyjava import config, jimport, to_java +from scyjava import config, jinstance, to_java config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -28,8 +28,7 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - DefaultFloatTable = jimport("org.scijava.table.DefaultFloatTable") - assert isinstance(table, DefaultFloatTable) + assert jinstance(table, "org.scijava.table.DefaultFloatTable") # Int table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -40,8 +39,7 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - DefaultIntTable = jimport("org.scijava.table.DefaultIntTable") - assert isinstance(table, DefaultIntTable) + assert jinstance(table, "org.scijava.table.DefaultIntTable") # Bool table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -51,8 +49,7 @@ def testPandasToTable(self): table = to_java(df) assert_same_table(table, df) - DefaultBoolTable = jimport("org.scijava.table.DefaultBoolTable") - assert isinstance(table, DefaultBoolTable) + assert jinstance(table, "org.scijava.table.DefaultBoolTable") # Mixed table. columns = ["header1", "header2", "header3", "header4", "header5"] @@ -71,5 +68,4 @@ def testPandasToTable(self): # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) - DefaultGenericTable = jimport("org.scijava.table.DefaultGenericTable") - assert isinstance(table, DefaultGenericTable) + assert jinstance(table, "org.scijava.table.DefaultGenericTable") From 030f91aa86ab01d04e19efca1302195f26684343 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 15:30:39 -0500 Subject: [PATCH 250/505] Clean up conversion tests * Fix float and double converter predicates. * Add tests for floating point infinity and NaN. * Use clearer and more consistent variable names. * Use jinstance function to test converted types. * Assert converted types and values more consistently. * Remove the convoluted stringified asserts. --- src/scyjava/_convert.py | 13 +++- tests/test_convert.py | 131 +++++++++++++++++++++++++--------------- 2 files changed, 92 insertions(+), 52 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 19600122..852fc569 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -3,6 +3,7 @@ """ import collections +import math from pathlib import Path from typing import Any, Callable, List, NamedTuple @@ -162,13 +163,21 @@ def _stock_java_converters() -> List[Converter]: # float -> java.lang.Float Converter( predicate=lambda obj: isinstance(obj, float) - and _jc.Float.MIN_VALUE <= obj <= _jc.Float.MAX_VALUE, + and ( + math.isinf(obj) + or math.isnan(obj) + or -_jc.Float.MAX_VALUE <= obj <= _jc.Float.MAX_VALUE + ), converter=_jc.Float, ), # float -> java.lang.Double Converter( predicate=lambda obj: isinstance(obj, float) - and _jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE, + and ( + math.isinf(obj) + or math.isnan(obj) + or -_jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE + ), converter=_jc.Double, priority=Priority.NORMAL - 1, ), diff --git a/tests/test_convert.py b/tests/test_convert.py index 2086c5c1..3d6e4759 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,3 +1,4 @@ +import math from os import getcwd from pathlib import Path @@ -44,16 +45,19 @@ def testClass(self): assert "java.util.Map" == jclass("java.util.Map").getName() def testBoolean(self): - jt = to_java(True) - assert jt.booleanValue() - pt = to_python(jt) - assert pt - assert "True" == str(pt) - jf = to_java(False) - assert not jf.booleanValue() - pf = to_python(jf) - assert not pf - assert "False" == str(pf) + jtrue = to_java(True) + assert jinstance(jtrue, "java.lang.Boolean") + assert jtrue.booleanValue() is True + ptrue = to_python(jtrue) + assert isinstance(ptrue, bool) + assert ptrue is True + + jfalse = to_java(False) + assert jinstance(jfalse, "java.lang.Boolean") + assert jfalse.booleanValue() is False + pfalse = to_python(jfalse) + assert isinstance(pfalse, bool) + assert pfalse is False def testByte(self): # NB we can't (yet) convert TO Bytes, since there is not (yet) @@ -65,63 +69,90 @@ def testByte(self): assert str(i) == str(pi) def testInteger(self): - i = 5 - ji = to_java(i) - assert i == ji.intValue() - pi = to_python(ji) - assert i == pi - assert str(i) == str(pi) + oint = 5 + jint = to_java(oint) + assert jinstance(jint, "java.lang.Integer") + assert oint == jint.intValue() + pint = to_python(jint) + assert isinstance(pint, int) + assert oint == pint def testLong(self): - long = 4000000001 - jlong = to_java(long) - assert long == jlong.longValue() + olong = 4000000001 + jlong = to_java(olong) + assert jinstance(jlong, "java.lang.Long") + assert olong == jlong.longValue() plong = to_python(jlong) - assert long == plong - assert str(long) == str(plong) + assert isinstance(plong, int) + assert olong == plong def testBigInteger(self): - bi = 9879999999999999789 - jbi = to_java(bi) - assert bi == int(str(jbi.toString())) + obi = 9879999999999999789 + jbi = to_java(obi) + assert jinstance(jbi, "java.math.BigInteger") + assert str(obi) == str(jbi.toString()) pbi = to_python(jbi) - assert bi == pbi - assert str(bi) == str(pbi) + assert isinstance(pbi, int) + assert obi == pbi def testFloat(self): - f = 5.0 - jf = to_java(f) - assert f == jf.floatValue() - pf = to_python(jf) - assert f == pf - assert str(f) == str(pf) + ofloat = 5.0 + jfloat = to_java(ofloat) + assert jinstance(jfloat, "java.lang.Float") + assert ofloat == jfloat.floatValue() + pfloat = to_python(jfloat) + assert isinstance(pfloat, float) + assert ofloat == pfloat def testDouble(self): - Float = jimport("java.lang.Float") - d = Float.MAX_VALUE * 2 - jd = to_java(d) - assert d == jd.doubleValue() - pd = to_python(jd) - assert d == pd - assert str(d) == str(pd) + odouble = 4.56e123 + jdouble = to_java(odouble) + assert jinstance(jdouble, "java.lang.Double") + assert odouble == jdouble.doubleValue() + pdouble = to_python(jdouble) + assert isinstance(pdouble, float) + assert odouble == pdouble + + def testInf(self): + jinf = to_java(math.inf) + assert jinstance(jinf, "java.lang.Float") + assert math.inf == jinf.floatValue() + pinf = to_python(jinf) + assert isinstance(pinf, float) + assert math.inf == pinf + + jninf = to_java(-math.inf) + assert jinstance(jninf, "java.lang.Float") + assert -math.inf == jninf.floatValue() + pninf = to_python(jninf) + assert isinstance(pninf, float) + assert -math.inf == pninf + + def testNaN(self): + jnan = to_java(math.nan) + assert jinstance(jnan, "java.lang.Float") + assert math.isnan(jnan.floatValue()) + pnan = to_python(jnan) + assert isinstance(pnan, float) + assert math.isnan(pnan) def testString(self): - s = "Hello world!" - js = to_java(s) - for e, a in zip(s, js.toCharArray()): + ostring = "Hello world!" + jstring = to_java(ostring) + assert jinstance(jstring, "java.lang.String") + for e, a in zip(ostring, jstring.toCharArray()): assert e == a - ps = to_python(js) - assert s == ps - assert str(s) == str(ps) + pstring = to_python(jstring) + assert ostring == pstring def testList(self): - list = "The quick brown fox jumps over the lazy dogs".split() - jlist = to_java(list) - for e, a in zip(list, jlist): + olist = "The quick brown fox jumps over the lazy dogs".split() + jlist = to_java(olist) + for e, a in zip(olist, jlist): assert e == to_python(a) plist = to_python(jlist) - assert list == plist - assert str(list) == str(plist) + assert olist == plist + assert str(olist) == str(plist) assert plist[1] == "quick" plist[7] = "silly" assert "The quick brown fox jumps over the silly dogs" == " ".join(plist) From 313dedddf1ee2a729c2da5264995a0ecd222483b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 15:59:20 -0500 Subject: [PATCH 251/505] Improve some docstrings --- src/scyjava/_convert.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 852fc569..9e37c163 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -79,7 +79,7 @@ def _convertIterable(obj: collections.abc.Iterable): def add_java_converter(converter: Converter): """ - Adds a converter to the list used by to_java + Add a converter to the list used by to_java. :param converter: A Converter going from python to java """ java_converters.append(converter) @@ -108,10 +108,10 @@ def to_java(obj: Any) -> Any: def _stock_java_converters() -> List[Converter]: """ - Returns all python-to-java converters supported out of the box! - This should only be called after the JVM has been started! + Construct the Python-to-Java converters supported out of the box. :returns: A list of Converters """ + start_jvm() return [ # Other (Exceptional) converter Converter( @@ -390,7 +390,7 @@ def __str__(self): def add_py_converter(converter: Converter): """ - Adds a converter to the list used by to_python + Add a converter to the list used by to_python. :param converter: A Converter from java to python """ py_converters.append(converter) @@ -430,10 +430,10 @@ def to_python(data: Any, gentle: bool = False) -> Any: def _stock_py_converters() -> List: """ - Returns all java-to-python converters supported out of the box! - This should only be called after the JVM has been started! + Construct the Java-to-Python converters supported out of the box. :returns: A list of Converters """ + start_jvm() converters = [ # Other (Exceptional) converter From ecfa252320a97044fb92b202a7a84f8d46e1f7b5 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 25 Oct 2022 10:49:41 -0500 Subject: [PATCH 252/505] Add optional hints to scyjava converter mechanism Hints are freeform key/value pairs that converters may use if desired to affect whether and how particular objects are converted between types. Co-authored-by: Curtis Rueden --- pyproject.toml | 2 +- src/scyjava/_convert.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index abf31eb7..bd1cef7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.7.1.dev0" +version = "1.8.0.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 9e37c163..4e876dff 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -3,6 +3,7 @@ """ import collections +import inspect import math from pathlib import Path from typing import Any, Callable, List, NamedTuple @@ -33,10 +34,20 @@ class Converter(NamedTuple): priority: float = Priority.NORMAL -def _convert(obj: Any, converters: List[Converter]) -> Any: +def _convert(obj: Any, converters: List[Converter], **hints: dict) -> Any: suitable_converters = filter(lambda c: c.predicate(obj), converters) prioritized = max(suitable_converters, key=lambda c: c.priority) - return prioritized.converter(obj) + + # check if selected converter supports hints + uses_hints = not isjava(prioritized.converter) and any( + p.kind == inspect.Parameter.VAR_KEYWORD + for p in inspect.signature(prioritized.converter).parameters.values() + ) + return ( + prioritized.converter(obj, **hints) + if uses_hints + else prioritized.converter(obj) + ) # -- Python to Java -- @@ -85,7 +96,7 @@ def add_java_converter(converter: Converter): java_converters.append(converter) -def to_java(obj: Any) -> Any: +def to_java(obj: Any, **hints: dict) -> Any: """ Recursively convert a Python object to a Java object. @@ -103,7 +114,7 @@ def to_java(obj: Any) -> Any: :raises TypeError: if the argument is not one of the aforementioned types. """ start_jvm() - return _convert(obj, java_converters) + return _convert(obj, java_converters, **hints) def _stock_java_converters() -> List[Converter]: From 8f53a50247f17b6e5479b41ab894386fa1b7295e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 16:25:10 -0500 Subject: [PATCH 253/505] Propagate conversion hints for predicates, too And while we're at it, stabilize the Converter API. Rather than calling the predicate and converter functions directly, we now introduce stable supports and convert methods that always accept hints kwargs, regardless of whether the linked predicate and/or converter functions do. --- src/scyjava/_convert.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4e876dff..a7d07fe0 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -28,26 +28,37 @@ class Priority: LAST = -1e300 +def _has_kwargs(f): + return not isjava(f) and any( + p.kind == inspect.Parameter.VAR_KEYWORD + for p in inspect.signature(f).parameters.values() + ) + + class Converter(NamedTuple): predicate: Callable[[Any], bool] converter: Callable[[Any], Any] priority: float = Priority.NORMAL + def supports(self, obj: Any, **hints: dict) -> bool: + return ( + self.predicate(obj, **hints) + if _has_kwargs(self.predicate) + else self.predicate(obj) + ) + + def convert(self, obj: Any, **hints: dict) -> Any: + return ( + self.converter(obj, **hints) + if _has_kwargs(self.converter) + else self.converter(obj) + ) + def _convert(obj: Any, converters: List[Converter], **hints: dict) -> Any: - suitable_converters = filter(lambda c: c.predicate(obj), converters) + suitable_converters = [c for c in converters if c.supports(obj, **hints)] prioritized = max(suitable_converters, key=lambda c: c.priority) - - # check if selected converter supports hints - uses_hints = not isjava(prioritized.converter) and any( - p.kind == inspect.Parameter.VAR_KEYWORD - for p in inspect.signature(prioritized.converter).parameters.values() - ) - return ( - prioritized.converter(obj, **hints) - if uses_hints - else prioritized.converter(obj) - ) + return prioritized.convert(obj, **hints) # -- Python to Java -- From 058bb5ca67b5d700b3a40b045b1c05c23380e00d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 16:27:10 -0500 Subject: [PATCH 254/505] Add byte and short converters utilizing hints And test them, to improve confidence that conversion hints work. --- src/scyjava/_convert.py | 28 ++++++++++++++++++++++++---- tests/test_convert.py | 25 ++++++++++++++++--------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index a7d07fe0..61aad856 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -163,15 +163,33 @@ def _stock_java_converters() -> List[Converter]: predicate=lambda obj: isinstance(obj, bool), converter=_jc.Boolean, ), + # int -> java.lang.Byte + Converter( + predicate=lambda obj, **hints: isinstance(obj, int) + and ("type" in hints and hints["type"] in ("b", "byte", "Byte")) + and _jc.Byte.MIN_VALUE <= obj <= _jc.Byte.MAX_VALUE, + converter=_jc.Byte, + priority=Priority.HIGH, + ), + # int -> java.lang.Short + Converter( + predicate=lambda obj, **hints: isinstance(obj, int) + and ("type" in hints and hints["type"] in ("s", "short", "Short")) + and _jc.Short.MIN_VALUE <= obj <= _jc.Short.MAX_VALUE, + converter=_jc.Short, + priority=Priority.HIGH, + ), # int -> java.lang.Integer Converter( - predicate=lambda obj: isinstance(obj, int) + predicate=lambda obj, **hints: isinstance(obj, int) + and ("type" not in hints or hints["type"] in ("i", "int", "Integer")) and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, converter=_jc.Integer, ), # int -> java.lang.Long Converter( - predicate=lambda obj: isinstance(obj, int) + predicate=lambda obj, **hints: isinstance(obj, int) + and ("type" not in hints or hints["type"] in ("j", "l", "long", "Long")) and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, converter=_jc.Long, priority=Priority.NORMAL - 1, @@ -184,7 +202,8 @@ def _stock_java_converters() -> List[Converter]: ), # float -> java.lang.Float Converter( - predicate=lambda obj: isinstance(obj, float) + predicate=lambda obj, **hints: isinstance(obj, float) + and ("type" not in hints or hints["type"] in ("f", "float", "Float")) and ( math.isinf(obj) or math.isnan(obj) @@ -194,7 +213,8 @@ def _stock_java_converters() -> List[Converter]: ), # float -> java.lang.Double Converter( - predicate=lambda obj: isinstance(obj, float) + predicate=lambda obj, **hints: isinstance(obj, float) + and ("type" not in hints or hints["type"] in ("d", "double", "Double")) and ( math.isinf(obj) or math.isnan(obj) diff --git a/tests/test_convert.py b/tests/test_convert.py index 3d6e4759..4ac429a2 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -2,8 +2,6 @@ from os import getcwd from pathlib import Path -from jpype import JByte - from scyjava import ( Converter, config, @@ -60,13 +58,22 @@ def testBoolean(self): assert pfalse is False def testByte(self): - # NB we can't (yet) convert TO Bytes, since there is not (yet) - # a great type to convert FROM. We convert python ints to Integers - i = 5 - ji = JByte(i) - pi = to_python(ji) - assert i == pi - assert str(i) == str(pi) + obyte = 5 + jbyte = to_java(obyte, type="b") + assert jinstance(jbyte, "java.lang.Byte") + assert obyte == jbyte.byteValue() + pbyte = to_python(jbyte) + assert isinstance(pbyte, int) + assert obyte == pbyte + + def testShort(self): + oshort = 5 + jshort = to_java(oshort, type="s") + assert jinstance(jshort, "java.lang.Short") + assert oshort == jshort.shortValue() + pshort = to_python(jshort) + assert isinstance(pshort, int) + assert oshort == pshort def testInteger(self): oint = 5 From 17341a37c2681959c6eb8bd930ea35c0a8be0776 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 17 Jun 2022 12:56:57 -0500 Subject: [PATCH 255/505] Add some basic tests: jclass, jimport, jinstance --- tests/test_basics.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_basics.py diff --git a/tests/test_basics.py b/tests/test_basics.py new file mode 100644 index 00000000..6a228011 --- /dev/null +++ b/tests/test_basics.py @@ -0,0 +1,45 @@ +import re + +import scyjava + + +class TestBasics(object): + """ + Tests basic scyjava functions. + """ + + def test_jclass(self): + """ + Tests the jclass function. + """ + c = scyjava.jclass("java.lang.Object") + assert scyjava.jinstance(c, "java.lang.Class") + assert str(c.toString()) == "class java.lang.Object" + + def test_jimport(self): + """ + Tests the jimport function. + """ + Object = scyjava.jimport("java.lang.Object") + assert Object is not None + assert str(Object) + o = Object() + assert scyjava.jinstance(o, "java.lang.Object") + assert re.match("java.lang.Object@[0-9a-f]{8}", str(o.toString())) + + def test_jinstance(self): + """ + Tests the jinstance function. + """ + jstr = scyjava.to_java("Hello") + assert scyjava.jinstance(jstr, "java.lang.String") + + jint = scyjava.to_java(5) + assert scyjava.jinstance(jint, "java.lang.Integer") + + jfloat = scyjava.to_java(3.5) + assert scyjava.jinstance(jfloat, "java.lang.Float") + + jlist = scyjava.to_java([3, 2, 1]) + assert scyjava.jinstance(jlist, "java.util.List") + assert scyjava.jinstance(jlist, "java.util.ArrayList") From e97b89045993ac475a213d3728df4d957dd66884 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 16:10:06 -0500 Subject: [PATCH 256/505] Add is_jarray function The is a step toward JPype encapsulation. --- src/scyjava/__init__.py | 1 + src/scyjava/_convert.py | 16 ++++++++-------- src/scyjava/_java.py | 5 +++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 5f86ca86..cacc5917 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -97,6 +97,7 @@ from scyjava._java import ( # noqa: F401 JavaClasses, is_awt_initialized, + is_jarray, is_jvm_headless, isjava, jarray, diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 61aad856..6f6d3e06 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -8,9 +8,9 @@ from pathlib import Path from typing import Any, Callable, List, NamedTuple -from jpype import JArray, JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort +from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from ._java import JavaClasses, isjava, jclass, jimport, jinstance, start_jvm +from ._java import JavaClasses, is_jarray, isjava, jclass, jimport, jinstance, start_jvm # NB: We cannot use org.scijava.priority.Priority or other Java-side class @@ -608,9 +608,9 @@ def _stock_py_converters() -> List: converter=lambda obj: Path(str(obj)), priority=Priority.NORMAL + 1, ), - # JArray -> list + # jarray -> list Converter( - predicate=lambda obj: isinstance(obj, JArray), + predicate=lambda obj: is_jarray(obj), converter=lambda obj: [to_python(o) for o in obj], priority=Priority.VERY_LOW, ), @@ -693,20 +693,20 @@ def _supports_jarray_to_ndarray(obj): def _jarray_element_type(jarr): - if not isinstance(jarr, JArray): + if not is_jarray(jarr): return None element = jarr - while isinstance(element, JArray): + while is_jarray(element): element = element[0] return type(element) def _jarray_shape(jarr): - if not isinstance(jarr, JArray): + if not is_jarray(jarr): return None shape = [] element = jarr - while isinstance(element, JArray): + while is_jarray(element): shape.append(len(element)) element = element[0] return shape diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 463460f3..60861e7d 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -349,6 +349,11 @@ def isjava(data): return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) +def is_jarray(data): + """Return whether the given data object is a Java array.""" + return isinstance(data, jpype.JArray) + + @lru_cache(maxsize=None) def jimport(class_name): """ From 00465e6d91047d46c35b3e8c58ea614e62def1d1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 16:12:52 -0500 Subject: [PATCH 257/505] Add type hints to some java function return types --- src/scyjava/_java.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 60861e7d..65c16a37 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -273,12 +273,12 @@ def shutdown_jvm(): print(f"Exception during JVM shutdown: {e}") -def jvm_started(): +def jvm_started() -> bool: """Return true iff a Java virtual machine (JVM) has been started.""" return jpype.isJVMStarted() -def is_jvm_headless(): +def is_jvm_headless() -> bool: """ Return true iff Java is running in headless mode. @@ -288,10 +288,10 @@ def is_jvm_headless(): raise RuntimeError("JVM has not started yet!") GraphicsEnvironment = scyjava.jimport("java.awt.GraphicsEnvironment") - return GraphicsEnvironment.isHeadless() + return bool(GraphicsEnvironment.isHeadless()) -def is_awt_initialized(): +def is_awt_initialized() -> bool: """ Return true iff the AWT subsystem has been initialized. @@ -309,7 +309,7 @@ def is_awt_initialized(): return any(t.getName().startsWith("AWT-") for t in threads) -def when_jvm_starts(f): +def when_jvm_starts(f) -> None: """ Registers a function to be called when the JVM starts (or immediately). This is useful to defer construction of Java-dependent data structures @@ -327,7 +327,7 @@ def when_jvm_starts(f): _startup_callbacks.append(f) -def when_jvm_stops(f): +def when_jvm_stops(f) -> None: """ Registers a function to be called just before the JVM shuts down. This is useful to perform cleanup of Java-dependent data structures. @@ -344,12 +344,12 @@ def when_jvm_stops(f): # -- Java functions -- -def isjava(data): +def isjava(data) -> bool: """Return whether the given data object is a Java object.""" return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) -def is_jarray(data): +def is_jarray(data) -> bool: """Return whether the given data object is a Java array.""" return isinstance(data, jpype.JArray) @@ -389,7 +389,7 @@ def jclass(data): raise TypeError("Cannot glean class from data of type: " + str(type(data))) -def jinstance(obj, jtype): +def jinstance(obj, jtype) -> bool: """ Test if the given object is an instance of a particular Java type. @@ -403,7 +403,7 @@ def jinstance(obj, jtype): return isinstance(obj, jtype) -def jstacktrace(exc): +def jstacktrace(exc) -> str: """ Extract the Java-side stack trace from a Java exception. @@ -425,7 +425,7 @@ def jstacktrace(exc): PrintWriter = jimport("java.io.PrintWriter") sw = StringWriter() exc.printStackTrace(PrintWriter(sw, True)) - return sw.toString() + return str(sw) except BaseException: return "" From 09d550b5f31fc607e8b156656cfde092bb1109cf Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Fri, 17 Jun 2022 23:39:18 +0200 Subject: [PATCH 258/505] test_convert: remove bad converter after testing Otherwise, it could stick around and pollute other tests. Co-authored-by: Curtis Rueden --- tests/test_convert.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index 4ac429a2..45174dca 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -4,8 +4,10 @@ from scyjava import ( Converter, + add_java_converter, config, jarray, + java_converters, jclass, jimport, jinstance, @@ -313,18 +315,17 @@ def test_conversion_priority(self): String = jimport("java.lang.String") invader = "Not Hello World" - from scyjava import add_java_converter - - add_java_converter( - Converter( - predicate=lambda obj: isinstance(obj, str), - converter=lambda obj: String(invader.encode("utf-8"), "utf-8"), - priority=100, - ) + bad_converter = Converter( + predicate=lambda obj: isinstance(obj, str), + converter=lambda obj: String(invader.encode("utf-8"), "utf-8"), + priority=100, ) + add_java_converter(bad_converter) # Ensure that the conversion uses our new converter s = "Hello world!" js = to_java(s) for e, a in zip(invader, js.toCharArray()): assert e == a + + java_converters.remove(bad_converter) From 845392e276757044d8d4ccc623f7991fe747aa95 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:25:06 -0500 Subject: [PATCH 259/505] Fix type hinting of hints dictionaries For Python <3.10, I think we need to use typing.Dict, not dict directly. --- src/scyjava/_convert.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 6f6d3e06..4c1f8bdf 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -6,7 +6,7 @@ import inspect import math from pathlib import Path -from typing import Any, Callable, List, NamedTuple +from typing import Any, Callable, Dict, List, NamedTuple from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort @@ -40,14 +40,14 @@ class Converter(NamedTuple): converter: Callable[[Any], Any] priority: float = Priority.NORMAL - def supports(self, obj: Any, **hints: dict) -> bool: + def supports(self, obj: Any, **hints: Dict) -> bool: return ( self.predicate(obj, **hints) if _has_kwargs(self.predicate) else self.predicate(obj) ) - def convert(self, obj: Any, **hints: dict) -> Any: + def convert(self, obj: Any, **hints: Dict) -> Any: return ( self.converter(obj, **hints) if _has_kwargs(self.converter) @@ -55,7 +55,7 @@ def convert(self, obj: Any, **hints: dict) -> Any: ) -def _convert(obj: Any, converters: List[Converter], **hints: dict) -> Any: +def _convert(obj: Any, converters: List[Converter], **hints: Dict) -> Any: suitable_converters = [c for c in converters if c.supports(obj, **hints)] prioritized = max(suitable_converters, key=lambda c: c.priority) return prioritized.convert(obj, **hints) @@ -107,7 +107,7 @@ def add_java_converter(converter: Converter): java_converters.append(converter) -def to_java(obj: Any, **hints: dict) -> Any: +def to_java(obj: Any, **hints: Dict) -> Any: """ Recursively convert a Python object to a Java object. From 7932c01969a5c5971f680bd07cc796406373a32c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:26:12 -0500 Subject: [PATCH 260/505] Add conversion hints for BigInteger and BigDecimal The other numeric types all have them, so why not BigInteger and BigDecimal too? Otherwise, you cannot make a Big number without the input value being outside of small number ranges. --- src/scyjava/_convert.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4c1f8bdf..ca6bc18d 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -196,7 +196,10 @@ def _stock_java_converters() -> List[Converter]: ), # int -> java.math.BigInteger Converter( - predicate=lambda obj: isinstance(obj, int), + predicate=lambda obj, **hints: isinstance(obj, int) + and ( + "type" not in hints or hints["type"] in ("bi", "bigint", "BigInteger") + ), converter=lambda obj: _jc.BigInteger(str(obj)), priority=Priority.NORMAL - 2, ), @@ -225,7 +228,10 @@ def _stock_java_converters() -> List[Converter]: ), # float -> java.math.BigDecimal Converter( - predicate=lambda obj: isinstance(obj, float), + predicate=lambda obj, **hints: isinstance(obj, float) + and ( + "type" not in hints or hints["type"] in ("bd", "bigdec", "BigDecimal") + ), converter=lambda obj: _jc.BigDecimal(str(obj)), priority=Priority.NORMAL - 2, ), From 0db828442e2d1cdd6e11cef2d32fc60265937578 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:27:12 -0500 Subject: [PATCH 261/505] to_java: update docstring to explain hints arg --- src/scyjava/_convert.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index ca6bc18d..c3db643b 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -120,7 +120,38 @@ def to_java(obj: Any, **hints: Dict) -> Any: * set -> LinkedHashSet * list -> ArrayList + There is typically one single destination conversion type and value that + makes sense. For example, Python str always converts to java.lang.String. + But in some cases, there are multiple options that can be controlled by + passing key/value pairs as hints. The base scyjava library includes: + + * int + type='byte' -> Byte + * int + type='short' -> Short + * int + type='int' -> Integer + * int + type='long' -> Long + * int + type='bigint' -> BigInteger + * float + type='float' -> Float + * float + type='double' -> Double + * float + type='bigdec' -> BigDecimal + + But the scyjava conversion framework is extensible and other + packages may introduce converters supporting additional hints. + + In the absence of a hint, scyjava makes a best effort to use a sensible + destination type and value: + + * int values in [-2**31, 2**31-1] convert to Integer + * int values in [-2**63, 2**63-1] but outside int range convert to Long + * int values outside Java long range convert to BigInteger + * conversion of int to Byte or Short must be requested via a hint + * float values in Float range convert to Float + * float inf, -inf, and nan convert to Float + * float values in Double range but outside float range convert to Double + * float values outside double range convert to BigDecimal + :param obj: The Python object to convert. + :param hints: An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. """ From 02971ec1e086996060065db6b18c53f8a89cacc3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:52:43 -0500 Subject: [PATCH 262/505] is_version_at_least: ensure bool return value --- src/scyjava/_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 441e45db..31a0178e 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -59,7 +59,7 @@ def is_version_at_least(actual_version, minimum_version): See org.scijava.VersionUtils.compare(String, String) for further details. """ VersionUtils = jimport("org.scijava.util.VersionUtils") - return VersionUtils.compare(actual_version, minimum_version) >= 0 + return bool(VersionUtils.compare(actual_version, minimum_version) >= 0) def compare_version(version, java_class_version): From 23a713f2081f9a0d5efe8abc7776dda4e5206916 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:37:49 -0500 Subject: [PATCH 263/505] Add type hints in more places --- src/scyjava/_arrays.py | 8 +++++--- src/scyjava/_convert.py | 4 ++-- src/scyjava/_java.py | 12 ++++++------ src/scyjava/_versions.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/scyjava/_arrays.py b/src/scyjava/_arrays.py index 306568b2..be3b9715 100644 --- a/src/scyjava/_arrays.py +++ b/src/scyjava/_arrays.py @@ -2,8 +2,10 @@ Utility functions for working with and reasoning about arrays. """ +from typing import Any -def is_arraylike(arr): + +def is_arraylike(arr: Any) -> bool: """ Return True iff the object is arraylike: possessing .shape, .dtype, .__array__, and .ndim attributes. @@ -19,7 +21,7 @@ def is_arraylike(arr): ) -def is_memoryarraylike(arr): +def is_memoryarraylike(arr: Any) -> bool: """ Return True iff the object is memoryarraylike: an arraylike object whose .data type is memoryview. @@ -34,7 +36,7 @@ def is_memoryarraylike(arr): ) -def is_xarraylike(xarr): +def is_xarraylike(xarr: Any) -> bool: """ Return True iff the object is xarraylike: possessing .values, .dims, and .coords attributes, diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index c3db643b..7370cf54 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -99,7 +99,7 @@ def _convertIterable(obj: collections.abc.Iterable): java_converters: List[Converter] = [] -def add_java_converter(converter: Converter): +def add_java_converter(converter: Converter) -> None: """ Add a converter to the list used by to_java. :param converter: A Converter going from python to java @@ -467,7 +467,7 @@ def __str__(self): py_converters: List[Converter] = [] -def add_py_converter(converter: Converter): +def add_py_converter(converter: Converter) -> None: """ Add a converter to the list used by to_python. :param converter: A Converter from java to python diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 65c16a37..f661a339 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -10,7 +10,7 @@ import sys from functools import lru_cache from pathlib import Path -from typing import Callable +from typing import Callable, Sequence import jpype import jpype.config @@ -76,7 +76,7 @@ def inner(self): # -- JVM functions -- -def jvm_version(): +def jvm_version() -> str: """ Gets the version of the JVM as a tuple, with each dot-separated digit as one element. @@ -147,7 +147,7 @@ def jvm_version(): return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=None): +def start_jvm(options=None) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling @@ -231,7 +231,7 @@ def start_jvm(options=None): callback() -def shutdown_jvm(): +def shutdown_jvm() -> None: """Shutdown the JVM. This function makes a best effort to clean up Java resources first. @@ -355,7 +355,7 @@ def is_jarray(data) -> bool: @lru_cache(maxsize=None) -def jimport(class_name): +def jimport(class_name: str): """ Import a class from Java to Python. @@ -430,7 +430,7 @@ def jstacktrace(exc) -> str: return "" -def jarray(kind, lengths): +def jarray(kind, lengths: Sequence): """ Create a new n-dimensional Java array. diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 31a0178e..2fb19db8 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -10,7 +10,7 @@ _logger = logging.getLogger(__name__) -def get_version(java_class_or_python_package): +def get_version(java_class_or_python_package) -> str: """ Return the version of a Java class or Python package. @@ -48,7 +48,7 @@ def get_version(java_class_or_python_package): raise RuntimeError("Cannot determine version! Is pkg_resources installed?") -def is_version_at_least(actual_version, minimum_version): +def is_version_at_least(actual_version: str, minimum_version: str) -> bool: """ Return a boolean on a version comparison. Requires org.scijava:scijava-common on the classpath. From dc7bce7a87c8ac416190a0cc07a72918e24d86a0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 11:30:16 -0500 Subject: [PATCH 264/505] README: update API documentation for 1.8.0 release Note that this removes the constant decorator method, which does not yet work outside of scyjava, and is not currently part of __all__. At some point in future we might make it work (see scijava/scyjava#40), at which point we would add it to __all__ and put it back in the README. --- README.md | 91 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index bd1ad095..26e30831 100644 --- a/README.md +++ b/README.md @@ -150,21 +150,15 @@ True >>> help(scyjava) ... FUNCTIONS - add_java_converter(converter: scyjava.Converter) - Adds a converter to the list used by to_java + add_java_converter(converter: scyjava._convert.Converter) -> None + Add a converter to the list used by to_java. :param converter: A Converter going from python to java - add_py_converter(converter: scyjava.Converter) - Adds a converter to the list used by to_python + add_py_converter(converter: scyjava._convert.Converter) -> None + Add a converter to the list used by to_python. :param converter: A Converter from java to python - constant(func: Callable[[], Any], cache=True) -> Callable[[], Any] - Turns a function into a property of this module - Functions decorated with this property must have a - leading underscore! - :param func: The function to turn into a property - - get_version(java_class_or_python_package) + 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 @@ -178,14 +172,14 @@ FUNCTIONS See org.scijava.VersionUtils.getVersion(Class) for further details. - is_arraylike(arr) + is_arraylike(arr: Any) -> bool Return True iff the object is arraylike: possessing .shape, .dtype, .__array__, and .ndim attributes. :param arr: The object to check for arraylike properties :return: True iff the object is arraylike - is_awt_initialized() + is_awt_initialized() -> bool Return true iff the AWT subsystem has been initialized. Java starts up its AWT subsystem automatically and implicitly, as @@ -195,19 +189,22 @@ FUNCTIONS those actions via the jpype.setupGuiEnvironment wrapper function; see the Troubleshooting section of the scyjava README for details. - is_jvm_headless() + is_jarray(data) -> bool + Return whether the given data object is a Java array. + + is_jvm_headless() -> bool Return true iff Java is running in headless mode. :raises RuntimeError: If the JVM has not started yet. - is_memoryarraylike(arr) + is_memoryarraylike(arr: Any) -> bool Return True iff the object is memoryarraylike: an arraylike object whose .data type is memoryview. :param arr: The object to check for memoryarraylike properties :return: True iff the object is memoryarraylike - is_version_at_least(actual_version, minimum_version) + is_version_at_least(actual_version: str, minimum_version: str) -> bool Return a boolean on a version comparison. Requires org.scijava:scijava-common on the classpath. @@ -216,7 +213,7 @@ FUNCTIONS See org.scijava.VersionUtils.compare(String, String) for further details. - is_xarraylike(xarr) + is_xarraylike(xarr: Any) -> bool Return True iff the object is xarraylike: possessing .values, .dims, and .coords attributes, and whose .values are arraylike. @@ -224,10 +221,10 @@ FUNCTIONS :param arr: The object to check for xarraylike properties :return: True iff the object is xarraylike - isjava(data) + isjava(data) -> bool Return whether the given data object is a Java object. - jarray(kind, lengths) + jarray(kind, lengths: Sequence) Create a new n-dimensional Java array. :param kind: The type of array to create. This can either be a particular @@ -258,14 +255,21 @@ FUNCTIONS :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. - jimport(class_name) + jimport(class_name: str) Import a class from Java to Python. :param class_name: Name of the class to import. :returns: A pointer to the class, which can be used to e.g. instantiate objects of that class. - jstacktrace(exc) + jinstance(obj, jtype) -> bool + Test if the given object is an instance of a particular Java type. + + :param obj: The object to check. + :param jtype: The Java type, as either a jimported class or as a string. + :returns: True iff the object is an instance of that Java type. + + jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. Example of usage: @@ -281,10 +285,10 @@ FUNCTIONS :returns: A multi-line string containing the stack trace, or empty string if no stack trace could be extracted. - jvm_started() + jvm_started() -> bool Return true iff a Java virtual machine (JVM) has been started. - jvm_version() + jvm_version() -> str Gets the version of the JVM as a tuple, with each dot-separated digit as one element. Characters in the version string beyond only @@ -307,7 +311,7 @@ FUNCTIONS JVM in-process. If the version cannot be deduced, a RuntimeError with the cause is raised. - shutdown_jvm() + shutdown_jvm() -> None Shutdown the JVM. This function makes a best effort to clean up Java resources first. @@ -326,7 +330,7 @@ FUNCTIONS Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - start_jvm(options=None) + start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling this function directly is typically not necessary, because the first @@ -336,7 +340,7 @@ FUNCTIONS :param options: List of options to pass to the JVM. For example: ['-Djava.awt.headless=true', '-Xmx4g'] - to_java(obj: Any) -> Any + to_java(obj: Any, **hints: Dict) -> Any Recursively convert a Python object to a Java object. Supported types include: @@ -348,7 +352,38 @@ FUNCTIONS * set -> LinkedHashSet * list -> ArrayList + There is typically one single destination conversion type and value that + makes sense. For example, Python str always converts to java.lang.String. + But in some cases, there are multiple options that can be controlled by + passing key/value pairs as hints. The base scyjava library includes: + + * int + type='byte' -> Byte + * int + type='short' -> Short + * int + type='int' -> Integer + * int + type='long' -> Long + * int + type='bigint' -> BigInteger + * float + type='float' -> Float + * float + type='double' -> Double + * float + type='bigdec' -> BigDecimal + + But the scyjava conversion framework is extensible and other + packages may introduce converters supporting additional hints. + + In the absence of a hint, scyjava makes a best effort to use a sensible + destination type and value: + + * int values in [-2**31, 2**31-1] convert to Integer + * int values in [-2**63, 2**63-1] but outside int range convert to Long + * int values outside Java long range convert to BigInteger + * conversion of int to Byte or Short must be requested via a hint + * float values in Float range convert to Float + * float inf, -inf, and nan convert to Float + * float values in Double range but outside float range convert to Double + * float values outside double range convert to BigDecimal + :param obj: The Python object to convert. + :param hints: An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. :returns: A corresponding Java object with the same contents. :raises TypeError: if the argument is not one of the aforementioned types. @@ -374,7 +409,7 @@ FUNCTIONS :raises TypeError: if the argument is not one of the aforementioned types, and the gentle flag is not set. - when_jvm_starts(f) + when_jvm_starts(f) -> None Registers a function to be called when the JVM starts (or immediately). This is useful to defer construction of Java-dependent data structures until the JVM is known to be available. If the JVM has already been @@ -382,7 +417,7 @@ FUNCTIONS :param f: Function to invoke when scyjava.start_jvm() is called. - when_jvm_stops(f) + when_jvm_stops(f) -> None Registers a function to be called just before the JVM shuts down. This is useful to perform cleanup of Java-dependent data structures. From ee94ece8ce2da15e5384cd7be027d20110cf5a1b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 12:00:37 -0500 Subject: [PATCH 265/505] Release version 1.8.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd1cef7e..c7a75c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.8.0.dev0" +version = "1.8.0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From d57251edc1c22457eaaf165f1ffc4d15a6b3fd92 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 3 Nov 2022 12:00:53 -0500 Subject: [PATCH 266/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7a75c7c..492d9861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.8.0" +version = "1.8.1.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 74e66bfd923a98746a11b3d3f8510eb6496f4218 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 8 Nov 2022 11:20:27 -0600 Subject: [PATCH 267/505] Set _convert_table priority to HIGH Fixes a bug where org.scijava.table.Table is converted to a lisit instead of a pandas DataFrame. --- src/scyjava/_convert.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 7370cf54..355d5070 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -666,8 +666,7 @@ def _stock_py_converters() -> List: # org.scijava.table.Table -> pandas.DataFrame converters.append( Converter( - predicate=_is_table, - converter=_convert_table, + predicate=_is_table, converter=_convert_table, priority=Priority.HIGH ) ) From b365ed0769b06e82299871c8c597370c43ccda2f Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 9 Nov 2022 11:41:03 -0600 Subject: [PATCH 268/505] Add tests for Table to Pandas conversion This commit adds tests for 74e66bfd923a98746a11b3d3f8510eb6496f4218 which fixes scijava table -> pandas dataframe conversion priority. We did not catch this bug because we were only testing one direction (Pandas to SciJava Table). --- tests/test_pandas.py | 76 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 505eaf26..3fb785e3 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,8 +1,9 @@ import numpy as np import numpy.testing as npt import pandas as pd +from jpype import JBoolean, JFloat, JInt, JString -from scyjava import config, jinstance, to_java +from scyjava import config, jimport, jinstance, to_java, to_python config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -69,3 +70,76 @@ def testPandasToTable(self): # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) assert jinstance(table, "org.scijava.table.DefaultGenericTable") + + def testTabletoPandas(self): + # Float table + table = jimport("org.scijava.table.DefaultFloatTable")() + table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.setRowCount(7) + array = np.random.random(size=(7, 5)) + + table = self._fill_table(table, array, JFloat) + df = to_python(table) + + assert_same_table(table, df) + for col in df.columns: + assert df.dtypes[col] == np.float64 + + # Int table + table = jimport("org.scijava.table.DefaultIntTable")() + table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.setRowCount(7) + array = np.random.random(size=(7, 5)) * 100 + array = array.astype("int32") + + table = self._fill_table(table, array, JInt) + df = to_python(table) + + assert_same_table(table, df) + for col in df.columns: + assert df.dtypes[col] == np.int64 + + # Bool table + table = jimport("org.scijava.table.DefaultBoolTable")() + table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.setRowCount(7) + array = np.random.random(size=(7, 5)) > 0.5 + + table = self._fill_table(table, array, JBoolean) + df = to_python(table) + + assert_same_table(table, df) + for col in df.columns: + assert df.dtypes[col] == np.bool8 + + # Mixed table + table = jimport("org.scijava.table.DefaultGenericTable")() + table.appendColumns(["header1", "header2", "header3", "header4"]) + table.setRowCount(7) + array_float = np.random.random(size=(7, 1)) + array_int = np.random.random(size=(7, 1)) * 100 + array_int = array_int.astype("int32") + array_bool = np.random.random(size=(7, 1)) > 0.5 + array_str = np.array(["foo", "bar", "foobar", "barfoo", "oofrab", "oof", "rab"]) + + # fill mixed table + for i in range(table.getRowCount()): + table.set(0, i, JFloat(array_float[i])) + table.set(1, i, JInt(array_int[i].item())) + table.set(2, i, JBoolean(array_bool[i])) + table.set(3, i, JString(array_str[i])) + + df = to_python(table) + # Table types cannot be the same here, unless we want to cast. + # assert_same_table(table, df) + assert type(df["header1"][0]) == float + assert type(df["header2"][0]) == int + assert type(df["header3"][0]) == bool + assert type(df["header4"][0]) == str + + def _fill_table(self, table, ndarr, type): + for i in range(table.getColumnCount()): + s = ndarr[:, i] + for j in range(table.getRowCount()): + table.setValue(i, j, type(s[j])) + return table From 517d4d98acb1fca0bbf867b9f492fd29e7b8001e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 11 Nov 2022 16:48:52 -0600 Subject: [PATCH 269/505] Rename mainline branch from master to main --- .github/workflows/build.yml | 4 ++-- README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f896b6b2..8a8395b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,12 +3,12 @@ name: build on: push: branches: - - master + - main tags: - "*-[0-9]+.*" pull_request: branches: - - master + - main jobs: build-cross-platform: diff --git a/README.md b/README.md index 26e30831..2d69778b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![build status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/scijava/scyjava/branch/master/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) +[![codecov](https://codecov.io/gh/scijava/scyjava/branch/main/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) Supercharged Java access from Python. diff --git a/pyproject.toml b/pyproject.toml index 492d9861..e7553655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dev = [ [project.urls] homepage = "https://github.com/scijava/scyjava" -documentation = "https://github.com/scijava/scyjava/blob/master/README.md" +documentation = "https://github.com/scijava/scyjava/blob/main/README.md" source = "https://github.com/scijava/scyjava" download = "https://pypi.org/project/scyjava/" tracker = "https://github.com/scijava/scyjava/issues" From 517393f3838efb5dc98f4e275417e55791d9834e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 11 Nov 2022 17:10:25 -0600 Subject: [PATCH 270/505] Fix JavaClasses dostring The decorator is java_import, not java_class. --- src/scyjava/_java.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index f661a339..73dd5042 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -39,9 +39,9 @@ class JavaClasses: from scyjava import JavaClasses class MyJavaClasses(JavaClasses): - @JavaClasses.java_class + @JavaClasses.java_import def String(self): return "java.lang.String" - @JavaClasses.java_class + @JavaClasses.java_import def Integer(self): return "java.lang.Integer" # ... and many more ... From 8e035e190f3be19805e3371ad99af31bc65ac88c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 11 Nov 2022 17:15:48 -0600 Subject: [PATCH 271/505] Release version 1.8.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e7553655..1bae33c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.8.1.dev0" +version = "1.8.1" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From ca3c0bbd9369eb1a34335c423e6f8b6bbc8eabc6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 11 Nov 2022 17:17:31 -0600 Subject: [PATCH 272/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1bae33c8..2251d6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.8.1" +version = "1.8.2.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 1cb6bdf9ae741e77f20b8f1d6aee25d06e0afb60 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 14:25:08 -0500 Subject: [PATCH 273/505] Depend on jep at minimum compatible version This version incorporates ninia/jep#394, which we need for jep to work with conda-based environments on Linux without setting LD_PRELOAD. --- dev-environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-environment.yml b/dev-environment.yml index bb4e4780..5ea47ea5 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -39,5 +39,6 @@ dependencies: # Project from source - pip - pip: + - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - validate-pyproject[all] - -e . From a22ffddad5fe59ae88408e7f8edc7eca6c682996 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 14:31:48 -0500 Subject: [PATCH 274/505] Add a shell script for running tests via jep --- bin/jep-test.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 bin/jep-test.sh diff --git a/bin/jep-test.sh b/bin/jep-test.sh new file mode 100755 index 00000000..883013f7 --- /dev/null +++ b/bin/jep-test.sh @@ -0,0 +1,27 @@ + #!/bin/sh + +# Executes the pytest framework through JEP via jgo, so that +# the surrounding JVM includes scijava-table on the classpath. +# +# Arguments to this shell script are translated into an argument +# list to the pytest.main function. A weak attempt at handling +# special characters, e.g. single quotation marks and backslashes, +# is made, but there are surely other non-working cases. +# +# Usage examples: +# bin/jep-test.sh +# bin/jep-test.sh tests/test_basics.py +# bin/jep-test.sh tests/test_convert.py::TestConvert::test2DStringArray + +if [ $# -gt 0 ] +then + a=$(echo "$@" | sed 's/\\/\\\\/g') # escape backslashes + a=$(echo "$a" | sed 's/'\''/\\'\''/g') # escape single quotes + a=$(echo "$a" | sed 's/ /'\'','\''/g') # replace space with ',' + argString="['$a']" +else + argString="" +fi +echo "import pytest; import sys; pytest.main($argString)" > jep_test.py +jgo -Djava.library.path="$CONDA_PREFIX/lib/python3.10/site-packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py +rm jep_test.py From dc8083d3a2b4f9e2554bbe4347cedaa5656760a8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 16:35:40 -0500 Subject: [PATCH 275/505] Detect when scyjava is started via Jep This does not make everything work, but it adds the initial state for tracking which mode scyjava should operate within. Co-authored-by: Amandine Tournay (Kitwaii) --- pyproject.toml | 2 +- src/scyjava/config.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2251d6dc..bcd8f9b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.8.2.dev0" +version = "1.9.0.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] diff --git a/src/scyjava/config.py b/src/scyjava/config.py index a6ef2754..38506f27 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -1,3 +1,4 @@ +import enum import logging import os import pathlib @@ -17,6 +18,19 @@ _shortcuts = {} +class Mode(enum.Enum): + JEP = "jep" + JPYPE = "jpype" + + +try: + import jep # noqa: F401 + + mode = Mode.JEP +except ImportError: + mode = Mode.JPYPE + + def add_endpoints(*new_endpoints): """ DEPRECATED since v1.2.1 From e758590c7a78211866c0b1de3829381b145fd16b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 7 Jun 2022 16:50:56 -0500 Subject: [PATCH 276/505] Implement jinstance method for Jep And use it in more places. It's too bad that this is needed, but using isinstance directly on Jep-wrapped Java objects does not quite work. Co-authored-by: Amandine Tournay (Kitwaii) --- src/scyjava/_convert.py | 38 +++++++++++++++++++------------------- src/scyjava/_java.py | 5 +++++ tests/test_convert.py | 6 +++--- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 355d5070..80969c93 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -311,7 +311,7 @@ class JavaObject: def __init__(self, jobj, intended_class=None): if intended_class is None: intended_class = _jc.Object - if not isinstance(jobj, intended_class): + if not jinstance(jobj, intended_class): raise TypeError( f"Not a {intended_class.getName()}: {jclass(jobj).getName()}" ) @@ -553,95 +553,95 @@ def _stock_py_converters() -> List: ), # java.lang.Boolean -> bool Converter( - predicate=lambda obj: isinstance(obj, _jc.Boolean), + predicate=lambda obj: jinstance(obj, _jc.Boolean), converter=lambda obj: obj.booleanValue(), ), # java.lang.Byte -> int Converter( - predicate=lambda obj: isinstance(obj, _jc.Byte), + predicate=lambda obj: jinstance(obj, _jc.Byte), converter=lambda obj: int(obj.byteValue()), ), # java.lang.Character -> str Converter( - predicate=lambda obj: isinstance(obj, _jc.Character), + predicate=lambda obj: jinstance(obj, _jc.Character), converter=lambda obj: str, ), # java.lang.Double -> float Converter( - predicate=lambda obj: isinstance(obj, _jc.Double), + predicate=lambda obj: jinstance(obj, _jc.Double), converter=lambda obj: float(obj.doubleValue()), ), # java.lang.Float -> float Converter( - predicate=lambda obj: isinstance(obj, _jc.Float), + predicate=lambda obj: jinstance(obj, _jc.Float), converter=lambda obj: float(obj.floatValue()), ), # java.lang.Integer -> int Converter( - predicate=lambda obj: isinstance(obj, _jc.Integer), + predicate=lambda obj: jinstance(obj, _jc.Integer), converter=lambda obj: int(obj.intValue()), ), # java.lang.Long -> int Converter( - predicate=lambda obj: isinstance(obj, _jc.Long), + predicate=lambda obj: jinstance(obj, _jc.Long), converter=lambda obj: int(obj.longValue()), ), # java.lang.Short -> int Converter( - predicate=lambda obj: isinstance(obj, _jc.Short), + predicate=lambda obj: jinstance(obj, _jc.Short), converter=lambda obj: int(obj.shortValue()), ), # java.lang.String -> str Converter( - predicate=lambda obj: isinstance(obj, _jc.String), + predicate=lambda obj: jinstance(obj, _jc.String), converter=lambda obj: str(obj), ), # java.math.BigInteger -> int Converter( - predicate=lambda obj: isinstance(obj, _jc.BigInteger), + predicate=lambda obj: jinstance(obj, _jc.BigInteger), converter=lambda obj: int(str(obj)), ), # java.math.BigDecimal -> float Converter( - predicate=lambda obj: isinstance(obj, _jc.BigDecimal), + predicate=lambda obj: jinstance(obj, _jc.BigDecimal), converter=lambda obj: float(str(obj)), ), # java.util.List -> scyjava.JavaList (list-like) Converter( - predicate=lambda obj: isinstance(obj, _jc.List), + predicate=lambda obj: jinstance(obj, _jc.List), converter=JavaList, ), # java.util.Map -> scyjava.JavaMap (dict-like) Converter( - predicate=lambda obj: isinstance(obj, _jc.Map), + predicate=lambda obj: jinstance(obj, _jc.Map), converter=JavaMap, ), # java.util.Set -> scyjava.JavaSet (set-like) Converter( - predicate=lambda obj: isinstance(obj, _jc.Set), + predicate=lambda obj: jinstance(obj, _jc.Set), converter=JavaSet, ), # java.util.Collection -> scyjava.JavaCollection (collections.abc.Collection) Converter( - predicate=lambda obj: isinstance(obj, _jc.Collection), + predicate=lambda obj: jinstance(obj, _jc.Collection), converter=JavaCollection, priority=Priority.NORMAL - 1, ), # java.lang.Iterable -> scyjava.JavaIterable (collections.abc.Iterable) Converter( - predicate=lambda obj: isinstance(obj, _jc.Iterable), + predicate=lambda obj: jinstance(obj, _jc.Iterable), converter=JavaIterable, priority=Priority.NORMAL - 1, ), # java.util.Iterator -> scyjava.JavaIterator (collections.abc.Iterator) Converter( - predicate=lambda obj: isinstance(obj, _jc.Iterator), + predicate=lambda obj: jinstance(obj, _jc.Iterator), converter=JavaIterator, priority=Priority.NORMAL - 1, ), # java.nio.file.Path -> pathlib.Path Converter( - predicate=lambda obj: isinstance(obj, _jc.Path), + predicate=lambda obj: jinstance(obj, _jc.Path), converter=lambda obj: Path(str(obj)), priority=Priority.NORMAL + 1, ), diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 73dd5042..12fa0fc6 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -17,6 +17,7 @@ from jgo import jgo import scyjava.config +from scyjava.config import Mode, mode _logger = logging.getLogger(__name__) @@ -400,6 +401,10 @@ def jinstance(obj, jtype) -> bool: if isinstance(jtype, str): jtype = jimport(jtype) + if mode == Mode.JEP: + return isinstance(obj, jtype.__pytype__) + + assert mode == Mode.JPYPE return isinstance(obj, jtype) diff --git a/tests/test_convert.py b/tests/test_convert.py index 45174dca..08a09942 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -277,7 +277,7 @@ def testGentle(self): Object = jimport("java.lang.Object") unknown_thing = Object() converted_thing = to_python(unknown_thing, gentle=True) - assert isinstance(converted_thing, Object) + assert jinstance(converted_thing, Object) bad_conversion = None try: bad_conversion = to_python(unknown_thing) @@ -302,12 +302,12 @@ def testStructureWithSomeUnsupportedItems(self): # Convert it back to Python. pdict = to_python(jmap) assert pdict["list"][0] == "a" - assert isinstance(pdict["list"][1], Object) + assert jinstance(pdict["list"][1], Object) assert pdict["list"][2] == 1 assert "x" in pdict["set"] assert 2 in pdict["set"] assert len(pdict["set"]) == 3 - assert isinstance(pdict["object"], Object) + assert jinstance(pdict["object"], Object) assert pdict["foo"] == "bar" def test_conversion_priority(self): From ccf1b8188daa566ab0c916a5665adcf727950d0d Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Tue, 7 Jun 2022 16:39:18 -0500 Subject: [PATCH 277/505] Make JVM functions cognizant of Jep mode Including: * jvm_version * start_jvm * shutdown_jvm * jvm_started * isjava * jimport * jarray Co-authored-by: Curtis Rueden --- src/scyjava/_java.py | 91 ++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 12fa0fc6..206e9a6d 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -9,6 +9,7 @@ import subprocess import sys from functools import lru_cache +from importlib import import_module from pathlib import Path from typing import Callable, Sequence @@ -96,11 +97,18 @@ def jvm_version() -> str: .getProperty('java.version') .split('.') - In case the JVM is not started yet,a best effort is made to deduce + In case the JVM is not started yet, a best effort is made to deduce the version from the environment without actually starting up the JVM in-process. If the version cannot be deduced, a RuntimeError with the cause is raised. """ + if mode == Mode.JEP: + System = jimport("java.lang.System") + version = str(System.getProperty("java.version")) + return tuple(map(int, version.split("."))) + + assert mode == Mode.JPYPE + jvm_version = jpype.getJVMVersion() if jvm_version and jvm_version[0]: # JPype already knew the version. @@ -109,8 +117,7 @@ def jvm_version() -> str: return jvm_version # JPype was clueless, which means the JVM has probably not started yet. - # Let's look for a java executable, and ask it directly with 'java - # -version'. + # Let's look for a java executable, and ask via 'java -version'. default_jvm_path = jpype.getDefaultJVMPath() if not default_jvm_path: @@ -138,12 +145,12 @@ def jvm_version() -> str: if java is None: raise RuntimeError(f"No java executable found inside: {p}") - version = subprocess.check_output( + output = subprocess.check_output( [str(java), "-version"], stderr=subprocess.STDOUT ).decode() - m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', version) + m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output) if not m: - raise RuntimeError(f"Inscrutable java command output:\n{version}") + raise RuntimeError(f"Inscrutable java command output:\n{output}") return tuple(map(int, m.group(1).split("."))) @@ -164,6 +171,8 @@ def start_jvm(options=None) -> None: _logger.debug("The JVM is already running.") return + assert mode == Mode.JPYPE + # retrieve endpoint and repositories from scyjava config endpoints = scyjava.config.endpoints repositories = scyjava.config.get_repositories() @@ -250,10 +259,17 @@ def shutdown_jvm() -> None: Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. + + :raises RuntimeError: if this method is called while in Jep mode. """ if not jvm_started(): return + if mode == Mode.JEP: + raise RuntimeError("Cannot shut down the JVM in Jep mode.") + + assert mode == Mode.JPYPE + # invoke registered shutdown callback functions for callback in _shutdown_callbacks: try: @@ -276,6 +292,11 @@ def shutdown_jvm() -> None: def jvm_started() -> bool: """Return true iff a Java virtual machine (JVM) has been started.""" + if mode == Mode.JEP: + return True + + assert mode == Mode.JPYPE + return jpype.isJVMStarted() @@ -347,6 +368,10 @@ def when_jvm_stops(f) -> None: def isjava(data) -> bool: """Return whether the given data object is a Java object.""" + if mode == Mode.JEP: + return jinstance(data, "java.lang.Object") + + assert mode == Mode.JPYPE return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) @@ -364,6 +389,12 @@ def jimport(class_name: str): :returns: A pointer to the class, which can be used to e.g. instantiate objects of that class. """ + if mode == Mode.JEP: + module_path = class_name.rsplit(".", 1) + module = import_module(module_path[0], module_path[1]) + return getattr(module, module_path[1]) + + assert mode == Mode.JPYPE start_jvm() return jpype.JClass(class_name) @@ -461,25 +492,35 @@ def jarray(kind, lengths: Sequence): lengths = [lengths] arraytype = kind - start_jvm() - - # build up the array type - kinds = { - "b": jpype.JByte, - "c": jpype.JChar, - "d": jpype.JDouble, - "f": jpype.JFloat, - "i": jpype.JInt, - "j": jpype.JLong, - "s": jpype.JShort, - "z": jpype.JBoolean, - } - if arraytype in kinds: - arraytype = kinds[arraytype] - for _ in range(len(lengths)): - arraytype = jpype.JArray(arraytype) - # instantiate the n-dimensional array - arr = arraytype(lengths[0]) + if mode == Mode.JEP: + import jep # noqa: F401 + + # build up the array type + for _ in range(len(lengths) - 1): + arraytype = jep.jarray(0, arraytype) + # instantiate the n-dimensional array + arr = jep.jarray(lengths[0], arraytype) + + elif mode == Mode.JPYPE: + start_jvm() + + # build up the array type + kinds = { + "b": jpype.JByte, + "c": jpype.JChar, + "d": jpype.JDouble, + "f": jpype.JFloat, + "i": jpype.JInt, + "j": jpype.JLong, + "s": jpype.JShort, + "z": jpype.JBoolean, + } + if arraytype in kinds: + arraytype = kinds[arraytype] + for _ in range(len(lengths)): + arraytype = jpype.JArray(arraytype) + # instantiate the n-dimensional array + arr = arraytype(lengths[0]) if len(lengths) > 1: for i in range(len(arr)): From 9ae583a0a4770638bf3fc37ec2bffe79ffa9350e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 17 Jun 2022 13:31:09 -0500 Subject: [PATCH 278/505] Improve jclass function case logic Problems still to solve for the Jep class case. --- src/scyjava/_java.py | 42 +++++++++++++++++++++++++++++++++--------- tests/test_convert.py | 6 ++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 206e9a6d..eefc174b 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -403,21 +403,45 @@ def jclass(data): """ Obtain a Java class object. - :param data: The object from which to glean the class. Supported types include: - A. Name of a class to look up, analogous to - Class.forName("java.lang.String"); - B. A jpype.JClass object analogous to String.class; - C. A jpype.JObject instance analogous to o.getClass(). + + A. Name of a class to look up -- e.g. "java.lang.String" -- + which returns the equivalent of Class.forName("java.lang.String"). + + B. A static-style class reference -- e.g. String -- + which returns the equivalent of String.class. + + C. A Java object -- e.g. foo -- + which returns the equivalent of foo.getClass(). + + Note that if you pass a java.lang.Class object, you will get back Class.class, + i.e. the Java class for the Class class. :-) + + :param data: The object from which to glean the class. :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. """ - if isinstance(data, jpype.JClass): - return data.class_ - if isinstance(data, jpype.JObject): - return data.getClass() if isinstance(data, str): + # Name of a class -- case (A) above. return jclass(jimport(data)) + + if mode == Mode.JPYPE: + start_jvm() + if isinstance(data, jpype.JClass): + # JPype object representing a static-style class -- case (B) above. + return data.class_ + elif mode == Mode.JEP: + if str(type(data)) == "": + # Jep object representing a static-style class -- case (B) above. + raise ValueError( + "Jep does not support Java class objects " + + "-- see https://github.com/ninia/jep/issues/405" + ) + + # A Java object -- case (C) above. + if jinstance(data, "java.lang.Object"): + return data.getClass() + raise TypeError("Cannot glean class from data of type: " + str(type(data))) diff --git a/tests/test_convert.py b/tests/test_convert.py index 08a09942..cdd6d9d4 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -2,6 +2,8 @@ from os import getcwd from pathlib import Path +import pytest + from scyjava import ( Converter, add_java_converter, @@ -14,6 +16,7 @@ to_java, to_python, ) +from scyjava.config import Mode, mode config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -24,6 +27,9 @@ def testClass(self): """ Tests class detection from Java objects. """ + if mode == Mode.JEP: + pytest.skip("The jclass function does not work yet in Jep mode.") + int_class = jclass(to_java(5)) assert "java.lang.Integer" == int_class.getName() From c5b8d7c702018f05dab36e4d134911b6cc28809e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Oct 2022 16:10:06 -0500 Subject: [PATCH 279/505] Implement is_jarray for Jep --- src/scyjava/_java.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index eefc174b..e40765ca 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -377,6 +377,10 @@ def isjava(data) -> bool: def is_jarray(data) -> bool: """Return whether the given data object is a Java array.""" + if mode == Mode.JEP: + return str(type(data)) == "" + + assert mode == Mode.JPYPE return isinstance(data, jpype.JArray) From 026fabb06325451183916d48cb68052c327abbf2 Mon Sep 17 00:00:00 2001 From: "Amandine Tournay (Kitwaii)" Date: Tue, 7 Jun 2022 17:00:51 -0500 Subject: [PATCH 280/505] Split out some converters as JPype-specific Co-authored-by: Curtis Rueden --- src/scyjava/_convert.py | 84 ++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 80969c93..18844489 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -10,7 +10,17 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from ._java import JavaClasses, is_jarray, isjava, jclass, jimport, jinstance, start_jvm +from ._java import ( + JavaClasses, + Mode, + is_jarray, + isjava, + jclass, + jimport, + jinstance, + mode, + start_jvm, +) # NB: We cannot use org.scijava.priority.Priority or other Java-side class @@ -527,30 +537,6 @@ def _stock_py_converters() -> List: converter=lambda obj: obj, priority=Priority.EXTREMELY_HIGH, ), - # JBoolean -> bool - Converter( - predicate=lambda obj: isinstance(obj, JBoolean), - converter=bool, - priority=Priority.NORMAL + 1, - ), - # JByte/JInt/JLong/JShort -> int - Converter( - predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), - converter=int, - priority=Priority.NORMAL + 1, - ), - # JDouble/JFloat -> float - Converter( - predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), - converter=float, - priority=Priority.NORMAL + 1, - ), - # JChar -> str - Converter( - predicate=lambda obj: isinstance(obj, JChar), - converter=str, - priority=Priority.NORMAL + 1, - ), # java.lang.Boolean -> bool Converter( predicate=lambda obj: jinstance(obj, _jc.Boolean), @@ -652,16 +638,6 @@ def _stock_py_converters() -> List: priority=Priority.VERY_LOW, ), ] - - if _import_numpy(required=False): - # primitive array -> numpy.ndarray - converters.append( - Converter( - predicate=_supports_jarray_to_ndarray, - converter=_jarray_to_ndarray, - ) - ) - if _import_pandas(required=False): # org.scijava.table.Table -> pandas.DataFrame converters.append( @@ -670,6 +646,44 @@ def _stock_py_converters() -> List: ) ) + if mode == Mode.JPYPE: + converters.extend( + [ + # JBoolean -> bool + Converter( + predicate=lambda obj: isinstance(obj, JBoolean), + converter=bool, + priority=Priority.NORMAL + 1, + ), + # JByte/JInt/JLong/JShort -> int + Converter( + predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), + converter=int, + priority=Priority.NORMAL + 1, + ), + # JDouble/JFloat -> float + Converter( + predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), + converter=float, + priority=Priority.NORMAL + 1, + ), + # JChar -> str + Converter( + predicate=lambda obj: isinstance(obj, JChar), + converter=str, + priority=Priority.NORMAL + 1, + ), + ] + ) + if _import_numpy(required=False): + # primitive array -> numpy.ndarray + converters.append( + Converter( + predicate=_supports_jarray_to_ndarray, + converter=_jarray_to_ndarray, + ) + ) + return converters From eeecff0613724304468b3230f9efc751e82d6ace Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 1 Nov 2022 13:27:13 -0500 Subject: [PATCH 281/505] test_arrays: remove JPype-specific code And start adding needed case logic for Jep. But these tests don't pass yet using Jep. --- tests/test_arrays.py | 78 ++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 52131371..88cfb931 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -1,59 +1,51 @@ import numpy as np -from jpype import JArray, JDouble, JInt -from scyjava import jarray, to_python +from scyjava import is_jarray, jarray, to_python +from scyjava.config import Mode, mode class TestArrays(object): def test_non_primitive_jarray(self): pass - def test_jarray_to_ndarray_1d(self): + def test_jarray1d_to_python(self): nums = [11, 6, 2, 15, 5] jints = jarray("i", len(nums)) for i in range(len(nums)): jints[i] = nums[i] - assert isinstance(jints, JArray(JInt)) + assert is_jarray(jints) assert len(nums) == len(jints) for i in range(len(nums)): assert nums[i] == jints[i] - pints = to_python(jints) - assert isinstance(pints, np.ndarray) - assert np.int32 == pints.dtype - assert (5,) == pints.shape - for i in range(len(nums)): - assert nums[i] == pints[i] + def assert_array_conversion_works(jarr, expected): + pobj = to_python(jarr) - def test_jarray_to_ndarray_1d_updates(self): - nums_init = [11, 6, 2, 15, 5] - nums_delta = [4, 100, 36, 133, 3] - jints = jarray("i", len(nums_init)) - for i in range(len(nums_init)): - jints[i] = nums_init[i] + if mode == Mode.JEP: + assert isinstance(pobj, list) + assert all(isinstance(v, int) for v in pobj) + assert len(expected) == len(pobj) - # assert narr initial state - pints = to_python(jints) - assert isinstance(pints, np.ndarray) - assert np.int32 == pints.dtype - assert (5,) == pints.shape - for i in range(len(nums_init)): - assert nums_init[i] == pints[i] + elif mode == Mode.JPYPE: + assert isinstance(pobj, np.ndarray) + assert np.int32 == pobj.dtype + assert (len(expected),) == pobj.shape - # change jint data state - for i in range(len(nums_delta)): - jints[i] = nums_delta[i] + for i in range(len(expected)): + assert expected[i] == pobj[i] - # assert narr delta state - pints = to_python(jints) - assert isinstance(pints, np.ndarray) - assert np.int32 == pints.dtype - assert (5,) == pints.shape - for i in range(len(nums_delta)): - assert nums_delta[i] == pints[i] + assert_array_conversion_works(jints, nums) + + # mutate Java array element values + deltas = [4, 100, 36, 133, 3] + for i in range(len(deltas)): + jints[i] = deltas[i] - def test_jarray_to_ndarray_2d(self): + # convert to Python again and make sure it matches + assert_array_conversion_works(jints, deltas) + + def test_jarray2d_to_python(self): nums = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], @@ -66,19 +58,27 @@ def test_jarray_to_ndarray_2d(self): for j in range(len(nums[i])): jdoubles[i][j] = nums[i][j] - assert isinstance(jdoubles, JArray(JArray(JDouble))) + assert is_jarray(jdoubles) assert 5 == len(jdoubles) assert 3 == len(jdoubles[0]) pdoubles = to_python(jdoubles) - assert isinstance(pdoubles, np.ndarray) - assert np.float64 == pdoubles.dtype - assert (5, 3) == pdoubles.shape + + if mode == Mode.JEP: + assert isinstance(pdoubles, list) + assert all(isinstance(v, list) for v in pdoubles) + assert len(nums) == len(pdoubles) + + elif mode == Mode.JPYPE: + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape + for i in range(len(nums)): for j in range(len(nums[i])): assert nums[i][j] == pdoubles[i][j] - def test_jarray_to_ndarray_2d_updates(self): + def test_jarray2d_to_python_updates(self): nums_init = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], From f43ac26921a802f813581ccc53f30cee5a7303fe Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 4 Nov 2022 13:30:11 -0500 Subject: [PATCH 282/505] Error for 2+ dimensional jep arrays They don't work. --- src/scyjava/_java.py | 6 +++--- tests/test_arrays.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index e40765ca..03079690 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -523,9 +523,9 @@ def jarray(kind, lengths: Sequence): if mode == Mode.JEP: import jep # noqa: F401 - # build up the array type - for _ in range(len(lengths) - 1): - arraytype = jep.jarray(0, arraytype) + # TODO: Support n-d arrays + if len(lengths) > 1: + raise RuntimeError("jep cannot support 2+ dimensional arrays!") # instantiate the n-dimensional array arr = jep.jarray(lengths[0], arraytype) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 88cfb931..216b140f 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from scyjava import is_jarray, jarray, to_python from scyjava.config import Mode, mode @@ -46,6 +47,9 @@ def assert_array_conversion_works(jarr, expected): assert_array_conversion_works(jints, deltas) def test_jarray2d_to_python(self): + if mode is Mode.JEP: + pytest.skip("Jep doesn't support 2-d arrays") + nums = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], @@ -65,9 +69,7 @@ def test_jarray2d_to_python(self): pdoubles = to_python(jdoubles) if mode == Mode.JEP: - assert isinstance(pdoubles, list) - assert all(isinstance(v, list) for v in pdoubles) - assert len(nums) == len(pdoubles) + raise RuntimeError("Not supported") elif mode == Mode.JPYPE: assert isinstance(pdoubles, np.ndarray) @@ -79,6 +81,9 @@ def test_jarray2d_to_python(self): assert nums[i][j] == pdoubles[i][j] def test_jarray2d_to_python_updates(self): + if mode is Mode.JEP: + pytest.skip("Jep doesn't support 2-d arrays") + nums_init = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], From c6d6fe2413b64a6c3b2bd3c74bc32224bad7bbcd Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 4 Nov 2022 14:46:42 -0500 Subject: [PATCH 283/505] Skip jclass function test when running jep Jep doesn't support Java class objects (yet!). --- tests/test_basics.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index 6a228011..b36eb402 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,6 +1,9 @@ import re +import pytest + import scyjava +from scyjava.config import Mode, mode class TestBasics(object): @@ -12,6 +15,8 @@ def test_jclass(self): """ Tests the jclass function. """ + if mode == Mode.JEP: + pytest.skip("Jep does not support Java class objects!") c = scyjava.jclass("java.lang.Object") assert scyjava.jinstance(c, "java.lang.Class") assert str(c.toString()) == "class java.lang.Object" From f7be0d6dc99d548fee0c17559e3731d9548928fb Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 4 Nov 2022 14:47:32 -0500 Subject: [PATCH 284/505] Differentiate jep conversion from jpype conversion Jep doesn't bring in Numpy right away, like JPype does. Thus, this conversion will output a Python list, not a Numpy array. --- tests/test_convert.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index cdd6d9d4..54f87fdb 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -2,6 +2,7 @@ from os import getcwd from pathlib import Path +import numpy as np import pytest from scyjava import ( @@ -187,9 +188,16 @@ def testPrimitiveIntArray(self): for i in range(len(arr)): arr[i] = i # NB: assign Python int into Java int! py_arr = to_python(arr) - assert type(py_arr).__name__ == "ndarray" + if mode == Mode.JEP: + assert type(py_arr).__name__ == "list" + # JPype brings in a Numpy dependency from the start. + # This dependency enables the Numpy converters + # Since they take precedence, we'll actually see a ndarray + # output from the conversion. + elif mode == Mode.JPYPE: + assert type(py_arr).__name__ == "ndarray" # NB: Comparing ndarray vs list results in a list of bools. - assert all(py_arr == [0, 1, 2, 3]) + assert np.array_equal(py_arr, [0, 1, 2, 3]) def test2DStringArray(self): String = jimport("java.lang.String") From ace3ed56e49163c27873720fb3c6a44de9577d9e Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Fri, 4 Nov 2022 15:24:52 -0500 Subject: [PATCH 285/505] Skip 2D array conversion on JEP It can't handle that yet. --- tests/test_convert.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_convert.py b/tests/test_convert.py index 54f87fdb..91830079 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -200,6 +200,9 @@ def testPrimitiveIntArray(self): assert np.array_equal(py_arr, [0, 1, 2, 3]) def test2DStringArray(self): + if mode == Mode.JEP: + pytest.skip("jep cannot support 2+ dimensional arrays!") + String = jimport("java.lang.String") arr = jarray(String, [3, 5]) for i in range(len(arr)): From 87232a8ab51ce9fcf43fd889d0eb1fd16729d0ea Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 7 Nov 2022 15:11:44 -0600 Subject: [PATCH 286/505] The return of ND arrays! They're slower than primitive arrays, but better to have slow arrays than no arrays? --- src/scyjava/_java.py | 29 ++++++++++++++++++++++++----- tests/test_arrays.py | 35 +++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 03079690..8a05a381 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -523,11 +523,30 @@ def jarray(kind, lengths: Sequence): if mode == Mode.JEP: import jep # noqa: F401 - # TODO: Support n-d arrays - if len(lengths) > 1: - raise RuntimeError("jep cannot support 2+ dimensional arrays!") - # instantiate the n-dimensional array - arr = jep.jarray(lengths[0], arraytype) + if len(lengths) == 1: + # Fast case: 1-d array (we can use primitives) + arr = jep.jarray(lengths[0], arraytype) + else: + # Slow case: n-d array (we cannot use primitives) + # See https://github.com/ninia/jep/issues/439 + kinds = { + "b": jimport("java.lang.Byte"), + "c": jimport("java.lang.Character"), + "d": jimport("java.lang.Double"), + "f": jimport("java.lang.Float"), + "i": jimport("java.lang.Integer"), + "j": jimport("java.lang.Long"), + "s": jimport("java.lang.Short"), + "z": jimport("java.lang.Boolean"), + } + if arraytype in kinds: + arraytype = kinds[arraytype] + kind = arraytype + # build up the array type + for _ in range(len(lengths) - 1): + arraytype = jep.jarray(0, arraytype) + # instantiate the n-dimensional array + arr = jep.jarray(lengths[0], arraytype) elif mode == Mode.JPYPE: start_jvm() diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 216b140f..fca796d3 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -1,5 +1,4 @@ import numpy as np -import pytest from scyjava import is_jarray, jarray, to_python from scyjava.config import Mode, mode @@ -47,9 +46,6 @@ def assert_array_conversion_works(jarr, expected): assert_array_conversion_works(jints, deltas) def test_jarray2d_to_python(self): - if mode is Mode.JEP: - pytest.skip("Jep doesn't support 2-d arrays") - nums = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], @@ -69,7 +65,9 @@ def test_jarray2d_to_python(self): pdoubles = to_python(jdoubles) if mode == Mode.JEP: - raise RuntimeError("Not supported") + assert isinstance(pdoubles, list) + assert all(isinstance(v, list) for v in pdoubles) + assert len(nums) == len(pdoubles) elif mode == Mode.JPYPE: assert isinstance(pdoubles, np.ndarray) @@ -81,9 +79,6 @@ def test_jarray2d_to_python(self): assert nums[i][j] == pdoubles[i][j] def test_jarray2d_to_python_updates(self): - if mode is Mode.JEP: - pytest.skip("Jep doesn't support 2-d arrays") - nums_init = [ [1.2, 3.4, 5.6], [7.8, 9.1, 2.3], @@ -105,9 +100,15 @@ def test_jarray2d_to_python_updates(self): # assert narr initial state pdoubles = to_python(jdoubles) - assert isinstance(pdoubles, np.ndarray) - assert np.float64 == pdoubles.dtype - assert (5, 3) == pdoubles.shape + if mode == Mode.JEP: + assert isinstance(pdoubles, list) + assert isinstance(pdoubles[0][0], float) + assert len(pdoubles) == 5 + assert len(pdoubles[0]) == 3 + elif mode == Mode.JPYPE: + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape for i in range(len(nums_init)): for j in range(len(nums_init[i])): assert nums_init[i][j] == pdoubles[i][j] @@ -119,9 +120,15 @@ def test_jarray2d_to_python_updates(self): # assert narr delta state pdoubles = to_python(jdoubles) - assert isinstance(pdoubles, np.ndarray) - assert np.float64 == pdoubles.dtype - assert (5, 3) == pdoubles.shape + if mode == Mode.JEP: + assert isinstance(pdoubles, list) + assert isinstance(pdoubles[0][0], float) + assert len(pdoubles) == 5 + assert len(pdoubles[0]) == 3 + elif mode == Mode.JPYPE: + assert isinstance(pdoubles, np.ndarray) + assert np.float64 == pdoubles.dtype + assert (5, 3) == pdoubles.shape for i in range(len(nums_delta)): for j in range(len(nums_delta[i])): assert nums_delta[i][j] == pdoubles[i][j] From 655eb5729c6acc24384b9b5a9967b2c57c74377d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 7 Nov 2022 15:53:12 -0600 Subject: [PATCH 287/505] Inform flake8 of our Python 3.7 minimum Unfortunately, flake8 must still be configured via setup.cfg See https://github.com/PyCQA/flake8/issues/234 Therefore, it still thinks we support Python 3.6. No longer! --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index e87259ee..110a77e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,3 +4,5 @@ # See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 max-line-length = 88 extend-ignore = E203 +# Unfortunately, flake8 doesn't pick up on our declaration in pyproject.toml +min_python_version = 3.7.0 From 4262ef2fe2a6b222a3830862722bab5fc96caa74 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 7 Nov 2022 15:54:04 -0600 Subject: [PATCH 288/505] Fix python <-> Java path conversion --- src/scyjava/_convert.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 18844489..de28345e 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -15,6 +15,7 @@ Mode, is_jarray, isjava, + jarray, jclass, jimport, jinstance, @@ -279,7 +280,10 @@ def _stock_java_converters() -> List[Converter]: # pathlib.Path -> java.nio.file.Path Converter( predicate=lambda obj: isinstance(obj, Path), - converter=lambda obj: _jc.Paths.get(str(obj)), + # Pass an empty String array in addition to our path + # To make it clear to jep that we want the string-args version + # JPype is smart enough to know that, but it doesn't mind the extra args + converter=lambda obj: _jc.Paths.get(str(obj), jarray(_jc.String, [0])), priority=Priority.NORMAL + 1, ), # pandas.DataFrame -> org.scijava.table.Table From 7971c61d110c832d24c436e171ca688674fad48d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 7 Nov 2022 16:38:45 -0600 Subject: [PATCH 289/505] Fix jclass for jep backend --- src/scyjava/_java.py | 2 +- tests/test_convert.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 8a05a381..a50293b0 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -435,7 +435,7 @@ def jclass(data): # JPype object representing a static-style class -- case (B) above. return data.class_ elif mode == Mode.JEP: - if str(type(data)) == "": + if str(type(data.getClass())) == "": # Jep object representing a static-style class -- case (B) above. raise ValueError( "Jep does not support Java class objects " diff --git a/tests/test_convert.py b/tests/test_convert.py index 91830079..27bd53dc 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -314,7 +314,16 @@ def testStructureWithSomeUnsupportedItems(self): "foo": "bar", } ) - assert "java.util.LinkedHashMap" == jclass(jmap).getName() + + if mode == Mode.JPYPE: + assert "java.util.LinkedHashMap" == jclass(jmap).getName() + elif mode == Mode.JEP: + with pytest.raises(ValueError) as exc: + assert "java.util.LinkedHashMap" == jclass(jmap).getName() + assert ( + "ValueError: Jep does not support Java class objects " + + "-- see https://github.com/ninia/jep/issues/405" + ) == exc.exconly() # Convert it back to Python. pdict = to_python(jmap) From a0cc2ecaa1c23d3ac0ed3996be38d88dc935d779 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Mon, 7 Nov 2022 17:20:15 -0600 Subject: [PATCH 290/505] jvm_version: fix jep implementation --- src/scyjava/_java.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index a50293b0..04f55f2b 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -105,6 +105,8 @@ def jvm_version() -> str: if mode == Mode.JEP: System = jimport("java.lang.System") version = str(System.getProperty("java.version")) + # Get everything up to the hyphen + version = version.split("-")[0] return tuple(map(int, version.split("."))) assert mode == Mode.JPYPE From 1614101f1198d2345924aac6872a411c6724a32c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 7 Nov 2022 22:11:03 -0600 Subject: [PATCH 291/505] Make test_version much simpler It now passes in both JPype and Jep modes. --- tests/test_version.py | 54 ++++--------------------------------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index 8fb1587f..58ed3c18 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,7 +5,6 @@ import toml import scyjava -from scyjava import get_version def _expected_version(): @@ -16,54 +15,9 @@ def _expected_version(): return pyproject["project"]["version"] -def test_version_dunder(): - """ - Ensure that the dunder variable matches _expected_version. - """ +def test_version(): + # First, ensure that the version is correct assert _expected_version() == scyjava.__version__ - -@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") -def test_version_importlib(): - """ - Ensure that, with scyjava.version.version unavailable, - importlib.metadata is used next WITH python 3.8+. - """ - # Remove scyjava.version - sys.modules["scyjava.version"] = None - # Ensure scyjava.__version__ matches importlib.metadata.version() - - assert _expected_version() == get_version("scyjava") - - -@pytest.mark.skipif( - sys.version_info >= (3, 8), reason="importlib used instead for Python 3.8+" -) -def test_version_pkg_resources(): - """ - Ensure that, with scyjava.version.version AND - importlib.metadata unavailable, - pkg_resources is used next. - """ - # Remove importlib.metadata - sys.modules["importlib.metadata"] = None - # Ensure scyjava.__version__ matches - # pkg_resources.get_distribution().version - - assert _expected_version() == get_version("scyjava") - - -def test_version_unavailable(): - """ - Ensure that an exception is raised if none of these strategies works. - """ - # Remove importlib.metadata - sys.modules["importlib.metadata"] = None - # Remove pkg_resources - sys.modules["pkg_resources"] = None - # Ensure scyjava.__version__ raises an exception. - with pytest.raises(RuntimeError) as e_info: - get_version("scyjava") - assert ( - "RuntimeError: Cannot determine version! Is pkg_resources installed?" - ) == e_info.exconly() + # Then, ensure that we get the correct version via get_version + assert _expected_version() == scyjava.get_version("scyjava") From a01ba4753a5bfee54e5fcf0c365a46522d7e14d0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 17 Nov 2022 16:02:27 -0600 Subject: [PATCH 292/505] Fix the pandas tests for jep --- tests/test_pandas.py | 49 +++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 3fb785e3..24598535 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,9 +1,8 @@ import numpy as np import numpy.testing as npt import pandas as pd -from jpype import JBoolean, JFloat, JInt, JString -from scyjava import config, jimport, jinstance, to_java, to_python +from scyjava import config, jarray, jimport, jinstance, to_java, to_python config.endpoints.append("org.scijava:scijava-table") config.add_option("-Djava.awt.headless=true") @@ -21,8 +20,9 @@ def assert_same_table(table, df): class TestPandas(object): def testPandasToTable(self): - # Float table. columns = ["header1", "header2", "header3", "header4", "header5"] + + # Float table. array = np.random.random(size=(7, 5)) df = pd.DataFrame(array, columns=columns) @@ -32,7 +32,6 @@ def testPandasToTable(self): assert jinstance(table, "org.scijava.table.DefaultFloatTable") # Int table. - columns = ["header1", "header2", "header3", "header4", "header5"] array = np.random.random(size=(7, 5)) * 100 array = array.astype("int") @@ -43,7 +42,6 @@ def testPandasToTable(self): assert jinstance(table, "org.scijava.table.DefaultIntTable") # Bool table. - columns = ["header1", "header2", "header3", "header4", "header5"] array = np.random.random(size=(7, 5)) > 0.5 df = pd.DataFrame(array, columns=columns) @@ -53,7 +51,6 @@ def testPandasToTable(self): assert jinstance(table, "org.scijava.table.DefaultBoolTable") # Mixed table. - columns = ["header1", "header2", "header3", "header4", "header5"] array = np.random.random(size=(7, 5)) df = pd.DataFrame(array, columns=columns) @@ -72,13 +69,23 @@ def testPandasToTable(self): assert jinstance(table, "org.scijava.table.DefaultGenericTable") def testTabletoPandas(self): + Boolean = jimport("java.lang.Boolean") + Double = jimport("java.lang.Double") + Float = jimport("java.lang.Float") + Integer = jimport("java.lang.Integer") + String = jimport("java.lang.String") + + columns = jarray(String, [5]) + for i in range(5): + columns[i] = f"header{i + 1}" + # Float table table = jimport("org.scijava.table.DefaultFloatTable")() - table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.appendColumns(columns) table.setRowCount(7) array = np.random.random(size=(7, 5)) - table = self._fill_table(table, array, JFloat) + table = self._fill_table(table, array, lambda v: Float(float(v))) df = to_python(table) assert_same_table(table, df) @@ -87,12 +94,12 @@ def testTabletoPandas(self): # Int table table = jimport("org.scijava.table.DefaultIntTable")() - table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.appendColumns(columns) table.setRowCount(7) array = np.random.random(size=(7, 5)) * 100 array = array.astype("int32") - table = self._fill_table(table, array, JInt) + table = self._fill_table(table, array, lambda v: Integer(int(v))) df = to_python(table) assert_same_table(table, df) @@ -101,11 +108,11 @@ def testTabletoPandas(self): # Bool table table = jimport("org.scijava.table.DefaultBoolTable")() - table.appendColumns(["header1", "header2", "header3", "header4", "header5"]) + table.appendColumns(columns) table.setRowCount(7) array = np.random.random(size=(7, 5)) > 0.5 - table = self._fill_table(table, array, JBoolean) + table = self._fill_table(table, array, lambda v: Boolean(bool(v))) df = to_python(table) assert_same_table(table, df) @@ -114,20 +121,23 @@ def testTabletoPandas(self): # Mixed table table = jimport("org.scijava.table.DefaultGenericTable")() - table.appendColumns(["header1", "header2", "header3", "header4"]) + table.appendColumns(columns) table.setRowCount(7) array_float = np.random.random(size=(7, 1)) array_int = np.random.random(size=(7, 1)) * 100 array_int = array_int.astype("int32") array_bool = np.random.random(size=(7, 1)) > 0.5 array_str = np.array(["foo", "bar", "foobar", "barfoo", "oofrab", "oof", "rab"]) + array_double = np.random.random(size=(7, 1)) + array_double = array_double.astype("float64") # fill mixed table for i in range(table.getRowCount()): - table.set(0, i, JFloat(array_float[i])) - table.set(1, i, JInt(array_int[i].item())) - table.set(2, i, JBoolean(array_bool[i])) - table.set(3, i, JString(array_str[i])) + table.set(0, i, Float(float(array_float[i]))) + table.set(1, i, Integer(int(array_int[i].item()))) + table.set(2, i, Boolean(bool(array_bool[i]))) + table.set(3, i, String(array_str[i])) + table.set(4, i, Double(float(array_double[i]))) df = to_python(table) # Table types cannot be the same here, unless we want to cast. @@ -136,10 +146,11 @@ def testTabletoPandas(self): assert type(df["header2"][0]) == int assert type(df["header3"][0]) == bool assert type(df["header4"][0]) == str + assert type(df["header5"][0]) == float - def _fill_table(self, table, ndarr, type): + def _fill_table(self, table, ndarr, ctor): for i in range(table.getColumnCount()): s = ndarr[:, i] for j in range(table.getRowCount()): - table.setValue(i, j, type(s[j])) + table.setValue(i, j, ctor(s[j])) return table From b221aa43084bc4896c942fe5061bb880636b99a2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 08:57:26 -0600 Subject: [PATCH 293/505] Run both JPype and Jep tests in one place This dispenses with the separate bin/jep-test.sh in favor of letting bin/test.sh run the test suite in both modes, ensuring that day-to-day development will continue to test both modes on a regular basis. --- bin/jep-test.sh | 27 ------------------------- bin/test.sh | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 27 deletions(-) delete mode 100755 bin/jep-test.sh diff --git a/bin/jep-test.sh b/bin/jep-test.sh deleted file mode 100755 index 883013f7..00000000 --- a/bin/jep-test.sh +++ /dev/null @@ -1,27 +0,0 @@ - #!/bin/sh - -# Executes the pytest framework through JEP via jgo, so that -# the surrounding JVM includes scijava-table on the classpath. -# -# Arguments to this shell script are translated into an argument -# list to the pytest.main function. A weak attempt at handling -# special characters, e.g. single quotation marks and backslashes, -# is made, but there are surely other non-working cases. -# -# Usage examples: -# bin/jep-test.sh -# bin/jep-test.sh tests/test_basics.py -# bin/jep-test.sh tests/test_convert.py::TestConvert::test2DStringArray - -if [ $# -gt 0 ] -then - a=$(echo "$@" | sed 's/\\/\\\\/g') # escape backslashes - a=$(echo "$a" | sed 's/'\''/\\'\''/g') # escape single quotes - a=$(echo "$a" | sed 's/ /'\'','\''/g') # replace space with ',' - argString="['$a']" -else - argString="" -fi -echo "import pytest; import sys; pytest.main($argString)" > jep_test.py -jgo -Djava.library.path="$CONDA_PREFIX/lib/python3.10/site-packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py -rm jep_test.py diff --git a/bin/test.sh b/bin/test.sh index 82cd2203..b52ce7ec 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -1,11 +1,63 @@ #!/bin/sh +# Executes the pytest framework in both JPype and Jep modes. +# +# Usage examples: +# bin/test.sh +# bin/test.sh tests/test_basics.py +# bin/test.sh tests/test_convert.py::TestConvert::test2DStringArray + +set -e + dir=$(dirname "$0") cd "$dir/.." +echo +echo "-------------------------------------------" +echo "| Testing JPype mode (Java inside Python) |" +echo "-------------------------------------------" + if [ $# -gt 0 ] then python -m pytest -p no:faulthandler $@ else python -m pytest -p no:faulthandler tests/ fi + +echo +echo "-------------------------------------------" +echo "| Testing Jep mode (Python inside Java) |" +echo "-------------------------------------------" + +# Discern the Jep installation. +site_packages=$(python -c 'import sys; print(next(p for p in sys.path if p.endswith("site-packages")))') +test -d "$site_packages/jep" || { + echo "[ERROR] Failed to detect Jep installation in current environment!" 1>&2 + exit 1 +} + +# We execute the pytest framework through Jep via jgo, so that +# the surrounding JVM includes scijava-table on the classpath. +# +# Arguments to the shell script are translated into an argument +# list to the pytest.main function. A weak attempt at handling +# special characters, e.g. single quotation marks and backslashes, +# is made, but there are surely other non-working cases. + +if [ $# -gt 0 ] +then + a=$(echo "$@" | sed 's/\\/\\\\/g') # escape backslashes + a=$(echo "$a" | sed 's/'\''/\\'\''/g') # escape single quotes + a=$(echo "$a" | sed 's/ /'\'','\''/g') # replace space with ',' + argString="['$a']" +else + argString="" +fi +echo " +import pytest, sys +result = pytest.main($argString) +if result: + sys.exit(result) +" > jep_test.py +jgo -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py +rm -f jep_test.py From 9ae47e2d59abe96a669df428c6281d28beba9c76 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 10:20:52 -0600 Subject: [PATCH 294/505] test_version: remove unused imports --- tests/test_version.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index 58ed3c18..54113873 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,7 +1,5 @@ -import sys from pathlib import Path -import pytest import toml import scyjava From dcaa9743d6e8cabea2d4870657fa657d5bd053a8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 11:07:26 -0600 Subject: [PATCH 295/505] pyproject.toml: add jep as dev dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index bcd8f9b8..34eb780f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dev = [ "build", "flake8", "isort", + "jep", "pytest", "pytest-cov", "numpy", From 2a1f9054ad2cc97e8c969ae22bb16560b1210fe7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 11:17:17 -0600 Subject: [PATCH 296/505] CI: cache the local Maven repository cache So that Maven (via jgo) doesn't have to redownload stuff every time. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a8395b0..591444b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: with: java-version: '8' distribution: 'zulu' + cache: 'maven' - name: Install ScyJava run: | From 75286538e4c7a226bc99cbbf7067ab2d587d388d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 11:38:07 -0600 Subject: [PATCH 297/505] jvm_version: fix weird docstring line wrapping --- src/scyjava/_java.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 04f55f2b..95a9159a 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -80,19 +80,16 @@ def inner(self): def jvm_version() -> str: """ - Gets the version of the JVM as a tuple, - with each dot-separated digit as one element. - Characters in the version string beyond only - numbers and dots are ignored, in line - with the java.version system property. + Gets the version of the JVM as a tuple, with each dot-separated digit + as one element. Characters in the version string beyond only numbers + and dots are ignored, in line with the java.version system property. Examples: * OpenJDK 17.0.1 -> [17, 0, 1] * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] * OpenJDK 1.8.0_312 -> [1, 8, 0] - If the JVM is already started, - this function should return the equivalent of: + If the JVM is already started, this function returns the equivalent of: jimport('java.lang.System') .getProperty('java.version') .split('.') From 4409ef34336e95e6db8574cf0100b79f7352f13d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 11:39:41 -0600 Subject: [PATCH 298/505] jvm_version: handle CalledProcessError The function is supposed to raise RuntimeError, not CalledProcessError, if the version cannot be determined. See also imagej/pyimagej#237. --- src/scyjava/_java.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 95a9159a..c808747a 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -144,9 +144,13 @@ def jvm_version() -> str: if java is None: raise RuntimeError(f"No java executable found inside: {p}") - output = subprocess.check_output( - [str(java), "-version"], stderr=subprocess.STDOUT - ).decode() + try: + output = subprocess.check_output( + [str(java), "-version"], stderr=subprocess.STDOUT + ).decode() + except subprocess.CalledProcessError as e: + raise RuntimeError("System call to java failed") from e + m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output) if not m: raise RuntimeError(f"Inscrutable java command output:\n{output}") From 59b04ceae86e8353fe6c5c3618c65493f682b52f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 12:47:56 -0600 Subject: [PATCH 299/505] Make jep testing more verbose --- bin/test.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/test.sh b/bin/test.sh index b52ce7ec..61fd506a 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -49,15 +49,18 @@ then a=$(echo "$@" | sed 's/\\/\\\\/g') # escape backslashes a=$(echo "$a" | sed 's/'\''/\\'\''/g') # escape single quotes a=$(echo "$a" | sed 's/ /'\'','\''/g') # replace space with ',' - argString="['$a']" + argString="['-v', '$a']" else argString="" fi echo " -import pytest, sys +import logging, sys, pytest, scyjava +scyjava._logger.addHandler(logging.StreamHandler(sys.stderr)) +scyjava._logger.setLevel(logging.DEBUG) +scyjava.config.set_verbose(2) result = pytest.main($argString) if result: sys.exit(result) " > jep_test.py -jgo -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py +jgo -vv -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py rm -f jep_test.py From 224c3cced5c06f7c6f76aa30db02b92681458e0b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 13:16:26 -0600 Subject: [PATCH 300/505] Return values for functions with return type Rather than calling pass and letting the function end and returning None implicitly, which is considered bad style if the function can return an explicit value elsewhere. --- src/scyjava/_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index de28345e..d211c0f2 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -789,7 +789,7 @@ def _is_table(obj: Any) -> bool: return jinstance(obj, "org.scijava.table.Table") except BaseException: # No worries if scijava-table is not available. - pass + return False def _convert_table(obj: Any): @@ -798,7 +798,7 @@ def _convert_table(obj: Any): return _table_to_pandas(obj) except BaseException: # No worries if scijava-table is not available. - pass + return None def _import_pandas(required=True): From f4420b56b83cb808da6279479ff0b0d7af8f28ce Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 13:50:45 -0600 Subject: [PATCH 301/505] bin/test.sh: enable the SciJava Maven repository We can't assume the environment will have a .jgorc including maven.scijava.org -- and we need it for org.scijava:scijava-table. --- bin/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test.sh b/bin/test.sh index 61fd506a..b3f723d5 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -62,5 +62,5 @@ result = pytest.main($argString) if result: sys.exit(result) " > jep_test.py -jgo -vv -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py +jgo -vv -r scijava.public=https://maven.scijava.org/content/groups/public -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py rm -f jep_test.py From 568bb58e53b1572dd72add9a0a31dab0f05b4bd9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 18 Nov 2022 14:05:26 -0600 Subject: [PATCH 302/505] Add a dummy pom.xml, to make setup-java happy --- pom.xml | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..fe3d1b25 --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.scijava + scyjava + 0-SNAPSHOT + + + validate + + + maven-enforcer-plugin + 3.1.0 + + + enforce-rules + + enforce + + + + + + + + +/* _\|/_ + (o o) + +----oOO-{_}-OOo-------------------------------------------------------------------+ + | ┌────────────────────────────────────────────────────────┐ | + | │╺┳╸╻ ╻╻┏━┓ ╻┏━┓ ┏┓╻┏━┓╺┳╸ ┏━┓┏━╸╺┳╸╻ ╻┏━┓╻ ╻ ╻ ╻│ | + | │ ┃ ┣━┫┃┗━┓ ┃┗━┓ ┃┗┫┃ ┃ ┃ ┣━┫┃ ┃ ┃ ┃┣━┫┃ ┃ ┗┳┛│ | + | │ ╹ ╹ ╹╹┗━┛ ╹┗━┛ ╹ ╹┗━┛ ╹ ╹ ╹┗━╸ ╹ ┗━┛╹ ╹┗━╸┗━╸ ╹ │ | + | └────────────────────────────────────────────────────────┘ | + | ┌──────────────────────────────────────────────┐ | + | │┏━┓ ┏┳┓┏━┓╻ ╻┏━╸┏┓╻ ┏━┓┏━┓┏━┓ ┏┓┏━╸┏━╸╺┳╸╻│ | + | │┣━┫ ┃┃┃┣━┫┃┏┛┣╸ ┃┗┫ ┣━┛┣┳┛┃ ┃ ┃┣╸ ┃ ┃ ╹│ | + | │╹ ╹ ╹ ╹╹ ╹┗┛ ┗━╸╹ ╹ ╹ ╹┗╸┗━┛┗━┛┗━╸┗━╸ ╹ ╹│ | + | └──────────────────────────────────────────────┘ | + | ┌───────────────────────────────────────────────────────────────────────────┐ | + | │╺┳╸╻ ╻╻┏━┓ ┏━┓┏━┓┏┳┓ ╻ ╻┏┳┓╻ ┏━╸╻ ╻╻┏━┓╺┳╸┏━┓ ┏━┓┏┓╻╻ ╻ ╻ ╺┳╸┏━┓│ | + | │ ┃ ┣━┫┃┗━┓ ┣━┛┃ ┃┃┃┃ ┏╋┛┃┃┃┃ ┣╸ ┏╋┛┃┗━┓ ┃ ┗━┓ ┃ ┃┃┗┫┃ ┗┳┛ ┃ ┃ ┃│ | + | │ ╹ ╹ ╹╹┗━┛ ╹ ┗━┛╹ ╹╹╹ ╹╹ ╹┗━╸ ┗━╸╹ ╹╹┗━┛ ╹ ┗━┛ ┗━┛╹ ╹┗━╸ ╹ ╹ ┗━┛│ | + | └───────────────────────────────────────────────────────────────────────────┘ | + |┌────────────────────────────────────────────────────────────────────────────────┐| + |│┏┳┓┏━┓╻┏ ┏━╸ ╺┳╸╻ ╻┏━╸ ┏━┓┏━╸╺┳╸╻ ╻┏━┓ ┏┓┏━┓╻ ╻┏━┓ ┏━┓┏━╸╺┳╸╻┏━┓┏┓╻╻┏━┓│| + |│┃┃┃┣━┫┣┻┓┣╸ ┃ ┣━┫┣╸ ┗━┓┣╸ ┃ ┃ ┃┣━┛╺━╸ ┃┣━┫┃┏┛┣━┫ ┣━┫┃ ┃ ┃┃ ┃┃┗┫ ┗━┓│| + |│╹ ╹╹ ╹╹ ╹┗━╸ ╹ ╹ ╹┗━╸ ┗━┛┗━╸ ╹ ┗━┛╹ ┗━┛╹ ╹┗┛ ╹ ╹ ╹ ╹┗━╸ ╹ ╹┗━┛╹ ╹ ┗━┛│| + |└────────────────────────────────────────────────────────────────────────────────┘| + |┌───────────────────────────────────────────────────────────────────────────────┐ | + |│ ┓┏━╸┏━┓┏━╸╻ ╻┏━╸ ┏┳┓┏━┓╻ ╻┏━╸┏┓╻ ┓ ┏━╸╻ ╻┏┓╻┏━╸╺┳╸╻┏━┓┏┓╻ ╻ ╻┏━┓┏━┓╻┏ │ | + |│ ┃ ┣━┫┃ ┣━┫┣╸ ╹ ┃┃┃┣━┫┃┏┛┣╸ ┃┗┫ ┣╸ ┃ ┃┃┗┫┃ ┃ ┃┃ ┃┃┗┫ ┃╻┃┃ ┃┣┳┛┣┻┓ │ | + |│ ┗━╸╹ ╹┗━╸╹ ╹┗━╸╹ ╹ ╹╹ ╹┗┛ ┗━╸╹ ╹ ╹ ┗━┛╹ ╹┗━╸ ╹ ╹┗━┛╹ ╹ ┗┻┛┗━┛╹┗╸╹ ╹╹│ | + |└───────────────────────────────────────────────────────────────────────────────┘ | + | ┌──────────────────────────────────────────────────────────┐ | + | │┏━┓╻ ┏━╸┏━┓┏━┓┏━╸ ┏━┓╻ ╻┏┓╻ ┓┏┳┓┏━┓╻┏ ┏━╸ ┓ ╺┳╸┏━┓│ | + | │┣━┛┃ ┣╸ ┣━┫┗━┓┣╸ ┣┳┛┃ ┃┃┗┫ ┃┃┃┣━┫┣┻┓┣╸ ┃ ┃ ┃│ | + | │╹ ┗━╸┗━╸╹ ╹┗━┛┗━╸ ╹┗╸┗━┛╹ ╹ ╹ ╹╹ ╹╹ ╹┗━╸ ╹ ┗━┛│ | + | └──────────────────────────────────────────────────────────┘ | + | ┌────────────────────────────────────────────────────────────────────────────┐ | + | │┏━┓┏━╸┏━╸ ┏━┓╻ ╻┏━┓╻╻ ┏━┓┏┓ ╻ ┏━╸ ┏┓ ╻ ╻╻╻ ╺┳┓ ┏━┓┏━╸╺┳╸╻┏━┓┏┓╻┏━┓ │ | + | │┗━┓┣╸ ┣╸ ┣━┫┃┏┛┣━┫┃┃ ┣━┫┣┻┓┃ ┣╸ ┣┻┓┃ ┃┃┃ ┃┃ ┣━┫┃ ┃ ┃┃ ┃┃┗┫┗━┓ │ | + | │┗━┛┗━╸┗━╸ ╹ ╹┗┛ ╹ ╹╹┗━╸╹ ╹┗━┛┗━╸┗━╸ ┗━┛┗━┛╹┗━╸╺┻┛ ╹ ╹┗━╸ ╹ ╹┗━┛╹ ╹┗━┛╹│ | + | └────────────────────────────────────────────────────────────────────────────┘ | + +---------------------------------------------------------------------------------*/ + + + + + + + + From 06df74dc4ed238ab7d9cce558a24a53a0f5a89c8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 16 Feb 2023 16:30:59 -0600 Subject: [PATCH 303/505] Reformat with Black 23 --- src/scyjava/_java.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index c808747a..1f6fa54b 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -204,7 +204,6 @@ def start_jvm(options=None) -> None: or not os.environ["JAVA_HOME"] or not os.path.isdir(os.environ["JAVA_HOME"]) ): - _logger.debug("JAVA_HOME not set. Will try to infer it from sys.path.") libjvm_win = Path("Library") / "jre" / "bin" / "server" / "jvm.dll" From 84230c975309cdd399f02c881c56c153dc0f73bf Mon Sep 17 00:00:00 2001 From: Karl Duderstadt Date: Fri, 29 Jul 2022 16:53:29 +0200 Subject: [PATCH 304/505] Add an object for SciJava scripting with Python Requires org.scijava:scripting-python on the Java side, and calling scyjava.enable_python_scripting(context) on the Python side. Co-authored-by: Curtis Rueden --- README.md | 8 +++ src/scyjava/__init__.py | 1 + src/scyjava/_script.py | 107 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/scyjava/_script.py diff --git a/README.md b/README.md index 2d69778b..fa9955d5 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,14 @@ FUNCTIONS Add a converter to the list used by to_python. :param converter: A Converter from java to python + enable_python_scripting(context) + Adds a Python script runner object to the ObjectService of the given + SciJava context. Intended for use in conjunction with + 'org.scijava:scripting-python'. + + :param context: The org.scijava.Context containing the ObjectService + where the PythonScriptRunner should be injected. + get_version(java_class_or_python_package) -> str Return the version of a Java class or Python package. diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index cacc5917..106e5c9a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -112,6 +112,7 @@ when_jvm_starts, when_jvm_stops, ) +from scyjava._script import enable_python_scripting # noqa: F401 from scyjava._versions import ( # noqa: F401 compare_version, get_version, diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py new file mode 100644 index 00000000..61b54cdc --- /dev/null +++ b/src/scyjava/_script.py @@ -0,0 +1,107 @@ +""" +Logic for making Python available to Java as a SciJava scripting language. + +For the Java side of this functionality, see +https://github.com/scijava/scripting-python. +""" + +import ast +import sys +import threading +import traceback + +from jpype import JImplements, JOverride + +from ._convert import to_java +from ._java import jimport + + +def enable_python_scripting(context): + """ + Adds a Python script runner object to the ObjectService of the given + SciJava context. Intended for use in conjunction with + 'org.scijava:scripting-python'. + + :param context: The org.scijava.Context containing the ObjectService + where the PythonScriptRunner should be injected. + """ + ObjectService = jimport("org.scijava.object.ObjectService") + + class ScriptContextWriter: + def __init__(self, std): + self._std_default = std + self._thread_to_context = {} + + def addScriptContext(self, thread, scriptContext): + self._thread_to_context[thread] = scriptContext + + def removeScriptContext(self, thread): + if thread in self._thread_to_context: + del self._thread_to_context[thread] + + def flush(self): + self._std_default.flush() + + def write(self, s): + if threading.currentThread() in self._thread_to_context: + self._thread_to_context[threading.currentThread()].getWriter().write( + to_java(s) + ) + else: + self._std_default.write(s) + + # Q: Is there a better way to manage stdout in conjunction with the script runner? + stdoutContextWriter = ScriptContextWriter(sys.stdout) + sys.stdout = stdoutContextWriter + + @JImplements("java.util.function.Function") + class PythonScriptRunner: + @JOverride + def apply(self, arg): + # Copy script bindings/vars into script locals. + script_locals = {} + for key in arg.vars.keys(): + script_locals[key] = arg.vars[key] + + stdoutContextWriter.addScriptContext( + threading.currentThread(), arg.scriptContext + ) + + return_value = None + try: + # NB: Execute the block, except for the last statement, + # which we evaluate instead to get its return value. + # Credit: https://stackoverflow.com/a/39381428/1207769 + + block = ast.parse(str(arg.script), mode="exec") + last = None + if len(block.body) > 0 and hasattr(block.body[-1], "value"): + # Last statement of the script looks like an expression. Evaluate! + last = ast.Expression(block.body.pop().value) + + _globals = {} + exec(compile(block, "", mode="exec"), _globals, script_locals) + if last is not None: + return_value = eval( + compile(last, "", mode="eval"), _globals, script_locals + ) + except Exception: + error_writer = arg.scriptContext.getErrorWriter() + if error_writer is not None: + error_writer.write(to_java(traceback.format_exc())) + + stdoutContextWriter.removeScriptContext(threading.currentThread()) + + # Copy script locals back into script bindings/vars. + for key in script_locals.keys(): + try: + arg.vars[key] = to_java(script_locals[key]) + except Exception: + error_writer = arg.scriptContext.getErrorWriter() + if error_writer is not None: + error_writer.write(to_java(traceback.format_exc())) + + return to_java(return_value) + + objectService = context.service(ObjectService) + objectService.addObject(PythonScriptRunner(), "PythonScriptRunner") From 5d1efca239fc71e0a47a0e64a3800cc82c24ce0a Mon Sep 17 00:00:00 2001 From: Karl Duderstadt Date: Thu, 16 Feb 2023 19:50:27 +0100 Subject: [PATCH 305/505] SciJava Python scripting: no final eval of assignments This change ensures the last line of scripts are only evaulated for a return value if they do not contain an assignment. --- src/scyjava/_script.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 61b54cdc..874e666f 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -75,7 +75,11 @@ def apply(self, arg): block = ast.parse(str(arg.script), mode="exec") last = None - if len(block.body) > 0 and hasattr(block.body[-1], "value"): + if ( + len(block.body) > 0 + and hasattr(block.body[-1], "value") + and not isinstance(block.body[-1], ast.Assign) + ): # Last statement of the script looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) From efb4982b0ca3a414b0a370809695873e5ecd1171 Mon Sep 17 00:00:00 2001 From: Karl Duderstadt Date: Thu, 16 Feb 2023 20:41:10 +0100 Subject: [PATCH 306/505] SciJava Python scripting:use PythonObjectSupplier for unsupported types When python objects cannot be converted by to_java, return them wrapped in the PythonObjectSupplier. --- src/scyjava/_script.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 874e666f..f0d83a1f 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -54,6 +54,15 @@ def write(self, s): stdoutContextWriter = ScriptContextWriter(sys.stdout) sys.stdout = stdoutContextWriter + @JImplements("java.util.function.Supplier") + class PythonObjectSupplier: + def __init__(self, obj): + self.obj = obj + + @JOverride + def get(self): + return self.obj + @JImplements("java.util.function.Function") class PythonScriptRunner: @JOverride @@ -101,9 +110,10 @@ def apply(self, arg): try: arg.vars[key] = to_java(script_locals[key]) except Exception: - error_writer = arg.scriptContext.getErrorWriter() - if error_writer is not None: - error_writer.write(to_java(traceback.format_exc())) + arg.vars[key] = PythonObjectSupplier(script_locals[key]) + # error_writer = arg.scriptContext.getErrorWriter() + # if error_writer is not None: + # error_writer.write(to_java(traceback.format_exc())) return to_java(return_value) From 013c260efb2d5f0219fa00559e41fc6761e97b40 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 16 Feb 2023 16:55:10 -0600 Subject: [PATCH 307/505] Use Mambaforge and cache downloads Adapted from the same fix in the PyImageJ repository: imagej/pyimagej@09a188cabee8d89a849ff92cbf32de7d0c717adf. --- .github/workflows/build.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 591444b6..49e75821 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,18 +84,27 @@ jobs: shell: bash -l {0} steps: - uses: actions/checkout@v2 + - name: Cache conda + uses: actions/cache@v2 + env: + # Increase this value to reset cache if dev-environment.yml has not changed + CACHE_NUMBER: 0 + with: + path: ~/conda_pkgs_dir + key: + ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('dev-environment.yml') }} - uses: conda-incubator/setup-miniconda@v2 with: # Create env with dev packages auto-update-conda: true python-version: 3.9 + miniforge-variant: Mambaforge environment-file: dev-environment.yml # Activate scyjava-dev environment activate-environment: scyjava-dev auto-activate-base: false # Use mamba for faster setup use-mamba: true - mamba-version: "*" - name: Test scyjava run: | bin/test.sh --cov-report=xml --cov=. From 61ecb9a9aaeef300603b5ac2c13b7985b0221d05 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 20 Feb 2023 18:14:10 -0600 Subject: [PATCH 308/505] bin/test.sh: return non-zero when tests fail Otherwise, the CI can erroneously pass. --- bin/test.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/test.sh b/bin/test.sh index b3f723d5..41ef092d 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -23,6 +23,7 @@ then else python -m pytest -p no:faulthandler tests/ fi +jpypeCode=$? echo echo "-------------------------------------------" @@ -63,4 +64,9 @@ if result: sys.exit(result) " > jep_test.py jgo -vv -r scijava.public=https://maven.scijava.org/content/groups/public -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py +jepCode=$? rm -f jep_test.py + +test "$jpypeCode" -ne 0 && exit "$jpypeCode" +test "$jepCode" -ne 0 && exit "$jepCode" +exit 0 From b6597e4f7f05a1f74f3821c7e993793d794105e8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 20 Feb 2023 18:30:35 -0600 Subject: [PATCH 309/505] Release version 1.9.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 34eb780f..f9ea80b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.9.0.dev0" +version = "1.9.0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From c4bfeb6bb3a3aeb928fc7c341ccf258960c07e24 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 20 Feb 2023 18:31:54 -0600 Subject: [PATCH 310/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f9ea80b3..25f79a44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.9.0" +version = "1.9.1.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 18294c42cce110934e65aaae7a3f9a828d7e253f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 10 Mar 2023 16:34:35 -0600 Subject: [PATCH 311/505] Fix deprecated bool8 usage --- src/scyjava/_convert.py | 2 +- tests/test_pandas.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index d211c0f2..c757b3ef 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -708,7 +708,7 @@ def _jarray_to_ndarray(jarr): element_type = _jarray_element_type(jarr) # fmt: off jarraytype_map = { - JBoolean: np.bool8, + JBoolean: np.bool_, JByte: np.int8, # JChar: np.???, JDouble: np.float64, diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 24598535..d96fd4f0 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -117,7 +117,7 @@ def testTabletoPandas(self): assert_same_table(table, df) for col in df.columns: - assert df.dtypes[col] == np.bool8 + assert df.dtypes[col] == np.bool_ # Mixed table table = jimport("org.scijava.table.DefaultGenericTable")() From 9bab39d8cb9acf3459a488b3dbb7e856636c8e4e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 4 May 2023 13:28:00 -0500 Subject: [PATCH 312/505] lint.sh: exit non-zero if any step fails --- bin/lint.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/lint.sh b/bin/lint.sh index e2b6ddba..3d90d260 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -3,7 +3,13 @@ dir=$(dirname "$0") cd "$dir/.." +exitCode=0 black src tests +code=$?; test $code -eq 0 || exitCode=$code isort src tests +code=$?; test $code -eq 0 || exitCode=$code python -m flake8 src tests +code=$?; test $code -eq 0 || exitCode=$code validate-pyproject pyproject.toml +code=$?; test $code -eq 0 || exitCode=$code +exit $exitCode From e614bb9e0f84168c4221386a5e23e99c8133417c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 31 May 2023 12:14:38 -0500 Subject: [PATCH 313/505] Avoid converting objects when not necessary This is accomplished by keeping the converters sorted by priority, then invoking them one by one in order until we find a working one. Co-authored-by: Gabriel Selzer --- src/scyjava/_convert.py | 34 +++++++++++++++++++++++++++++----- tests/test_convert.py | 7 +++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index c757b3ef..c2bc565b 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -5,6 +5,7 @@ import collections import inspect import math +from bisect import insort from pathlib import Path from typing import Any, Callable, Dict, List, NamedTuple @@ -39,6 +40,10 @@ class Priority: LAST = -1e300 +def _priority(thing): + return getattr(thing, "priority", Priority.NORMAL) + + def _has_kwargs(f): return not isjava(f) and any( p.kind == inspect.Parameter.VAR_KEYWORD @@ -65,11 +70,27 @@ def convert(self, obj: Any, **hints: Dict) -> Any: else self.converter(obj) ) + def __lt__(self, other): + return self.priority < _priority(other) + + def __le__(self, other): + return self.priority <= _priority(other) + + def __gt__(self, other): + return self.priority > _priority(other) + + def __ge__(self, other): + return self.priority >= _priority(other) + def _convert(obj: Any, converters: List[Converter], **hints: Dict) -> Any: - suitable_converters = [c for c in converters if c.supports(obj, **hints)] - prioritized = max(suitable_converters, key=lambda c: c.priority) - return prioritized.convert(obj, **hints) + # NB: The given converters are assumed to be sorted ascending by priority, + # meaning lower-priority items appear earlier than higher-priority ones. + # But we want to try the higher priority converters first, so we + # need to iterate the given converters list starting at the end. + for converter in reversed(converters): + if converter.supports(obj, **hints): + return converter.convert(obj, **hints) # -- Python to Java -- @@ -115,7 +136,7 @@ def add_java_converter(converter: Converter) -> None: Add a converter to the list used by to_java. :param converter: A Converter going from python to java """ - java_converters.append(converter) + insort(java_converters, converter) def to_java(obj: Any, **hints: Dict) -> Any: @@ -204,6 +225,9 @@ def _stock_java_converters() -> List[Converter]: Converter( predicate=lambda obj: isinstance(obj, bool), converter=_jc.Boolean, + # NB: Must be higher priority than the int converters, + # because the bool type extends the int type! + priority=Priority.NORMAL + 1, ), # int -> java.lang.Byte Converter( @@ -486,7 +510,7 @@ def add_py_converter(converter: Converter) -> None: Add a converter to the list used by to_python. :param converter: A Converter from java to python """ - py_converters.append(converter) + insort(py_converters, converter) def to_python(data: Any, gentle: bool = False) -> Any: diff --git a/tests/test_convert.py b/tests/test_convert.py index 27bd53dc..c6634455 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -14,6 +14,7 @@ jclass, jimport, jinstance, + py_converters, to_java, to_python, ) @@ -355,3 +356,9 @@ def test_conversion_priority(self): assert e == a java_converters.remove(bad_converter) + + def test_converter_priority(self): + assert len(java_converters) > 0 + assert sorted(java_converters) == java_converters + assert len(py_converters) > 0 + assert sorted(py_converters) == py_converters From f5126156209d4e8fb7260a6ca6d87533242ebb55 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 31 May 2023 12:24:42 -0500 Subject: [PATCH 314/505] Give converters a name property And make str(Converter) yield a representation helpful for debugging. --- src/scyjava/_convert.py | 101 ++++++++++++++++++++++------------------ tests/test_convert.py | 1 + 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index c2bc565b..4118b858 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -55,6 +55,7 @@ class Converter(NamedTuple): predicate: Callable[[Any], bool] converter: Callable[[Any], Any] priority: float = Priority.NORMAL + name: str = "" def supports(self, obj: Any, **hints: Dict) -> bool: return ( @@ -82,6 +83,9 @@ def __gt__(self, other): def __ge__(self, other): return self.priority >= _priority(other) + def __str__(self): + return self.name + def _convert(obj: Any, converters: List[Converter], **hints: Dict) -> Any: # NB: The given converters are assumed to be sorted ascending by priority, @@ -198,70 +202,70 @@ def _stock_java_converters() -> List[Converter]: """ start_jvm() return [ - # Other (Exceptional) converter Converter( + name="Other (Exceptional) converter", predicate=lambda obj: True, converter=_raise_type_exception, priority=Priority.EXTREMELY_LOW - 1, ), - # None -> None Converter( + name="None -> None", predicate=lambda obj: obj is None, converter=lambda obj: None, priority=Priority.EXTREMELY_HIGH + 1, ), - # Java object identity Converter( + name="Java object identity", predicate=isjava, converter=lambda obj: obj, priority=Priority.EXTREMELY_HIGH, ), - # str -> java.lang.String Converter( + name="str -> java.lang.String", predicate=lambda obj: isinstance(obj, str), converter=lambda obj: _jc.String(obj.encode("utf-8"), "utf-8"), ), - # bool -> java.lang.Boolean Converter( + name="bool -> java.lang.Boolean", predicate=lambda obj: isinstance(obj, bool), converter=_jc.Boolean, # NB: Must be higher priority than the int converters, # because the bool type extends the int type! priority=Priority.NORMAL + 1, ), - # int -> java.lang.Byte Converter( + name="int -> java.lang.Byte", predicate=lambda obj, **hints: isinstance(obj, int) and ("type" in hints and hints["type"] in ("b", "byte", "Byte")) and _jc.Byte.MIN_VALUE <= obj <= _jc.Byte.MAX_VALUE, converter=_jc.Byte, priority=Priority.HIGH, ), - # int -> java.lang.Short Converter( + name="int -> java.lang.Short", predicate=lambda obj, **hints: isinstance(obj, int) and ("type" in hints and hints["type"] in ("s", "short", "Short")) and _jc.Short.MIN_VALUE <= obj <= _jc.Short.MAX_VALUE, converter=_jc.Short, priority=Priority.HIGH, ), - # int -> java.lang.Integer Converter( + name="int -> java.lang.Integer", predicate=lambda obj, **hints: isinstance(obj, int) and ("type" not in hints or hints["type"] in ("i", "int", "Integer")) and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, converter=_jc.Integer, ), - # int -> java.lang.Long Converter( + name="int -> java.lang.Long", predicate=lambda obj, **hints: isinstance(obj, int) and ("type" not in hints or hints["type"] in ("j", "l", "long", "Long")) and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, converter=_jc.Long, priority=Priority.NORMAL - 1, ), - # int -> java.math.BigInteger Converter( + name="int -> java.math.BigInteger", predicate=lambda obj, **hints: isinstance(obj, int) and ( "type" not in hints or hints["type"] in ("bi", "bigint", "BigInteger") @@ -269,8 +273,8 @@ def _stock_java_converters() -> List[Converter]: converter=lambda obj: _jc.BigInteger(str(obj)), priority=Priority.NORMAL - 2, ), - # float -> java.lang.Float Converter( + name="float -> java.lang.Float", predicate=lambda obj, **hints: isinstance(obj, float) and ("type" not in hints or hints["type"] in ("f", "float", "Float")) and ( @@ -280,8 +284,8 @@ def _stock_java_converters() -> List[Converter]: ), converter=_jc.Float, ), - # float -> java.lang.Double Converter( + name="float -> java.lang.Double", predicate=lambda obj, **hints: isinstance(obj, float) and ("type" not in hints or hints["type"] in ("d", "double", "Double")) and ( @@ -292,8 +296,8 @@ def _stock_java_converters() -> List[Converter]: converter=_jc.Double, priority=Priority.NORMAL - 1, ), - # float -> java.math.BigDecimal Converter( + name="float -> java.math.BigDecimal", predicate=lambda obj, **hints: isinstance(obj, float) and ( "type" not in hints or hints["type"] in ("bd", "bigdec", "BigDecimal") @@ -301,8 +305,8 @@ def _stock_java_converters() -> List[Converter]: converter=lambda obj: _jc.BigDecimal(str(obj)), priority=Priority.NORMAL - 2, ), - # pathlib.Path -> java.nio.file.Path Converter( + name="pathlib.Path -> java.nio.file.Path", predicate=lambda obj: isinstance(obj, Path), # Pass an empty String array in addition to our path # To make it clear to jep that we want the string-args version @@ -310,24 +314,24 @@ def _stock_java_converters() -> List[Converter]: converter=lambda obj: _jc.Paths.get(str(obj), jarray(_jc.String, [0])), priority=Priority.NORMAL + 1, ), - # pandas.DataFrame -> org.scijava.table.Table Converter( + name="pandas.DataFrame -> org.scijava.table.Table", predicate=lambda obj: type(obj).__name__ == "DataFrame", converter=_pandas_to_table, priority=Priority.NORMAL + 1, ), - # collections.abc.Mapping -> java.util.Map Converter( + name="collections.abc.Mapping -> java.util.Map", predicate=lambda obj: isinstance(obj, collections.abc.Mapping), converter=_convertMap, ), - # collections.abc.Set -> java.util.Set Converter( + name="collections.abc.Set -> java.util.Set", predicate=lambda obj: isinstance(obj, collections.abc.Set), converter=_convertSet, ), - # collections.abc.Iterable -> java.util.Iterable Converter( + name="collections.abc.Iterable -> java.util.Iterable", predicate=lambda obj: isinstance(obj, collections.abc.Iterable), converter=_convertIterable, priority=Priority.NORMAL - 1, @@ -553,150 +557,155 @@ def _stock_py_converters() -> List: start_jvm() converters = [ - # Other (Exceptional) converter Converter( + name="Other (Exceptional) converter", predicate=lambda obj: True, converter=_raise_type_exception, priority=Priority.EXTREMELY_LOW - 1, ), - # Python object identity Converter( + name="Python object identity", predicate=lambda obj: not isjava(obj), converter=lambda obj: obj, priority=Priority.EXTREMELY_HIGH, ), - # java.lang.Boolean -> bool Converter( + name="java.lang.Boolean -> bool", predicate=lambda obj: jinstance(obj, _jc.Boolean), converter=lambda obj: obj.booleanValue(), ), - # java.lang.Byte -> int Converter( + name="java.lang.Byte -> int", predicate=lambda obj: jinstance(obj, _jc.Byte), converter=lambda obj: int(obj.byteValue()), ), - # java.lang.Character -> str Converter( + name="java.lang.Character -> str", predicate=lambda obj: jinstance(obj, _jc.Character), converter=lambda obj: str, ), - # java.lang.Double -> float Converter( + name="java.lang.Double -> float", predicate=lambda obj: jinstance(obj, _jc.Double), converter=lambda obj: float(obj.doubleValue()), ), - # java.lang.Float -> float Converter( + name="java.lang.Float -> float", predicate=lambda obj: jinstance(obj, _jc.Float), converter=lambda obj: float(obj.floatValue()), ), - # java.lang.Integer -> int Converter( + name="java.lang.Integer -> int", predicate=lambda obj: jinstance(obj, _jc.Integer), converter=lambda obj: int(obj.intValue()), ), - # java.lang.Long -> int Converter( + name="java.lang.Long -> int", predicate=lambda obj: jinstance(obj, _jc.Long), converter=lambda obj: int(obj.longValue()), ), - # java.lang.Short -> int Converter( + name="java.lang.Short -> int", predicate=lambda obj: jinstance(obj, _jc.Short), converter=lambda obj: int(obj.shortValue()), ), - # java.lang.String -> str Converter( + name="java.lang.String -> str", predicate=lambda obj: jinstance(obj, _jc.String), converter=lambda obj: str(obj), ), - # java.math.BigInteger -> int Converter( + name="java.math.BigInteger -> int", predicate=lambda obj: jinstance(obj, _jc.BigInteger), converter=lambda obj: int(str(obj)), ), - # java.math.BigDecimal -> float Converter( + name="java.math.BigDecimal -> float", predicate=lambda obj: jinstance(obj, _jc.BigDecimal), converter=lambda obj: float(str(obj)), ), - # java.util.List -> scyjava.JavaList (list-like) Converter( + name="java.util.List -> scyjava.JavaList (list-like)", predicate=lambda obj: jinstance(obj, _jc.List), converter=JavaList, ), - # java.util.Map -> scyjava.JavaMap (dict-like) Converter( + name="java.util.Map -> scyjava.JavaMap (dict-like)", predicate=lambda obj: jinstance(obj, _jc.Map), converter=JavaMap, ), - # java.util.Set -> scyjava.JavaSet (set-like) Converter( + name="java.util.Set -> scyjava.JavaSet (set-like)", predicate=lambda obj: jinstance(obj, _jc.Set), converter=JavaSet, ), - # java.util.Collection -> scyjava.JavaCollection (collections.abc.Collection) Converter( + name="java.util.Collection -> " + "scyjava.JavaCollection (collections.abc.Collection)", predicate=lambda obj: jinstance(obj, _jc.Collection), converter=JavaCollection, priority=Priority.NORMAL - 1, ), - # java.lang.Iterable -> scyjava.JavaIterable (collections.abc.Iterable) Converter( + name="java.lang.Iterable -> " + "scyjava.JavaIterable (collections.abc.Iterable)", predicate=lambda obj: jinstance(obj, _jc.Iterable), converter=JavaIterable, priority=Priority.NORMAL - 1, ), - # java.util.Iterator -> scyjava.JavaIterator (collections.abc.Iterator) Converter( + name="java.util.Iterator -> " + "scyjava.JavaIterator (collections.abc.Iterator)", predicate=lambda obj: jinstance(obj, _jc.Iterator), converter=JavaIterator, priority=Priority.NORMAL - 1, ), - # java.nio.file.Path -> pathlib.Path Converter( + name="java.nio.file.Path -> pathlib.Path", predicate=lambda obj: jinstance(obj, _jc.Path), converter=lambda obj: Path(str(obj)), priority=Priority.NORMAL + 1, ), - # jarray -> list Converter( + name="jarray -> list", predicate=lambda obj: is_jarray(obj), converter=lambda obj: [to_python(o) for o in obj], priority=Priority.VERY_LOW, ), ] if _import_pandas(required=False): - # org.scijava.table.Table -> pandas.DataFrame converters.append( Converter( - predicate=_is_table, converter=_convert_table, priority=Priority.HIGH + name="org.scijava.table.Table -> pandas.DataFrame", + predicate=_is_table, + converter=_convert_table, + priority=Priority.HIGH, ) ) if mode == Mode.JPYPE: converters.extend( [ - # JBoolean -> bool Converter( + name="JBoolean -> bool", predicate=lambda obj: isinstance(obj, JBoolean), converter=bool, priority=Priority.NORMAL + 1, ), - # JByte/JInt/JLong/JShort -> int Converter( + name="JByte/JInt/JLong/JShort -> int", predicate=lambda obj: isinstance(obj, (JByte, JInt, JLong, JShort)), converter=int, priority=Priority.NORMAL + 1, ), - # JDouble/JFloat -> float Converter( + name="JDouble/JFloat -> float", predicate=lambda obj: isinstance(obj, (JDouble, JFloat)), converter=float, priority=Priority.NORMAL + 1, ), - # JChar -> str Converter( + name="JChar -> str", predicate=lambda obj: isinstance(obj, JChar), converter=str, priority=Priority.NORMAL + 1, @@ -704,9 +713,9 @@ def _stock_py_converters() -> List: ] ) if _import_numpy(required=False): - # primitive array -> numpy.ndarray converters.append( Converter( + name="primitive array -> numpy.ndarray", predicate=_supports_jarray_to_ndarray, converter=_jarray_to_ndarray, ) diff --git a/tests/test_convert.py b/tests/test_convert.py index c6634455..9ff85662 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -343,6 +343,7 @@ def test_conversion_priority(self): invader = "Not Hello World" bad_converter = Converter( + name=f"test_conversion_priority: str -> '{invader}'", predicate=lambda obj: isinstance(obj, str), converter=lambda obj: String(invader.encode("utf-8"), "utf-8"), priority=100, From 5cfb904cfdd7c10eca7f8adcee76f15883940d4b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 31 May 2023 12:40:15 -0500 Subject: [PATCH 315/505] Add debug logging for type conversions --- src/scyjava/__init__.py | 5 +++++ src/scyjava/_convert.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 106e5c9a..56c9cc1a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -174,10 +174,15 @@ def __getattr__(name): def _initialize_converters(): + _logger.debug("Initializing type converters") + for converter in _stock_java_converters(): add_java_converter(converter) + _logger.debug("Java converters:{'\n-'.join(java_converters)}") + for converter in _stock_py_converters(): add_py_converter(converter) + _logger.debug("Python converters:{'\n-'.join(py_converters)}") when_jvm_starts(_initialize_converters) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4118b858..fa63cc16 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -4,6 +4,7 @@ import collections import inspect +import logging import math from bisect import insort from pathlib import Path @@ -24,6 +25,8 @@ start_jvm, ) +_logger = logging.getLogger(__name__) + # NB: We cannot use org.scijava.priority.Priority or other Java-side class # here because we don't want to impose Java-side dependencies, and we don't @@ -92,9 +95,14 @@ def _convert(obj: Any, converters: List[Converter], **hints: Dict) -> Any: # meaning lower-priority items appear earlier than higher-priority ones. # But we want to try the higher priority converters first, so we # need to iterate the given converters list starting at the end. + debug = hints.get("debug", False) + log = _logger.info if debug else _logger.debug + log(f"Converting object of type {type(obj)} with hints {hints}") for converter in reversed(converters): if converter.supports(obj, **hints): + log(f"- {converter} supports") return converter.convert(obj, **hints) + log(f"- {converter} does not support") # -- Python to Java -- From 3b5c27a4ce0f824f029b2d0645f69ccb4887c956 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 31 May 2023 14:34:01 -0500 Subject: [PATCH 316/505] Run jep test at INFO level, not DEBUG The DEBUG level is too granular for normal use. In particular, the message "The JVM is already running" appears many times when running in debug mode. We could change that message to print only in JPYPE mode, but I think INFO is a better fit for CI generally anyway. --- bin/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test.sh b/bin/test.sh index 41ef092d..678c6f23 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -57,7 +57,7 @@ fi echo " import logging, sys, pytest, scyjava scyjava._logger.addHandler(logging.StreamHandler(sys.stderr)) -scyjava._logger.setLevel(logging.DEBUG) +scyjava._logger.setLevel(logging.INFO) scyjava.config.set_verbose(2) result = pytest.main($argString) if result: From fbf8cb574bc797397416cc13f4da7e10b4fed4b8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 6 Jul 2023 14:18:52 -0500 Subject: [PATCH 317/505] Be more conservative about overriding sys.stdout Unfortunately, in Jupyter notebooks (at least with ipykernel 6.x), there is already stdout redirection going on, to support printing outputs into the browser below the cell. And for some reason, even though the existing sys.stdout we are overriding *is* an ipykernel output object, it somehow stops being able to yield output below the cells after we replace it. Maybe Jupyter replaces the stdout more than once, and we have a stale object instead of the latest/current one? Regardless, we want to avoid overriding stdout permanently, in favor of using the contextlib.redirect_stdout function to override it only during script evaluation. It's not clear that this function will actually do the right thing if/when multiple Python scripts are executed concurrently, since effectively there will be nested `with redirect_stdout` blocks in that scenario. But this change should make Jupyter notebooks behave better again in the vastly most common case where SciJava Python scripts are not being executed (because why would you execute a SciJava Python script from inside a Python kernel Jupyter notebook, when you can just run the code directly in the cell?). --- src/scyjava/_script.py | 72 ++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index f0d83a1f..5a1fd29e 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -9,6 +9,7 @@ import sys import threading import traceback +from contextlib import redirect_stdout from jpype import JImplements, JOverride @@ -40,19 +41,17 @@ def removeScriptContext(self, thread): del self._thread_to_context[thread] def flush(self): - self._std_default.flush() + self._writer().flush() def write(self, s): - if threading.currentThread() in self._thread_to_context: - self._thread_to_context[threading.currentThread()].getWriter().write( - to_java(s) - ) - else: - self._std_default.write(s) - - # Q: Is there a better way to manage stdout in conjunction with the script runner? + self._writer().write(s) + + def _writer(self): + return self._thread_to_context.get( + threading.currentThread(), self._std_default + ) + stdoutContextWriter = ScriptContextWriter(sys.stdout) - sys.stdout = stdoutContextWriter @JImplements("java.util.function.Supplier") class PythonObjectSupplier: @@ -77,31 +76,36 @@ def apply(self, arg): ) return_value = None - try: - # NB: Execute the block, except for the last statement, - # which we evaluate instead to get its return value. - # Credit: https://stackoverflow.com/a/39381428/1207769 - - block = ast.parse(str(arg.script), mode="exec") - last = None - if ( - len(block.body) > 0 - and hasattr(block.body[-1], "value") - and not isinstance(block.body[-1], ast.Assign) - ): - # Last statement of the script looks like an expression. Evaluate! - last = ast.Expression(block.body.pop().value) - - _globals = {} - exec(compile(block, "", mode="exec"), _globals, script_locals) - if last is not None: - return_value = eval( - compile(last, "", mode="eval"), _globals, script_locals + with redirect_stdout(stdoutContextWriter): + try: + # NB: Execute the block, except for the last statement, + # which we evaluate instead to get its return value. + # Credit: https://stackoverflow.com/a/39381428/1207769 + + block = ast.parse(str(arg.script), mode="exec") + last = None + if ( + len(block.body) > 0 + and hasattr(block.body[-1], "value") + and not isinstance(block.body[-1], ast.Assign) + ): + # Last statement looks like an expression. Evaluate! + last = ast.Expression(block.body.pop().value) + + _globals = {} + exec( + compile(block, "", mode="exec"), _globals, script_locals ) - except Exception: - error_writer = arg.scriptContext.getErrorWriter() - if error_writer is not None: - error_writer.write(to_java(traceback.format_exc())) + if last is not None: + return_value = eval( + compile(last, "", mode="eval"), + _globals, + script_locals, + ) + except Exception: + error_writer = arg.scriptContext.getErrorWriter() + if error_writer is not None: + error_writer.write(to_java(traceback.format_exc())) stdoutContextWriter.removeScriptContext(threading.currentThread()) From 92b498d11befeb201c44b6a02e80adf4a7738bb2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 6 Jul 2023 15:22:14 -0500 Subject: [PATCH 318/505] Add integration test to validate scripting feature --- bin/test.sh | 20 +++++++++++++++++ it/scripting.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 it/scripting.py diff --git a/bin/test.sh b/bin/test.sh index 678c6f23..3ecabad8 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -25,6 +25,25 @@ else fi jpypeCode=$? +echo +echo "-------------------------------------------" +echo "| Running integration tests (JPype only) |" +echo "-------------------------------------------" +itCode=0 +for t in it/*.py +do + python "$t" + code=$? + printf -- "--> %s " "$t" + if [ "$code" -eq 0 ] + then + echo "[OK]" + else + echo "[FAILED]" + itCode=$code + fi +done + echo echo "-------------------------------------------" echo "| Testing Jep mode (Python inside Java) |" @@ -68,5 +87,6 @@ jepCode=$? rm -f jep_test.py test "$jpypeCode" -ne 0 && exit "$jpypeCode" +test "$itCode" -ne 0 && exit "$itCode" test "$jepCode" -ne 0 && exit "$jepCode" exit 0 diff --git a/it/scripting.py b/it/scripting.py new file mode 100644 index 00000000..ee28357a --- /dev/null +++ b/it/scripting.py @@ -0,0 +1,59 @@ +""" +Test the enable_python_scripting function, and subsequent use of +the SciJava Python script language (org.scijava:scripting-python). + +As a side effect, this script also tests Maven dependency resolution. +""" + +import sys +import scyjava + +scyjava.config.endpoints.extend([ + "org.scijava:scijava-common:2.94.2", + "org.scijava:scripting-python:MANAGED" +]) + +# Create minimal SciJava context with a ScriptService. +Context = scyjava.jimport("org.scijava.Context") +ScriptService = scyjava.jimport("org.scijava.script.ScriptService") +# HACK: Avoid "[ERROR] Cannot create plugin" spam. +WidgetService = scyjava.jimport("org.scijava.widget.WidgetService") +ctx = Context(ScriptService, WidgetService) + +# Enable the Python script language. +scyjava.enable_python_scripting(ctx) + +# Assert that the Python script language is available. +ss = ctx.service("org.scijava.script.ScriptService") +lang = ss.getLanguageByName("Python") +assert lang is not None and "Python" in lang.getNames() + +# Construct a script. +script = """ +#@ String name +#@ int age +#@output String statement +statement = f"Hello, {name}! In one year you will be {age + 1} years old." +"A wild return value appears!" +""" +StringReader = scyjava.jimport("java.io.StringReader") +ScriptInfo = scyjava.jimport("org.scijava.script.ScriptInfo") +info = ScriptInfo(ctx, "script.py", StringReader(script)) +info.setLanguage(lang) + +# Run the script. +future = ss.run(info, True, "name", "Chuckles", "age", 13) +try: + module = future.get() + outputs = module.getOutputs() + statement = outputs["statement"] + return_value = module.getReturnValue() +except Exception as e: + sys.stderr.write("-- SCRIPT EXECUTION FAILED --\n") + trace = scyjava.jstacktrace(e) + if trace: + sys.stderr.write(f"{trace}\n") + raise e + +assert statement == "Hello, Chuckles! In one year you will be 14 years old." +assert return_value == "A wild return value appears!" From d4dc918cd0fbf9e788a98d508e4243938ad71266 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 6 Jul 2023 15:46:07 -0500 Subject: [PATCH 319/505] Lint the integration tests, too --- bin/lint.sh | 6 +++--- it/scripting.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/lint.sh b/bin/lint.sh index 3d90d260..88822268 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,11 +4,11 @@ dir=$(dirname "$0") cd "$dir/.." exitCode=0 -black src tests +black src it tests code=$?; test $code -eq 0 || exitCode=$code -isort src tests +isort src it tests code=$?; test $code -eq 0 || exitCode=$code -python -m flake8 src tests +python -m flake8 src it tests code=$?; test $code -eq 0 || exitCode=$code validate-pyproject pyproject.toml code=$?; test $code -eq 0 || exitCode=$code diff --git a/it/scripting.py b/it/scripting.py index ee28357a..0a8bf684 100644 --- a/it/scripting.py +++ b/it/scripting.py @@ -6,12 +6,12 @@ """ import sys + import scyjava -scyjava.config.endpoints.extend([ - "org.scijava:scijava-common:2.94.2", - "org.scijava:scripting-python:MANAGED" -]) +scyjava.config.endpoints.extend( + ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] +) # Create minimal SciJava context with a ScriptService. Context = scyjava.jimport("org.scijava.Context") From 19e1e6f7e73536a868e84c2a696cd0bfeb07e2ca Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 6 Jul 2023 16:18:58 -0500 Subject: [PATCH 320/505] Skip jep mode unit tests on macOS The behavior on macOS CI is too flaky, so for now we skip. --- bin/test.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bin/test.sh b/bin/test.sh index 3ecabad8..e9848c7c 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -73,7 +73,12 @@ then else argString="" fi -echo " +if [ "$(uname -s)" = "Darwin" ] +then + echo "Skipping jep tests on macOS due to flakiness" + jepCode=0 +else + echo "# AUTOGENERATED test file for jep; safe to delete. import logging, sys, pytest, scyjava scyjava._logger.addHandler(logging.StreamHandler(sys.stderr)) scyjava._logger.setLevel(logging.INFO) @@ -82,9 +87,14 @@ result = pytest.main($argString) if result: sys.exit(result) " > jep_test.py -jgo -vv -r scijava.public=https://maven.scijava.org/content/groups/public -Djava.library.path="$site_packages/jep" black.ninia:jep:jep.Run+org.scijava:scijava-table jep_test.py -jepCode=$? -rm -f jep_test.py + jgo -vv \ + -r scijava.public=https://maven.scijava.org/content/groups/public \ + -Djava.library.path="$site_packages/jep" \ + black.ninia:jep:jep.Run+org.scijava:scijava-table \ + jep_test.py + jepCode=$? + rm -f jep_test.py +fi test "$jpypeCode" -ne 0 && exit "$jpypeCode" test "$itCode" -ne 0 && exit "$itCode" From 332df50ae571fa76a8c8aa4cb858276477d8189c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 15:01:16 -0500 Subject: [PATCH 321/505] Move flake8 configuration into pyproject.toml Thanks, flake8-pyproject! --- .pre-commit-config.yaml | 4 +++- dev-environment.yml | 2 ++ pyproject.toml | 10 ++++++++++ setup.cfg | 8 -------- 4 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c77da0ec..ad95eb31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,9 @@ repos: rev: 4.0.1 hooks: - id: flake8 - additional_dependencies: [flake8-typing-imports==1.7.0] + additional_dependencies: + - "flake8-typing-imports" + - "Flake8-pyproject" # Next, sort imports - repo: https://github.com/PyCQA/isort rev: 5.10.1 diff --git a/dev-environment.yml b/dev-environment.yml index 5ea47ea5..ba8cde90 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -32,6 +32,7 @@ dependencies: - black - build - flake8 + - flake8-typing-imports - isort - pytest - pytest-cov @@ -40,5 +41,6 @@ dependencies: - pip - pip: - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d + - flake8-pyproject - validate-pyproject[all] - -e . diff --git a/pyproject.toml b/pyproject.toml index 25f79a44..75fc31e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,8 @@ dev = [ "black", "build", "flake8", + "flake8-pyproject", + "flake8-typing-imports", "isort", "jep", "pytest", @@ -69,5 +71,13 @@ include-package-data = false where = ["src"] namespaces = false +# Thanks to Flake8-pyproject, we can configure flake8 here! +[tool.flake8] +exclude = ["bin", "build", "dist"] +extend-ignore = ["E203"] +# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 +max-line-length = 88 +min_python_version = "3.7" + [tool.isort] profile = "black" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 110a77e6..00000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -# TODO: Move all configuration into pyproject.toml - -[flake8] -# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 -max-line-length = 88 -extend-ignore = E203 -# Unfortunately, flake8 doesn't pick up on our declaration in pyproject.toml -min_python_version = 3.7.0 From cb64936520ad6c1665c753aa7ee6e2180c30fb52 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 18:03:45 -0500 Subject: [PATCH 322/505] Move integration tests beneath tests folder Fortunately, it does not confuse pytest to have this folder there. --- bin/lint.sh | 6 +++--- bin/test.sh | 2 +- {it => tests/it}/scripting.py | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename {it => tests/it}/scripting.py (100%) diff --git a/bin/lint.sh b/bin/lint.sh index 88822268..3d90d260 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,11 +4,11 @@ dir=$(dirname "$0") cd "$dir/.." exitCode=0 -black src it tests +black src tests code=$?; test $code -eq 0 || exitCode=$code -isort src it tests +isort src tests code=$?; test $code -eq 0 || exitCode=$code -python -m flake8 src it tests +python -m flake8 src tests code=$?; test $code -eq 0 || exitCode=$code validate-pyproject pyproject.toml code=$?; test $code -eq 0 || exitCode=$code diff --git a/bin/test.sh b/bin/test.sh index e9848c7c..da27567c 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -30,7 +30,7 @@ echo "-------------------------------------------" echo "| Running integration tests (JPype only) |" echo "-------------------------------------------" itCode=0 -for t in it/*.py +for t in tests/it/*.py do python "$t" code=$? diff --git a/it/scripting.py b/tests/it/scripting.py similarity index 100% rename from it/scripting.py rename to tests/it/scripting.py From d876eb268545072e37e6ed44332b60d4711f4d90 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 18:05:03 -0500 Subject: [PATCH 323/505] Improve what gets packaged into the tarball The resulting tarball now includes the following additional files: * MANIFEST.in * Makefile * UNLICENSE * bin/check.sh * bin/clean.sh * bin/lint.sh * bin/setup.sh * bin/test.sh * dev-environment.yml * environment.yml * tests/it/scripting.py --- MANIFEST.in | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..b362ccce --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +# Metadata +include MANIFEST.in +include Makefile +include README.md +include UNLICENSE +include pyproject.toml + +# Conda Environment Files +include environment.yml +include dev-environment.yml + +# Directory inclusion +graft src +graft tests +graft bin + +# File exclusion +global-exclude __pycache__ +global-exclude *.py[doc] From 2dd3c232267418fc9f00f4e929f1315d84a6e585 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 18:08:45 -0500 Subject: [PATCH 324/505] Release version 1.9.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75fc31e1..445b4fee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.9.1.dev0" +version = "1.9.1" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 706ef8e15be267c92aaac079539460e956008004 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 18:10:09 -0500 Subject: [PATCH 325/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 445b4fee..77967fa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.9.1" +version = "1.9.2.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From ac62992577cfeefe97a0baefaa2b65b7ef79e0f9 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 7 Jul 2023 18:23:50 -0500 Subject: [PATCH 326/505] CI: install needed flake8 helper packages --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49e75821..826687b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: - name: Flake code run: | - python -m pip install flake8 + python -m pip install flake8 Flake8-pyproject flake8-typing-imports python -m flake8 src tests - name: Check import ordering From b8a3fb09b7068f5234784f621de3eddb8a551610 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 16:25:44 -0600 Subject: [PATCH 327/505] Fix linter errors in test_pandas --- tests/test_pandas.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index d96fd4f0..2eee5e6c 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -142,11 +142,11 @@ def testTabletoPandas(self): df = to_python(table) # Table types cannot be the same here, unless we want to cast. # assert_same_table(table, df) - assert type(df["header1"][0]) == float - assert type(df["header2"][0]) == int - assert type(df["header3"][0]) == bool - assert type(df["header4"][0]) == str - assert type(df["header5"][0]) == float + assert type(df["header1"][0]) is float + assert type(df["header2"][0]) is int + assert type(df["header3"][0]) is bool + assert type(df["header4"][0]) is str + assert type(df["header5"][0]) is float def _fill_table(self, table, ndarr, ctor): for i in range(table.getColumnCount()): From 9e3b0e203064bf900e8f809907aa67988da98738 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 17:16:21 -0600 Subject: [PATCH 328/505] Update minimum Python version to 3.8 I want to use the f"{foo=}" f-string feature. --- .github/workflows/build.yml | 1 - dev-environment.yml | 2 +- environment.yml | 2 +- pyproject.toml | 5 ++--- src/scyjava/_versions.py | 17 ++--------------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 826687b9..7aa9b17a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,6 @@ jobs: macos-latest ] python-version: [ - '3.7', '3.8', '3.9', '3.10' diff --git a/dev-environment.yml b/dev-environment.yml index ba8cde90..ca3722a5 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -19,7 +19,7 @@ channels: - conda-forge - defaults dependencies: - - python >= 3.7 + - python >= 3.8 # Project dependencies - jpype1 >= 1.3.0 - jgo diff --git a/environment.yml b/environment.yml index 1ec5d04c..c225059a 100644 --- a/environment.yml +++ b/environment.yml @@ -20,7 +20,7 @@ channels: - conda-forge - defaults dependencies: - - python >= 3.7 + - python >= 3.8 # Project dependencies - jpype1 >= 1.3.0 - jgo diff --git a/pyproject.toml b/pyproject.toml index 77967fa9..14a4f9ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Intended Audience :: Education", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -31,7 +30,7 @@ classifiers = [ ] # NB: Keep this in sync with environment.yml AND dev-environment.yml! -requires-python = ">=3.7" +requires-python = ">=3.8" dependencies = [ "jpype1 >= 1.3.0", "jgo", @@ -77,7 +76,7 @@ exclude = ["bin", "build", "dist"] extend-ignore = ["E203"] # See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 max-line-length = 88 -min_python_version = "3.7" +min_python_version = "3.8" [tool.isort] profile = "black" diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 2fb19db8..532c590f 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -3,7 +3,7 @@ """ import logging -from importlib.util import find_spec +from importlib.metadata import version from ._java import isjava, jimport @@ -32,20 +32,7 @@ def get_version(java_class_or_python_package) -> str: return str(VersionUtils.getVersion(java_class_or_python_package)) # Assume we were given a Python package name. - - if find_spec("importlib.metadata"): - # Fastest, but requires Python 3.8+. - from importlib.metadata import version - - return version(java_class_or_python_package) - - if find_spec("pkg_resources"): - # Slower, but works on Python 3.7. - from pkg_resources import get_distribution - - return get_distribution(java_class_or_python_package).version - - raise RuntimeError("Cannot determine version! Is pkg_resources installed?") + return version(java_class_or_python_package) def is_version_at_least(actual_version: str, minimum_version: str) -> bool: From 02984cd6dbf712f77b121554d1e899a65976cf1d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 14:42:55 -0600 Subject: [PATCH 329/505] Make JVM version test an integration test It needs to run in an isolated Python+Java process so that we can truly validate the before-and-after of version detection. Otherwise, the test simply checks the same thing twice, which is version reporting *after* the JVM is initialized. --- tests/it/jvm_version.py | 22 ++++++++++++++++++++++ tests/test_jvm.py | 27 --------------------------- 2 files changed, 22 insertions(+), 27 deletions(-) create mode 100644 tests/it/jvm_version.py delete mode 100644 tests/test_jvm.py diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py new file mode 100644 index 00000000..79e38d75 --- /dev/null +++ b/tests/it/jvm_version.py @@ -0,0 +1,22 @@ +""" +Test the jvm_version() function. +""" + +import scyjava + +assert not scyjava.jvm_started() + +before_version = scyjava.jvm_version() +assert before_version is not None +assert len(before_version) >= 3 +assert before_version[0] > 0 + +scyjava.config.add_option("-Djava.awt.headless=true") +scyjava.start_jvm() + +after_version = scyjava.jvm_version() +assert after_version is not None +assert len(after_version) >= 3 +assert after_version[0] > 0 + +assert before_version == after_version diff --git a/tests/test_jvm.py b/tests/test_jvm.py deleted file mode 100644 index cc429c8e..00000000 --- a/tests/test_jvm.py +++ /dev/null @@ -1,27 +0,0 @@ -import scyjava - - -class TestJVM(object): - """ - Tests scyjava JVM management functions. - """ - - def test_jvm_version(self): - """ - Tests the jvm_version() function. - """ - - before_version = scyjava.jvm_version() - assert before_version is not None - assert len(before_version) >= 3 - assert before_version[0] > 0 - - scyjava.config.add_option("-Djava.awt.headless=true") - scyjava.start_jvm() - - after_version = scyjava.jvm_version() - assert after_version is not None - assert len(after_version) >= 3 - assert after_version[0] > 0 - - assert before_version == after_version From d1cb1d1525fbcfa3d196ff002ead328cc3424986 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 14:44:47 -0600 Subject: [PATCH 330/505] Add an integration test for scyjava's AWT logic --- tests/it/awt.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/it/awt.py diff --git a/tests/it/awt.py b/tests/it/awt.py new file mode 100644 index 00000000..304c088c --- /dev/null +++ b/tests/it/awt.py @@ -0,0 +1,24 @@ +""" +Test scyjava AWT-related functions. +""" + +import sys + +import scyjava + +assert not scyjava.jvm_started() +scyjava.start_jvm() + +if scyjava.is_jvm_headless(): + # NB: We did not configure the JVM to run in headless mode. + # But it is still headless, which indicates we are running + # on a headless system, such as continuous integration (CI). + # In that case, we are not able to perform this test. + sys.exit(0) + +assert not scyjava.is_awt_initialized() + +Frame = scyjava.jimport("java.awt.Frame") +f = Frame() + +assert scyjava.is_awt_initialized() From 9aa2a59f733b5e95d9855638a41a9ada2c827ae2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 11:27:48 -0600 Subject: [PATCH 331/505] Add shortcuts for JVM config Including: 1. heap allocation and limits 2. headless mode 3. interprocess debugging with JDWP --- src/scyjava/config.py | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 38506f27..e2cc0073 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -166,6 +166,75 @@ def get_classpath(): return jpype.getClassPath() +def set_heap_min(mb: int = None, gb: int = None): + """ + Set the initial amount of memory to allocate to the Java heap. + + Either mb or gb, but not both, must be given. + + Shortcut for passing -Xms###m or -Xms###g to Java. + + :param mb: + The ### of megabytes of memory Java should start with. + :param gb: + The ### of gigabytes of memory Java should start with. + :raise ValueError: If exactly one of mb or gb is not given. + """ + add_option(f"-Xms{_mem_value(mb, gb)}") + + +def set_heap_max(mb: int = None, gb: int = None): + """ + Shortcut for passing -Xmx###m or -Xmx###g to Java. + + Either mb or gb, but not both, must be given. + + :param mb: + The maximum ### of megabytes of memory Java is allowed to use. + :param gb: + The maximum ### of gigabytes of memory Java is allowed to use. + :raise ValueError: If exactly one of mb or gb is not given. + """ + add_option(f"-Xmx{_mem_value(mb, gb)}") + + +def _mem_value(mb: int = None, gb: int = None) -> str: + # fmt: off + if mb is not None and gb is None: return f"{mb}m" # noqa: E701 + if gb is not None and mb is None: return f"{gb}g" # noqa: E701 + # fmt: on + raise ValueError("Exactly one of mb or gb must be given.") + + +def enable_headless_mode(): + """ + Enable headless mode, for running Java without a display. + This mode prevents any graphical elements from popping up. + Shortcut for passing -Djava.awt.headless=true to Java. + """ + add_option("-Djava.awt.headless=true") + + +def enable_remote_debugging(port: int = 8000, suspend: bool = False): + """ + Enable the JDWP debugger, listening on the given port of localhost. + Shortcut for -agentlib:jdwp=transport=dt_socket,address=localhost:. + + :param port: + The port to listen on for client debuggers (e.g. IDEs). + :param suspend: + If True, pause when starting up the JVM until a client debugger connects. + """ + jdwp_args = { + "transport": "dt_socket", + "server": "y", + "suspend": "y" if suspend else "n", + "address": f"localhost:{port}", + } + arg_string = ",".join(f"{k}={v}" for k, v in jdwp_args.items()) + add_option(f"-agentlib:jdwp={arg_string}") + + def add_option(option): global _options _options.append(option) From d80f664d53d42f69cb0a747fee5ee7ca47f95092 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 14:24:06 -0600 Subject: [PATCH 332/505] Add convenience functions for checking JVM config After the JVM has started, it is nice to be able to easily check characteristics of the JVM from the Runtime API, including current and maximum heap sizes, as well as number of available processors. (It is also nice to be able to easily split infinitives in a commit message.) Relatedly, it can be nice to be able to call the garbage collector directly. Note that we do not expose the Runtime.getRuntime().freeMemory() method, because it can be confusing: one would naively expect freeMemory() to equal maxMemory() - usedMemory(), but in actuality it represents the amount of free memory within the bounds of the memory already reserved by the JVM *at the moment*. So it is typically not a very useful number. --- src/scyjava/__init__.py | 5 +++ src/scyjava/_java.py | 88 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 56c9cc1a..5cce680d 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -96,6 +96,8 @@ ) from scyjava._java import ( # noqa: F401 JavaClasses, + available_processors, + gc, is_awt_initialized, is_jarray, is_jvm_headless, @@ -107,6 +109,9 @@ jstacktrace, jvm_started, jvm_version, + memory_max, + memory_total, + memory_used, shutdown_jvm, start_jvm, when_jvm_starts, diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 1f6fa54b..734339c3 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -66,7 +66,7 @@ def java_import(func: Callable[[], str]) -> Callable[[], jpype.JClass]: @property def inner(self): if not jvm_started(): - raise Exception() + raise RuntimeError("JVM has not started yet!") try: return jimport(func(self)) except TypeError: @@ -302,6 +302,80 @@ def jvm_started() -> bool: return jpype.isJVMStarted() +def gc() -> None: + """ + Do a round of Java garbage collection. + + This function is a shortcut for Java's System.gc(). + + :raise RuntimeError: if the JVM has not yet been started. + """ + _jc.System.gc() + + +def memory_total() -> int: + """ + Get the total amount of memory currently reserved by the JVM. + + This number will always be less than or equal to memory_max(). + + In case the JVM was configured with -Xms flag upon startup (e.g. using + the scyjava.config.set_heap_min function), the initial value will typically + correspond approximately, but not exactly, to the configured value, + although it is likely to grow over time as more Java objects are allocated. + + This function is a shortcut for Java's Runtime.getRuntime().totalMemory(). + + :return: The total memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + """ + return int(_jc.Runtime.getRuntime().totalMemory()) + + +def memory_max() -> int: + """ + Get the maximum amount of memory that the JVM will attempt to use. + + This number will always be greater than or equal to memory_total(). + + In case the JVM was configured with -Xmx flag upon startup (e.g. using + the scyjava.config.set_heap_max function), the value will typically + correspond approximately, but not exactly, to the configured value. + + This function is a shortcut for Java's Runtime.getRuntime().maxMemory(). + + :return: The maximum memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + """ + return int(_jc.Runtime.getRuntime().maxMemory()) + + +def memory_used() -> int: + """ + Get the amount of memory currently in use by the JVM. + + This function is a shortcut for + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(). + + :return: The used memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + """ + return memory_total() - int(_jc.Runtime.getRuntime().freeMemory()) + + +def available_processors() -> int: + """ + Get the number of processors available to the JVM. + + This function is a shortcut for Java's + Runtime.getRuntime().availableProcessors(). + + :return: The number of available processors. + :raise RuntimeError: if the JVM has not yet been started. + """ + return int(_jc.Runtime.getRuntime().availableProcessors()) + + def is_jvm_headless() -> bool: """ Return true iff Java is running in headless mode. @@ -575,3 +649,15 @@ def jarray(kind, lengths: Sequence): for i in range(len(arr)): arr[i] = jarray(kind, lengths[1:]) return arr + + +# fmt: off +class _JavaClasses(JavaClasses): + @JavaClasses.java_import + def Runtime(self): return "java.lang.Runtime" # noqa: E272 + @JavaClasses.java_import + def System(self): return "java.lang.System" # noqa: E272 +# fmt: on + + +_jc = _JavaClasses() From ed313be86fb282eb6e22e1c373aea534f4c061fb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 14:45:00 -0600 Subject: [PATCH 333/505] Add integration test for scyjava's heap functions There are two classes of functions: 1. scyjava.config functions to instruct Java how much memory to use; and 2. scyjava._java functions to report back how much it is actually using. This integration test uses both to validate that it all works properly. --- tests/it/java_heap.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/it/java_heap.py diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py new file mode 100644 index 00000000..4f51c74d --- /dev/null +++ b/tests/it/java_heap.py @@ -0,0 +1,34 @@ +""" +Test scyjava JVM memory-related functions. +""" + +import scyjava + +mb_initial = 50 # initial MB of memory to snarf up + +scyjava.config.set_heap_min(mb=mb_initial) +scyjava.config.set_heap_max(gb=1) + +assert not scyjava.jvm_started() +scyjava.start_jvm() + +assert scyjava.available_processors() >= 1 + +mb_max = scyjava.memory_max() // 1024 // 1024 +mb_total = scyjava.memory_total() // 1024 // 1024 +mb_used = scyjava.memory_used() // 1024 // 1024 + +# Used memory should be less than the current memory total, +# which should be less than the maximum heap size. +assert mb_used <= mb_total <= mb_max, f"{mb_used=} {mb_total=} {mb_max=}" + +# The maximum heap size should be approximately 1 GB. +assert 900 <= mb_max <= 1024, f"{mb_max=}" + +# Most of that memory should still be free; i.e., +# we should not be using more than a few MB yet. +assert mb_used <= 5, f"{mb_used=}" + +# The total MB available to Java at this moment +# should be close to our requested initial amount. +assert abs(mb_total - mb_initial) < 5, f"{mb_total=} {mb_initial=}" From bbc649790870bd632f11bb63e66416686ed81f78 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 14:51:08 -0600 Subject: [PATCH 334/505] Add integration test for headless mode config --- tests/it/headless.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/it/headless.py diff --git a/tests/it/headless.py b/tests/it/headless.py new file mode 100644 index 00000000..97ee3852 --- /dev/null +++ b/tests/it/headless.py @@ -0,0 +1,19 @@ +""" +Test scyjava headless mode. +""" + +import scyjava + +scyjava.config.enable_headless_mode() + +assert not scyjava.jvm_started() +scyjava.start_jvm() + +assert scyjava.is_jvm_headless() + +Frame = scyjava.jimport("java.awt.Frame") +try: + f = Frame() + assert False, "HeadlessException should have occurred" +except Exception as e: + assert "java.awt.HeadlessException" == str(e) From 2bf33195d732f0acad7807805fe0d319a3ed95bb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 21 Nov 2023 17:00:48 -0600 Subject: [PATCH 335/505] Sync README with current help output --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index fa9955d5..c4e7699a 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,15 @@ FUNCTIONS Add a converter to the list used by to_python. :param converter: A Converter from java to python + available_processors() -> int + Get the number of processors available to the JVM. + + This function is a shortcut for Java's + Runtime.getRuntime().availableProcessors(). + + :return: The number of available processors. + :raise RuntimeError: if the JVM has not yet been started. + enable_python_scripting(context) Adds a Python script runner object to the ObjectService of the given SciJava context. Intended for use in conjunction with @@ -166,6 +175,13 @@ FUNCTIONS :param context: The org.scijava.Context containing the ObjectService where the PythonScriptRunner should be injected. + gc() -> None + Do a round of Java garbage collection. + + This function is a shortcut for Java's System.gc(). + + :raise RuntimeError: if the JVM has not yet been started. + get_version(java_class_or_python_package) -> str Return the version of a Java class or Python package. @@ -254,12 +270,21 @@ FUNCTIONS jclass(data) Obtain a Java class object. - :param data: The object from which to glean the class. Supported types include: - A. Name of a class to look up, analogous to - Class.forName("java.lang.String"); - B. A jpype.JClass object analogous to String.class; - C. A jpype.JObject instance analogous to o.getClass(). + + A. Name of a class to look up -- e.g. "java.lang.String" -- + which returns the equivalent of Class.forName("java.lang.String"). + + B. A static-style class reference -- e.g. String -- + which returns the equivalent of String.class. + + C. A Java object -- e.g. foo -- + which returns the equivalent of foo.getClass(). + + Note that if you pass a java.lang.Class object, you will get back Class.class, + i.e. the Java class for the Class class. :-) + + :param data: The object from which to glean the class. :returns: A java.lang.Class object, suitable for use with reflection. :raises TypeError: if the argument is not one of the aforementioned types. @@ -297,28 +322,63 @@ FUNCTIONS Return true iff a Java virtual machine (JVM) has been started. jvm_version() -> str - Gets the version of the JVM as a tuple, - with each dot-separated digit as one element. - Characters in the version string beyond only - numbers and dots are ignored, in line - with the java.version system property. + Gets the version of the JVM as a tuple, with each dot-separated digit + as one element. Characters in the version string beyond only numbers + and dots are ignored, in line with the java.version system property. Examples: * OpenJDK 17.0.1 -> [17, 0, 1] * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] * OpenJDK 1.8.0_312 -> [1, 8, 0] - If the JVM is already started, - this function should return the equivalent of: + If the JVM is already started, this function returns the equivalent of: jimport('java.lang.System') .getProperty('java.version') .split('.') - In case the JVM is not started yet,a best effort is made to deduce + In case the JVM is not started yet, a best effort is made to deduce the version from the environment without actually starting up the JVM in-process. If the version cannot be deduced, a RuntimeError with the cause is raised. + memory_max() -> int + Get the maximum amount of memory that the JVM will attempt to use. + + This number will always be greater than or equal to memory_total(). + + In case the JVM was configured with -Xmx flag upon startup (e.g. using + the scyjava.config.set_heap_max function), the value will typically + correspond approximately, but not exactly, to the configured value. + + This function is a shortcut for Java's Runtime.getRuntime().maxMemory(). + + :return: The maximum memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + + memory_total() -> int + Get the total amount of memory currently reserved by the JVM. + + This number will always be less than or equal to memory_max(). + + In case the JVM was configured with -Xms flag upon startup (e.g. using + the scyjava.config.set_heap_min function), the initial value will typically + correspond approximately, but not exactly, to the configured value, + although it is likely to grow over time as more Java objects are allocated. + + This function is a shortcut for Java's Runtime.getRuntime().totalMemory(). + + :return: The total memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + + memory_used() -> int + Get the amount of memory currently in use by the JVM. + + This function is a shortcut for + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(). + + :return: The used memory in bytes. + :raise RuntimeError: if the JVM has not yet been started. + shutdown_jvm() -> None Shutdown the JVM. @@ -338,6 +398,8 @@ FUNCTIONS Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. + :raises RuntimeError: if this method is called while in Jep mode. + start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From 19a41f884280fe250b2701dc4b7dae32ad909f50 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 6 Dec 2023 11:28:52 -0600 Subject: [PATCH 336/505] Bump minor version digit New API was added in #63. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14a4f9ca..4c368d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.9.2.dev0" +version = "1.10.0.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 2ffd04a3b4b76c9b1da4bff6ef3f8f5759e9df01 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 6 Dec 2023 11:40:43 -0600 Subject: [PATCH 337/505] Skip AWT integration test on macOS It hangs, due to macOS's threading strictness. --- tests/it/awt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/it/awt.py b/tests/it/awt.py index 304c088c..452cc309 100644 --- a/tests/it/awt.py +++ b/tests/it/awt.py @@ -2,10 +2,15 @@ Test scyjava AWT-related functions. """ +import platform import sys import scyjava +if platform.system() == "Darwin": + # NB: This test would hang on macOS, due to AWT threading issues. + sys.exit(0) + assert not scyjava.jvm_started() scyjava.start_jvm() From 5be0963be5ffb51e318b9b0c93f22d5543755700 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 6 Dec 2023 11:26:13 -0600 Subject: [PATCH 338/505] Use enable_headless_mode() function as appropriate --- README.md | 6 +++--- src/scyjava/__init__.py | 2 +- src/scyjava/_java.py | 2 +- tests/it/jvm_version.py | 2 +- tests/test_convert.py | 2 +- tests/test_pandas.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c4e7699a..e7c17314 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ u'1.8.0_152-release' ```python >>> from scyjava import config, jimport ->>> config.add_option('-Djava.awt.headless=true') +>>> config.enable_headless_mode() >>> config.add_repositories({'scijava.public': 'https://maven.scijava.org/content/groups/public'}) >>> config.endpoints.append('net.imagej:imagej:2.1.0') >>> ImageJ = jimport('net.imagej.ImageJ') @@ -408,7 +408,7 @@ FUNCTIONS fly with the configuration specified via the scijava.config mechanism. :param options: List of options to pass to the JVM. For example: - ['-Djava.awt.headless=true', '-Xmx4g'] + ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] to_java(obj: Any, **hints: Dict) -> Any Recursively convert a Python object to a Java object. @@ -506,7 +506,7 @@ unless you do one of two things: ```python from scyjava import config, jimport - config.add_option('-Djava.awt.headless=true') + config.enable_headless_mode() ``` In which case, you'll get `java.awt.HeadlessException` instead of a diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 5cce680d..35deb874 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -11,7 +11,7 @@ Use Maven artifacts from remote repositories: >>> from scyjava import config, jimport - >>> config.add_option('-Djava.awt.headless=true') + >>> config.enable_headless_mode() >>> config.add_repositories({ ... 'scijava.public': 'https://maven.scijava.org/content/groups/public', ... }) diff --git a/src/scyjava/_java.py b/src/scyjava/_java.py index 734339c3..e9a7a3c1 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_java.py @@ -167,7 +167,7 @@ def start_jvm(options=None) -> None: fly with the configuration specified via the scijava.config mechanism. :param options: List of options to pass to the JVM. For example: - ['-Djava.awt.headless=true', '-Xmx4g'] + ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] """ # if JVM is already running -- break if jvm_started(): diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py index 79e38d75..669875bf 100644 --- a/tests/it/jvm_version.py +++ b/tests/it/jvm_version.py @@ -11,7 +11,7 @@ assert len(before_version) >= 3 assert before_version[0] > 0 -scyjava.config.add_option("-Djava.awt.headless=true") +scyjava.config.enable_headless_mode() scyjava.start_jvm() after_version = scyjava.jvm_version() diff --git a/tests/test_convert.py b/tests/test_convert.py index 9ff85662..a02db1e8 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -21,7 +21,7 @@ from scyjava.config import Mode, mode config.endpoints.append("org.scijava:scijava-table") -config.add_option("-Djava.awt.headless=true") +config.enable_headless_mode() class TestConvert(object): diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 2eee5e6c..8fc4bc3a 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -5,7 +5,7 @@ from scyjava import config, jarray, jimport, jinstance, to_java, to_python config.endpoints.append("org.scijava:scijava-table") -config.add_option("-Djava.awt.headless=true") +config.enable_headless_mode() def assert_same_table(table, df): From 6885d0bb8b6c1f2dbd83d041e09018f65217af6d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 12:58:08 -0500 Subject: [PATCH 339/505] Use imperative tense in test code docstrings --- tests/test_basics.py | 8 ++++---- tests/test_convert.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index b36eb402..c858b672 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -8,12 +8,12 @@ class TestBasics(object): """ - Tests basic scyjava functions. + Test basic scyjava functions. """ def test_jclass(self): """ - Tests the jclass function. + Test the jclass function. """ if mode == Mode.JEP: pytest.skip("Jep does not support Java class objects!") @@ -23,7 +23,7 @@ def test_jclass(self): def test_jimport(self): """ - Tests the jimport function. + Test the jimport function. """ Object = scyjava.jimport("java.lang.Object") assert Object is not None @@ -34,7 +34,7 @@ def test_jimport(self): def test_jinstance(self): """ - Tests the jinstance function. + Test the jinstance function. """ jstr = scyjava.to_java("Hello") assert scyjava.jinstance(jstr, "java.lang.String") diff --git a/tests/test_convert.py b/tests/test_convert.py index a02db1e8..e9f0489d 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -27,7 +27,7 @@ class TestConvert(object): def testClass(self): """ - Tests class detection from Java objects. + Test class detection from Java objects. """ if mode == Mode.JEP: pytest.skip("The jclass function does not work yet in Jep mode.") From c1ebb2173c156b13b0b45179e3a080ceb24942d6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 12:59:19 -0500 Subject: [PATCH 340/505] Use absolute and relative imports as per PEP8 Absolute imports are the Pythonic recommendation in general. However, for the toplevel __init__.py, relative imports are acceptable, and perhaps even preferred for succinectness. --- src/scyjava/__init__.py | 10 +++++----- src/scyjava/_convert.py | 2 +- src/scyjava/_script.py | 4 ++-- src/scyjava/_versions.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 35deb874..98e1c4f7 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -70,12 +70,12 @@ from functools import lru_cache from typing import Any, Callable, Dict -from scyjava._arrays import ( # noqa: F401 +from ._arrays import ( # noqa: F401 is_arraylike, is_memoryarraylike, is_xarraylike, ) -from scyjava._convert import ( # noqa: F401 +from ._convert import ( # noqa: F401 Converter, JavaCollection, JavaIterable, @@ -94,7 +94,7 @@ to_java, to_python, ) -from scyjava._java import ( # noqa: F401 +from ._java import ( # noqa: F401 JavaClasses, available_processors, gc, @@ -117,8 +117,8 @@ when_jvm_starts, when_jvm_stops, ) -from scyjava._script import enable_python_scripting # noqa: F401 -from scyjava._versions import ( # noqa: F401 +from ._script import enable_python_scripting # noqa: F401 +from ._versions import ( # noqa: F401 compare_version, get_version, is_version_at_least, diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index fa63cc16..f3b2f27b 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -12,7 +12,7 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from ._java import ( +from scyjava._java import ( JavaClasses, Mode, is_jarray, diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 5a1fd29e..883b0188 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -13,8 +13,8 @@ from jpype import JImplements, JOverride -from ._convert import to_java -from ._java import jimport +from scyjava._convert import to_java +from scyjava._java import jimport def enable_python_scripting(context): diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 532c590f..88c45030 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -5,7 +5,7 @@ import logging from importlib.metadata import version -from ._java import isjava, jimport +from scyjava._java import isjava, jimport _logger = logging.getLogger(__name__) From 5c6de23837ecdfd284db4289e4d9d3fe783a41eb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 12:54:28 -0500 Subject: [PATCH 341/505] Split java type functions to a separate class --- src/scyjava/__init__.py | 18 +- src/scyjava/_convert.py | 12 +- src/scyjava/{_java.py => _jvm.py} | 277 ++---------------------------- src/scyjava/_script.py | 2 +- src/scyjava/_types.py | 248 ++++++++++++++++++++++++++ src/scyjava/_versions.py | 3 +- 6 files changed, 284 insertions(+), 276 deletions(-) rename src/scyjava/{_java.py => _jvm.py} (61%) create mode 100644 src/scyjava/_types.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 98e1c4f7..ee7294fe 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -94,19 +94,12 @@ to_java, to_python, ) -from ._java import ( # noqa: F401 - JavaClasses, +from ._jvm import ( # noqa: F401 available_processors, gc, is_awt_initialized, - is_jarray, is_jvm_headless, - isjava, - jarray, - jclass, jimport, - jinstance, - jstacktrace, jvm_started, jvm_version, memory_max, @@ -118,6 +111,15 @@ when_jvm_stops, ) from ._script import enable_python_scripting # noqa: F401 +from ._types import ( # noqa: F401 + JavaClasses, + is_jarray, + isjava, + jarray, + jclass, + jinstance, + jstacktrace, +) from ._versions import ( # noqa: F401 compare_version, get_version, diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index f3b2f27b..e5fd069f 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -12,17 +12,19 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from scyjava._java import ( - JavaClasses, +from scyjava._jvm import ( Mode, + jimport, + mode, + start_jvm, +) +from scyjava._types import ( + JavaClasses, is_jarray, isjava, jarray, jclass, - jimport, jinstance, - mode, - start_jvm, ) _logger = logging.getLogger(__name__) diff --git a/src/scyjava/_java.py b/src/scyjava/_jvm.py similarity index 61% rename from src/scyjava/_java.py rename to src/scyjava/_jvm.py index e9a7a3c1..3c41fc6d 100644 --- a/src/scyjava/_java.py +++ b/src/scyjava/_jvm.py @@ -1,5 +1,5 @@ """ -Utility functions for working with the Java and JVM. +Utility functions for working with the Java Virtual Machine. """ import atexit @@ -11,7 +11,6 @@ from functools import lru_cache from importlib import import_module from pathlib import Path -from typing import Callable, Sequence import jpype import jpype.config @@ -26,58 +25,6 @@ _shutdown_callbacks = [] -class JavaClasses: - """ - Utility class used to make importing frequently-used Java classes - significantly easier and more readable. - - Benefits: - * Minimal boilerplate - * Lazy evaluation - * Usable within type hints - - Example: - - from scyjava import JavaClasses - - class MyJavaClasses(JavaClasses): - @JavaClasses.java_import - def String(self): return "java.lang.String" - @JavaClasses.java_import - def Integer(self): return "java.lang.Integer" - # ... and many more ... - - jc = MyJavaClasses() - - def parse_number_with_java(s: "jc.String") -> "jc.Integer": - return jc.Integer.parseInt(s) - """ - - def java_import(func: Callable[[], str]) -> Callable[[], jpype.JClass]: - """ - A decorator used to lazily evaluate a java import. - func is a function of a Python class that takes no arguments and - returns a string identifying a Java class by name. - - Using that function, this decorator creates a property - that when called, imports the class identified by the function. - """ - - @property - def inner(self): - if not jvm_started(): - raise RuntimeError("JVM has not started yet!") - try: - return jimport(func(self)) - except TypeError: - return None - - return inner - - -# -- JVM functions -- - - def jvm_version() -> str: """ Gets the version of the JVM as a tuple, with each dot-separated digit @@ -308,9 +255,11 @@ def gc() -> None: This function is a shortcut for Java's System.gc(). - :raise RuntimeError: if the JVM has not yet been started. + :raises RuntimeError: If the JVM has not started yet. """ - _jc.System.gc() + _assert_jvm_started() + System = jimport("java.lang.System") + System.gc() def memory_total() -> int: @@ -329,7 +278,7 @@ def memory_total() -> int: :return: The total memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().totalMemory()) + return int(_runtime().totalMemory()) def memory_max() -> int: @@ -347,7 +296,7 @@ def memory_max() -> int: :return: The maximum memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().maxMemory()) + return int(_runtime().maxMemory()) def memory_used() -> int: @@ -360,7 +309,7 @@ def memory_used() -> int: :return: The used memory in bytes. :raise RuntimeError: if the JVM has not yet been started. """ - return memory_total() - int(_jc.Runtime.getRuntime().freeMemory()) + return memory_total() - int(_runtime().freeMemory()) def available_processors() -> int: @@ -373,7 +322,7 @@ def available_processors() -> int: :return: The number of available processors. :raise RuntimeError: if the JVM has not yet been started. """ - return int(_jc.Runtime.getRuntime().availableProcessors()) + return int(_runtime().availableProcessors()) def is_jvm_headless() -> bool: @@ -439,27 +388,6 @@ def when_jvm_stops(f) -> None: _shutdown_callbacks.append(f) -# -- Java functions -- - - -def isjava(data) -> bool: - """Return whether the given data object is a Java object.""" - if mode == Mode.JEP: - return jinstance(data, "java.lang.Object") - - assert mode == Mode.JPYPE - return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) - - -def is_jarray(data) -> bool: - """Return whether the given data object is a Java array.""" - if mode == Mode.JEP: - return str(type(data)) == "" - - assert mode == Mode.JPYPE - return isinstance(data, jpype.JArray) - - @lru_cache(maxsize=None) def jimport(class_name: str): """ @@ -479,185 +407,12 @@ def jimport(class_name: str): return jpype.JClass(class_name) -def jclass(data): - """ - Obtain a Java class object. - - Supported types include: - - A. Name of a class to look up -- e.g. "java.lang.String" -- - which returns the equivalent of Class.forName("java.lang.String"). - - B. A static-style class reference -- e.g. String -- - which returns the equivalent of String.class. - - C. A Java object -- e.g. foo -- - which returns the equivalent of foo.getClass(). - - Note that if you pass a java.lang.Class object, you will get back Class.class, - i.e. the Java class for the Class class. :-) - - :param data: The object from which to glean the class. - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. - """ - if isinstance(data, str): - # Name of a class -- case (A) above. - return jclass(jimport(data)) - - if mode == Mode.JPYPE: - start_jvm() - if isinstance(data, jpype.JClass): - # JPype object representing a static-style class -- case (B) above. - return data.class_ - elif mode == Mode.JEP: - if str(type(data.getClass())) == "": - # Jep object representing a static-style class -- case (B) above. - raise ValueError( - "Jep does not support Java class objects " - + "-- see https://github.com/ninia/jep/issues/405" - ) - - # A Java object -- case (C) above. - if jinstance(data, "java.lang.Object"): - return data.getClass() - - raise TypeError("Cannot glean class from data of type: " + str(type(data))) - - -def jinstance(obj, jtype) -> bool: - """ - Test if the given object is an instance of a particular Java type. - - :param obj: The object to check. - :param jtype: The Java type, as either a jimported class or as a string. - :returns: True iff the object is an instance of that Java type. - """ - if isinstance(jtype, str): - jtype = jimport(jtype) - - if mode == Mode.JEP: - return isinstance(obj, jtype.__pytype__) - - assert mode == Mode.JPYPE - return isinstance(obj, jtype) - - -def jstacktrace(exc) -> str: - """ - Extract the Java-side stack trace from a Java exception. - - Example of usage: - - from scyjava import jimport, jstacktrace - try: - Integer = jimport('java.lang.Integer') - nan = Integer.parseInt('not a number') - except Exception as exc: - print(jstacktrace(exc)) - - :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string - if no stack trace could be extracted. - """ - try: - StringWriter = jimport("java.io.StringWriter") - PrintWriter = jimport("java.io.PrintWriter") - sw = StringWriter() - exc.printStackTrace(PrintWriter(sw, True)) - return str(sw) - except BaseException: - return "" - - -def jarray(kind, lengths: Sequence): - """ - Create a new n-dimensional Java array. - - :param kind: The type of array to create. This can either be a particular - type of object as obtained from jimport, or else a special code for one of - the eight primitive array types: - * 'b' for byte - * 'c' for char - * 'd' for double - * 'f' for float - * 'i' for int - * 'j' for long - * 's' for short - * 'z' for boolean - :param lengths: List of lengths for the array. For example: - `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. - You can pass a single integer to make a 1-dimensional array of that length. - :returns: The newly allocated array - """ - if isinstance(kind, str): - kind = kind.lower() - if isinstance(lengths, int): - lengths = [lengths] - arraytype = kind - - if mode == Mode.JEP: - import jep # noqa: F401 - - if len(lengths) == 1: - # Fast case: 1-d array (we can use primitives) - arr = jep.jarray(lengths[0], arraytype) - else: - # Slow case: n-d array (we cannot use primitives) - # See https://github.com/ninia/jep/issues/439 - kinds = { - "b": jimport("java.lang.Byte"), - "c": jimport("java.lang.Character"), - "d": jimport("java.lang.Double"), - "f": jimport("java.lang.Float"), - "i": jimport("java.lang.Integer"), - "j": jimport("java.lang.Long"), - "s": jimport("java.lang.Short"), - "z": jimport("java.lang.Boolean"), - } - if arraytype in kinds: - arraytype = kinds[arraytype] - kind = arraytype - # build up the array type - for _ in range(len(lengths) - 1): - arraytype = jep.jarray(0, arraytype) - # instantiate the n-dimensional array - arr = jep.jarray(lengths[0], arraytype) - - elif mode == Mode.JPYPE: - start_jvm() - - # build up the array type - kinds = { - "b": jpype.JByte, - "c": jpype.JChar, - "d": jpype.JDouble, - "f": jpype.JFloat, - "i": jpype.JInt, - "j": jpype.JLong, - "s": jpype.JShort, - "z": jpype.JBoolean, - } - if arraytype in kinds: - arraytype = kinds[arraytype] - for _ in range(len(lengths)): - arraytype = jpype.JArray(arraytype) - # instantiate the n-dimensional array - arr = arraytype(lengths[0]) - - if len(lengths) > 1: - for i in range(len(arr)): - arr[i] = jarray(kind, lengths[1:]) - return arr - - -# fmt: off -class _JavaClasses(JavaClasses): - @JavaClasses.java_import - def Runtime(self): return "java.lang.Runtime" # noqa: E272 - @JavaClasses.java_import - def System(self): return "java.lang.System" # noqa: E272 -# fmt: on +def _assert_jvm_started(): + if not jvm_started(): + raise RuntimeError("JVM has not started yet!") -_jc = _JavaClasses() +def _runtime(): + _assert_jvm_started() + Runtime = jimport("java.lang.Runtime") + return Runtime.getRuntime() diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 883b0188..74939f1f 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -14,7 +14,7 @@ from jpype import JImplements, JOverride from scyjava._convert import to_java -from scyjava._java import jimport +from scyjava._jvm import jimport def enable_python_scripting(context): diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py new file mode 100644 index 00000000..d53332c9 --- /dev/null +++ b/src/scyjava/_types.py @@ -0,0 +1,248 @@ +""" +Utility functions for working with and reasoning about Java types. +""" +from typing import Union, Tuple, Callable, Sequence, Any + +import jpype + +from scyjava._jvm import jimport, jvm_started, start_jvm +from scyjava.config import Mode, mode + + +class JavaClasses: + """ + Utility class used to make importing frequently-used Java classes + significantly easier and more readable. + + Benefits: + * Minimal boilerplate + * Lazy evaluation + * Usable within type hints + + Example: + + from scyjava import JavaClasses + + class MyJavaClasses(JavaClasses): + @JavaClasses.java_import + def String(self): return "java.lang.String" + @JavaClasses.java_import + def Integer(self): return "java.lang.Integer" + # ... and many more ... + + jc = MyJavaClasses() + + def parse_number_with_java(s: "jc.String") -> "jc.Integer": + return jc.Integer.parseInt(s) + """ + + def java_import(func: Callable[[], str]) -> property: + """ + A decorator used to lazily evaluate a java import. + func is a function of a Python class that takes no arguments and + returns a string identifying a Java class by name. + + Using that function, this decorator creates a property + that when called, imports the class identified by the function. + """ + + @property + def inner(self): + if not jvm_started(): + raise Exception() + try: + return jimport(func(self)) + except TypeError: + return None + + return inner + + +def jclass(data): + """ + Obtain a Java class object. + + Supported types include: + + A. Name of a class to look up -- e.g. "java.lang.String" -- + which returns the equivalent of Class.forName("java.lang.String"). + + B. A static-style class reference -- e.g. String -- + which returns the equivalent of String.class. + + C. A Java object -- e.g. foo -- + which returns the equivalent of foo.getClass(). + + Note that if you pass a java.lang.Class object, you will get back Class.class, + i.e. the Java class for the Class class. :-) + + :param data: The object from which to glean the class. + :returns: A java.lang.Class object, suitable for use with reflection. + :raises TypeError: if the argument is not one of the aforementioned types. + """ + if isinstance(data, str): + # Name of a class -- case (A) above. + return jclass(jimport(data)) + + if mode == Mode.JPYPE: + start_jvm() + if isinstance(data, jpype.JClass): + # JPype object representing a static-style class -- case (B) above. + return data.class_ + elif mode == Mode.JEP: + if str(type(data.getClass())) == "": + # Jep object representing a static-style class -- case (B) above. + raise ValueError( + "Jep does not support Java class objects " + + "-- see https://github.com/ninia/jep/issues/405" + ) + + # A Java object -- case (C) above. + if jinstance(data, "java.lang.Object"): + return data.getClass() + + raise TypeError("Cannot glean class from data of type: " + str(type(data))) + + +def jstacktrace(exc) -> str: + """ + Extract the Java-side stack trace from a Java exception. + + Example of usage: + + from scyjava import jimport, jstacktrace + try: + Integer = jimport('java.lang.Integer') + nan = Integer.parseInt('not a number') + except Exception as exc: + print(jstacktrace(exc)) + + :param exc: The Java Throwable from which to extract the stack trace. + :returns: A multi-line string containing the stack trace, or empty string + if no stack trace could be extracted. + """ + try: + StringWriter = jimport("java.io.StringWriter") + PrintWriter = jimport("java.io.PrintWriter") + sw = StringWriter() + exc.printStackTrace(PrintWriter(sw, True)) + return str(sw) + except BaseException: + return "" + + +def isjava(data) -> bool: + """Return whether the given data object is a Java object.""" + if mode == Mode.JEP: + return jinstance(data, "java.lang.Object") + + assert mode == Mode.JPYPE + return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) + + +def is_jarray(data: Any) -> bool: + """Return whether the given data object is a Java array.""" + if mode == Mode.JEP: + return str(type(data)) == "" + + assert mode == Mode.JPYPE + return isinstance(data, jpype.JArray) + + +def jinstance(obj, jtype) -> bool: + """ + Test if the given object is an instance of a particular Java type. + + :param obj: The object to check. + :param jtype: The Java type, as either a jimported class or as a string. + :returns: True iff the object is an instance of that Java type. + """ + if isinstance(jtype, str): + jtype = jimport(jtype) + + if mode == Mode.JEP: + return isinstance(obj, jtype.__pytype__) + + assert mode == Mode.JPYPE + return isinstance(obj, jtype) + + +def jarray(kind, lengths: Sequence): + """ + Create a new n-dimensional Java array. + + :param kind: The type of array to create. This can either be a particular + type of object as obtained from jimport, or else a special code for one of + the eight primitive array types: + * 'b' for byte + * 'c' for char + * 'd' for double + * 'f' for float + * 'i' for int + * 'j' for long + * 's' for short + * 'z' for boolean + :param lengths: List of lengths for the array. For example: + `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. + You can pass a single integer to make a 1-dimensional array of that length. + :returns: The newly allocated array + """ + if isinstance(kind, str): + kind = kind.lower() + if isinstance(lengths, int): + lengths = [lengths] + arraytype = kind + + if mode == Mode.JEP: + import jep # noqa: F401 + + if len(lengths) == 1: + # Fast case: 1-d array (we can use primitives) + arr = jep.jarray(lengths[0], arraytype) + else: + # Slow case: n-d array (we cannot use primitives) + # See https://github.com/ninia/jep/issues/439 + kinds = { + "b": jimport("java.lang.Byte"), + "c": jimport("java.lang.Character"), + "d": jimport("java.lang.Double"), + "f": jimport("java.lang.Float"), + "i": jimport("java.lang.Integer"), + "j": jimport("java.lang.Long"), + "s": jimport("java.lang.Short"), + "z": jimport("java.lang.Boolean"), + } + if arraytype in kinds: + arraytype = kinds[arraytype] + kind = arraytype + # build up the array type + for _ in range(len(lengths) - 1): + arraytype = jep.jarray(0, arraytype) + # instantiate the n-dimensional array + arr = jep.jarray(lengths[0], arraytype) + + elif mode == Mode.JPYPE: + start_jvm() + + # build up the array type + kinds = { + "b": jpype.JByte, + "c": jpype.JChar, + "d": jpype.JDouble, + "f": jpype.JFloat, + "i": jpype.JInt, + "j": jpype.JLong, + "s": jpype.JShort, + "z": jpype.JBoolean, + } + if arraytype in kinds: + arraytype = kinds[arraytype] + for _ in range(len(lengths)): + arraytype = jpype.JArray(arraytype) + # instantiate the n-dimensional array + arr = arraytype(lengths[0]) + + if len(lengths) > 1: + for i in range(len(arr)): + arr[i] = jarray(kind, lengths[1:]) + return arr diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index 88c45030..c1695db7 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -5,7 +5,8 @@ import logging from importlib.metadata import version -from scyjava._java import isjava, jimport +from scyjava._jvm import jimport +from scyjava._types import isjava _logger = logging.getLogger(__name__) From ec5147889c65daa27bfbb9cc77550fdb0e23adf3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 12:54:28 -0500 Subject: [PATCH 342/505] Add functions to reason about Java numeric types --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 79 +++++++++++++++++++++++++++++++++++++++++ tests/test_types.py | 27 ++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 tests/test_types.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ee7294fe..6643c3cc 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -119,6 +119,7 @@ jclass, jinstance, jstacktrace, + numeric_bounds, ) from ._versions import ( # noqa: F401 compare_version, diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index d53332c9..ad84a8a6 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -140,6 +140,38 @@ def isjava(data) -> bool: return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) +def is_jbyte(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Byte") + + +def is_jshort(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Short") + + +def is_jinteger(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Integer") + + +def is_jlong(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Long") + + +def is_jfloat(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Float") + + +def is_jdouble(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Double") + + +def is_jboolean(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Boolean") + + +def is_jcharacter(the_type: type) -> bool: + return _is_jtype(the_type, "java.lang.Character") + + def is_jarray(data: Any) -> bool: """Return whether the given data object is a Java array.""" if mode == Mode.JEP: @@ -246,3 +278,50 @@ def jarray(kind, lengths: Sequence): for i in range(len(arr)): arr[i] = jarray(kind, lengths[1:]) return arr + + +def numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float], Tuple[None, None]]: + """ + Get the minimum and maximum values for the given numeric type. + For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), + whereas a Java double returns (float("-inf"), float("inf")). + + :param the_type: The type whose minimum and maximum values are needed. + :return: + The minimum and maximum values as a two-element tuple of int or float, + or a two-element tuple of None if no known bounds. + """ + if is_jbyte(the_type): + Byte = jimport("java.lang.Byte") + return int(Byte.MIN_VALUE), int(Byte.MAX_VALUE) + + if is_jshort(the_type): + Short = jimport("java.lang.Short") + return int(Short.MIN_VALUE), int(Short.MAX_VALUE) + + if is_jinteger(the_type): + Integer = jimport("java.lang.Integer") + return int(Integer.MIN_VALUE), int(Integer.MAX_VALUE) + + if is_jlong(the_type): + Long = jimport("java.lang.Long") + return int(Long.MIN_VALUE), int(Long.MAX_VALUE) + + if is_jfloat(the_type) or is_jdouble(the_type): + return float("-inf"), float("inf") + + return None, None + + +def _is_jtype(the_type: type, class_name: str) -> bool: + """ + Test if the given type object is *exactly* the specified Java type. + + :param the_type: The type object to check. + :param class_name: The fully qualified Java class name in string form. + :returns: True iff the type is exactly that Java type. + """ + # NB: Stringify the type to support both bridge modes. Ex: + # * JPype: + # * Jep: + return f"class '{class_name}'" in str(the_type) diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 00000000..9bf8c146 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,27 @@ +import scyjava +from scyjava import to_java, numeric_bounds + + +class TestTypes(object): + """ + Test Java-type-related functions. + """ + + def test_numeric_bounds(self): + v_byte = to_java(1, type='byte') + v_short = to_java(2, type='short') + v_int = to_java(3, type='int') + v_long = to_java(4, type='long') + v_bigint = to_java(5, type='bigint') + v_float = to_java(6.7, type='float') + v_double = to_java(7.8, type='double') + v_bigdec = to_java(8.9, type='bigdec') + + assert (-128, 127) == numeric_bounds(type(v_byte)) + assert (-32768, 32767) == numeric_bounds(type(v_short)) + assert (-2147483648, 2147483647) == numeric_bounds(type(v_int)) + assert (-9223372036854775808, 9223372036854775807) == numeric_bounds(type(v_long)) + assert (None, None) == numeric_bounds(type(v_bigint)) + assert (float("-inf"), float("inf")) == numeric_bounds(type(v_float)) + assert (float("-inf"), float("inf")) == numeric_bounds(type(v_double)) + assert (None, None) == numeric_bounds(type(v_bigdec)) From 91d9733fad3bc12f5d3577c701cdeb451d92e243 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 13:08:06 -0500 Subject: [PATCH 343/505] Fix import source for bridge mode attributes --- src/scyjava/_convert.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index e5fd069f..da1fb2e0 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -13,9 +13,7 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort from scyjava._jvm import ( - Mode, jimport, - mode, start_jvm, ) from scyjava._types import ( @@ -26,6 +24,7 @@ jclass, jinstance, ) +from scyjava.config import Mode, mode _logger = logging.getLogger(__name__) From 33c4ea1502eb6ec0ed3a4fd83b562280f23458dc Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 13:20:28 -0500 Subject: [PATCH 344/505] Stop using infinity for float/double bounds Instead, we use Float/Double.MAX_VALUE. --- src/scyjava/_types.py | 11 ++++++++--- tests/test_types.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index ad84a8a6..83090f4d 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -284,7 +284,7 @@ def numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float] """ Get the minimum and maximum values for the given numeric type. For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), - whereas a Java double returns (float("-inf"), float("inf")). + whereas a Java double returns (double(-Double.MAX_VALUE), double(Double.MAX_VALUE)). :param the_type: The type whose minimum and maximum values are needed. :return: @@ -307,8 +307,13 @@ def numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float] Long = jimport("java.lang.Long") return int(Long.MIN_VALUE), int(Long.MAX_VALUE) - if is_jfloat(the_type) or is_jdouble(the_type): - return float("-inf"), float("inf") + if is_jfloat(the_type): + Float = jimport("java.lang.Float") + return float(-Float.MAX_VALUE), float(Float.MAX_VALUE) + + if is_jdouble(the_type): + Double = jimport("java.lang.Double") + return float(-Double.MAX_VALUE), float(Double.MAX_VALUE) return None, None diff --git a/tests/test_types.py b/tests/test_types.py index 9bf8c146..2cd10926 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -22,6 +22,6 @@ def test_numeric_bounds(self): assert (-2147483648, 2147483647) == numeric_bounds(type(v_int)) assert (-9223372036854775808, 9223372036854775807) == numeric_bounds(type(v_long)) assert (None, None) == numeric_bounds(type(v_bigint)) - assert (float("-inf"), float("inf")) == numeric_bounds(type(v_float)) - assert (float("-inf"), float("inf")) == numeric_bounds(type(v_double)) + assert (-3.4028234663852886e+38, 3.4028234663852886e+38) == numeric_bounds(type(v_float)) + assert (-1.7976931348623157e+308, 1.7976931348623157e+308) == numeric_bounds(type(v_double)) assert (None, None) == numeric_bounds(type(v_bigdec)) From 1b472911b0b41b48dfe39be0545a81e972c1f1e0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 13:26:00 -0500 Subject: [PATCH 345/505] Make the linter happy --- src/scyjava/__init__.py | 12 ++---------- src/scyjava/_convert.py | 14 ++------------ src/scyjava/_types.py | 6 ++++-- tests/test_types.py | 31 ++++++++++++++++++------------- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 6643c3cc..4bc3e17d 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -70,11 +70,7 @@ from functools import lru_cache from typing import Any, Callable, Dict -from ._arrays import ( # noqa: F401 - is_arraylike, - is_memoryarraylike, - is_xarraylike, -) +from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike # noqa: F401 from ._convert import ( # noqa: F401 Converter, JavaCollection, @@ -121,11 +117,7 @@ jstacktrace, numeric_bounds, ) -from ._versions import ( # noqa: F401 - compare_version, - get_version, - is_version_at_least, -) +from ._versions import compare_version, get_version, is_version_at_least # noqa: F401 __version__ = get_version("scyjava") __all__ = [ diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index da1fb2e0..413dca65 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -12,18 +12,8 @@ from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort -from scyjava._jvm import ( - jimport, - start_jvm, -) -from scyjava._types import ( - JavaClasses, - is_jarray, - isjava, - jarray, - jclass, - jinstance, -) +from scyjava._jvm import jimport, start_jvm +from scyjava._types import JavaClasses, is_jarray, isjava, jarray, jclass, jinstance from scyjava.config import Mode, mode _logger = logging.getLogger(__name__) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 83090f4d..f38f43a3 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -1,7 +1,7 @@ """ Utility functions for working with and reasoning about Java types. """ -from typing import Union, Tuple, Callable, Sequence, Any +from typing import Any, Callable, Sequence, Tuple, Union import jpype @@ -280,7 +280,9 @@ def jarray(kind, lengths: Sequence): return arr -def numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float], Tuple[None, None]]: +def numeric_bounds( + the_type: type, +) -> Union[Tuple[int, int], Tuple[float, float], Tuple[None, None]]: """ Get the minimum and maximum values for the given numeric type. For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), diff --git a/tests/test_types.py b/tests/test_types.py index 2cd10926..cc6adc44 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,4 @@ -import scyjava -from scyjava import to_java, numeric_bounds +from scyjava import numeric_bounds, to_java class TestTypes(object): @@ -8,20 +7,26 @@ class TestTypes(object): """ def test_numeric_bounds(self): - v_byte = to_java(1, type='byte') - v_short = to_java(2, type='short') - v_int = to_java(3, type='int') - v_long = to_java(4, type='long') - v_bigint = to_java(5, type='bigint') - v_float = to_java(6.7, type='float') - v_double = to_java(7.8, type='double') - v_bigdec = to_java(8.9, type='bigdec') + v_byte = to_java(1, type="byte") + v_short = to_java(2, type="short") + v_int = to_java(3, type="int") + v_long = to_java(4, type="long") + v_bigint = to_java(5, type="bigint") + v_float = to_java(6.7, type="float") + v_double = to_java(7.8, type="double") + v_bigdec = to_java(8.9, type="bigdec") assert (-128, 127) == numeric_bounds(type(v_byte)) assert (-32768, 32767) == numeric_bounds(type(v_short)) assert (-2147483648, 2147483647) == numeric_bounds(type(v_int)) - assert (-9223372036854775808, 9223372036854775807) == numeric_bounds(type(v_long)) + assert (-9223372036854775808, 9223372036854775807) == numeric_bounds( + type(v_long) + ) assert (None, None) == numeric_bounds(type(v_bigint)) - assert (-3.4028234663852886e+38, 3.4028234663852886e+38) == numeric_bounds(type(v_float)) - assert (-1.7976931348623157e+308, 1.7976931348623157e+308) == numeric_bounds(type(v_double)) + assert (-3.4028234663852886e38, 3.4028234663852886e38) == numeric_bounds( + type(v_float) + ) + assert (-1.7976931348623157e308, 1.7976931348623157e308) == numeric_bounds( + type(v_double) + ) assert (None, None) == numeric_bounds(type(v_bigdec)) From ec6a1ab9d6107af35e2e32b223c173e1b34ca9c4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Jul 2023 17:57:32 -0500 Subject: [PATCH 346/505] Ignore F401 in __init__.py Because the nicest modular Pythonic design is to import all your public API from supporting files into __init__.py, so they are available from the toplevel, and we don't want flake8 yelling at us about it. Thanks to @gselzer and https://stackoverflow.com/a/58029222/1207769. --- pyproject.toml | 1 + src/scyjava/__init__.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4c368d59..f3065760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ extend-ignore = ["E203"] # See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 max-line-length = 88 min_python_version = "3.8" +per-file-ignores = "__init__.py:F401" [tool.isort] profile = "black" diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 4bc3e17d..5cd1fc77 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -70,8 +70,8 @@ from functools import lru_cache from typing import Any, Callable, Dict -from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike # noqa: F401 -from ._convert import ( # noqa: F401 +from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike +from ._convert import ( Converter, JavaCollection, JavaIterable, @@ -106,8 +106,8 @@ when_jvm_starts, when_jvm_stops, ) -from ._script import enable_python_scripting # noqa: F401 -from ._types import ( # noqa: F401 +from ._script import enable_python_scripting +from ._types import ( JavaClasses, is_jarray, isjava, @@ -117,7 +117,7 @@ jstacktrace, numeric_bounds, ) -from ._versions import compare_version, get_version, is_version_at_least # noqa: F401 +from ._versions import compare_version, get_version, is_version_at_least __version__ = get_version("scyjava") __all__ = [ From 112c4bdc5ed4d8474c2699ef5011ca9c1c7eb5d6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 5 Dec 2023 19:24:40 -0600 Subject: [PATCH 347/505] Fix docstring errors and update README --- README.md | 67 ++++++++++++++++++++++++++--------------- src/scyjava/_convert.py | 31 +++++++++++-------- src/scyjava/_jvm.py | 18 ++++++----- src/scyjava/_types.py | 14 ++++----- 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index e7c17314..eb22bbb8 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ FUNCTIONS This function is a shortcut for Java's System.gc(). - :raise RuntimeError: if the JVM has not yet been started. + :raise RuntimeError: If the JVM has not started yet. get_version(java_class_or_python_package) -> str Return the version of a Java class or Python package. @@ -213,13 +213,13 @@ FUNCTIONS those actions via the jpype.setupGuiEnvironment wrapper function; see the Troubleshooting section of the scyjava README for details. - is_jarray(data) -> bool + is_jarray(data: Any) -> bool Return whether the given data object is a Java array. is_jvm_headless() -> bool Return true iff Java is running in headless mode. - :raises RuntimeError: If the JVM has not started yet. + :raise RuntimeError: If the JVM has not started yet. is_memoryarraylike(arr: Any) -> bool Return True iff the object is memoryarraylike: @@ -265,7 +265,7 @@ FUNCTIONS :param lengths: List of lengths for the array. For example: `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. You can pass a single integer to make a 1-dimensional array of that length. - :returns: The newly allocated array + :return: The newly allocated array jclass(data) Obtain a Java class object. @@ -285,22 +285,23 @@ FUNCTIONS i.e. the Java class for the Class class. :-) :param data: The object from which to glean the class. - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. + :return: A java.lang.Class object, suitable for use with reflection. + :raise TypeError: if the argument is not one of the aforementioned types. jimport(class_name: str) Import a class from Java to Python. :param class_name: Name of the class to import. - :returns: A pointer to the class, which can be used to - e.g. instantiate objects of that class. + :return: + A pointer to the class, which can be used to + e.g. instantiate objects of that class. jinstance(obj, jtype) -> bool Test if the given object is an instance of a particular Java type. :param obj: The object to check. :param jtype: The Java type, as either a jimported class or as a string. - :returns: True iff the object is an instance of that Java type. + :return: True iff the object is an instance of that Java type. jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. @@ -315,7 +316,7 @@ FUNCTIONS print(jstacktrace(exc)) :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string + :return: A multi-line string containing the stack trace, or empty string if no stack trace could be extracted. jvm_started() -> bool @@ -379,8 +380,18 @@ FUNCTIONS :return: The used memory in bytes. :raise RuntimeError: if the JVM has not yet been started. + numeric_bounds(the_type: type) -> Union[Tuple[int, int], Tuple[float, float], Tuple[NoneType, NoneType]] + Get the minimum and maximum values for the given numeric type. + For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), + whereas a Java double returns (float(-Double.MAX_VALUE), float(Double.MAX_VALUE)). + + :param the_type: The type whose minimum and maximum values are needed. + :return: + The minimum and maximum values as a two-element tuple of int or float, + or a two-element tuple of None if no known bounds. + shutdown_jvm() -> None - Shutdown the JVM. + Shut down the JVM. This function makes a best effort to clean up Java resources first. In particular, shutdown hooks registered with scyjava.when_jvm_stops @@ -398,7 +409,7 @@ FUNCTIONS Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - :raises RuntimeError: if this method is called while in Jep mode. + :raise RuntimeError: if this method is called while in Jep mode. start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can @@ -407,8 +418,9 @@ FUNCTIONS time a scyjava function needing a JVM is invoked, one is started on the fly with the configuration specified via the scijava.config mechanism. - :param options: List of options to pass to the JVM. For example: - ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + :param options: + List of options to pass to the JVM. + For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] to_java(obj: Any, **hints: Dict) -> Any Recursively convert a Python object to a Java object. @@ -451,11 +463,13 @@ FUNCTIONS * float values in Double range but outside float range convert to Double * float values outside double range convert to BigDecimal - :param obj: The Python object to convert. - :param hints: An optional dictionary of hints, to help scyjava - make decisions about how to do the conversion. - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :param obj: + The Python object to convert. + :param hints: + An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. + :return: A corresponding Java object with the same contents. + :raise TypeError: if the argument is not one of the aforementioned types. to_python(data: Any, gentle: bool = False) -> Any Recursively convert a Java object to a Python object. @@ -472,12 +486,15 @@ FUNCTIONS * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. - :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types, - and the gentle flag is not set. + :param data: + The Java object to convert. + :param gentle: + If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. + :return: A corresponding Python object with the same contents. + :raise TypeError: + if the argument is not one of the aforementioned types, + and the gentle flag is not set. when_jvm_starts(f) -> None Registers a function to be called when the JVM starts (or immediately). diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 413dca65..af1583b5 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -184,11 +184,13 @@ def to_java(obj: Any, **hints: Dict) -> Any: * float values in Double range but outside float range convert to Double * float values outside double range convert to BigDecimal - :param obj: The Python object to convert. - :param hints: An optional dictionary of hints, to help scyjava - make decisions about how to do the conversion. - :returns: A corresponding Java object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types. + :param obj: + The Python object to convert. + :param hints: + An optional dictionary of hints, to help scyjava + make decisions about how to do the conversion. + :return: A corresponding Java object with the same contents. + :raise TypeError: if the argument is not one of the aforementioned types. """ start_jvm() return _convert(obj, java_converters, **hints) @@ -197,7 +199,7 @@ def to_java(obj: Any, **hints: Dict) -> Any: def _stock_java_converters() -> List[Converter]: """ Construct the Python-to-Java converters supported out of the box. - :returns: A list of Converters + :return: A list of Converters """ start_jvm() return [ @@ -532,12 +534,15 @@ def to_python(data: Any, gentle: bool = False) -> Any: * Iterable -> collections.abc.Iterable * Iterator -> collections.abc.Iterator - :param data: The Java object to convert. - :param gentle: If set, and the type cannot be converted, leaves - the data alone rather than raising a TypeError. - :returns: A corresponding Python object with the same contents. - :raises TypeError: if the argument is not one of the aforementioned types, - and the gentle flag is not set. + :param data: + The Java object to convert. + :param gentle: + If set, and the type cannot be converted, leaves + the data alone rather than raising a TypeError. + :return: A corresponding Python object with the same contents. + :raise TypeError: + if the argument is not one of the aforementioned types, + and the gentle flag is not set. """ start_jvm() try: @@ -551,7 +556,7 @@ def to_python(data: Any, gentle: bool = False) -> Any: def _stock_py_converters() -> List: """ Construct the Java-to-Python converters supported out of the box. - :returns: A list of Converters + :return: A list of Converters """ start_jvm() diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 3c41fc6d..2e2350a8 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -113,8 +113,9 @@ def start_jvm(options=None) -> None: time a scyjava function needing a JVM is invoked, one is started on the fly with the configuration specified via the scijava.config mechanism. - :param options: List of options to pass to the JVM. For example: - ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + :param options: + List of options to pass to the JVM. + For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] """ # if JVM is already running -- break if jvm_started(): @@ -191,7 +192,7 @@ def start_jvm(options=None) -> None: def shutdown_jvm() -> None: - """Shutdown the JVM. + """Shut down the JVM. This function makes a best effort to clean up Java resources first. In particular, shutdown hooks registered with scyjava.when_jvm_stops @@ -209,7 +210,7 @@ def shutdown_jvm() -> None: Note that if the JVM is not already running, then this function does nothing! In particular, shutdown hooks are skipped in this situation. - :raises RuntimeError: if this method is called while in Jep mode. + :raise RuntimeError: if this method is called while in Jep mode. """ if not jvm_started(): return @@ -255,7 +256,7 @@ def gc() -> None: This function is a shortcut for Java's System.gc(). - :raises RuntimeError: If the JVM has not started yet. + :raise RuntimeError: If the JVM has not started yet. """ _assert_jvm_started() System = jimport("java.lang.System") @@ -329,7 +330,7 @@ def is_jvm_headless() -> bool: """ Return true iff Java is running in headless mode. - :raises RuntimeError: If the JVM has not started yet. + :raise RuntimeError: If the JVM has not started yet. """ if not jvm_started(): raise RuntimeError("JVM has not started yet!") @@ -394,8 +395,9 @@ def jimport(class_name: str): Import a class from Java to Python. :param class_name: Name of the class to import. - :returns: A pointer to the class, which can be used to - e.g. instantiate objects of that class. + :return: + A pointer to the class, which can be used to + e.g. instantiate objects of that class. """ if mode == Mode.JEP: module_path = class_name.rsplit(".", 1) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index f38f43a3..c064eb7a 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -77,8 +77,8 @@ def jclass(data): i.e. the Java class for the Class class. :-) :param data: The object from which to glean the class. - :returns: A java.lang.Class object, suitable for use with reflection. - :raises TypeError: if the argument is not one of the aforementioned types. + :return: A java.lang.Class object, suitable for use with reflection. + :raise TypeError: if the argument is not one of the aforementioned types. """ if isinstance(data, str): # Name of a class -- case (A) above. @@ -118,7 +118,7 @@ def jstacktrace(exc) -> str: print(jstacktrace(exc)) :param exc: The Java Throwable from which to extract the stack trace. - :returns: A multi-line string containing the stack trace, or empty string + :return: A multi-line string containing the stack trace, or empty string if no stack trace could be extracted. """ try: @@ -187,7 +187,7 @@ def jinstance(obj, jtype) -> bool: :param obj: The object to check. :param jtype: The Java type, as either a jimported class or as a string. - :returns: True iff the object is an instance of that Java type. + :return: True iff the object is an instance of that Java type. """ if isinstance(jtype, str): jtype = jimport(jtype) @@ -217,7 +217,7 @@ def jarray(kind, lengths: Sequence): :param lengths: List of lengths for the array. For example: `jarray('z', [3, 7])` is the equivalent of `new boolean[3][7]` in Java. You can pass a single integer to make a 1-dimensional array of that length. - :returns: The newly allocated array + :return: The newly allocated array """ if isinstance(kind, str): kind = kind.lower() @@ -286,7 +286,7 @@ def numeric_bounds( """ Get the minimum and maximum values for the given numeric type. For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), - whereas a Java double returns (double(-Double.MAX_VALUE), double(Double.MAX_VALUE)). + whereas a Java double returns (float(-Double.MAX_VALUE), float(Double.MAX_VALUE)). :param the_type: The type whose minimum and maximum values are needed. :return: @@ -326,7 +326,7 @@ def _is_jtype(the_type: type, class_name: str) -> bool: :param the_type: The type object to check. :param class_name: The fully qualified Java class name in string form. - :returns: True iff the type is exactly that Java type. + :return: True iff the type is exactly that Java type. """ # NB: Stringify the type to support both bridge modes. Ex: # * JPype: From 5f5a957921199d651912a03435c8cd1239c1845d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 8 May 2024 14:36:34 -0500 Subject: [PATCH 348/505] Fix bug in SciJava script writer access --- src/scyjava/_script.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 74939f1f..ec73f906 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -47,9 +47,8 @@ def write(self, s): self._writer().write(s) def _writer(self): - return self._thread_to_context.get( - threading.currentThread(), self._std_default - ) + ctx = self._thread_to_context.get(threading.currentThread()) + return self._std_default if ctx is None else ctx.getWriter() stdoutContextWriter = ScriptContextWriter(sys.stdout) From 755e1336925a4c9cbabc7a83ec10b82f5183b194 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 9 May 2024 10:27:45 -0500 Subject: [PATCH 349/505] Add blank lines demanded by black on CI --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 5cd1fc77..ce20173b 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -66,6 +66,7 @@ >>> jset.toString() '[1, 2, 3]' """ + import logging from functools import lru_cache from typing import Any, Callable, Dict diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index c064eb7a..34c2cafc 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -1,6 +1,7 @@ """ Utility functions for working with and reasoning about Java types. """ + from typing import Any, Callable, Sequence, Tuple, Union import jpype From e610df805c688e2c787d36fcca7aa230587a1f6d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 9 May 2024 10:53:32 -0500 Subject: [PATCH 350/505] Only assert 7 hex digits, not 8 The CI failed when a 7-digit hex code did not match the expectation. --- tests/test_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index c858b672..042b436c 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -30,7 +30,7 @@ def test_jimport(self): assert str(Object) o = Object() assert scyjava.jinstance(o, "java.lang.Object") - assert re.match("java.lang.Object@[0-9a-f]{8}", str(o.toString())) + assert re.match("java.lang.Object@[0-9a-f]{7}", str(o.toString())) def test_jinstance(self): """ From aa3876b8bfe078ac5cd3a3bc60d82cd856c55959 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 11 Jun 2024 11:13:52 -0500 Subject: [PATCH 351/505] Release version 1.10.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f3065760..81c8a35d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.0.dev0" +version = "1.10.0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 6868f0cad9c8dc4714ac91322623122433952176 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 11 Jun 2024 11:23:21 -0500 Subject: [PATCH 352/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 81c8a35d..73b85600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.0" +version = "1.10.1.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From da30759cad8450e29b990aa25ce639f15d70b245 Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Sat, 20 Jul 2024 17:26:44 -0700 Subject: [PATCH 353/505] chore: move flake8-typing-imports to pip section --- dev-environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-environment.yml b/dev-environment.yml index ca3722a5..f407d73e 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -32,7 +32,6 @@ dependencies: - black - build - flake8 - - flake8-typing-imports - isort - pytest - pytest-cov @@ -42,5 +41,6 @@ dependencies: - pip: - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - flake8-pyproject + - flake8-typing-imports - validate-pyproject[all] - -e . From a7a151791d810df6bf04095cf214ad2ca9ff919b Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Sat, 20 Jul 2024 17:27:49 -0700 Subject: [PATCH 354/505] chore: add assertpy for readable test failures --- dev-environment.yml | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/dev-environment.yml b/dev-environment.yml index f407d73e..03444184 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -28,6 +28,7 @@ dependencies: - numpy - pandas # Developer tools + - assertpy - autopep8 - black - build diff --git a/pyproject.toml b/pyproject.toml index 73b85600..56679eb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ [project.optional-dependencies] # NB: Keep this in sync with dev-environment.yml! dev = [ + "assertpy", "autopep8", "black", "build", From a1e48da56007ee98d70aab2db9e2f5a4cde82e2b Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Sat, 20 Jul 2024 17:28:23 -0700 Subject: [PATCH 355/505] chore: refactor java_heap tests for readable errors --- tests/it/java_heap.py | 44 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index 4f51c74d..fafa58a9 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -1,34 +1,28 @@ """ -Test scyjava JVM memory-related functions. -""" - -import scyjava - -mb_initial = 50 # initial MB of memory to snarf up + Test scyjava JVM memory-related functions. + """ + import math + from assertpy import assert_that -scyjava.config.set_heap_min(mb=mb_initial) -scyjava.config.set_heap_max(gb=1) + import scyjava -assert not scyjava.jvm_started() -scyjava.start_jvm() + mb_initial = 50 # initial MB of memory to snarf up -assert scyjava.available_processors() >= 1 + scyjava.config.set_heap_min(mb=mb_initial) + scyjava.config.set_heap_max(gb=1) -mb_max = scyjava.memory_max() // 1024 // 1024 -mb_total = scyjava.memory_total() // 1024 // 1024 -mb_used = scyjava.memory_used() // 1024 // 1024 + assert not scyjava.jvm_started() + scyjava.start_jvm() -# Used memory should be less than the current memory total, -# which should be less than the maximum heap size. -assert mb_used <= mb_total <= mb_max, f"{mb_used=} {mb_total=} {mb_max=}" + assert scyjava.available_processors() >= 1 -# The maximum heap size should be approximately 1 GB. -assert 900 <= mb_max <= 1024, f"{mb_max=}" + mb_max = scyjava.memory_max() // 1024 // 1024 + mb_total = scyjava.memory_total() // 1024 // 1024 + mb_used = scyjava.memory_used() // 1024 // 1024 -# Most of that memory should still be free; i.e., -# we should not be using more than a few MB yet. -assert mb_used <= 5, f"{mb_used=}" + assert_that(mb_used, 'Used memory should be less than the current memory total').is_less_than_or_equal_to(mb_total) + assert_that(mb_total, 'current memory total should be less than maximum memory').is_less_than_or_equal_to(mb_max) + assert_that(mb_max, 'maximum heap size should be approx. 1 GB').is_between(900, 1024) -# The total MB available to Java at this moment -# should be close to our requested initial amount. -assert abs(mb_total - mb_initial) < 5, f"{mb_total=} {mb_initial=}" + assert_that(mb_used, 'most memory should be available').is_less_than(5) + assert_that(mb_total, 'total memory should be close to initial').is_close_to(mb_initial, tolerance=5) \ No newline at end of file From 56df79931bdc584d2fc33138652a74d084f78278 Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Sat, 20 Jul 2024 17:29:18 -0700 Subject: [PATCH 356/505] fix: loosen tolerance for comparing memory consumption --- tests/it/java_heap.py | 45 +++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index fafa58a9..e8d5a869 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -1,28 +1,35 @@ """ - Test scyjava JVM memory-related functions. - """ - import math - from assertpy import assert_that +Test scyjava JVM memory-related functions. +""" +import math +from assertpy import assert_that + +import scyjava + + +def magnitude(x: int) -> int: + return math.floor(math.log10(abs(x))) + - import scyjava +mb_initial = 50 # initial MB of memory to snarf up - mb_initial = 50 # initial MB of memory to snarf up +scyjava.config.set_heap_min(mb=mb_initial) +scyjava.config.set_heap_max(gb=1) - scyjava.config.set_heap_min(mb=mb_initial) - scyjava.config.set_heap_max(gb=1) +assert not scyjava.jvm_started() +scyjava.start_jvm() - assert not scyjava.jvm_started() - scyjava.start_jvm() +assert scyjava.available_processors() >= 1 - assert scyjava.available_processors() >= 1 +mb_max = scyjava.memory_max() // 1024 // 1024 +mb_total = scyjava.memory_total() // 1024 // 1024 +mb_used = scyjava.memory_used() // 1024 // 1024 - mb_max = scyjava.memory_max() // 1024 // 1024 - mb_total = scyjava.memory_total() // 1024 // 1024 - mb_used = scyjava.memory_used() // 1024 // 1024 +assert_that(mb_used, 'Used memory should be less than the current memory total').is_less_than_or_equal_to(mb_total) +assert_that(mb_total, 'current memory total should be less than maximum memory').is_less_than_or_equal_to(mb_max) +assert_that(mb_max, 'maximum heap size should be approx. 1 GB').is_between(900, 1024) - assert_that(mb_used, 'Used memory should be less than the current memory total').is_less_than_or_equal_to(mb_total) - assert_that(mb_total, 'current memory total should be less than maximum memory').is_less_than_or_equal_to(mb_max) - assert_that(mb_max, 'maximum heap size should be approx. 1 GB').is_between(900, 1024) +tolerance = pow(10, magnitude(mb_initial)) - assert_that(mb_used, 'most memory should be available').is_less_than(5) - assert_that(mb_total, 'total memory should be close to initial').is_close_to(mb_initial, tolerance=5) \ No newline at end of file +assert_that(mb_used, 'most memory should be available').is_less_than(tolerance) +assert_that(mb_total, 'total memory should be close to initial').is_close_to(mb_initial, tolerance=tolerance) \ No newline at end of file From fa38c5261e8736f525abcc3d099166c36e9bb344 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 23 Jul 2024 16:46:11 -0500 Subject: [PATCH 357/505] Make the linter happy again --- tests/it/java_heap.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index e8d5a869..93bd81ec 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -1,7 +1,9 @@ """ Test scyjava JVM memory-related functions. """ + import math + from assertpy import assert_that import scyjava @@ -25,11 +27,17 @@ def magnitude(x: int) -> int: mb_total = scyjava.memory_total() // 1024 // 1024 mb_used = scyjava.memory_used() // 1024 // 1024 -assert_that(mb_used, 'Used memory should be less than the current memory total').is_less_than_or_equal_to(mb_total) -assert_that(mb_total, 'current memory total should be less than maximum memory').is_less_than_or_equal_to(mb_max) -assert_that(mb_max, 'maximum heap size should be approx. 1 GB').is_between(900, 1024) +assert_that( + mb_used, "Used memory should be less than the current memory total" +).is_less_than_or_equal_to(mb_total) +assert_that( + mb_total, "current memory total should be less than maximum memory" +).is_less_than_or_equal_to(mb_max) +assert_that(mb_max, "maximum heap size should be approx. 1 GB").is_between(900, 1024) tolerance = pow(10, magnitude(mb_initial)) -assert_that(mb_used, 'most memory should be available').is_less_than(tolerance) -assert_that(mb_total, 'total memory should be close to initial').is_close_to(mb_initial, tolerance=tolerance) \ No newline at end of file +assert_that(mb_used, "most memory should be available").is_less_than(tolerance) +assert_that(mb_total, "total memory should be close to initial").is_close_to( + mb_initial, tolerance=tolerance +) From eec97e89d1447d1617d382896a04e670c7cea3c0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 23 Jul 2024 16:52:20 -0500 Subject: [PATCH 358/505] Simplify tolerance reasoning in java_heap test --- tests/it/java_heap.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index 93bd81ec..0d0461a9 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -2,18 +2,12 @@ Test scyjava JVM memory-related functions. """ -import math - from assertpy import assert_that import scyjava - -def magnitude(x: int) -> int: - return math.floor(math.log10(abs(x))) - - mb_initial = 50 # initial MB of memory to snarf up +mb_tolerance = 10 # ceiling of expected MB in use scyjava.config.set_heap_min(mb=mb_initial) scyjava.config.set_heap_max(gb=1) @@ -35,9 +29,7 @@ def magnitude(x: int) -> int: ).is_less_than_or_equal_to(mb_max) assert_that(mb_max, "maximum heap size should be approx. 1 GB").is_between(900, 1024) -tolerance = pow(10, magnitude(mb_initial)) - -assert_that(mb_used, "most memory should be available").is_less_than(tolerance) +assert_that(mb_used, "most memory should be available").is_less_than(mb_tolerance) assert_that(mb_total, "total memory should be close to initial").is_close_to( - mb_initial, tolerance=tolerance + mb_initial, tolerance=mb_tolerance ) From b414c552ba59816d1e5f63d11e5402c6a7df0483 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 23 Jul 2024 16:56:47 -0500 Subject: [PATCH 359/505] Test Pythons from 3.8 - 3.12 But still only three Pythons. Five Pythons is too many Pythons. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7aa9b17a..a7c3422c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,8 +23,8 @@ jobs: ] python-version: [ '3.8', - '3.9', - '3.10' + '3.10', + '3.12' ] steps: From 5662f94ac19d3427403e8e1505cca7df1b328ce9 Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Wed, 24 Jul 2024 21:06:48 -0700 Subject: [PATCH 360/505] add ruff and remove everything it replaces --- Makefile | 3 +++ bin/fmt.sh | 11 +++++++++++ bin/lint.sh | 8 ++------ dev-environment.yml | 8 +------- pyproject.toml | 28 ++++++++++++---------------- 5 files changed, 29 insertions(+), 29 deletions(-) create mode 100755 bin/fmt.sh diff --git a/Makefile b/Makefile index a2eaae67..5827c144 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ check: lint: check bin/lint.sh +fmt: check + bin/fmt.sh + test: check bin/test.sh diff --git a/bin/fmt.sh b/bin/fmt.sh new file mode 100755 index 00000000..cd04d02e --- /dev/null +++ b/bin/fmt.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +dir=$(dirname "$0") +cd "$dir/.." + +exitCode=0 +ruff check --fix +code=$?; test $code -eq 0 || exitCode=$code +ruff format +code=$?; test $code -eq 0 || exitCode=$code +exit $exitCode diff --git a/bin/lint.sh b/bin/lint.sh index 3d90d260..8ad9456a 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,12 +4,8 @@ dir=$(dirname "$0") cd "$dir/.." exitCode=0 -black src tests +ruff check code=$?; test $code -eq 0 || exitCode=$code -isort src tests -code=$?; test $code -eq 0 || exitCode=$code -python -m flake8 src tests -code=$?; test $code -eq 0 || exitCode=$code -validate-pyproject pyproject.toml +ruff format --check code=$?; test $code -eq 0 || exitCode=$code exit $exitCode diff --git a/dev-environment.yml b/dev-environment.yml index 03444184..0f58783b 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -29,19 +29,13 @@ dependencies: - pandas # Developer tools - assertpy - - autopep8 - - black - build - - flake8 - - isort - pytest - pytest-cov + - ruff - toml # Project from source - pip - pip: - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - - flake8-pyproject - - flake8-typing-imports - - validate-pyproject[all] - -e . diff --git a/pyproject.toml b/pyproject.toml index 56679eb4..5c0c3eb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,20 +40,14 @@ dependencies = [ # NB: Keep this in sync with dev-environment.yml! dev = [ "assertpy", - "autopep8", - "black", "build", - "flake8", - "flake8-pyproject", - "flake8-typing-imports", - "isort", "jep", "pytest", "pytest-cov", "numpy", "pandas", - "toml", - "validate-pyproject[all]", + "ruff", + "toml" ] [project.urls] @@ -72,13 +66,15 @@ where = ["src"] namespaces = false # Thanks to Flake8-pyproject, we can configure flake8 here! -[tool.flake8] -exclude = ["bin", "build", "dist"] +[tool.ruff] +line-length = 88 +src = ["src", "tests"] +include = ["pyproject.toml", "src/**/*.py", "tests/**/*.py"] +extend-exclude = ["bin", "build", "dist"] + +[tool.ruff.lint] extend-ignore = ["E203"] -# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 -max-line-length = 88 -min_python_version = "3.8" -per-file-ignores = "__init__.py:F401" -[tool.isort] -profile = "black" +[tool.ruff.lint.per-file-ignores] +# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +"__init__.py" = ["E402", "F401"] From 8e2eea4f68e6c7e88b9ad8288eadae7966bcdca8 Mon Sep 17 00:00:00 2001 From: jschneidereit Date: Thu, 25 Jul 2024 12:20:51 -0700 Subject: [PATCH 361/505] replace various clean code tools with ruff --- .github/workflows/build.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7c3422c..daeadfeb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,22 +56,10 @@ jobs: - uses: actions/setup-python@v3 - name: Lint code - uses: psf/black@stable - - - name: Flake code - run: | - python -m pip install flake8 Flake8-pyproject flake8-typing-imports - python -m flake8 src tests - - - name: Check import ordering - uses: isort/isort-action@master - with: - configuration: --check-only - - - name: Validate pyproject.toml run: | - python -m pip install validate-pyproject[all] - python -m validate_pyproject pyproject.toml + python -m pip install ruff + ruff check + ruff format --check conda-dev-test: name: Conda Setup & Code Coverage From 8eadd12533af31fd75f0e11ce0e997ccd16e425e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 12 Aug 2024 11:19:01 -0500 Subject: [PATCH 362/505] Remove tainted default channel See: * https://www.anaconda.com/blog/is-conda-free * https://www.theregister.com/2024/08/08/anaconda_puts_the_squeeze_on/ --- dev-environment.yml | 1 - environment.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index 03444184..67379769 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -17,7 +17,6 @@ name: scyjava-dev channels: - conda-forge - - defaults dependencies: - python >= 3.8 # Project dependencies diff --git a/environment.yml b/environment.yml index c225059a..c1038c57 100644 --- a/environment.yml +++ b/environment.yml @@ -18,7 +18,6 @@ name: scyjava channels: - conda-forge - - defaults dependencies: - python >= 3.8 # Project dependencies From 45cb7525eb7387a5347df2308b7c5297747d53f0 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 23 Aug 2024 11:16:15 -0500 Subject: [PATCH 363/505] Bump openjdk version to 11 This commit resolves an unusual bug where pytest fails to collect the results of the jep test, resulting in failing CI jobs. I was able to reproduce this locally, sometimes, with openjdk=8. There must be a race condition involved with the bug as it doesn't always fail. The crash is a seg fault. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index daeadfeb..924b789a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/setup-java@v3 with: - java-version: '8' + java-version: '11' distribution: 'zulu' cache: 'maven' From 2dcf3dbbd06f900cbe25b09e87d640356809fe1d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 23 Aug 2024 13:17:12 -0500 Subject: [PATCH 364/505] Move build dependency to pip section Because we are no longer using the anaconda default channel we need to obtain build from pypi. --- dev-environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-environment.yml b/dev-environment.yml index 0f58783b..6d79470a 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -29,7 +29,6 @@ dependencies: - pandas # Developer tools - assertpy - - build - pytest - pytest-cov - ruff @@ -38,4 +37,5 @@ dependencies: - pip - pip: - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d + - build - -e . From a0ae875ca77c70976277cb3b0bf59a682f1b84cf Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 23 Aug 2024 13:50:25 -0500 Subject: [PATCH 365/505] Replace old pre-commit config with ruff config See: https://github.com/astral-sh/ruff-pre-commit --- .pre-commit-config.yaml | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad95eb31..06533676 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,29 +1,8 @@ -repos: - # First, autoflake the code to avoid issues that could be solved quickly - - repo: https://github.com/myint/autoflake - rev: v1.4 - hooks: - - id: autoflake - args: ["--in-place", "--remove-all-unused-imports"] - # Then, flake - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: - - "flake8-typing-imports" - - "Flake8-pyproject" - # Next, sort imports - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 - hooks: - - id: isort - # Finally, lint - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.10.1 - hooks: - - id: validate-pyproject +- repo: https://github.com/astral-sh/ruff-pre-commit + # ruff version + rev: v0.6.2 + hooks: + # run the linter + - id: ruff + # run the formatter + - id: ruff-format From 305d54f9afbb5960c8b96ceed73f7ff2d6274b49 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 26 Aug 2024 11:32:36 -0500 Subject: [PATCH 366/505] Move build dependency to conda-forge channel Because we can no longer use the default conda channel we have to source `build` from either pypi or the conda-forge channel. The `build` package is aliased as `python-build` on the conda-forge channel. --- dev-environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-environment.yml b/dev-environment.yml index 38729e64..40c14833 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -30,11 +30,11 @@ dependencies: - assertpy - pytest - pytest-cov + - python-build - ruff - toml # Project from source - pip - pip: - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - - build - -e . From ec34f70e019fd6b0710c5cc4fa7fea92375c0487 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 26 Aug 2024 15:41:53 -0500 Subject: [PATCH 367/505] Add validate-pyproject back to lint checks --- .github/workflows/build.yml | 5 +++++ bin/lint.sh | 2 ++ dev-environment.yml | 1 + pyproject.toml | 3 ++- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 924b789a..1720747a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,11 @@ jobs: ruff check ruff format --check + - name: Validate pyproject.toml + run: | + python -m pip install validate-pyproject[all] + python -m validate_pyproject pyproject.toml + conda-dev-test: name: Conda Setup & Code Coverage runs-on: ubuntu-latest diff --git a/bin/lint.sh b/bin/lint.sh index 8ad9456a..1cf86826 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -8,4 +8,6 @@ ruff check code=$?; test $code -eq 0 || exitCode=$code ruff format --check code=$?; test $code -eq 0 || exitCode=$code +validate-pyproject pyproject.toml +code=$?; test $code -eq 0 || exitCode=$code exit $exitCode diff --git a/dev-environment.yml b/dev-environment.yml index 40c14833..d8d25682 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -33,6 +33,7 @@ dependencies: - python-build - ruff - toml + - validate-pyproject # Project from source - pip - pip: diff --git a/pyproject.toml b/pyproject.toml index 5c0c3eb2..7e768074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ dev = [ "numpy", "pandas", "ruff", - "toml" + "toml", + "validate-pyproject[all]" ] [project.urls] From 6bb37f8fe45ef4a802503e660925daf4131bc548 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Tue, 1 Oct 2024 15:25:37 -0500 Subject: [PATCH 368/505] CI: Use Miniforge over Mambaforge Mambaforge is being sunsetted by the end of 2024 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1720747a..7dfdfdcd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: # Create env with dev packages auto-update-conda: true python-version: 3.9 - miniforge-variant: Mambaforge + miniforge-version: latest environment-file: dev-environment.yml # Activate scyjava-dev environment activate-environment: scyjava-dev From 2195b1350a216437deeec4ad3252176b9c30ad43 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 14 Oct 2024 16:49:24 -0500 Subject: [PATCH 369/505] Remove no longer relevant comment --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e768074..f5d3df55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ include-package-data = false where = ["src"] namespaces = false -# Thanks to Flake8-pyproject, we can configure flake8 here! +# ruff configuration [tool.ruff] line-length = 88 src = ["src", "tests"] From d05ba2cf220ad1c05b0ad75260a08b15234c560e Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 14 Oct 2024 17:05:45 -0500 Subject: [PATCH 370/505] Add validate-pyproject back to pre-commit ruff does not validate the pyproject.toml. We need to add the validate-pyproject hook back to pre-commit. --- .pre-commit-config.yaml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06533676..db349533 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,13 @@ -- repo: https://github.com/astral-sh/ruff-pre-commit - # ruff version - rev: v0.6.2 - hooks: - # run the linter - - id: ruff - # run the formatter - - id: ruff-format +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # ruff version + rev: v0.6.2 + hooks: + # run the linter + - id: ruff + # run the formatter + - id: ruff-format + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.10.1 + hooks: + - id: validate-pyproject From a607b5c93d03af695916e25527e67ea9fbbced7a Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 3 Dec 2024 14:44:00 -0600 Subject: [PATCH 371/505] Limit jpype1 highest version to 1.5.0 jpype version 1.5.1 fails to start the JVM on Windows for currently unknown reasons. See #70 and jpype-project/jpype#1242. --- dev-environment.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index d8d25682..2243e1c0 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -20,7 +20,7 @@ channels: dependencies: - python >= 3.8 # Project dependencies - - jpype1 >= 1.3.0 + - jpype1 >= 1.3.0, <= 1.5.0 - jgo - openjdk >= 8, < 12 # Test dependencies diff --git a/environment.yml b/environment.yml index c1038c57..b5bfa733 100644 --- a/environment.yml +++ b/environment.yml @@ -21,7 +21,7 @@ channels: dependencies: - python >= 3.8 # Project dependencies - - jpype1 >= 1.3.0 + - jpype1 >= 1.3.0, <= 1.5.0 - jgo - openjdk >= 8 # Project from source diff --git a/pyproject.toml b/pyproject.toml index f5d3df55..fcfbaa38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ # NB: Keep this in sync with environment.yml AND dev-environment.yml! requires-python = ">=3.8" dependencies = [ - "jpype1 >= 1.3.0", + "jpype1 >= 1.3.0, <= 1.5.0", "jgo", ] From ccce61c4cbbe310944b33644865d8f7d7995699d Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 3 Dec 2024 14:53:32 -0600 Subject: [PATCH 372/505] Add py311 and py312 classifiers to the toml This matches our configuration for pyimagej. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fcfbaa38..fb17f5bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: The Unlicense (Unlicense)", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", From 900d251aa2a221916846d9b1a91af13c5cdb303a Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 11 Dec 2024 10:36:01 -0600 Subject: [PATCH 373/505] Release version 1.10.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fb17f5bc..5683aa53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.1.dev0" +version = "1.10.1" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 7816c3a91bd48505e56206bffb31e95c7051b0e0 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 11 Dec 2024 10:38:49 -0600 Subject: [PATCH 374/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5683aa53..d0b7197d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.1" +version = "1.10.2.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 1768e62be3f86b7a60225289b69ba28944c853a3 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Wed, 15 Jan 2025 10:32:18 -0600 Subject: [PATCH 375/505] Bump pre-commit ruff rev to 0.9.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db349533..08149aa4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # ruff version - rev: v0.6.2 + rev: v0.9.1 hooks: # run the linter - id: ruff From d96e3b7ccf4afa0316aeaae0b80b40b7606af306 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 29 Jan 2025 15:13:06 -0600 Subject: [PATCH 376/505] CI: upgrade actions/cache from v2 to v4 See https://github.com/actions/toolkit/discussions/1890 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7dfdfdcd..41578cfa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Cache conda - uses: actions/cache@v2 + uses: actions/cache@v4 env: # Increase this value to reset cache if dev-environment.yml has not changed CACHE_NUMBER: 0 From 09c236e882f8a4d0974527dbc64dfb1ac6748054 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 29 Jan 2025 16:09:03 -0600 Subject: [PATCH 377/505] CI: try setup-miniconda@v3 to fix Miniforge setup --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 41578cfa..8b246a01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,7 +85,7 @@ jobs: path: ~/conda_pkgs_dir key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('dev-environment.yml') }} - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: # Create env with dev packages auto-update-conda: true From 4c48a8bb0f7b51e0e7b37408f9f397a574434424 Mon Sep 17 00:00:00 2001 From: Sameeul Samee Date: Mon, 3 Feb 2025 10:22:05 -0500 Subject: [PATCH 378/505] Relax jpype1 version constrain --- dev-environment.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index 2243e1c0..d8d25682 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -20,7 +20,7 @@ channels: dependencies: - python >= 3.8 # Project dependencies - - jpype1 >= 1.3.0, <= 1.5.0 + - jpype1 >= 1.3.0 - jgo - openjdk >= 8, < 12 # Test dependencies diff --git a/environment.yml b/environment.yml index b5bfa733..c1038c57 100644 --- a/environment.yml +++ b/environment.yml @@ -21,7 +21,7 @@ channels: dependencies: - python >= 3.8 # Project dependencies - - jpype1 >= 1.3.0, <= 1.5.0 + - jpype1 >= 1.3.0 - jgo - openjdk >= 8 # Project from source diff --git a/pyproject.toml b/pyproject.toml index d0b7197d..93f10f3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ # NB: Keep this in sync with environment.yml AND dev-environment.yml! requires-python = ">=3.8" dependencies = [ - "jpype1 >= 1.3.0, <= 1.5.0", + "jpype1 >= 1.3.0", "jgo", ] From ed767c4fb5b1914fb382270ec233bee5e4cab5b0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 3 Feb 2025 20:08:19 -0600 Subject: [PATCH 379/505] Release version 1.10.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 93f10f3e..1a894825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.2.dev0" +version = "1.10.2" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From b66fc06cd592cc21c14f94ea49c950622a912dc7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 3 Feb 2025 20:11:39 -0600 Subject: [PATCH 380/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1a894825..bbcc8b80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.10.2" +version = "1.10.3.dev0" description = "Supercharged Java access from Python" license = {text = "The Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 5b6195aa77900cad183b2d606b6e4c956700456c Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 13 Feb 2025 10:48:41 -0600 Subject: [PATCH 381/505] Combine output lines when checking version Suggested by @gselzer to work around failure to detect Java version noted in https://forum.image.sc/t/pyimagej-on-windows-10/107934/11 when JAVA_TOOL_OPTIONS is set --- src/scyjava/_jvm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 2e2350a8..d150ddcb 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -98,6 +98,7 @@ def jvm_version() -> str: except subprocess.CalledProcessError as e: raise RuntimeError("System call to java failed") from e + output = output.replace('\n', ' ').replace('\r', '') m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output) if not m: raise RuntimeError(f"Inscrutable java command output:\n{output}") From d1583a1bb4b1fc795829bdb877eb2e8ba46af49c Mon Sep 17 00:00:00 2001 From: hinerm Date: Thu, 13 Feb 2025 10:53:49 -0600 Subject: [PATCH 382/505] Formatting fix --- src/scyjava/_jvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index d150ddcb..1a6c5ca1 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -98,7 +98,7 @@ def jvm_version() -> str: except subprocess.CalledProcessError as e: raise RuntimeError("System call to java failed") from e - output = output.replace('\n', ' ').replace('\r', '') + output = output.replace("\n", " ").replace("\r", "") m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output) if not m: raise RuntimeError(f"Inscrutable java command output:\n{output}") From f55d40f55c694bd610948990e5f3c208715b498d Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 20 Mar 2025 16:00:40 -0700 Subject: [PATCH 383/505] Fix python scripting to allow global imports and added testfile --- src/scyjava/_script.py | 3 +- tests/it/script_scope.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/it/script_scope.py diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index ec73f906..7ce10d60 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -91,7 +91,8 @@ def apply(self, arg): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - _globals = {} + _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} + exec( compile(block, "", mode="exec"), _globals, script_locals ) diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py new file mode 100644 index 00000000..9965d7f1 --- /dev/null +++ b/tests/it/script_scope.py @@ -0,0 +1,61 @@ +""" +Test the enable_python_scripting function, but here explictly testing import scope for declared functions. +""" + +import sys + +import scyjava + +scyjava.config.endpoints.extend( + ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] +) + +# Create minimal SciJava context with a ScriptService. +Context = scyjava.jimport("org.scijava.Context") +ScriptService = scyjava.jimport("org.scijava.script.ScriptService") +# HACK: Avoid "[ERROR] Cannot create plugin" spam. +WidgetService = scyjava.jimport("org.scijava.widget.WidgetService") +ctx = Context(ScriptService, WidgetService) + +# Enable the Python script language. +scyjava.enable_python_scripting(ctx) + +# Assert that the Python script language is available. +ss = ctx.service("org.scijava.script.ScriptService") +lang = ss.getLanguageByName("Python") +assert lang is not None and "Python" in lang.getNames() + +# Construct a script. +script = """ +#@ int age +#@output String cbrt_age +import math + +def calculate_cbrt(age): + return round(math.cbrt(age)) + +cbrt_age = calculate_cbrt(age) +# cbrt_age = round(math.cbrt(age)) +f"The rounded cube root of my age is {cbrt_age}" +""" +StringReader = scyjava.jimport("java.io.StringReader") +ScriptInfo = scyjava.jimport("org.scijava.script.ScriptInfo") +info = ScriptInfo(ctx, "script.py", StringReader(script)) +info.setLanguage(lang) + +# Run the script. +future = ss.run(info, True, "age", 13) +try: + module = future.get() + outputs = module.getOutputs() + statement = outputs["cbrt_age"] + return_value = module.getReturnValue() +except Exception as e: + sys.stderr.write("-- SCRIPT EXECUTION FAILED --\n") + trace = scyjava.jstacktrace(e) + if trace: + sys.stderr.write(f"{trace}\n") + raise e + +assert return_value == "The rounded cube root of my age is 2" +assert statement == "2" From e260032af6f47fa2e440628e66f9d54413d8f6f6 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 21 Mar 2025 00:26:07 -0700 Subject: [PATCH 384/505] Updated tests, fixed scoping issues --- src/scyjava/_script.py | 12 ++++++++---- tests/it/script_scope.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 7ce10d60..0ba1b280 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -91,15 +91,19 @@ def apply(self, arg): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} - + # _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} + # _globals = {__builtins__: builtins, '__name__': '__main__','__file__': '', '__package__': None,} + # _globals.update(globals()) + # _globals = None + # _globals = locals() + script_globals = script_locals exec( - compile(block, "", mode="exec"), _globals, script_locals + compile(block, "", mode="exec"), script_globals, script_locals ) if last is not None: return_value = eval( compile(last, "", mode="eval"), - _globals, + script_globals, script_locals, ) except Exception: diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index 9965d7f1..2374fb8f 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -29,6 +29,7 @@ script = """ #@ int age #@output String cbrt_age +import numpy as np import math def calculate_cbrt(age): @@ -57,5 +58,5 @@ def calculate_cbrt(age): sys.stderr.write(f"{trace}\n") raise e -assert return_value == "The rounded cube root of my age is 2" assert statement == "2" +assert return_value == "The rounded cube root of my age is 2" From e02e167e8ca59b2889e69fbe12edf4496ffb2332 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 21 Mar 2025 00:32:55 -0700 Subject: [PATCH 385/505] Added documentation --- src/scyjava/_script.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 0ba1b280..2d86cd8e 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -90,20 +90,18 @@ def apply(self, arg): ): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - - # _globals = {name: module for name, module in sys.modules.items() if name != '__main__'} - # _globals = {__builtins__: builtins, '__name__': '__main__','__file__': '', '__package__': None,} - # _globals.update(globals()) - # _globals = None - # _globals = locals() - script_globals = script_locals + # See here for why this implementation: https://docs.python.org/3/library/functions.html#exec + # When `exec` gets two separate objects as *globals* and *locals*, the code will be executed as if it were embedded in a class definition. + # This means functions and classes defined in the executed code will not be able to access variables assigned at the top level + # (as the “top level” variables are treated as class variables in a class definition). + _globals = script_locals exec( - compile(block, "", mode="exec"), script_globals, script_locals + compile(block, "", mode="exec"), _globals, script_locals ) if last is not None: return_value = eval( compile(last, "", mode="eval"), - script_globals, + _globals, script_locals, ) except Exception: From cb577c07542464a1baeaa3f2666ba5e52a3e5f58 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 21 Mar 2025 11:00:34 -0500 Subject: [PATCH 386/505] Fix up exec comment to please ruff --- src/scyjava/_script.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index 2d86cd8e..fef0a0e0 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -90,11 +90,16 @@ def apply(self, arg): ): # Last statement looks like an expression. Evaluate! last = ast.Expression(block.body.pop().value) - # See here for why this implementation: https://docs.python.org/3/library/functions.html#exec - # When `exec` gets two separate objects as *globals* and *locals*, the code will be executed as if it were embedded in a class definition. - # This means functions and classes defined in the executed code will not be able to access variables assigned at the top level - # (as the “top level” variables are treated as class variables in a class definition). + + # NB: When `exec` gets two separate objects as *globals* and + # *locals*, the code will be executed as if it were embedded in + # a class definition. This means functions and classes defined + # in the executed code will not be able to access variables + # assigned at the top level, because the "top level" variables + # are treated as class variables in a class definition. + # See: https://docs.python.org/3/library/functions.html#exec _globals = script_locals + exec( compile(block, "", mode="exec"), _globals, script_locals ) From 835ead7838a20069abd0c821e7de0f3b7a52b2a2 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 21 Mar 2025 12:49:22 -0500 Subject: [PATCH 387/505] Use assertpy in integration tests So that when assertions fail, we get more information about how. --- tests/it/awt.py | 9 ++++++--- tests/it/headless.py | 15 +++++++-------- tests/it/java_heap.py | 9 +++++---- tests/it/jvm_version.py | 18 ++++++++++-------- tests/it/script_scope.py | 9 ++++++--- tests/it/scripting.py | 11 ++++++++--- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/tests/it/awt.py b/tests/it/awt.py index 452cc309..9e746715 100644 --- a/tests/it/awt.py +++ b/tests/it/awt.py @@ -7,11 +7,14 @@ import scyjava +from assertpy import assert_that + if platform.system() == "Darwin": # NB: This test would hang on macOS, due to AWT threading issues. sys.exit(0) -assert not scyjava.jvm_started() +assert_that(scyjava.jvm_started()).is_false() + scyjava.start_jvm() if scyjava.is_jvm_headless(): @@ -21,9 +24,9 @@ # In that case, we are not able to perform this test. sys.exit(0) -assert not scyjava.is_awt_initialized() +assert_that(scyjava.is_awt_initialized()).is_false() Frame = scyjava.jimport("java.awt.Frame") f = Frame() -assert scyjava.is_awt_initialized() +assert_that(scyjava.is_awt_initialized()).is_true() diff --git a/tests/it/headless.py b/tests/it/headless.py index 97ee3852..6f21f376 100644 --- a/tests/it/headless.py +++ b/tests/it/headless.py @@ -4,16 +4,15 @@ import scyjava +from assertpy import assert_that + scyjava.config.enable_headless_mode() -assert not scyjava.jvm_started() +assert_that(scyjava.jvm_started()).is_false() scyjava.start_jvm() - -assert scyjava.is_jvm_headless() +assert_that(scyjava.is_jvm_headless()).is_true() Frame = scyjava.jimport("java.awt.Frame") -try: - f = Frame() - assert False, "HeadlessException should have occurred" -except Exception as e: - assert "java.awt.HeadlessException" == str(e) +assert_that(Frame).raises(Exception).when_called_with().is_equal_to( + "java.awt.HeadlessException" +) diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index 0d0461a9..77267ae3 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -2,20 +2,21 @@ Test scyjava JVM memory-related functions. """ -from assertpy import assert_that - import scyjava +from assertpy import assert_that + mb_initial = 50 # initial MB of memory to snarf up mb_tolerance = 10 # ceiling of expected MB in use scyjava.config.set_heap_min(mb=mb_initial) scyjava.config.set_heap_max(gb=1) -assert not scyjava.jvm_started() +assert_that(scyjava.jvm_started()).is_false() + scyjava.start_jvm() -assert scyjava.available_processors() >= 1 +assert_that(scyjava.available_processors()).is_greater_than_or_equal_to(1) mb_max = scyjava.memory_max() // 1024 // 1024 mb_total = scyjava.memory_total() // 1024 // 1024 diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py index 669875bf..a79d20bc 100644 --- a/tests/it/jvm_version.py +++ b/tests/it/jvm_version.py @@ -4,19 +4,21 @@ import scyjava -assert not scyjava.jvm_started() +from assertpy import assert_that + +assert_that(scyjava.jvm_started()).is_false() before_version = scyjava.jvm_version() -assert before_version is not None -assert len(before_version) >= 3 -assert before_version[0] > 0 +assert_that(before_version).is_not_none() +assert_that(len(before_version)).is_greater_than_or_equal_to(3) +assert_that(before_version[0]).is_greater_than(0) scyjava.config.enable_headless_mode() scyjava.start_jvm() after_version = scyjava.jvm_version() -assert after_version is not None -assert len(after_version) >= 3 -assert after_version[0] > 0 +assert_that(after_version).is_not_none() +assert_that(len(after_version)).is_greater_than_or_equal_to(3) +assert_that(after_version[0]).is_greater_than(0) -assert before_version == after_version +assert_that(before_version).is_equal_to(after_version) diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index 2374fb8f..45dbedcd 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -6,6 +6,8 @@ import scyjava +from assertpy import assert_that + scyjava.config.endpoints.extend( ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] ) @@ -23,7 +25,8 @@ # Assert that the Python script language is available. ss = ctx.service("org.scijava.script.ScriptService") lang = ss.getLanguageByName("Python") -assert lang is not None and "Python" in lang.getNames() +assert_that(lang).is_not_none() +assert_that(lang.getNames()).contains("Python") # Construct a script. script = """ @@ -58,5 +61,5 @@ def calculate_cbrt(age): sys.stderr.write(f"{trace}\n") raise e -assert statement == "2" -assert return_value == "The rounded cube root of my age is 2" +assert_that(statement).is_equal_to("2") +assert_that(return_value).is_equal_to("The rounded cube root of my age is 2") diff --git a/tests/it/scripting.py b/tests/it/scripting.py index 0a8bf684..48d24b5a 100644 --- a/tests/it/scripting.py +++ b/tests/it/scripting.py @@ -9,6 +9,8 @@ import scyjava +from assertpy import assert_that + scyjava.config.endpoints.extend( ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] ) @@ -26,7 +28,8 @@ # Assert that the Python script language is available. ss = ctx.service("org.scijava.script.ScriptService") lang = ss.getLanguageByName("Python") -assert lang is not None and "Python" in lang.getNames() +assert_that(lang).is_not_none() +assert_that(lang.getNames()).contains("Python") # Construct a script. script = """ @@ -55,5 +58,7 @@ sys.stderr.write(f"{trace}\n") raise e -assert statement == "Hello, Chuckles! In one year you will be 14 years old." -assert return_value == "A wild return value appears!" +assert_that(statement).is_equal_to( + "Hello, Chuckles! In one year you will be 14 years old." +) +assert_that(return_value).is_equal_to("A wild return value appears!") From e4aeebafd3bd481134002407e8b0cef7f7d247e1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 21 Mar 2025 14:02:19 -0500 Subject: [PATCH 388/505] Print script errors even when no error writer --- src/scyjava/_script.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index fef0a0e0..a371b2bb 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -110,9 +110,15 @@ def apply(self, arg): script_locals, ) except Exception: + error_message = traceback.format_exc() error_writer = arg.scriptContext.getErrorWriter() - if error_writer is not None: - error_writer.write(to_java(traceback.format_exc())) + if error_writer is None: + # Emit error message to stderr stream. + error_writer = sys.stderr + else: + # Emit error message to designated error writer. + error_message = to_java(error_message) + error_writer.write(error_message) stdoutContextWriter.removeScriptContext(threading.currentThread()) @@ -122,9 +128,6 @@ def apply(self, arg): arg.vars[key] = to_java(script_locals[key]) except Exception: arg.vars[key] = PythonObjectSupplier(script_locals[key]) - # error_writer = arg.scriptContext.getErrorWriter() - # if error_writer is not None: - # error_writer.write(to_java(traceback.format_exc())) return to_java(return_value) From d6b118a3b0da494a23ba2306120e83ff33c08557 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 21 Mar 2025 14:04:42 -0500 Subject: [PATCH 389/505] Make cube root computation work in Python <3.11 --- tests/it/script_scope.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index 45dbedcd..923bdaf3 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -33,10 +33,9 @@ #@ int age #@output String cbrt_age import numpy as np -import math def calculate_cbrt(age): - return round(math.cbrt(age)) + return round(age ** (1. / 3)) cbrt_age = calculate_cbrt(age) # cbrt_age = round(math.cbrt(age)) From 124e3a4a630bb148c545f62271ee83cc87344706 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 21 Mar 2025 15:11:33 -0700 Subject: [PATCH 390/505] Explicit test to check if function can retrieve module into local namespace --- tests/it/script_scope.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index 923bdaf3..fc751fd8 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -35,10 +35,11 @@ import numpy as np def calculate_cbrt(age): - return round(age ** (1. / 3)) + # check whether defined function can import module from global namespace + if round(age ** (1. / 3)) == round(np.cbrt(age)): + return round(age ** (1. /3)) cbrt_age = calculate_cbrt(age) -# cbrt_age = round(math.cbrt(age)) f"The rounded cube root of my age is {cbrt_age}" """ StringReader = scyjava.jimport("java.io.StringReader") From 9ec3bb4ebd35c7503749fc385e318f1581f484a0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Mar 2025 13:07:26 -0500 Subject: [PATCH 391/505] Add missing type helper function exports --- src/scyjava/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index ce20173b..f622685c 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -111,6 +111,14 @@ from ._types import ( JavaClasses, is_jarray, + is_jboolean, + is_jbyte, + is_jcharacter, + is_jdouble, + is_jfloat, + is_jinteger, + is_jlong, + is_jshort, isjava, jarray, jclass, From 141a3edde27200192b1b0f27c1375abacc35d023 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Mar 2025 13:00:24 -0500 Subject: [PATCH 392/505] Guard against invalid mode in jarray function All the other functions already guard with an assertion. --- src/scyjava/_types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 34c2cafc..ef0318ad 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -275,6 +275,9 @@ def jarray(kind, lengths: Sequence): # instantiate the n-dimensional array arr = arraytype(lengths[0]) + else: + raise RuntimeError(f"Invalid mode: {mode}") + if len(lengths) > 1: for i in range(len(arr)): arr[i] = jarray(kind, lengths[1:]) From 1e54c4b8de0c98213bd18e1e457eaa2b9e80edd4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Mar 2025 13:25:56 -0500 Subject: [PATCH 393/505] Add down-the-middle testing of the jclass function --- tests/test_types.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_types.py b/tests/test_types.py index cc6adc44..7ccf23be 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,5 @@ -from scyjava import numeric_bounds, to_java +from scyjava import jclass, jimport, numeric_bounds, to_java +from scyjava.config import Mode, mode class TestTypes(object): @@ -30,3 +31,22 @@ def test_numeric_bounds(self): type(v_double) ) assert (None, None) == numeric_bounds(type(v_bigdec)) + + def test_jclass(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + + # A. Name of a class to look up -- e.g. "java.lang.String" -> String.class + a_cls = jclass("java.lang.String") + assert a_cls.getName() == "java.lang.String" + + # B. A static-style class reference -- String -> String.class + String = jimport("java.lang.String") + b_cls = jclass(String) + assert b_cls.getName() == "java.lang.String" + + # C. A Java object -- String("hello") -> "hello".getClass() + v_str = to_java("gubernatorial") + c_cls = jclass(v_str) + assert c_cls.getName() == "java.lang.String" From 0a8141cd2c25c30e5a3a1aa023a4576e5ee76d93 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 26 Mar 2025 13:27:11 -0500 Subject: [PATCH 394/505] Add some functions for Java object introspection --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index f622685c..4ba3a416 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,6 +124,7 @@ jclass, jinstance, jstacktrace, + methods, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index ef0318ad..fa0ae972 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -324,6 +324,55 @@ def numeric_bounds( return None, None +def methods(data) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available methods. + + :param data: The object or class to inspect. + :return: List of table rows with columns "name", "arguments", and "returns". + """ + + if not isjava(data): + raise ValueError("Not a Java object") + + cls = data if jinstance(data, "java.lang.Class") else jclass(data) + + methods = cls.getMethods() + + # NB: Methods are returned in inconsistent order. + # Arrays.sort(methods, (m1, m2) -> { + # final int nameComp = m1.getName().compareTo(m2.getName()) + # if (nameComp != 0) return nameComp + # final int pCount1 = m1.getParameterCount() + # final int pCount2 = m2.getParameterCount() + # if (pCount1 != pCount2) return pCount1 - pCount2 + # final Class[] pTypes1 = m1.getParameterTypes() + # final Class[] pTypes2 = m2.getParameterTypes() + # for (int i = 0; i < pTypes1.length; i++) { + # final int typeComp = ClassUtils.compare(pTypes1[i], pTypes2[i]) + # if (typeComp != 0) return typeComp + # } + # return ClassUtils.compare(m1.getReturnType(), m2.getReturnType()) + # }) + + table = [] + + for m in methods: + name = m.getName() + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + table.append( + { + "name": name, + "arguments": args, + "returns": returns, + } + ) + + return table + + def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From 7b8a6c4b08f6786bc7123948716516cc039b6646 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Wed, 26 Mar 2025 22:15:19 -0700 Subject: [PATCH 395/505] Update methods() functionality --- src/scyjava/__init__.py | 1 + src/scyjava/_types.py | 55 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 4ba3a416..139e5917 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -125,6 +125,7 @@ jinstance, jstacktrace, methods, + find_java_methods, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index fa0ae972..f69c6fa1 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -324,7 +324,7 @@ def numeric_bounds( return None, None -def methods(data) -> list[dict[str, Any]]: +def find_java_methods(data) -> list[dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. @@ -369,8 +369,59 @@ def methods(data) -> list[dict[str, Any]]: "returns": returns, } ) + sorted_table = sorted(table, key=lambda d: d["name"]) - return table + return sorted_table + + +def map_syntax(base_type): + """ + Maps a java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[C": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def methods(data) -> str: + table = find_java_methods(data) + + offset = max(list(map(lambda l: len(l["returns"]), table))) + all_methods = "" + + for entry in table: + entry["returns"] = map_syntax(entry["returns"]) + entry["arguments"] = [map_syntax(e) for e in entry["arguments"]] + + if not entry["arguments"]: + all_methods = ( + all_methods + + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}()\n' + ) + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + all_methods = ( + all_methods + + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}({arg_string})\n' + ) + print(all_methods) def _is_jtype(the_type: type, class_name: str) -> bool: From fe0bfe391d24a7b4b37f2b38e2ebb138c8c9d41b Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 00:29:02 -0700 Subject: [PATCH 396/505] Make progress on introspection methods --- src/scyjava/_types.py | 105 +++++++++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index f69c6fa1..fa53eae4 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -330,7 +330,7 @@ def find_java_methods(data) -> list[dict[str, Any]]: returning a table of its available methods. :param data: The object or class to inspect. - :return: List of table rows with columns "name", "arguments", and "returns". + :return: List of table rows with columns "name", "static", "arguments", and "returns". """ if not isjava(data): @@ -357,14 +357,17 @@ def find_java_methods(data) -> list[dict[str, Any]]: # }) table = [] + Modifier = jimport("java.lang.reflect.Modifier") for m in methods: name = m.getName() args = [c.getName() for c in m.getParameterTypes()] + mods = Modifier.isStatic(m.getModifiers()) returns = m.getReturnType().getName() table.append( { "name": name, + "static": mods, "arguments": args, "returns": returns, } @@ -374,7 +377,31 @@ def find_java_methods(data) -> list[dict[str, Any]]: return sorted_table -def map_syntax(base_type): +# TODO +def find_java_fields(data) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available fields. + + :param data: The object or class to inspect. + :return: List of table rows with columns "name", "arguments", and "returns". + """ + if not isjava(data): + raise ValueError("Not a Java object") + + cls = data if jinstance(data, "java.lang.Class") else jclass(data) + + fields = cls.getFields() + table = [] + + for f in fields: + name = f.getName() + table.append(name) + + return table + + +def _map_syntax(base_type): """ Maps a java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. @@ -385,7 +412,7 @@ def map_syntax(base_type): "[C": "char[]", "[D": "double[]", "[F": "float[]", - "[C": "int[]", + "[I": "int[]", "[J": "long[]", "[L": "[]", # array "[S": "short[]", @@ -394,33 +421,77 @@ def map_syntax(base_type): if base_type in basetype_mapping: return basetype_mapping[base_type] + # Handle the case of a returned array of an object elif base_type.__str__().startswith("[L"): return base_type.__str__()[2:-1] + "[]" else: return base_type +def _make_pretty_string(entry, offset): + """ + Prints the entry with a specific formatting and aligned style + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_val = f'{entry["returns"].__str__():<{offset}}' + # Handle whether to print static/instance modifiers + obj_name = f'{entry["name"]}' + modifier = f'{"*":>4}' if entry["static"] else f'{"":>4}' + + # Handle methods with no arguments + if not entry["arguments"]: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +# TODO +def fields(data) -> str: + """ + Writes data to a printed field names with the field value. + :param data: The object or class to inspect. + """ + table = find_java_fields(data) + + all_fields = "" + ################ + # FILL THIS IN # + ################ + + print(all_fields) + + +# TODO +def attrs(data): + """ + Writes data to a printed field names with the field value. Alias for `fields(data)`. + :param data: The object or class to inspect. + """ + fields(data) + + def methods(data) -> str: + """ + Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + + :param data: The object or class to inspect. + """ table = find_java_methods(data) offset = max(list(map(lambda l: len(l["returns"]), table))) all_methods = "" - for entry in table: - entry["returns"] = map_syntax(entry["returns"]) - entry["arguments"] = [map_syntax(e) for e in entry["arguments"]] + entry["returns"] = _map_syntax(entry["returns"]) + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string - if not entry["arguments"]: - all_methods = ( - all_methods - + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}()\n' - ) - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - all_methods = ( - all_methods - + f'{entry["returns"].__str__():<{offset}} = {entry["name"]}({arg_string})\n' - ) + # 4 added to align the asterisk with output. + print(f'{"":<{offset+4}}* indicates a static method') print(all_methods) From 25d769e11c0c394121b3f520d853432ab09fe017 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 00:34:05 -0700 Subject: [PATCH 397/505] Make linter happy --- src/scyjava/_types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index fa53eae4..c797ae0a 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -436,10 +436,10 @@ def _make_pretty_string(entry, offset): """ # A star implies that the method is a static method - return_val = f'{entry["returns"].__str__():<{offset}}' + return_val = f"{entry['returns'].__str__():<{offset}}" # Handle whether to print static/instance modifiers - obj_name = f'{entry["name"]}' - modifier = f'{"*":>4}' if entry["static"] else f'{"":>4}' + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" # Handle methods with no arguments if not entry["arguments"]: @@ -455,7 +455,7 @@ def fields(data) -> str: Writes data to a printed field names with the field value. :param data: The object or class to inspect. """ - table = find_java_fields(data) + # table = find_java_fields(data) all_fields = "" ################ @@ -482,7 +482,7 @@ def methods(data) -> str: """ table = find_java_methods(data) - offset = max(list(map(lambda l: len(l["returns"]), table))) + offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" for entry in table: entry["returns"] = _map_syntax(entry["returns"]) @@ -491,7 +491,7 @@ def methods(data) -> str: all_methods += entry_string # 4 added to align the asterisk with output. - print(f'{"":<{offset+4}}* indicates a static method') + print(f"{'':<{offset + 4}}* indicates a static method") print(all_methods) From 1a0fd7123c965f67f3b91c6b8783cf8786bf0f70 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 11:49:54 -0700 Subject: [PATCH 398/505] Add source code reporting to methods() function --- src/scyjava/_types.py | 57 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index c797ae0a..479e4e58 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -336,9 +336,9 @@ def find_java_methods(data) -> list[dict[str, Any]]: if not isjava(data): raise ValueError("Not a Java object") - cls = data if jinstance(data, "java.lang.Class") else jclass(data) + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - methods = cls.getMethods() + methods = jcls.getMethods() # NB: Methods are returned in inconsistent order. # Arrays.sort(methods, (m1, m2) -> { @@ -389,9 +389,9 @@ def find_java_fields(data) -> list[dict[str, Any]]: if not isjava(data): raise ValueError("Not a Java object") - cls = data if jinstance(data, "java.lang.Class") else jclass(data) + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - fields = cls.getFields() + fields = jcls.getFields() table = [] for f in fields: @@ -474,21 +474,64 @@ def attrs(data): fields(data) -def methods(data) -> str: +def get_source_code(data): + """ + Tries to find the source code using Scijava's SourceFinder' + :param data: The object or class to check for source code. + """ + types = jimport("org.scijava.util.Types") + sf = jimport("org.scijava.search.SourceFinder") + jstring = jimport("java.lang.String") + try: + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + if types.location(jcls).toString().startsWith(jstring("jrt")): + # Handles Java RunTime (jrt) exceptions. + return "GitHub source code not available" + url = sf.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring + except jimport("java.lang.IllegalArgumentException") as err: + return f"Illegal argument provided {err=}, {type(err)=}" + except Exception as err: + return f"Unexpected {err=}, {type(err)=}" + + +def methods(data, static: bool | None = None, source: bool = True) -> str: """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. :param data: The object or class to inspect. + :param static: Which methods to print. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all methods). + :param source: Whether to print any available source code. Default True. """ table = find_java_methods(data) + # Print source code offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" + if source: + urlstring = get_source_code(data) + print(f"URL: {urlstring}") + else: + pass + + # Print methods for entry in table: entry["returns"] = _map_syntax(entry["returns"]) entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string + if static is None: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + + elif static and entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + elif not static and not entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + else: + continue # 4 added to align the asterisk with output. print(f"{'':<{offset + 4}}* indicates a static method") From 640d57deb7393407fe6edcced2d728186133e3bb Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Thu, 27 Mar 2025 12:47:48 -0700 Subject: [PATCH 399/505] Implement fields introspection function --- src/scyjava/__init__.py | 5 ++- src/scyjava/_types.py | 67 ++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 139e5917..be386787 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,8 +124,11 @@ jclass, jinstance, jstacktrace, - methods, find_java_methods, + find_java_fields, + methods, + fields, + attrs, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 479e4e58..24e9387c 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -396,9 +396,11 @@ def find_java_fields(data) -> list[dict[str, Any]]: for f in fields: name = f.getName() - table.append(name) + ftype = f.getType().getName() + table.append({"name": name, "type": ftype}) + sorted_table = sorted(table, key=lambda d: d["name"]) - return table + return sorted_table def _map_syntax(base_type): @@ -441,39 +443,18 @@ def _make_pretty_string(entry, offset): obj_name = f"{entry['name']}" modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + # Handle fields + if entry["arguments"] is None: + return f"{return_val} = {obj_name}\n" + # Handle methods with no arguments - if not entry["arguments"]: + if len(entry["arguments"]) == 0: return f"{return_val} {modifier} = {obj_name}()\n" else: arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -# TODO -def fields(data) -> str: - """ - Writes data to a printed field names with the field value. - :param data: The object or class to inspect. - """ - # table = find_java_fields(data) - - all_fields = "" - ################ - # FILL THIS IN # - ################ - - print(all_fields) - - -# TODO -def attrs(data): - """ - Writes data to a printed field names with the field value. Alias for `fields(data)`. - :param data: The object or class to inspect. - """ - fields(data) - - def get_source_code(data): """ Tries to find the source code using Scijava's SourceFinder' @@ -496,6 +477,36 @@ def get_source_code(data): return f"Unexpected {err=}, {type(err)=}" +def fields(data) -> str: + """ + Writes data to a printed field names with the field value. + :param data: The object or class to inspect. + """ + table = find_java_fields(data) + if len(table) == 0: + print("No fields found") + return + + all_fields = "" + offset = max(list(map(lambda entry: len(entry["type"]), table))) + for entry in table: + entry["returns"] = _map_syntax(entry["type"]) + entry["static"] = False + entry["arguments"] = None + entry_string = _make_pretty_string(entry, offset) + all_fields += entry_string + + print(all_fields) + + +def attrs(data): + """ + Writes data to a printed field names with the field value. Alias for `fields(data)`. + :param data: The object or class to inspect. + """ + fields(data) + + def methods(data, static: bool | None = None, source: bool = True) -> str: """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. From aa3996c22b2d3e76932969a314725a92847a128d Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 13:49:01 -0700 Subject: [PATCH 400/505] Add partials, refactor, add java_source function --- src/scyjava/__init__.py | 6 +- src/scyjava/_types.py | 162 ++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 98 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index be386787..043374fc 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,11 +124,13 @@ jclass, jinstance, jstacktrace, - find_java_methods, - find_java_fields, + find_java, + java_source, methods, fields, attrs, + src, + java_source, numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 24e9387c..6fdced36 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Sequence, Tuple, Union import jpype +from functools import partial from scyjava._jvm import jimport, jvm_started, start_jvm from scyjava.config import Mode, mode @@ -324,46 +325,43 @@ def numeric_bounds( return None, None -def find_java_methods(data) -> list[dict[str, Any]]: +def find_java(data, aspect: str) -> list[dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. - :param data: The object or class to inspect. - :return: List of table rows with columns "name", "static", "arguments", and "returns". + :param data: The object or class or fully qualified class name to inspect. + :param aspect: Either 'methods' or 'fields' + :return: List of dicts with keys: "name", "static", "arguments", and "returns". """ - if not isjava(data): - raise ValueError("Not a Java object") + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) + except: + raise ValueError("Not a Java object") + Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - methods = jcls.getMethods() - - # NB: Methods are returned in inconsistent order. - # Arrays.sort(methods, (m1, m2) -> { - # final int nameComp = m1.getName().compareTo(m2.getName()) - # if (nameComp != 0) return nameComp - # final int pCount1 = m1.getParameterCount() - # final int pCount2 = m2.getParameterCount() - # if (pCount1 != pCount2) return pCount1 - pCount2 - # final Class[] pTypes1 = m1.getParameterTypes() - # final Class[] pTypes2 = m2.getParameterTypes() - # for (int i = 0; i < pTypes1.length; i++) { - # final int typeComp = ClassUtils.compare(pTypes1[i], pTypes2[i]) - # if (typeComp != 0) return typeComp - # } - # return ClassUtils.compare(m1.getReturnType(), m2.getReturnType()) - # }) + if aspect == "methods": + cls_aspects = jcls.getMethods() + elif aspect == "fields": + cls_aspects = jcls.getFields() + else: + return "`aspect` must be either 'fields' or 'methods'" table = [] - Modifier = jimport("java.lang.reflect.Modifier") - for m in methods: + for m in cls_aspects: name = m.getName() - args = [c.getName() for c in m.getParameterTypes()] + if aspect == "methods": + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + elif aspect == "fields": + args = None + returns = m.getType().getName() mods = Modifier.isStatic(m.getModifiers()) - returns = m.getReturnType().getName() table.append( { "name": name, @@ -377,32 +375,6 @@ def find_java_methods(data) -> list[dict[str, Any]]: return sorted_table -# TODO -def find_java_fields(data) -> list[dict[str, Any]]: - """ - Use Java reflection to introspect the given Java object, - returning a table of its available fields. - - :param data: The object or class to inspect. - :return: List of table rows with columns "name", "arguments", and "returns". - """ - if not isjava(data): - raise ValueError("Not a Java object") - - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - - fields = jcls.getFields() - table = [] - - for f in fields: - name = f.getName() - ftype = f.getType().getName() - table.append({"name": name, "type": ftype}) - sorted_table = sorted(table, key=lambda d: d["name"]) - - return sorted_table - - def _map_syntax(base_type): """ Maps a java BaseType annotation (see link below) in an Java array @@ -445,7 +417,7 @@ def _make_pretty_string(entry, offset): # Handle fields if entry["arguments"] is None: - return f"{return_val} = {obj_name}\n" + return f"{return_val} {modifier} = {obj_name}\n" # Handle methods with no arguments if len(entry["arguments"]) == 0: @@ -455,82 +427,65 @@ def _make_pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def get_source_code(data): +def java_source(data): """ Tries to find the source code using Scijava's SourceFinder' - :param data: The object or class to check for source code. + :param data: The object or class or fully qualified class name to check for source code. + :return: The URL of the java class """ types = jimport("org.scijava.util.Types") sf = jimport("org.scijava.search.SourceFinder") jstring = jimport("java.lang.String") try: + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) # check if data can be imported + except: + raise ValueError("Not a Java object") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) if types.location(jcls).toString().startsWith(jstring("jrt")): # Handles Java RunTime (jrt) exceptions. - return "GitHub source code not available" + raise ValueError("Java Builtin: GitHub source code not available") url = sf.sourceLocation(jcls, None) urlstring = url.toString() return urlstring except jimport("java.lang.IllegalArgumentException") as err: return f"Illegal argument provided {err=}, {type(err)=}" + except ValueError as err: + return f"{err}" + except TypeError: + return f"Not a Java class {str(type(data))}" except Exception as err: return f"Unexpected {err=}, {type(err)=}" -def fields(data) -> str: - """ - Writes data to a printed field names with the field value. - :param data: The object or class to inspect. - """ - table = find_java_fields(data) - if len(table) == 0: - print("No fields found") - return - - all_fields = "" - offset = max(list(map(lambda entry: len(entry["type"]), table))) - for entry in table: - entry["returns"] = _map_syntax(entry["type"]) - entry["static"] = False - entry["arguments"] = None - entry_string = _make_pretty_string(entry, offset) - all_fields += entry_string - - print(all_fields) - - -def attrs(data): - """ - Writes data to a printed field names with the field value. Alias for `fields(data)`. - :param data: The object or class to inspect. - """ - fields(data) - - -def methods(data, static: bool | None = None, source: bool = True) -> str: +def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. :param data: The object or class to inspect. - :param static: Which methods to print. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all methods). + :param aspect: Whether to print class fields or methods. + :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = find_java_methods(data) + table = find_java(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return # Print source code offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" if source: - urlstring = get_source_code(data) - print(f"URL: {urlstring}") - else: - pass + urlstring = java_source(data) + print(f"Source code URL: {urlstring}") # Print methods for entry in table: entry["returns"] = _map_syntax(entry["returns"]) - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] if static is None: entry_string = _make_pretty_string(entry, offset) all_methods += entry_string @@ -545,10 +500,25 @@ def methods(data, static: bool | None = None, source: bool = True) -> str: continue # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates a static method") + print(f"{'':<{offset + 4}}* indicates static modifier") print(all_methods) +methods = partial(_print_data, aspect="methods") +fields = partial(_print_data, aspect="fields") +attrs = partial(_print_data, aspect="fields") + + +def src(data): + """ + Prints the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + """ + source_url = java_source(data) + print(f"Source code URL: {source_url}") + + def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From 9352ff6a98620882e503d4ccb60b55ac210f4d6c Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 13:55:53 -0700 Subject: [PATCH 401/505] Refactor introspection code --- src/scyjava/__init__.py | 4 +- src/scyjava/_introspection.py | 203 ++++++++++++++++++++++++++++++++++ src/scyjava/_types.py | 195 -------------------------------- 3 files changed, 206 insertions(+), 196 deletions(-) create mode 100644 src/scyjava/_introspection.py diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 043374fc..b30f9090 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -124,6 +124,9 @@ jclass, jinstance, jstacktrace, + numeric_bounds, +) +from ._introspection import ( find_java, java_source, methods, @@ -131,7 +134,6 @@ attrs, src, java_source, - numeric_bounds, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py new file mode 100644 index 00000000..f136eaa8 --- /dev/null +++ b/src/scyjava/_introspection.py @@ -0,0 +1,203 @@ +""" +Introspection functions for reporting java classes and URL +""" + +from functools import partial +from typing import Any + +from scyjava._jvm import jimport +from scyjava._types import isjava, jinstance, jclass + + +def find_java(data, aspect: str) -> list[dict[str, Any]]: + """ + Use Java reflection to introspect the given Java object, + returning a table of its available methods. + + :param data: The object or class or fully qualified class name to inspect. + :param aspect: Either 'methods' or 'fields' + :return: List of dicts with keys: "name", "static", "arguments", and "returns". + """ + + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) + except: + raise ValueError("Not a Java object") + + Modifier = jimport("java.lang.reflect.Modifier") + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + + if aspect == "methods": + cls_aspects = jcls.getMethods() + elif aspect == "fields": + cls_aspects = jcls.getFields() + else: + return "`aspect` must be either 'fields' or 'methods'" + + table = [] + + for m in cls_aspects: + name = m.getName() + if aspect == "methods": + args = [c.getName() for c in m.getParameterTypes()] + returns = m.getReturnType().getName() + elif aspect == "fields": + args = None + returns = m.getType().getName() + mods = Modifier.isStatic(m.getModifiers()) + table.append( + { + "name": name, + "static": mods, + "arguments": args, + "returns": returns, + } + ) + sorted_table = sorted(table, key=lambda d: d["name"]) + + return sorted_table + + +def _map_syntax(base_type): + """ + Maps a java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[I": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + # Handle the case of a returned array of an object + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def _make_pretty_string(entry, offset): + """ + Prints the entry with a specific formatting and aligned style + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_val = f"{entry['returns'].__str__():<{offset}}" + # Handle whether to print static/instance modifiers + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + + # Handle fields + if entry["arguments"] is None: + return f"{return_val} {modifier} = {obj_name}\n" + + # Handle methods with no arguments + if len(entry["arguments"]) == 0: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +def java_source(data): + """ + Tries 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 + """ + types = jimport("org.scijava.util.Types") + sf = jimport("org.scijava.search.SourceFinder") + jstring = jimport("java.lang.String") + try: + if not isjava(data) and isinstance(data, str): + try: + data = jimport(data) # check if data can be imported + except: + raise ValueError("Not a Java object") + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) + if types.location(jcls).toString().startsWith(jstring("jrt")): + # Handles Java RunTime (jrt) exceptions. + raise ValueError("Java Builtin: GitHub source code not available") + url = sf.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring + except jimport("java.lang.IllegalArgumentException") as err: + return f"Illegal argument provided {err=}, {type(err)=}" + except ValueError as err: + return f"{err}" + except TypeError: + return f"Not a Java class {str(type(data))}" + except Exception as err: + return f"Unexpected {err=}, {type(err)=}" + + +def _print_data(data, aspect, static: bool | None = None, source: bool = True): + """ + Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + + :param data: The object or class to inspect. + :param aspect: Whether to print class fields or methods. + :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on + static vs. instance methods. Optional, default is None (prints all). + :param source: Whether to print any available source code. Default True. + """ + table = find_java(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return + + # Print source code + offset = max(list(map(lambda entry: len(entry["returns"]), table))) + all_methods = "" + if source: + urlstring = java_source(data) + print(f"Source code URL: {urlstring}") + + # Print methods + for entry in table: + entry["returns"] = _map_syntax(entry["returns"]) + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if static is None: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + + elif static and entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + elif not static and not entry["static"]: + entry_string = _make_pretty_string(entry, offset) + all_methods += entry_string + else: + continue + + # 4 added to align the asterisk with output. + print(f"{'':<{offset + 4}}* indicates static modifier") + print(all_methods) + + +methods = partial(_print_data, aspect="methods") +fields = partial(_print_data, aspect="fields") +attrs = partial(_print_data, aspect="fields") + + +def src(data): + """ + Prints the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + """ + source_url = java_source(data) + print(f"Source code URL: {source_url}") diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index 6fdced36..ef0318ad 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -5,7 +5,6 @@ from typing import Any, Callable, Sequence, Tuple, Union import jpype -from functools import partial from scyjava._jvm import jimport, jvm_started, start_jvm from scyjava.config import Mode, mode @@ -325,200 +324,6 @@ def numeric_bounds( return None, None -def find_java(data, aspect: str) -> list[dict[str, Any]]: - """ - Use Java reflection to introspect the given Java object, - returning a table of its available methods. - - :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either 'methods' or 'fields' - :return: List of dicts with keys: "name", "static", "arguments", and "returns". - """ - - if not isjava(data) and isinstance(data, str): - try: - data = jimport(data) - except: - raise ValueError("Not a Java object") - - Modifier = jimport("java.lang.reflect.Modifier") - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - - if aspect == "methods": - cls_aspects = jcls.getMethods() - elif aspect == "fields": - cls_aspects = jcls.getFields() - else: - return "`aspect` must be either 'fields' or 'methods'" - - table = [] - - for m in cls_aspects: - name = m.getName() - if aspect == "methods": - args = [c.getName() for c in m.getParameterTypes()] - returns = m.getReturnType().getName() - elif aspect == "fields": - args = None - returns = m.getType().getName() - mods = Modifier.isStatic(m.getModifiers()) - table.append( - { - "name": name, - "static": mods, - "arguments": args, - "returns": returns, - } - ) - sorted_table = sorted(table, key=lambda d: d["name"]) - - return sorted_table - - -def _map_syntax(base_type): - """ - Maps a java BaseType annotation (see link below) in an Java array - to a specific type with an Python interpretable syntax. - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 - """ - basetype_mapping = { - "[B": "byte[]", - "[C": "char[]", - "[D": "double[]", - "[F": "float[]", - "[I": "int[]", - "[J": "long[]", - "[L": "[]", # array - "[S": "short[]", - "[Z": "boolean[]", - } - - if base_type in basetype_mapping: - return basetype_mapping[base_type] - # Handle the case of a returned array of an object - elif base_type.__str__().startswith("[L"): - return base_type.__str__()[2:-1] + "[]" - else: - return base_type - - -def _make_pretty_string(entry, offset): - """ - Prints the entry with a specific formatting and aligned style - :param entry: Dictionary of class names, modifiers, arguments, and return values. - :param offset: Offset between the return value and the method. - """ - - # A star implies that the method is a static method - return_val = f"{entry['returns'].__str__():<{offset}}" - # Handle whether to print static/instance modifiers - obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" - - # Handle fields - if entry["arguments"] is None: - return f"{return_val} {modifier} = {obj_name}\n" - - # Handle methods with no arguments - if len(entry["arguments"]) == 0: - return f"{return_val} {modifier} = {obj_name}()\n" - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - return f"{return_val} {modifier} = {obj_name}({arg_string})\n" - - -def java_source(data): - """ - Tries 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 - """ - types = jimport("org.scijava.util.Types") - sf = jimport("org.scijava.search.SourceFinder") - jstring = jimport("java.lang.String") - try: - if not isjava(data) and isinstance(data, str): - try: - data = jimport(data) # check if data can be imported - except: - raise ValueError("Not a Java object") - jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if types.location(jcls).toString().startsWith(jstring("jrt")): - # Handles Java RunTime (jrt) exceptions. - raise ValueError("Java Builtin: GitHub source code not available") - url = sf.sourceLocation(jcls, None) - urlstring = url.toString() - return urlstring - except jimport("java.lang.IllegalArgumentException") as err: - return f"Illegal argument provided {err=}, {type(err)=}" - except ValueError as err: - return f"{err}" - except TypeError: - return f"Not a Java class {str(type(data))}" - except Exception as err: - return f"Unexpected {err=}, {type(err)=}" - - -def _print_data(data, aspect, static: bool | None = None, source: bool = True): - """ - Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. - - :param data: The object or class to inspect. - :param aspect: Whether to print class fields or methods. - :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. - """ - table = find_java(data, aspect) - if len(table) == 0: - print(f"No {aspect} found") - return - - # Print source code - offset = max(list(map(lambda entry: len(entry["returns"]), table))) - all_methods = "" - if source: - urlstring = java_source(data) - print(f"Source code URL: {urlstring}") - - # Print methods - for entry in table: - entry["returns"] = _map_syntax(entry["returns"]) - if entry["arguments"]: - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - if static is None: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - - elif static and entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - elif not static and not entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - else: - continue - - # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) - - -methods = partial(_print_data, aspect="methods") -fields = partial(_print_data, aspect="fields") -attrs = partial(_print_data, aspect="fields") - - -def src(data): - """ - Prints the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - """ - source_url = java_source(data) - print(f"Source code URL: {source_url}") - - def _is_jtype(the_type: type, class_name: str) -> bool: """ Test if the given type object is *exactly* the specified Java type. From fe602176b9505bffaf1db0ba4eb9ebf0d989b0f7 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 15:16:13 -0700 Subject: [PATCH 402/505] Add test cases for introspection functions --- tests/test_introspection.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_introspection.py diff --git a/tests/test_introspection.py b/tests/test_introspection.py new file mode 100644 index 00000000..e8405f39 --- /dev/null +++ b/tests/test_introspection.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Mar 28 13:58:54 2025 + +@author: ian +""" + +import scyjava +from scyjava.config import Mode, mode + +scyjava.config.endpoints.append("net.imagej:imagej") +scyjava.config.endpoints.append("net.imagej:imagej-legacy:MANAGED") + + +class TestIntrospection(object): + """ + Test introspection functionality. + """ + + def test_find_java_methods(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_String = "java.lang.String" + String = scyjava.jimport(str_String) + str_Obj = scyjava.find_java(str_String, "methods") + jimport_Obj = scyjava.find_java(String, "methods") + assert len(str_Obj) > 0 + assert len(jimport_Obj) > 0 + assert jimport_Obj is not None + assert jimport_Obj == str_Obj + + def test_find_java_fields(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_BitSet = "java.util.BitSet" + BitSet = scyjava.jimport(str_BitSet) + str_Obj = scyjava.find_java(str_BitSet, "fields") + bitset_Obj = scyjava.find_java(BitSet, "fields") + assert len(str_Obj) == 0 + assert len(bitset_Obj) == 0 + assert bitset_Obj is not None + assert bitset_Obj == str_Obj + + def test_find_source(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_SF = "org.scijava.search.SourceFinder" + SF = scyjava.jimport(str_SF) + source_strSF = scyjava.java_source(str_SF) + source_SF = scyjava.java_source(SF) + github_home = "https://github.com/" + assert source_strSF.startsWith(github_home) + assert source_SF.startsWith(github_home) + assert source_strSF == source_SF + + def test_imagej_legacy(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_RE = "ij.plugin.RoiEnlarger" + table = scyjava.find_java(str_RE, aspect="methods") + assert len([entry for entry in table if entry["static"]]) == 3 + github_home = "https://github.com/" + assert scyjava.java_source(str_RE).startsWith(github_home) From 7293d2343f4d8c6575f5461489719012127df0b8 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Fri, 28 Mar 2025 15:26:45 -0700 Subject: [PATCH 403/505] Lint code --- src/scyjava/__init__.py | 1 - src/scyjava/_introspection.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index b30f9090..c922cd01 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -133,7 +133,6 @@ fields, attrs, src, - java_source, ) from ._versions import compare_version, get_version, is_version_at_least diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py index f136eaa8..9cbf1998 100644 --- a/src/scyjava/_introspection.py +++ b/src/scyjava/_introspection.py @@ -22,8 +22,8 @@ def find_java(data, aspect: str) -> list[dict[str, Any]]: if not isjava(data) and isinstance(data, str): try: data = jimport(data) - except: - raise ValueError("Not a Java object") + except Exception as err: + raise ValueError(f"Not a Java object {err}") Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) @@ -124,8 +124,8 @@ def java_source(data): if not isjava(data) and isinstance(data, str): try: data = jimport(data) # check if data can be imported - except: - raise ValueError("Not a Java object") + except Exception as err: + raise ValueError(f"Not a Java object {err}") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) if types.location(jcls).toString().startsWith(jstring("jrt")): # Handles Java RunTime (jrt) exceptions. From 41230787cc12512a7bcbe8e17f42207437312fdc Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Sun, 30 Mar 2025 23:37:27 -0700 Subject: [PATCH 404/505] Improve introspection function documentation --- src/scyjava/_introspection.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspection.py index 9cbf1998..18e097e4 100644 --- a/src/scyjava/_introspection.py +++ b/src/scyjava/_introspection.py @@ -1,5 +1,5 @@ """ -Introspection functions for reporting java classes and URL +Introspection functions for reporting java class 'methods', 'fields', and source code URL. """ from functools import partial @@ -61,7 +61,7 @@ def find_java(data, aspect: str) -> list[dict[str, Any]]: def _map_syntax(base_type): """ - Maps a java BaseType annotation (see link below) in an Java array + Maps a Java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 """ @@ -113,7 +113,7 @@ def _make_pretty_string(entry, offset): def java_source(data): """ - Tries to find the source code using Scijava's SourceFinder' + Tries 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 """ @@ -147,10 +147,9 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. - :param data: The object or class to inspect. - :param aspect: Whether to print class fields or methods. - :param static: Filter on Static/Instance. Can be set as boolean to filter the class methods based on - static vs. instance methods. Optional, default is None (prints all). + :param data: The object or class to inspect or fully qualified class name. + :param aspect: Whether to print class 'fields' or 'methods'. + :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ table = find_java(data, aspect) @@ -188,6 +187,7 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): print(all_methods) +# The functions with short names for quick usage. methods = partial(_print_data, aspect="methods") fields = partial(_print_data, aspect="fields") attrs = partial(_print_data, aspect="fields") From 3533446337f8c43e57df729872b4ef6387c19d65 Mon Sep 17 00:00:00 2001 From: ian-coccimiglio Date: Sun, 30 Mar 2025 23:40:37 -0700 Subject: [PATCH 405/505] Add docstring to test_introspection.py --- tests/test_introspection.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_introspection.py b/tests/test_introspection.py index e8405f39..98cdaf4d 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ -Created on Fri Mar 28 13:58:54 2025 +Tests for introspection of java classes (fields and methods), as well as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 -@author: ian +@author: ian-coccimiglio """ import scyjava From 9516a72eaed28c6fd52424a682738a56b00c5e9a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:25:12 -0500 Subject: [PATCH 406/505] Wrap long line --- tests/test_introspection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_introspection.py b/tests/test_introspection.py index 98cdaf4d..37504ec8 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -1,5 +1,6 @@ """ -Tests for introspection of java classes (fields and methods), as well as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 +Tests for introspection of java classes (fields and methods), as well +as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 @author: ian-coccimiglio """ From 652022322727a62ffcf38a5c2771e11b469df631 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:26:16 -0500 Subject: [PATCH 407/505] Increment minor version digit The introspection functions are new API. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bbcc8b80..3a559b35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "ctrueden@wisc.edu"}] From 363e7bc6cd459282a88a037c5e368131b53aba3c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:29:35 -0500 Subject: [PATCH 408/505] Alphabetize introspection imports --- src/scyjava/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c922cd01..555effdb 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -91,6 +91,14 @@ to_java, to_python, ) +from ._introspection import ( + attrs, + fields, + find_java, + java_source, + methods, + src, +) from ._jvm import ( # noqa: F401 available_processors, gc, @@ -126,14 +134,6 @@ jstacktrace, numeric_bounds, ) -from ._introspection import ( - find_java, - java_source, - methods, - fields, - attrs, - src, -) from ._versions import compare_version, get_version, is_version_at_least __version__ = get_version("scyjava") From c8afba44be7595c909da35d2934829c7c2e2ae44 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:30:20 -0500 Subject: [PATCH 409/505] Shorten introspection to introspect For conversion functions, we use the name `convert`. So let's be consistent with introspection functions. It's also more concise. --- src/scyjava/__init__.py | 2 +- src/scyjava/{_introspection.py => _introspect.py} | 0 tests/{test_introspection.py => test_introspect.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/scyjava/{_introspection.py => _introspect.py} (100%) rename tests/{test_introspection.py => test_introspect.py} (100%) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 555effdb..e7a8e61d 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -91,7 +91,7 @@ to_java, to_python, ) -from ._introspection import ( +from ._introspect import ( attrs, fields, find_java, diff --git a/src/scyjava/_introspection.py b/src/scyjava/_introspect.py similarity index 100% rename from src/scyjava/_introspection.py rename to src/scyjava/_introspect.py diff --git a/tests/test_introspection.py b/tests/test_introspect.py similarity index 100% rename from tests/test_introspection.py rename to tests/test_introspect.py From 6bfec82e7eab7e70f6fe6a73c900717a48445aa5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:43:02 -0500 Subject: [PATCH 410/505] Add toplevel docstrings to test files --- tests/test_arrays.py | 4 ++++ tests/test_basics.py | 4 ++++ tests/test_convert.py | 4 ++++ tests/test_introspect.py | 4 ++-- tests/test_pandas.py | 4 ++++ tests/test_types.py | 4 ++++ tests/test_version.py | 4 ++++ 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index fca796d3..80f18911 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -1,3 +1,7 @@ +""" +Tests for array-related functions in _types submodule. +""" + import numpy as np from scyjava import is_jarray, jarray, to_python diff --git a/tests/test_basics.py b/tests/test_basics.py index 042b436c..76e2229c 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,3 +1,7 @@ +""" +Tests for key functions across all scyjava submodules. +""" + import re import pytest diff --git a/tests/test_convert.py b/tests/test_convert.py index e9f0489d..fcfabe10 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _convert submodule. +""" + import math from os import getcwd from pathlib import Path diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 37504ec8..a0d5872c 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -1,6 +1,6 @@ """ -Tests for introspection of java classes (fields and methods), as well -as the GitHub source code URLs. Created on Fri Mar 28 13:58:54 2025 +Tests for functions in _introspect submodule. +Created on Fri Mar 28 13:58:54 2025 @author: ian-coccimiglio """ diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 8fc4bc3a..c18d2435 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _pandas submodule. +""" + import numpy as np import numpy.testing as npt import pandas as pd diff --git a/tests/test_types.py b/tests/test_types.py index 7ccf23be..e4bdbc92 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _types submodule. +""" + from scyjava import jclass, jimport, numeric_bounds, to_java from scyjava.config import Mode, mode diff --git a/tests/test_version.py b/tests/test_version.py index 54113873..3b5fafcc 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,3 +1,7 @@ +""" +Tests for functions in _versions submodule. +""" + from pathlib import Path import toml From 37bd92e2b25f2ba87268767971b6c130bee90d48 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 12:45:13 -0500 Subject: [PATCH 411/505] Fix naming of versions test file The submodule is called versions; let's name the test file consistently. --- tests/{test_version.py => test_versions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_version.py => test_versions.py} (100%) diff --git a/tests/test_version.py b/tests/test_versions.py similarity index 100% rename from tests/test_version.py rename to tests/test_versions.py From 5f883f1119f9b0e05a371b0dbe69faa848ba0aab Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 13:45:19 -0500 Subject: [PATCH 412/505] Fix type hints to work with Python 3.8 --- src/scyjava/_introspect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 18e097e4..50d54bd4 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -3,13 +3,13 @@ """ from functools import partial -from typing import Any +from typing import Any, Dict, List, Optional from scyjava._jvm import jimport from scyjava._types import isjava, jinstance, jclass -def find_java(data, aspect: str) -> list[dict[str, Any]]: +def find_java(data, aspect: str) -> List[Dict[str, Any]]: """ Use Java reflection to introspect the given Java object, returning a table of its available methods. @@ -143,7 +143,7 @@ def java_source(data): return f"Unexpected {err=}, {type(err)=}" -def _print_data(data, aspect, static: bool | None = None, source: bool = True): +def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): """ Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. From 6282d8c399ed6381f6c2f85d09c8daaad40e1b08 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 13:45:31 -0500 Subject: [PATCH 413/505] CI: test Python 3.13 support --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b246a01..09a2f3d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: python-version: [ '3.8', '3.10', - '3.12' + '3.13' ] steps: From bded14f5e267eca69f71c376b11aed0b419980d7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:11:32 -0500 Subject: [PATCH 414/505] Rename find_java function to jreflect To me, the name `find_java` suggests we will be locating a JVM installation, rather than "finding" information about Java objects. The information doesn't need to be "found" or "located", but rather only introspected or interrogated. Technically, I suppose "introspection" implies read/access while "reflection" implies write/mutation, but `jintrospect` is rather clunky, whereas the term "reflection" is widely known in both Java and Python circles. --- src/scyjava/__init__.py | 2 +- src/scyjava/_introspect.py | 6 +++--- tests/test_introspect.py | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e7a8e61d..e42b51a6 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -94,8 +94,8 @@ from ._introspect import ( attrs, fields, - find_java, java_source, + jreflect, methods, src, ) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 50d54bd4..643969a0 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -9,10 +9,10 @@ from scyjava._types import isjava, jinstance, jclass -def find_java(data, aspect: str) -> List[Dict[str, Any]]: +def jreflect(data, aspect: str) -> List[Dict[str, Any]]: """ Use Java reflection to introspect the given Java object, - returning a table of its available methods. + returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. :param aspect: Either 'methods' or 'fields' @@ -152,7 +152,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = find_java(data, aspect) + table = jreflect(data, aspect) if len(table) == 0: print(f"No {aspect} found") return diff --git a/tests/test_introspect.py b/tests/test_introspect.py index a0d5872c..a12c0513 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -17,27 +17,27 @@ class TestIntrospection(object): Test introspection functionality. """ - def test_find_java_methods(self): + def test_jreflect_methods(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_String = "java.lang.String" String = scyjava.jimport(str_String) - str_Obj = scyjava.find_java(str_String, "methods") - jimport_Obj = scyjava.find_java(String, "methods") + str_Obj = scyjava.jreflect(str_String, "methods") + jimport_Obj = scyjava.jreflect(String, "methods") assert len(str_Obj) > 0 assert len(jimport_Obj) > 0 assert jimport_Obj is not None assert jimport_Obj == str_Obj - def test_find_java_fields(self): + def test_jreflect_fields(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_BitSet = "java.util.BitSet" BitSet = scyjava.jimport(str_BitSet) - str_Obj = scyjava.find_java(str_BitSet, "fields") - bitset_Obj = scyjava.find_java(BitSet, "fields") + str_Obj = scyjava.jreflect(str_BitSet, "fields") + bitset_Obj = scyjava.jreflect(BitSet, "fields") assert len(str_Obj) == 0 assert len(bitset_Obj) == 0 assert bitset_Obj is not None @@ -61,7 +61,7 @@ def test_imagej_legacy(self): # JEP does not support the jclass function. return str_RE = "ij.plugin.RoiEnlarger" - table = scyjava.find_java(str_RE, aspect="methods") + table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 github_home = "https://github.com/" assert scyjava.java_source(str_RE).startsWith(github_home) From 0778a893f11809643d3f6f1f6785bb79d761386a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:37:54 -0500 Subject: [PATCH 415/505] Add missing is_j* type methods to README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index eb22bbb8..5a2cff1f 100644 --- a/README.md +++ b/README.md @@ -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. From 21ffae9015b182e38c7ffb228cbf62b8db64b4cf Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:45:40 -0500 Subject: [PATCH 416/505] Use imperative tense for function docstrings --- src/scyjava/_introspect.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 643969a0..bc5aa4c4 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -61,7 +61,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: def _map_syntax(base_type): """ - Maps a Java BaseType annotation (see link below) in an Java array + Map a Java BaseType annotation (see link below) in an Java array to a specific type with an Python interpretable syntax. https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 """ @@ -88,7 +88,7 @@ def _map_syntax(base_type): def _make_pretty_string(entry, offset): """ - Prints the entry with a specific formatting and aligned style + Print the entry with a specific formatting and aligned style. :param entry: Dictionary of class names, modifiers, arguments, and return values. :param offset: Offset between the return value and the method. """ @@ -113,7 +113,7 @@ def _make_pretty_string(entry, offset): def java_source(data): """ - Tries to find the source code using Scijava's SourceFinder + 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 """ @@ -145,7 +145,8 @@ def java_source(data): def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): """ - Writes data to a printed string of class methods with inputs, static modifier, arguments, and return values. + Write data to a printed string of class methods with inputs, static modifier, + arguments, and return values. :param data: The object or class to inspect or fully qualified class name. :param aspect: Whether to print class 'fields' or 'methods'. @@ -195,7 +196,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True def src(data): """ - Prints the source code URL for a Java class, object, or class name. + Print the source code URL for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string """ From bb06ed1f1999542fb3eba0dde63fd47232aeedfc Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:46:14 -0500 Subject: [PATCH 417/505] Wrap >88 lines, and make quoting more consistent --- src/scyjava/_introspect.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index bc5aa4c4..17b844fb 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -1,5 +1,6 @@ """ -Introspection functions for reporting java class 'methods', 'fields', and source code URL. +Introspection functions for reporting Java +class methods, fields, and source code URL. """ from functools import partial @@ -15,7 +16,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: returning a table of its available methods or fields. :param data: The object or class or fully qualified class name to inspect. - :param aspect: Either 'methods' or 'fields' + :param aspect: Either "methods" or "fields" :return: List of dicts with keys: "name", "static", "arguments", and "returns". """ @@ -33,7 +34,7 @@ def jreflect(data, aspect: str) -> List[Dict[str, Any]]: elif aspect == "fields": cls_aspects = jcls.getFields() else: - return "`aspect` must be either 'fields' or 'methods'" + return '`aspect` must be either "fields" or "methods"' table = [] @@ -114,7 +115,8 @@ def _make_pretty_string(entry, offset): def java_source(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. + :param data: + The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ types = jimport("org.scijava.util.Types") @@ -149,8 +151,10 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True arguments, and return values. :param data: The object or class to inspect or fully qualified class name. - :param aspect: Whether to print class 'fields' or 'methods'. - :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). + :param aspect: Whether to print class "fields" or "methods". + :param static: + Boolean filter on Static or Instance methods. + Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ table = jreflect(data, aspect) From 553d552a4316ac0229fa3ea1f01cf014f54ad477 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 2 Apr 2025 14:46:49 -0500 Subject: [PATCH 418/505] Add introspection functions to the README --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 5a2cff1f..8891917e 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,12 @@ FUNCTIONS You can pass a single integer to make a 1-dimensional array of that length. :return: The newly allocated array + java_source(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. @@ -319,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) -> 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: Either "methods" or "fields" + :return: List of dicts with keys: "name", "static", "arguments", and "returns". + jstacktrace(exc) -> str Extract the Java-side stack trace from a Java exception. @@ -427,6 +441,11 @@ FUNCTIONS :raise RuntimeError: if this method is called while in Jep mode. + src(data) + Print the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string + start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From e6826879ac8d6c885bbaf99b24f690ed3316aeae Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sat, 19 Apr 2025 08:04:01 -0500 Subject: [PATCH 419/505] Update license metadata format to new standard --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bbcc8b80..301f2831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "scyjava" version = "1.10.3.dev0" description = "Supercharged Java access from Python" -license = {text = "The Unlicense"} +license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] readme = "README.md" keywords = ["java", "maven", "cross-language"] @@ -21,7 +21,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "License :: OSI Approved :: The Unlicense (Unlicense)", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", "Operating System :: MacOS", From aebbfd963edc0893207b84f7d08b10091ebc4dc8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Apr 2025 18:24:10 -0500 Subject: [PATCH 420/505] Fix syntax of license declaration --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 301f2831..1332f1a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "scyjava" version = "1.10.3.dev0" description = "Supercharged Java access from Python" -license = "Unlicense" +license = {text = "Unlicense"} authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] readme = "README.md" keywords = ["java", "maven", "cross-language"] From 28af43b7073fd3bf31b9d8d62921d2b2fc654c7e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 20 Apr 2025 15:58:04 -0400 Subject: [PATCH 421/505] feat: add auto-fetch with cjdk --- .github/workflows/build.yml | 10 +++- dev-environment.yml | 1 + pyproject.toml | 2 + src/scyjava/_cjdk_fetch.py | 99 +++++++++++++++++++++++++++++++++++++ src/scyjava/_jvm.py | 39 ++++++++++++++- 5 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/scyjava/_cjdk_fetch.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b246a01..92cd367c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,13 @@ jobs: '3.10', '3.12' ] + java-version: ['11'] + include: + # one test without java to test cjdk fallback + - os: ubuntu-latest + python-version: '3.12' + java-version: '' + steps: - uses: actions/checkout@v2 @@ -35,8 +42,9 @@ jobs: python-version: ${{matrix.python-version}} - uses: actions/setup-java@v3 + if: matrix.java-version != '' with: - java-version: '11' + java-version: ${{matrix.java-version}} distribution: 'zulu' cache: 'maven' diff --git a/dev-environment.yml b/dev-environment.yml index d8d25682..2e0f6eea 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -37,5 +37,6 @@ dependencies: # Project from source - pip - pip: + - cjdk - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - -e . diff --git a/pyproject.toml b/pyproject.toml index 1332f1a9..9db6da72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,9 @@ dependencies = [ [project.optional-dependencies] # NB: Keep this in sync with dev-environment.yml! +cjdk = ["cjdk"] dev = [ + "scyjava[cjdk]", "assertpy", "build", "jep", diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py new file mode 100644 index 00000000..a620aad0 --- /dev/null +++ b/src/scyjava/_cjdk_fetch.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import logging +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +_logger = logging.getLogger(__name__) +_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 +_DEFAULT_MAVEN_SHA = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 +_DEFAULT_JAVA_VENDOR = "zulu-jre" +_DEFAULT_JAVA_VERSION = "11" + + +def cjdk_fetch_java( + vendor: str = "", version: str = "", raise_on_error: bool = True +) -> None: + """Fetch java using cjdk and add it to the PATH.""" + try: + import cjdk + except ImportError as e: + if raise_on_error is True: + raise ImportError( + "No JVM found. Please install `cjdk` to use the fetch_java feature." + ) from e + _logger.info("cjdk is not installed. Skipping automatic fetching of java.") + return + + if not vendor: + vendor = os.getenv("JAVA_VENDOR", _DEFAULT_JAVA_VENDOR) + version = os.getenv("JAVA_VERSION", _DEFAULT_JAVA_VERSION) + + _logger.info(f"No JVM found, fetching {vendor}:{version} using cjdk...") + home = cjdk.java_home(vendor=vendor, version=version) + _add_to_path(str(home / "bin")) + os.environ["JAVA_HOME"] = str(home) + + +def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) -> None: + """Fetch Maven using cjdk and add it to the PATH.""" + try: + import cjdk + except ImportError as e: + if raise_on_error is True: + raise ImportError( + "Please install `cjdk` to use the fetch_java feature." + ) from e + _logger.info("cjdk is not installed. Skipping automatic fetching of Maven.") + return + + # if url was passed as an argument, or env_var, use it with provided sha + # otherwise, use default values for both + if url := url or os.getenv("MAVEN_URL", ""): + sha = sha or os.getenv("MAVEN_SHA", "") + else: + url = _DEFAULT_MAVEN_URL + sha = _DEFAULT_MAVEN_SHA + + # fix urls to have proper prefix for cjdk + if url.startswith("http"): + if url.endswith(".tar.gz"): + url = url.replace("http", "tgz+http") + elif url.endswith(".zip"): + url = url.replace("http", "zip+http") + + # determine sha type based on length (cjdk requires specifying sha type) + # assuming hex-encoded SHA, length should be 40, 64, or 128 + kwargs = {} + if sha_len := len(sha): # empty sha is fine... we just don't pass it + sha_lengths = {40: "sha1", 64: "sha256", 128: "sha512"} + if sha_len not in sha_lengths: + raise ValueError( + "MAVEN_SHA be a valid sha1, sha256, or sha512 hash." + f"Got invalid SHA length: {sha_len}. " + ) + kwargs = {sha_lengths[sha_len]: sha} + + maven_dir = cjdk.cache_package("Maven", url, **kwargs) + if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None): + _add_to_path(maven_bin.parent, front=True) + else: + raise RuntimeError("Failed to find Maven executable in the downloaded package.") + + +def _add_to_path(path: Path | str, front: bool = False) -> None: + """Add a path to the PATH environment variable. + + If front is True, the path is added to the front of the PATH. + By default, the path is added to the end of the PATH. + If the path is already in the PATH, it is not added again. + """ + + current_path = os.environ.get("PATH", "") + if (path := str(path)) in current_path: + return + new_path = [path, current_path] if front else [current_path, path] + os.environ["PATH"] = os.pathsep.join(new_path) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 1a6c5ca1..0b8f215e 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -6,6 +6,7 @@ import logging import os import re +import shutil import subprocess import sys from functools import lru_cache @@ -16,6 +17,7 @@ import jpype.config from jgo import jgo +from scyjava._cjdk_fetch import cjdk_fetch_java, cjdk_fetch_maven import scyjava.config from scyjava.config import Mode, mode @@ -106,7 +108,7 @@ def jvm_version() -> str: return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=None) -> None: +def start_jvm(options=None, *, fetch_java: bool | None = None) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling @@ -117,6 +119,13 @@ def start_jvm(options=None) -> None: :param options: List of options to pass to the JVM. For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + :param fetch_java: + Whether to automatically fetch a JRE (and/or maven) using + [`cjdk`](https://github.com/cachedjdk/cjdk) if java and maven executables are + not found. Requires `cjdk` to be installed. See README for details. + - If `None` (default), then fetching will only occur if `cjdk` is available. + - If `True`, an exception will be raised if `cjdk` is not available. + - If `False`, no attempt to import `cjdk` is be made. """ # if JVM is already running -- break if jvm_started(): @@ -132,8 +141,14 @@ def start_jvm(options=None) -> None: # use the logger to notify user that endpoints are being added _logger.debug("Adding jars from endpoints {0}".format(endpoints)) + if fetch_java is not False and not is_jvm_available(): + cjdk_fetch_java(raise_on_error=fetch_java is True) + # get endpoints and add to JPype class path if len(endpoints) > 0: + if not shutil.which("mvn") and fetch_java is not False: + cjdk_fetch_maven(raise_on_error=fetch_java is True) + endpoints = endpoints[:1] + sorted(endpoints[1:]) _logger.debug("Using endpoints %s", endpoints) _, workspace = jgo.resolve_dependencies( @@ -340,6 +355,28 @@ def is_jvm_headless() -> bool: return bool(GraphicsEnvironment.isHeadless()) +def is_jvm_available() -> bool: + """ + Return True if the JVM is available, suppressing stderr on macos. + """ + from unittest.mock import patch + + subprocess_check_output = subprocess.check_output + + def _silent_check_output(*args, **kwargs): + # also suppress stderr on calls to subprocess.check_output + kwargs.setdefault("stderr", subprocess.DEVNULL) + return subprocess_check_output(*args, **kwargs) + + try: + with patch.object(subprocess, "check_output", new=_silent_check_output): + jpype.getDefaultJVMPath() + # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home` + except (jpype.JVMNotFoundException, subprocess.CalledProcessError): + return False + return True + + def is_awt_initialized() -> bool: """ Return true iff the AWT subsystem has been initialized. From aa913dd2bb2132bf36a59c2fe5da06b89429f194 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 20 Apr 2025 16:06:32 -0400 Subject: [PATCH 422/505] reorg --- src/scyjava/_cjdk_fetch.py | 38 ++++++++++++++++++++++++++++++++++++-- src/scyjava/_jvm.py | 38 +++++++------------------------------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py index a620aad0..3cdcfbab 100644 --- a/src/scyjava/_cjdk_fetch.py +++ b/src/scyjava/_cjdk_fetch.py @@ -2,8 +2,12 @@ import logging import os +import shutil +import subprocess from typing import TYPE_CHECKING +import jpype + if TYPE_CHECKING: from pathlib import Path @@ -14,6 +18,34 @@ _DEFAULT_JAVA_VERSION = "11" +def ensure_jvm_available(raise_on_error: bool = True) -> None: + """Ensure that the JVM is available, or raise if `raise_on_error` is True.""" + if not is_jvm_available(): + cjdk_fetch_java(raise_on_error=raise_on_error) + if not shutil.which("mvn"): + cjdk_fetch_maven(raise_on_error=raise_on_error) + + +def is_jvm_available() -> bool: + """Return True if the JVM is available, suppressing stderr on macos.""" + from unittest.mock import patch + + subprocess_check_output = subprocess.check_output + + def _silent_check_output(*args, **kwargs): + # also suppress stderr on calls to subprocess.check_output + kwargs.setdefault("stderr", subprocess.DEVNULL) + return subprocess_check_output(*args, **kwargs) + + try: + with patch.object(subprocess, "check_output", new=_silent_check_output): + jpype.getDefaultJVMPath() + # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home` + except (jpype.JVMNotFoundException, subprocess.CalledProcessError): + return False + return True + + def cjdk_fetch_java( vendor: str = "", version: str = "", raise_on_error: bool = True ) -> None: @@ -25,7 +57,7 @@ def cjdk_fetch_java( raise ImportError( "No JVM found. Please install `cjdk` to use the fetch_java feature." ) from e - _logger.info("cjdk is not installed. Skipping automatic fetching of java.") + _logger.info("JVM not found. Please install `cjdk` fetch java automatically.") return if not vendor: @@ -47,7 +79,9 @@ def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) raise ImportError( "Please install `cjdk` to use the fetch_java feature." ) from e - _logger.info("cjdk is not installed. Skipping automatic fetching of Maven.") + _logger.info( + "Maven not found. Please install `cjdk` fetch maven automatically." + ) return # if url was passed as an argument, or env_var, use it with provided sha diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 0b8f215e..a69a737a 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -6,7 +6,6 @@ import logging import os import re -import shutil import subprocess import sys from functools import lru_cache @@ -17,7 +16,7 @@ import jpype.config from jgo import jgo -from scyjava._cjdk_fetch import cjdk_fetch_java, cjdk_fetch_maven +from scyjava._cjdk_fetch import ensure_jvm_available import scyjava.config from scyjava.config import Mode, mode @@ -120,10 +119,12 @@ def start_jvm(options=None, *, fetch_java: bool | None = None) -> None: List of options to pass to the JVM. For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] :param fetch_java: - Whether to automatically fetch a JRE (and/or maven) using + Whether to automatically fetch a JRE (and maven) using [`cjdk`](https://github.com/cachedjdk/cjdk) if java and maven executables are - not found. Requires `cjdk` to be installed. See README for details. + not found. Requires `cjdk` to be installed, either manually, or via the + `scyjava[cjdk]` extra. - If `None` (default), then fetching will only occur if `cjdk` is available. + (A log info will be issued if `cjdk` is not available.) - If `True`, an exception will be raised if `cjdk` is not available. - If `False`, no attempt to import `cjdk` is be made. """ @@ -141,14 +142,11 @@ def start_jvm(options=None, *, fetch_java: bool | None = None) -> None: # use the logger to notify user that endpoints are being added _logger.debug("Adding jars from endpoints {0}".format(endpoints)) - if fetch_java is not False and not is_jvm_available(): - cjdk_fetch_java(raise_on_error=fetch_java is True) + if fetch_java is not False: + ensure_jvm_available(raise_on_error=fetch_java is True) # get endpoints and add to JPype class path if len(endpoints) > 0: - if not shutil.which("mvn") and fetch_java is not False: - cjdk_fetch_maven(raise_on_error=fetch_java is True) - endpoints = endpoints[:1] + sorted(endpoints[1:]) _logger.debug("Using endpoints %s", endpoints) _, workspace = jgo.resolve_dependencies( @@ -355,28 +353,6 @@ def is_jvm_headless() -> bool: return bool(GraphicsEnvironment.isHeadless()) -def is_jvm_available() -> bool: - """ - Return True if the JVM is available, suppressing stderr on macos. - """ - from unittest.mock import patch - - subprocess_check_output = subprocess.check_output - - def _silent_check_output(*args, **kwargs): - # also suppress stderr on calls to subprocess.check_output - kwargs.setdefault("stderr", subprocess.DEVNULL) - return subprocess_check_output(*args, **kwargs) - - try: - with patch.object(subprocess, "check_output", new=_silent_check_output): - jpype.getDefaultJVMPath() - # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home` - except (jpype.JVMNotFoundException, subprocess.CalledProcessError): - return False - return True - - def is_awt_initialized() -> bool: """ Return true iff the AWT subsystem has been initialized. From c9cf3cae8a0bb5503a64d4c14243c8957a0a2aed Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 20 Apr 2025 16:09:07 -0400 Subject: [PATCH 423/505] move import --- src/scyjava/_jvm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index a69a737a..13fe8667 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -16,7 +16,6 @@ import jpype.config from jgo import jgo -from scyjava._cjdk_fetch import ensure_jvm_available import scyjava.config from scyjava.config import Mode, mode @@ -143,6 +142,8 @@ def start_jvm(options=None, *, fetch_java: bool | None = None) -> None: _logger.debug("Adding jars from endpoints {0}".format(endpoints)) if fetch_java is not False: + from scyjava._cjdk_fetch import ensure_jvm_available + ensure_jvm_available(raise_on_error=fetch_java is True) # get endpoints and add to JPype class path From ace4df58d2d4ec62c46220b7cf8e312e7f002439 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Apr 2025 18:19:20 -0500 Subject: [PATCH 424/505] Avoid usage of pipe symbol in type hints --- src/scyjava/_cjdk_fetch.py | 4 ++-- src/scyjava/_jvm.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py index 3cdcfbab..81993355 100644 --- a/src/scyjava/_cjdk_fetch.py +++ b/src/scyjava/_cjdk_fetch.py @@ -4,7 +4,7 @@ import os import shutil import subprocess -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union import jpype @@ -118,7 +118,7 @@ def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) raise RuntimeError("Failed to find Maven executable in the downloaded package.") -def _add_to_path(path: Path | str, front: bool = False) -> None: +def _add_to_path(path: Union[Path, str], front: bool = False) -> None: """Add a path to the PATH environment variable. If front is True, the path is added to the front of the PATH. diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 13fe8667..90f42d06 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -11,6 +11,7 @@ from functools import lru_cache from importlib import import_module from pathlib import Path +from typing import Optional import jpype import jpype.config @@ -106,7 +107,7 @@ def jvm_version() -> str: return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=None, *, fetch_java: bool | None = None) -> None: +def start_jvm(options=None, *, fetch_java: Optional[bool] = None) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From 64ffbeebbe9ff314fb9256046ffdf26ab5097f6b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Apr 2025 18:38:09 -0500 Subject: [PATCH 425/505] CI: skip jep tests when running the javaless job * Include java version in job names, to disambiguate them. * As a hack for now, just fail if Java >= v17 or <8. --- .github/workflows/build.yml | 3 +-- bin/test.sh | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92cd367c..29b8797b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ on: jobs: build-cross-platform: - name: test ${{matrix.os}} - ${{matrix.python-version}} + name: test ${{matrix.os}} - ${{matrix.python-version}} - ${{matrix.java-version}} runs-on: ${{ matrix.os }} strategy: matrix: @@ -33,7 +33,6 @@ jobs: python-version: '3.12' java-version: '' - steps: - uses: actions/checkout@v2 diff --git a/bin/test.sh b/bin/test.sh index da27567c..db003331 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -73,7 +73,12 @@ then else argString="" fi -if [ "$(uname -s)" = "Darwin" ] +if ! java -version 2>&1 | grep -q '^openjdk version "\(1\.8\|9\|10\|11\|12\|13\|14\|15\|16\)\.' +then + echo "Skipping jep tests due to unsupported Java version:" + java -version || true + jepCode=0 +elif [ "$(uname -s)" = "Darwin" ] then echo "Skipping jep tests on macOS due to flakiness" jepCode=0 From ccb2271cf1e8056d8425a55c5d513080354101f0 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 20 Apr 2025 21:12:54 -0400 Subject: [PATCH 426/505] include cjdk by default --- pyproject.toml | 3 +-- src/scyjava/_cjdk_fetch.py | 46 +++++++++++--------------------------- src/scyjava/_jvm.py | 26 ++++++++++++--------- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9db6da72..89b42007 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,12 @@ requires-python = ">=3.8" dependencies = [ "jpype1 >= 1.3.0", "jgo", + "cjdk", ] [project.optional-dependencies] # NB: Keep this in sync with dev-environment.yml! -cjdk = ["cjdk"] dev = [ - "scyjava[cjdk]", "assertpy", "build", "jep", diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py index 81993355..27c8035f 100644 --- a/src/scyjava/_cjdk_fetch.py +++ b/src/scyjava/_cjdk_fetch.py @@ -6,6 +6,7 @@ import subprocess from typing import TYPE_CHECKING, Union +import cjdk import jpype if TYPE_CHECKING: @@ -18,12 +19,12 @@ _DEFAULT_JAVA_VERSION = "11" -def ensure_jvm_available(raise_on_error: bool = True) -> None: - """Ensure that the JVM is available, or raise if `raise_on_error` is True.""" +def ensure_jvm_available() -> None: + """Ensure that the JVM is available and Maven is installed.""" if not is_jvm_available(): - cjdk_fetch_java(raise_on_error=raise_on_error) + cjdk_fetch_java() if not shutil.which("mvn"): - cjdk_fetch_maven(raise_on_error=raise_on_error) + cjdk_fetch_maven() def is_jvm_available() -> bool: @@ -46,20 +47,8 @@ def _silent_check_output(*args, **kwargs): return True -def cjdk_fetch_java( - vendor: str = "", version: str = "", raise_on_error: bool = True -) -> None: +def cjdk_fetch_java(vendor: str = "", version: str = "") -> None: """Fetch java using cjdk and add it to the PATH.""" - try: - import cjdk - except ImportError as e: - if raise_on_error is True: - raise ImportError( - "No JVM found. Please install `cjdk` to use the fetch_java feature." - ) from e - _logger.info("JVM not found. Please install `cjdk` fetch java automatically.") - return - if not vendor: vendor = os.getenv("JAVA_VENDOR", _DEFAULT_JAVA_VENDOR) version = os.getenv("JAVA_VERSION", _DEFAULT_JAVA_VERSION) @@ -70,20 +59,8 @@ def cjdk_fetch_java( os.environ["JAVA_HOME"] = str(home) -def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) -> None: +def cjdk_fetch_maven(url: str = "", sha: str = "") -> None: """Fetch Maven using cjdk and add it to the PATH.""" - try: - import cjdk - except ImportError as e: - if raise_on_error is True: - raise ImportError( - "Please install `cjdk` to use the fetch_java feature." - ) from e - _logger.info( - "Maven not found. Please install `cjdk` fetch maven automatically." - ) - return - # if url was passed as an argument, or env_var, use it with provided sha # otherwise, use default values for both if url := url or os.getenv("MAVEN_URL", ""): @@ -104,7 +81,7 @@ def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) kwargs = {} if sha_len := len(sha): # empty sha is fine... we just don't pass it sha_lengths = {40: "sha1", 64: "sha256", 128: "sha512"} - if sha_len not in sha_lengths: + if sha_len not in sha_lengths: # pragma: no cover raise ValueError( "MAVEN_SHA be a valid sha1, sha256, or sha512 hash." f"Got invalid SHA length: {sha_len}. " @@ -114,8 +91,11 @@ def cjdk_fetch_maven(url: str = "", sha: str = "", raise_on_error: bool = True) maven_dir = cjdk.cache_package("Maven", url, **kwargs) if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None): _add_to_path(maven_bin.parent, front=True) - else: - raise RuntimeError("Failed to find Maven executable in the downloaded package.") + else: # pragma: no cover + raise RuntimeError( + "Failed to find Maven executable on system " + "PATH, and download via cjdk failed." + ) def _add_to_path(path: Union[Path, str], front: bool = False) -> None: diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 90f42d06..617b9d67 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -107,7 +107,7 @@ def jvm_version() -> str: return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=None, *, fetch_java: Optional[bool] = None) -> None: +def start_jvm(options=None, *, fetch_java: bool = True) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling @@ -119,14 +119,18 @@ def start_jvm(options=None, *, fetch_java: Optional[bool] = None) -> None: List of options to pass to the JVM. For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] :param fetch_java: - Whether to automatically fetch a JRE (and maven) using - [`cjdk`](https://github.com/cachedjdk/cjdk) if java and maven executables are - not found. Requires `cjdk` to be installed, either manually, or via the - `scyjava[cjdk]` extra. - - If `None` (default), then fetching will only occur if `cjdk` is available. - (A log info will be issued if `cjdk` is not available.) - - If `True`, an exception will be raised if `cjdk` is not available. - - If `False`, no attempt to import `cjdk` is be made. + If True (default), when a JVM/or maven cannot be located on the system, + [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download + a JRE distribution and set up the JVM. The following environment variables + may be used to configure the JRE and Maven distributions to download: + * `JAVA_VENDOR`: The vendor of the JRE distribution to download. + Defaults to "zulu-jre". + * `JAVA_VERSION`: The version of the JRE distribution to download. + Defaults to "11". + * `MAVEN_URL`: The URL of the Maven distribution to download. + Defaults to https://dlcdn.apache.org/maven/maven-3/3.9.9/ + * `MAVEN_SHA`: The SHA512 hash of the Maven distribution to download, if + providing a custom MAVEN_URL. """ # if JVM is already running -- break if jvm_started(): @@ -142,10 +146,10 @@ def start_jvm(options=None, *, fetch_java: Optional[bool] = None) -> None: # use the logger to notify user that endpoints are being added _logger.debug("Adding jars from endpoints {0}".format(endpoints)) - if fetch_java is not False: + if fetch_java: from scyjava._cjdk_fetch import ensure_jvm_available - ensure_jvm_available(raise_on_error=fetch_java is True) + ensure_jvm_available() # get endpoints and add to JPype class path if len(endpoints) > 0: From 0af3f650c310499a34c64654671ddef90267801d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Apr 2025 20:17:20 -0500 Subject: [PATCH 427/505] Bump to setuptools 77.0.0 to use new license field And bump the minimum Python version to 3.9, since that version of setuptools requires it. And raise the test ceiling from Python 3.12 to 3.13. --- .github/workflows/build.yml | 7 +++---- dev-environment.yml | 2 +- environment.yml | 2 +- pyproject.toml | 10 +++++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29b8797b..a6dbb292 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,15 +22,14 @@ jobs: macos-latest ] python-version: [ - '3.8', - '3.10', - '3.12' + '3.9', + '3.13' ] java-version: ['11'] include: # one test without java to test cjdk fallback - os: ubuntu-latest - python-version: '3.12' + python-version: '3.9' java-version: '' steps: diff --git a/dev-environment.yml b/dev-environment.yml index 2e0f6eea..bbacab49 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -18,7 +18,7 @@ name: scyjava-dev channels: - conda-forge dependencies: - - python >= 3.8 + - python >= 3.9 # Project dependencies - jpype1 >= 1.3.0 - jgo diff --git a/environment.yml b/environment.yml index c1038c57..d3e3af90 100644 --- a/environment.yml +++ b/environment.yml @@ -19,7 +19,7 @@ name: scyjava channels: - conda-forge dependencies: - - python >= 3.8 + - python >= 3.9 # Project dependencies - jpype1 >= 1.3.0 - jgo diff --git a/pyproject.toml b/pyproject.toml index 89b42007..8da9830c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] -requires = ["setuptools>=61.2"] +requires = ["setuptools>=77.0.0"] 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 = "Unlicense"} +license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] readme = "README.md" keywords = ["java", "maven", "cross-language"] @@ -16,11 +16,11 @@ classifiers = [ "Intended Audience :: Education", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", "Operating System :: MacOS", @@ -31,7 +31,7 @@ classifiers = [ ] # NB: Keep this in sync with environment.yml AND dev-environment.yml! -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "jpype1 >= 1.3.0", "jgo", From 3af5c6e0cdff27b25ff28bc56c7f14274af0baa5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Apr 2025 20:20:50 -0500 Subject: [PATCH 428/505] Make ruff happy --- src/scyjava/_jvm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 617b9d67..2035e349 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -11,7 +11,6 @@ from functools import lru_cache from importlib import import_module from pathlib import Path -from typing import Optional import jpype import jpype.config From ec51083975e281b9f5ceaf4e1bb2f25f5693d9e3 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 22 Apr 2025 22:47:44 -0400 Subject: [PATCH 429/505] chore: update env.yml files for cjdk --- dev-environment.yml | 4 ++-- environment.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index bbacab49..9fa75a79 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -12,7 +12,7 @@ # # In addition to the dependencies needed for using scyjava, it # includes tools for developer-related actions like running -# automated tests (pytest) and linting the code (black). If you +# automated tests (pytest) and linting the code (ruff). If you # want an environment without these tools, use environment.yml. name: scyjava-dev channels: @@ -22,6 +22,7 @@ dependencies: # Project dependencies - jpype1 >= 1.3.0 - jgo + - cjdk - openjdk >= 8, < 12 # Test dependencies - numpy @@ -37,6 +38,5 @@ dependencies: # Project from source - pip - pip: - - cjdk - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - -e . diff --git a/environment.yml b/environment.yml index d3e3af90..e222104b 100644 --- a/environment.yml +++ b/environment.yml @@ -12,7 +12,7 @@ # # It includes the dependencies needed for using scyjava, but not tools # for developer-related actions like running automated tests (pytest), -# linting the code (black), and generating the API documentation (sphinx). +# linting the code (ruff), and generating the API documentation (sphinx). # If you want an environment including these tools, use dev-environment.yml. name: scyjava @@ -23,6 +23,7 @@ dependencies: # Project dependencies - jpype1 >= 1.3.0 - jgo + - cjdk - openjdk >= 8 # Project from source - pip From 5753a593c80fcff17a5fdb66b7b7234bae0a7a7e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 22 Apr 2025 16:37:42 -0500 Subject: [PATCH 430/505] Remove openjdk conda dependency cjdk is lighter weight and offers more control over the JDK/JRE used. People can still use the openjdk package with scyjava, of course. --- dev-environment.yml | 1 - environment.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/dev-environment.yml b/dev-environment.yml index 9fa75a79..7bbcf289 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -23,7 +23,6 @@ dependencies: - jpype1 >= 1.3.0 - jgo - cjdk - - openjdk >= 8, < 12 # Test dependencies - numpy - pandas diff --git a/environment.yml b/environment.yml index e222104b..bb4bd198 100644 --- a/environment.yml +++ b/environment.yml @@ -24,7 +24,6 @@ dependencies: - jpype1 >= 1.3.0 - jgo - cjdk - - openjdk >= 8 # Project from source - pip - pip: From bf2ee8235dbfbd93c63e0135c3a75d19b83c0ea1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:09:05 -0500 Subject: [PATCH 431/505] Improve get_version method --- src/scyjava/_versions.py | 16 ++++++++++++---- tests/test_versions.py | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/scyjava/_versions.py b/src/scyjava/_versions.py index c1695db7..f1632195 100644 --- a/src/scyjava/_versions.py +++ b/src/scyjava/_versions.py @@ -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. @@ -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: diff --git a/tests/test_versions.py b/tests/test_versions.py index 3b5fafcc..d588a0b8 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -2,6 +2,7 @@ Tests for functions in _versions submodule. """ +from importlib.metadata import version from pathlib import Path import toml @@ -18,8 +19,18 @@ def _expected_version(): def test_version(): - # First, ensure that the version is correct - assert _expected_version() == scyjava.__version__ + sjver = _expected_version() - # Then, ensure that we get the correct version via get_version - assert _expected_version() == scyjava.get_version("scyjava") + # First, ensure that the version is correct. + assert sjver == scyjava.__version__ + + # Then, ensure that we get the correct version via get_version. + assert sjver == scyjava.get_version("scyjava") + assert sjver == scyjava.get_version(scyjava) + assert sjver == scyjava.get_version("scyjava.config") + assert sjver == scyjava.get_version(scyjava.config) + assert sjver == scyjava.get_version(scyjava.config.mode) + assert sjver == scyjava.get_version(scyjava.config.Mode) + + # And that we get the correct version of other things, too. + assert version("toml") == scyjava.get_version(toml) From 2d44fed2690032bf86191c8daa4218c8da92ea3b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:10:55 -0500 Subject: [PATCH 432/505] Test a little further into the GitHub source paths --- tests/test_introspect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index a12c0513..f4bb485c 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -51,9 +51,9 @@ def test_find_source(self): SF = scyjava.jimport(str_SF) source_strSF = scyjava.java_source(str_SF) source_SF = scyjava.java_source(SF) - github_home = "https://github.com/" - assert source_strSF.startsWith(github_home) - assert source_SF.startsWith(github_home) + repo_path = "https://github.com/scijava/scijava-search/" + assert source_strSF.startsWith(repo_path) + assert source_SF.startsWith(repo_path) assert source_strSF == source_SF def test_imagej_legacy(self): @@ -63,5 +63,5 @@ def test_imagej_legacy(self): str_RE = "ij.plugin.RoiEnlarger" table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 - github_home = "https://github.com/" - assert scyjava.java_source(str_RE).startsWith(github_home) + repo_path = "https://github.com/imagej/ImageJ/" + assert scyjava.java_source(str_RE).startsWith(repo_path) From 8846ce5429474e98e5941ff4425ca21553962631 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:15:42 -0500 Subject: [PATCH 433/505] Tweak management of multiple endpoints For consistency with scripting integration tests. --- tests/test_introspect.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index f4bb485c..3b7d659b 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -8,8 +8,9 @@ import scyjava from scyjava.config import Mode, mode -scyjava.config.endpoints.append("net.imagej:imagej") -scyjava.config.endpoints.append("net.imagej:imagej-legacy:MANAGED") +scyjava.config.endpoints.extend( + ["net.imagej:imagej", "net.imagej:imagej-legacy:MANAGED"] +) class TestIntrospection(object): From a31701b331ae09bfbc2518591411dcd3b2cee869 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 16:27:05 -0500 Subject: [PATCH 434/505] Rename java_source method to jsource More consistent with the rest of the library. --- README.md | 2 +- src/scyjava/__init__.py | 2 +- src/scyjava/_introspect.py | 6 +++--- tests/test_introspect.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8891917e..e78a7c6d 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ FUNCTIONS You can pass a single integer to make a 1-dimensional array of that length. :return: The newly allocated array - java_source(data) + 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. diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e42b51a6..166099a7 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -94,8 +94,8 @@ from ._introspect import ( attrs, fields, - java_source, jreflect, + jsource, methods, src, ) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 17b844fb..dc5bb676 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -112,7 +112,7 @@ def _make_pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def java_source(data): +def jsource(data): """ Try to find the source code using SciJava's SourceFinder. :param data: @@ -166,7 +166,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True offset = max(list(map(lambda entry: len(entry["returns"]), table))) all_methods = "" if source: - urlstring = java_source(data) + urlstring = jsource(data) print(f"Source code URL: {urlstring}") # Print methods @@ -204,5 +204,5 @@ def src(data): :param data: The Java class, object, or fully qualified class name as string """ - source_url = java_source(data) + source_url = jsource(data) print(f"Source code URL: {source_url}") diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 3b7d659b..3108c208 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -44,14 +44,14 @@ def test_jreflect_fields(self): assert bitset_Obj is not None assert bitset_Obj == str_Obj - def test_find_source(self): + def test_jsource(self): if mode == Mode.JEP: # JEP does not support the jclass function. return str_SF = "org.scijava.search.SourceFinder" SF = scyjava.jimport(str_SF) - source_strSF = scyjava.java_source(str_SF) - source_SF = scyjava.java_source(SF) + source_strSF = scyjava.jsource(str_SF) + source_SF = scyjava.jsource(SF) repo_path = "https://github.com/scijava/scijava-search/" assert source_strSF.startsWith(repo_path) assert source_SF.startsWith(repo_path) @@ -65,4 +65,4 @@ def test_imagej_legacy(self): table = scyjava.jreflect(str_RE, aspect="methods") assert len([entry for entry in table if entry["static"]]) == 3 repo_path = "https://github.com/imagej/ImageJ/" - assert scyjava.java_source(str_RE).startsWith(repo_path) + assert scyjava.jsource(str_RE).startsWith(repo_path) From 2713b6cc44ddf5ba0c9a261033a04f331ea26c02 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 17:18:02 -0500 Subject: [PATCH 435/505] Make jreflect function more powerful --- README.md | 8 ++--- src/scyjava/_introspect.py | 69 ++++++++++++++++++++++++-------------- tests/test_introspect.py | 2 +- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e78a7c6d..81337fce 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ AttributeError: 'list' object has no attribute 'stream' Traceback (most recent call last): File "", line 1, in 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 @@ -325,13 +325,13 @@ 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) -> List[Dict[str, Any]] + 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: Either "methods" or "fields" - :return: List of dicts with keys: "name", "static", "arguments", and "returns". + :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. diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index dc5bb676..413aa0db 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -10,54 +10,73 @@ class methods, fields, and source code URL. from scyjava._types import isjava, jinstance, jclass -def jreflect(data, aspect: str) -> List[Dict[str, Any]]: +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: Either "methods" or "fields" - :return: List of dicts with keys: "name", "static", "arguments", and "returns". + :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 err: - raise ValueError(f"Not a Java object {err}") + except Exception as e: + raise ValueError( + f"Object of type '{type(data).__name__}' is not a Java object" + ) from e - Modifier = jimport("java.lang.reflect.Modifier") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if aspect == "methods": - cls_aspects = jcls.getMethods() - elif aspect == "fields": - cls_aspects = jcls.getFields() - else: - return '`aspect` must be either "fields" or "methods"' + 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 m in cls_aspects: - name = m.getName() - if aspect == "methods": - args = [c.getName() for c in m.getParameterTypes()] - returns = m.getReturnType().getName() - elif aspect == "fields": - args = None - returns = m.getType().getName() - mods = Modifier.isStatic(m.getModifiers()) + 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 None) + ) table.append( { + "type": mtype, "name": name, - "static": mods, + "mods": mods, "arguments": args, "returns": returns, } ) - sorted_table = sorted(table, key=lambda d: d["name"]) - return sorted_table + return table def _map_syntax(base_type): @@ -98,7 +117,7 @@ def _make_pretty_string(entry, offset): return_val = f"{entry['returns'].__str__():<{offset}}" # Handle whether to print static/instance modifiers obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if entry["static"] else f"{'':>4}" + modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" # Handle fields if entry["arguments"] is None: diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 3108c208..09eb6921 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -63,6 +63,6 @@ def test_imagej_legacy(self): return str_RE = "ij.plugin.RoiEnlarger" table = scyjava.jreflect(str_RE, aspect="methods") - assert len([entry for entry in table if entry["static"]]) == 3 + assert sum(1 for entry in table if "static" in entry["mods"]) == 3 repo_path = "https://github.com/imagej/ImageJ/" assert scyjava.jsource(str_RE).startsWith(repo_path) From 92d7fb154e89df78a6f6c81d5f018680efaa54be Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 19:59:01 -0500 Subject: [PATCH 436/505] Split pretty-print functions to own subpackage * scyjava.fields -> scyjava.inspect.fields * scyjava.methods -> scyjava.inspect.methods * scyjava.src -> scyjava.inspect.src And add new `constructors` and `members` convenience functions. --- README.md | 5 -- src/scyjava/__init__.py | 4 - src/scyjava/_introspect.py | 120 +--------------------------- src/scyjava/inspect.py | 155 +++++++++++++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 127 deletions(-) create mode 100644 src/scyjava/inspect.py diff --git a/README.md b/README.md index 81337fce..cedcfcb1 100644 --- a/README.md +++ b/README.md @@ -441,11 +441,6 @@ FUNCTIONS :raise RuntimeError: if this method is called while in Jep mode. - src(data) - Print the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - start_jvm(options=None) -> None Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 166099a7..4f7c6a2f 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -92,12 +92,8 @@ to_python, ) from ._introspect import ( - attrs, - fields, jreflect, jsource, - methods, - src, ) from ._jvm import ( # noqa: F401 available_processors, diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 413aa0db..067abe73 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -3,8 +3,7 @@ class methods, fields, and source code URL. """ -from functools import partial -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from scyjava._jvm import jimport from scyjava._types import isjava, jinstance, jclass @@ -64,7 +63,7 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: returns = ( member.getReturnType().getName() if hasattr(member, "getReturnType") - else (member.getType().getName() if hasattr(member, "getType") else None) + else (member.getType().getName() if hasattr(member, "getType") else name) ) table.append( { @@ -79,58 +78,6 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: return table -def _map_syntax(base_type): - """ - Map a Java BaseType annotation (see link below) in an Java array - to a specific type with an Python interpretable syntax. - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 - """ - basetype_mapping = { - "[B": "byte[]", - "[C": "char[]", - "[D": "double[]", - "[F": "float[]", - "[I": "int[]", - "[J": "long[]", - "[L": "[]", # array - "[S": "short[]", - "[Z": "boolean[]", - } - - if base_type in basetype_mapping: - return basetype_mapping[base_type] - # Handle the case of a returned array of an object - elif base_type.__str__().startswith("[L"): - return base_type.__str__()[2:-1] + "[]" - else: - return base_type - - -def _make_pretty_string(entry, offset): - """ - Print the entry with a specific formatting and aligned style. - :param entry: Dictionary of class names, modifiers, arguments, and return values. - :param offset: Offset between the return value and the method. - """ - - # A star implies that the method is a static method - return_val = f"{entry['returns'].__str__():<{offset}}" - # Handle whether to print static/instance modifiers - obj_name = f"{entry['name']}" - modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" - - # Handle fields - if entry["arguments"] is None: - return f"{return_val} {modifier} = {obj_name}\n" - - # Handle methods with no arguments - if len(entry["arguments"]) == 0: - return f"{return_val} {modifier} = {obj_name}()\n" - else: - arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) - return f"{return_val} {modifier} = {obj_name}({arg_string})\n" - - def jsource(data): """ Try to find the source code using SciJava's SourceFinder. @@ -162,66 +109,3 @@ def jsource(data): return f"Not a Java class {str(type(data))}" except Exception as err: return f"Unexpected {err=}, {type(err)=}" - - -def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): - """ - Write data to a printed string of class methods with inputs, static modifier, - arguments, and return values. - - :param data: The object or class to inspect or fully qualified class name. - :param aspect: Whether to print class "fields" or "methods". - :param static: - Boolean filter on Static or Instance methods. - Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. - """ - table = jreflect(data, aspect) - if len(table) == 0: - print(f"No {aspect} found") - return - - # Print source code - offset = max(list(map(lambda entry: len(entry["returns"]), table))) - all_methods = "" - if source: - urlstring = jsource(data) - print(f"Source code URL: {urlstring}") - - # Print methods - for entry in table: - entry["returns"] = _map_syntax(entry["returns"]) - if entry["arguments"]: - entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - if static is None: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - - elif static and entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - elif not static and not entry["static"]: - entry_string = _make_pretty_string(entry, offset) - all_methods += entry_string - else: - continue - - # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) - - -# The functions with short names for quick usage. -methods = partial(_print_data, aspect="methods") -fields = partial(_print_data, aspect="fields") -attrs = partial(_print_data, aspect="fields") - - -def src(data): - """ - Print the source code URL for a Java class, object, or class name. - - :param data: The Java class, object, or fully qualified class name as string - """ - source_url = jsource(data) - print(f"Source code URL: {source_url}") diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py new file mode 100644 index 00000000..882cd612 --- /dev/null +++ b/src/scyjava/inspect.py @@ -0,0 +1,155 @@ +""" +High-level convenience functions for inspecting Java objects. +""" + +from typing import Optional + +from scyjava._introspect import jreflect, jsource + + +def members(data): + """ + Print all the members (constructors, fields, and methods) + for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="all") + + +def constructors(data): + """ + Print the constructors for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="constructors") + + +def fields(data): + """ + Print the fields for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="fields") + + +def methods(data): + """ + Print the methods for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + _print_data(data, aspect="methods") + + +def src(data): + """ + Print the source code URL for a Java class, object, or class name. + + :param data: The Java class, object, or fully qualified class name as string. + """ + source_url = jsource(data) + print(f"Source code URL: {source_url}") + + +def _map_syntax(base_type): + """ + Map a Java BaseType annotation (see link below) in an Java array + to a specific type with an Python interpretable syntax. + https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 + """ + basetype_mapping = { + "[B": "byte[]", + "[C": "char[]", + "[D": "double[]", + "[F": "float[]", + "[I": "int[]", + "[J": "long[]", + "[L": "[]", # array + "[S": "short[]", + "[Z": "boolean[]", + } + + if base_type in basetype_mapping: + return basetype_mapping[base_type] + # Handle the case of a returned array of an object + elif base_type.__str__().startswith("[L"): + return base_type.__str__()[2:-1] + "[]" + else: + return base_type + + +def _pretty_string(entry, offset): + """ + Print the entry with a specific formatting and aligned style. + + :param entry: Dictionary of class names, modifiers, arguments, and return values. + :param offset: Offset between the return value and the method. + """ + + # A star implies that the method is a static method + return_type = entry["returns"] or "void" + return_val = f"{return_type.__str__():<{offset}}" + # Handle whether to print static/instance modifiers + obj_name = f"{entry['name']}" + modifier = f"{'*':>4}" if "static" in entry["mods"] else f"{'':>4}" + + # Handle fields + if entry["arguments"] is None: + return f"{return_val} {modifier} = {obj_name}\n" + + # Handle methods with no arguments + if len(entry["arguments"]) == 0: + return f"{return_val} {modifier} = {obj_name}()\n" + else: + arg_string = ", ".join([r.__str__() for r in entry["arguments"]]) + return f"{return_val} {modifier} = {obj_name}({arg_string})\n" + + +def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): + """ + Write data to a printed table with inputs, static modifier, + arguments, and return values. + + :param data: The object or class to inspect or fully qualified class name. + :param static: + Boolean filter on Static or Instance methods. + Optional, default is None (prints all). + :param source: Whether to print any available source code. Default True. + """ + table = jreflect(data, aspect) + if len(table) == 0: + print(f"No {aspect} found") + return + + # Print source code + offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) + all_methods = "" + if source: + urlstring = jsource(data) + print(f"Source code URL: {urlstring}") + + # Print methods + for entry in table: + if entry["returns"]: + entry["returns"] = _map_syntax(entry["returns"]) + if entry["arguments"]: + entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] + if static is None: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + + elif static and "static" in entry["mods"]: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + elif not static and "static" not in entry["mods"]: + entry_string = _pretty_string(entry, offset) + all_methods += entry_string + else: + continue + + # 4 added to align the asterisk with output. + print(f"{'':<{offset + 4}}* indicates static modifier") + print(all_methods) From 30bd4d3a9b5e1535a4124d6310e3bde983823afe Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 20:48:13 -0500 Subject: [PATCH 437/505] Hide non-public scijava.config attrs --- src/scyjava/config.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index e2cc0073..712b2ab1 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -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" @@ -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): @@ -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): From a537c00f3e9cf04770fff11b02bd69d3b9f5035b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 20:58:24 -0500 Subject: [PATCH 438/505] Hide non-public scyjava.inspect attrs --- src/scyjava/inspect.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 882cd612..2ea01f14 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -2,9 +2,7 @@ High-level convenience functions for inspecting Java objects. """ -from typing import Optional - -from scyjava._introspect import jreflect, jsource +from scyjava import _introspect def members(data): @@ -50,7 +48,7 @@ def src(data): :param data: The Java class, object, or fully qualified class name as string. """ - source_url = jsource(data) + source_url = _introspect.jsource(data) print(f"Source code URL: {source_url}") @@ -108,7 +106,7 @@ def _pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True): +def _print_data(data, aspect, static: bool | None = None, source: bool = True): """ Write data to a printed table with inputs, static modifier, arguments, and return values. @@ -119,7 +117,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ - table = jreflect(data, aspect) + table = _introspect.jreflect(data, aspect) if len(table) == 0: print(f"No {aspect} found") return @@ -128,7 +126,7 @@ def _print_data(data, aspect, static: Optional[bool] = None, source: bool = True offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) all_methods = "" if source: - urlstring = jsource(data) + urlstring = _introspect.jsource(data) print(f"Source code URL: {urlstring}") # Print methods From cea10cd4322796f09ef885b2dca6fccea996ac86 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:02:12 -0500 Subject: [PATCH 439/505] Use jimport naming convention for Java types --- src/scyjava/_introspect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 067abe73..a00b5b88 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -85,9 +85,9 @@ def jsource(data): The object or class or fully qualified class name to check for source code. :return: The URL of the java class """ - types = jimport("org.scijava.util.Types") - sf = jimport("org.scijava.search.SourceFinder") - jstring = jimport("java.lang.String") + Types = jimport("org.scijava.util.Types") + SourceFinder = jimport("org.scijava.search.SourceFinder") + String = jimport("java.lang.String") try: if not isjava(data) and isinstance(data, str): try: @@ -95,10 +95,10 @@ def jsource(data): except Exception as err: raise ValueError(f"Not a Java object {err}") jcls = data if jinstance(data, "java.lang.Class") else jclass(data) - if types.location(jcls).toString().startsWith(jstring("jrt")): + if Types.location(jcls).toString().startsWith(String("jrt")): # Handles Java RunTime (jrt) exceptions. raise ValueError("Java Builtin: GitHub source code not available") - url = sf.sourceLocation(jcls, None) + url = SourceFinder.sourceLocation(jcls, None) urlstring = url.toString() return urlstring except jimport("java.lang.IllegalArgumentException") as err: From 3a46f1bde40d39e3bcb535032dd7aca8de9b1fd1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:51:46 -0500 Subject: [PATCH 440/505] Make output writer configurable --- src/scyjava/inspect.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 2ea01f14..13b5f036 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -2,54 +2,62 @@ High-level convenience functions for inspecting Java objects. """ +from sys import stdout as _stdout + from scyjava import _introspect -def members(data): +def members(data, writer=None): """ Print all the members (constructors, fields, and methods) for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="all") + _print_data(data, aspect="all", writer=writer) -def constructors(data): +def constructors(data, writer=None): """ Print the constructors for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="constructors") + _print_data(data, aspect="constructors", writer=writer) -def fields(data): +def fields(data, writer=None): """ Print the fields for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="fields") + _print_data(data, aspect="fields", writer=writer) -def methods(data): +def methods(data, writer=None): """ Print the methods for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ _print_data(data, aspect="methods") -def src(data): +def src(data, writer=None): """ Print the source code URL for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. + :param writer: Function to which output will be sent, sys.stdout.write by default. """ + writer = writer or _stdout.write source_url = _introspect.jsource(data) - print(f"Source code URL: {source_url}") + writer(f"Source code URL: {source_url}\n") def _map_syntax(base_type): @@ -106,7 +114,9 @@ def _pretty_string(entry, offset): return f"{return_val} {modifier} = {obj_name}({arg_string})\n" -def _print_data(data, aspect, static: bool | None = None, source: bool = True): +def _print_data( + data, aspect, static: bool | None = None, source: bool = True, writer=None +): """ Write data to a printed table with inputs, static modifier, arguments, and return values. @@ -117,9 +127,10 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): Optional, default is None (prints all). :param source: Whether to print any available source code. Default True. """ + writer = writer or _stdout.write table = _introspect.jreflect(data, aspect) if len(table) == 0: - print(f"No {aspect} found") + writer(f"No {aspect} found\n") return # Print source code @@ -127,7 +138,7 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): all_methods = "" if source: urlstring = _introspect.jsource(data) - print(f"Source code URL: {urlstring}") + writer(f"Source code URL: {urlstring}\n") # Print methods for entry in table: @@ -147,7 +158,8 @@ def _print_data(data, aspect, static: bool | None = None, source: bool = True): all_methods += entry_string else: continue + all_methods += "\n" # 4 added to align the asterisk with output. - print(f"{'':<{offset + 4}}* indicates static modifier") - print(all_methods) + writer(f"{'':<{offset + 4}}* indicates static modifier\n") + writer(all_methods) From 5fe146166d52ae4082e1f22491deac6603d023af Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 21:53:46 -0500 Subject: [PATCH 441/505] Replace print statements with logger calls --- src/scyjava/_jvm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 1a6c5ca1..742b6eb9 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -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(): @@ -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: From 2f11f0d8c5efb7719448c58cfe42b6ee372b89f0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 22:35:31 -0500 Subject: [PATCH 442/505] Add unit test for inspect.members function --- tests/test_inspect.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/test_inspect.py diff --git a/tests/test_inspect.py b/tests/test_inspect.py new file mode 100644 index 00000000..c99076d4 --- /dev/null +++ b/tests/test_inspect.py @@ -0,0 +1,29 @@ +""" +Tests for functions in inspect submodule. +""" + +from scyjava import inspect +from scyjava.config import mode, Mode + + +class TestInspect(object): + """ + Test scyjava.inspect convenience functions. + """ + + def test_inspect_members(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + members = [] + inspect.members("java.lang.Iterable", writer=members.append) + expected = [ + "Source code URL: java.lang.NullPointerException", + " * indicates static modifier", + "java.util.Iterator = iterator()", + "java.util.Spliterator = spliterator()", + "void = forEach(java.util.function.Consumer)", + "", + "", + ] + assert expected == "".join(members).split("\n") From f4266c40fa2c3dc8940b185fd72776bc39ab6734 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 24 Apr 2025 22:45:36 -0500 Subject: [PATCH 443/505] Ensure submodules are directly available This code worked: import scyjava print(scyjava.config) But this code didn't: import scyjava print(scyjava.inspect) Because scyjava.config was being imported in another file further down the chain. Better to be explicit about wanting both of these submodules available at the top level. --- src/scyjava/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 4f7c6a2f..e19cc790 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -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, From 1d9b507d0b51932a21af44d6e869411e8efb2923 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 25 Apr 2025 18:07:37 -0500 Subject: [PATCH 444/505] Let jsource also find Java library source code And do not try so hard with exception handling; it should be up to higher level functions like scyjava.inspect.src to catch such failures. --- src/scyjava/_introspect.py | 71 +++++++++++++++++++++++--------------- tests/test_inspect.py | 6 ++-- tests/test_introspect.py | 11 ++++++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index a00b5b88..9449fc8d 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -5,7 +5,7 @@ class methods, fields, and source code URL. from typing import Any, Dict, List -from scyjava._jvm import jimport +from scyjava._jvm import jimport, jvm_version from scyjava._types import isjava, jinstance, jclass @@ -78,34 +78,49 @@ def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: return table -def jsource(data): +def jsource(data) -> str: """ - Try to find the source code using SciJava's SourceFinder. + 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: - The object or class or fully qualified class name to check for source code. - :return: The URL of the java class + Object, class, or fully qualified class name for which to discern the source code location. + :return: URL of the class's source code. """ - Types = jimport("org.scijava.util.Types") + + 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") - String = jimport("java.lang.String") - try: - 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 Types.location(jcls).toString().startsWith(String("jrt")): - # Handles Java RunTime (jrt) exceptions. - raise ValueError("Java Builtin: GitHub source code not available") - url = SourceFinder.sourceLocation(jcls, None) - urlstring = url.toString() - return urlstring - except jimport("java.lang.IllegalArgumentException") as err: - return f"Illegal argument provided {err=}, {type(err)=}" - except ValueError as err: - return f"{err}" - except TypeError: - return f"Not a Java class {str(type(data))}" - except Exception as err: - return f"Unexpected {err=}, {type(err)=}" + url = SourceFinder.sourceLocation(jcls, None) + urlstring = url.toString() + return urlstring diff --git a/tests/test_inspect.py b/tests/test_inspect.py index c99076d4..4eca8d95 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -18,7 +18,9 @@ def test_inspect_members(self): members = [] inspect.members("java.lang.Iterable", writer=members.append) expected = [ - "Source code URL: java.lang.NullPointerException", + "Source code URL: " + "https://github.com/openjdk/jdk/blob/jdk-11-ga/" + "src/java.base/share/classes/java/lang/Iterable.java", " * indicates static modifier", "java.util.Iterator = iterator()", "java.util.Spliterator = spliterator()", @@ -26,4 +28,4 @@ def test_inspect_members(self): "", "", ] - assert expected == "".join(members).split("\n") + assert "".join(members).split("\n") == expected diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 09eb6921..bbfa93bc 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -57,6 +57,17 @@ def test_jsource(self): assert source_SF.startsWith(repo_path) assert source_strSF == source_SF + def test_jsource_jdk_class(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + jv = scyjava.jvm_version()[0] + source = scyjava.jsource("java.util.List") + assert ( + source == f"https://github.com/openjdk/jdk/blob/jdk-{jv}-ga/" + "src/java.base/share/classes/java/util/List.java" + ) + def test_imagej_legacy(self): if mode == Mode.JEP: # JEP does not support the jclass function. From 5c067473145d6cc49ba5255b3738a07c2b3d8bb3 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 25 Apr 2025 18:09:38 -0500 Subject: [PATCH 445/505] Be less aggressive with source code detection When invoking scyjava.inspect functions, they can optionally report the source URL at the top, before printing the members. But this only works if scijava-search is on the classpath. Let's let the source flag default to None, in which case it swallows source code URL detection failures gracefully, to make the common case of scijava-search not being available work without hassle. And let's have the various inspect functions accept static and source boolean flags, which get passed along to the internal _print_data routine, as was previously the case when they were partial functions. --- src/scyjava/inspect.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 13b5f036..6b0d9e8e 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -7,7 +7,7 @@ from scyjava import _introspect -def members(data, writer=None): +def members(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print all the members (constructors, fields, and methods) for a Java class, object, or class name. @@ -15,30 +15,34 @@ def members(data, writer=None): :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="all", writer=writer) + _print_data(data, aspect="all", static=static, source=source, writer=writer) -def constructors(data, writer=None): +def constructors( + data, static: bool | None = None, source: bool | None = None, writer=None +): """ Print the constructors for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="constructors", writer=writer) + _print_data( + data, aspect="constructors", static=static, source=source, writer=writer + ) -def fields(data, writer=None): +def fields(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print the fields for a Java class, object, or class name. :param data: The Java class, object, or fully qualified class name as string. :param writer: Function to which output will be sent, sys.stdout.write by default. """ - _print_data(data, aspect="fields", writer=writer) + _print_data(data, aspect="fields", static=static, source=source, writer=writer) -def methods(data, writer=None): +def methods(data, static: bool | None = None, source: bool | None = None, writer=None): """ Print the methods for a Java class, object, or class name. @@ -115,7 +119,7 @@ def _pretty_string(entry, offset): def _print_data( - data, aspect, static: bool | None = None, source: bool = True, writer=None + data, aspect, static: bool | None = None, source: bool | None = None, writer=None ): """ Write data to a printed table with inputs, static modifier, @@ -125,7 +129,11 @@ def _print_data( :param static: Boolean filter on Static or Instance methods. Optional, default is None (prints all). - :param source: Whether to print any available source code. Default True. + :param source: + Whether to discern and report a URL to the relevant source code. + Requires org.scijava:scijava-search to be on the classpath. + When set to None (the default), autodetects whether scijava-search + is available, reporting source URL if so, or leaving it out if not. """ writer = writer or _stdout.write table = _introspect.jreflect(data, aspect) @@ -136,9 +144,15 @@ def _print_data( # Print source code offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) all_methods = "" - if source: - urlstring = _introspect.jsource(data) - writer(f"Source code URL: {urlstring}\n") + if source or source is None: + try: + urlstring = _introspect.jsource(data) + writer(f"Source code URL: {urlstring}\n") + except TypeError: + if source: + writer( + "Classpath lacks scijava-search; no source code URL detection is available.\n" + ) # Print methods for entry in table: From e5e9cab0184ccfb09b96c30469bbca6b78fc8295 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 13:25:33 -0500 Subject: [PATCH 446/505] Add test for jreflect constructors --- tests/test_introspect.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/test_introspect.py b/tests/test_introspect.py index bbfa93bc..7c7fe994 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -39,11 +39,46 @@ def test_jreflect_fields(self): BitSet = scyjava.jimport(str_BitSet) str_Obj = scyjava.jreflect(str_BitSet, "fields") bitset_Obj = scyjava.jreflect(BitSet, "fields") - assert len(str_Obj) == 0 - assert len(bitset_Obj) == 0 + assert len(str_Obj) == len(bitset_Obj) == 0 assert bitset_Obj is not None assert bitset_Obj == str_Obj + def test_jreflect_ctors(self): + if mode == Mode.JEP: + # JEP does not support the jclass function. + return + str_ArrayList = "java.util.ArrayList" + ArrayList = scyjava.jimport(str_ArrayList) + str_Obj = scyjava.jreflect(str_ArrayList, "constructors") + arraylist_Obj = scyjava.jreflect(ArrayList, "constructors") + assert len(str_Obj) == len(arraylist_Obj) == 3 + arraylist_Obj.sort( + key=lambda row: f"{row['type']}:{row['name']}:{','.join(str(row['arguments']))}" + ) + assert arraylist_Obj == [ + { + "arguments": ["int"], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + { + "arguments": ["java.util.Collection"], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + { + "arguments": [], + "mods": ["public"], + "name": "java.util.ArrayList", + "returns": "java.util.ArrayList", + "type": "constructor", + }, + ] + def test_jsource(self): if mode == Mode.JEP: # JEP does not support the jclass function. From b083173f8618d01c919f306b62c7a1b1bebd21e5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 12:19:54 -0500 Subject: [PATCH 447/505] Let type annotation syntax to work with Python 3.9 And fix the development Python to 3.9, so that local testing catches problems like this sooner. --- dev-environment.yml | 2 +- src/scyjava/config.py | 2 ++ src/scyjava/inspect.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev-environment.yml b/dev-environment.yml index 7bbcf289..a2fb77ea 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -18,7 +18,7 @@ name: scyjava-dev channels: - conda-forge dependencies: - - python >= 3.9 + - python = 3.9 # Project dependencies - jpype1 >= 1.3.0 - jgo diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 712b2ab1..36093051 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import enum as _enum import logging as _logging import os as _os diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 6b0d9e8e..3058822e 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -2,6 +2,8 @@ High-level convenience functions for inspecting Java objects. """ +from __future__ import annotations + from sys import stdout as _stdout from scyjava import _introspect From 64dd34cf97d1d0d2bc8207dedf0affb79b10b6fa Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 14:28:15 -0500 Subject: [PATCH 448/505] Fix jvm_version return type declaration --- src/scyjava/_jvm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index acdd3f57..c9117d0b 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -25,16 +25,16 @@ _shutdown_callbacks = [] -def jvm_version() -> str: +def jvm_version() -> tuple[int, ...]: """ Gets the version of the JVM as a tuple, with each dot-separated digit as one element. Characters in the version string beyond only numbers and dots are ignored, in line with the java.version system property. Examples: - * OpenJDK 17.0.1 -> [17, 0, 1] - * OpenJDK 11.0.9.1-internal -> [11, 0, 9, 1] - * OpenJDK 1.8.0_312 -> [1, 8, 0] + * OpenJDK 17.0.1 -> (17, 0, 1) + * OpenJDK 11.0.9.1-internal -> (11, 0, 9, 1) + * OpenJDK 1.8.0_312 -> (1, 8, 0) If the JVM is already started, this function returns the equivalent of: jimport('java.lang.System') From 51f96a4fd6b56d31378c79cdebfbda058800f870 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 11:51:29 -0500 Subject: [PATCH 449/505] Generalize JVM source code expectations The tests won't necessarily run with Java 11. And if the Java version is 1.8 or earlier, use the second digit. --- src/scyjava/_introspect.py | 4 +++- tests/test_inspect.py | 14 ++++++++++---- tests/test_introspect.py | 10 +++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 9449fc8d..42680380 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -99,7 +99,9 @@ def jsource(data) -> str: cls_path = str(jcls.getName()).replace(".", "/") # Discern the Java version. - java_version = jvm_version()[0] + jv_digits = jvm_version() + assert jv_digits is not None and len(jv_digits) > 1 + java_version = jv_digits[1] if jv_digits[0] == 1 else jv_digits[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+ diff --git a/tests/test_inspect.py b/tests/test_inspect.py index 4eca8d95..d308307d 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -2,6 +2,8 @@ Tests for functions in inspect submodule. """ +import re + from scyjava import inspect from scyjava.config import mode, Mode @@ -18,9 +20,8 @@ def test_inspect_members(self): members = [] inspect.members("java.lang.Iterable", writer=members.append) expected = [ - "Source code URL: " - "https://github.com/openjdk/jdk/blob/jdk-11-ga/" - "src/java.base/share/classes/java/lang/Iterable.java", + "Source code URL: https://github.com/openjdk/jdk/blob/" + ".../share/classes/java/lang/Iterable.java", " * indicates static modifier", "java.util.Iterator = iterator()", "java.util.Spliterator = spliterator()", @@ -28,4 +29,9 @@ def test_inspect_members(self): "", "", ] - assert "".join(members).split("\n") == expected + pattern = ( + r"(https://github.com/openjdk/jdk/blob/)" + "[^ ]*(/share/classes/java/lang/Iterable\.java)" + ) + members_string = re.sub(pattern, r"\1...\2", "".join(members)) + assert members_string.split("\n") == expected diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 7c7fe994..e986dffd 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -96,12 +96,12 @@ def test_jsource_jdk_class(self): if mode == Mode.JEP: # JEP does not support the jclass function. return - jv = scyjava.jvm_version()[0] + jv_digits = scyjava.jvm_version() + jv = jv_digits[1] if jv_digits[0] == 1 else jv_digits[0] source = scyjava.jsource("java.util.List") - assert ( - source == f"https://github.com/openjdk/jdk/blob/jdk-{jv}-ga/" - "src/java.base/share/classes/java/util/List.java" - ) + assert source.startswith("https://github.com/openjdk/jdk/blob/") + assert source.endswith("/share/classes/java/util/List.java") + assert str(jv) in source def test_imagej_legacy(self): if mode == Mode.JEP: From e36a950c484ba52cca5085a71e73a00443649502 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 14:13:03 -0500 Subject: [PATCH 450/505] Add type hints and docstrings to scyjava.config --- src/scyjava/config.py | 129 ++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 29 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 36093051..17075972 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -3,7 +3,8 @@ import enum as _enum import logging as _logging import os as _os -import pathlib as _pathlib +from pathlib import Path +from typing import Sequence import jpype as _jpype from jgo import maven_scijava_repository as _scijava_public @@ -11,13 +12,13 @@ _logger = _logging.getLogger(__name__) -endpoints = [] +endpoints: list[str] = [] _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 = Path.home() / ".jgo" +_m2_repo = Path.home() / ".m2" / "repository" _options = [] _shortcuts = {} @@ -62,7 +63,11 @@ def get_endpoints(): return endpoints -def add_repositories(*args, **kwargs): +def add_repositories(*args, **kwargs) -> None: + """ + Add one or more Maven repositories to be used by jgo for downloading dependencies. + See the jgo documentation for details. + """ global _repositories for arg in args: _logger.debug("Adding repositories %s to %s", arg, _repositories) @@ -71,57 +76,92 @@ def add_repositories(*args, **kwargs): _repositories.update(kwargs) -def get_repositories(): +def get_repositories() -> dict[str, str]: + """ + Gets the Maven repositories jgo will use for downloading dependencies. + See the jgo documentation for details. + """ global _repositories return _repositories -def set_verbose(level): +def set_verbose(level: int) -> None: + """ + Set the level of verbosity for logging environment construction details. + + :param level: + 0 for quiet (default), 1 for verbose, 2 for extra verbose. + """ global _verbose _logger.debug("Setting verbose level to %d (was %d)", level, _verbose) _verbose = level -def get_verbose(): +def get_verbose() -> int: + """ + Get the level of verbosity for logging environment construction details. + """ global _verbose _logger.debug("Getting verbose level: %d", _verbose) return _verbose -def set_manage_deps(manage): +def set_manage_deps(manage: bool) -> None: + """ + Set whether jgo will resolve dependencies in managed mode. + See the jgo documentation for details. + """ global _manage_deps _logger.debug("Setting manage deps to %d (was %d)", manage, _manage_deps) _manage_deps = manage -def get_manage_deps(): +def get_manage_deps() -> bool: + """ + Get whether jgo will resolve dependencies in managed mode. + See the jgo documentation for details. + """ global _manage_deps return _manage_deps -def set_cache_dir(dir): +def set_cache_dir(cache_dir: Path | str) -> None: + """ + Set the location to use for the jgo environment cache. + See the jgo documentation for details. + """ global _cache_dir - _logger.debug("Setting cache dir to %s (was %s)", dir, _cache_dir) - _cache_dir = dir + _logger.debug("Setting cache dir to %s (was %s)", cache_dir, _cache_dir) + _cache_dir = cache_dir -def get_cache_dir(): +def get_cache_dir() -> Path: + """ + Get the location to use for the jgo environment cache. + See the jgo documentation for details. + """ global _cache_dir return _cache_dir -def set_m2_repo(dir): +def set_m2_repo(repo_dir : Path | str) -> None: + """ + Set the location to use for the local Maven repository cache. + """ global _m2_repo - _logger.debug("Setting m2 repo dir to %s (was %s)", dir, _m2_repo) - _m2_repo = dir + _logger.debug("Setting m2 repo dir to %s (was %s)", repo_dir, _m2_repo) + _m2_repo = repo_dir -def get_m2_repo(): +def get_m2_repo() -> Path: + """ + Get the location to use for the local Maven repository cache. + """ global _m2_repo return _m2_repo -def add_classpath(*path): +def add_classpath(*path) -> None: """ Add elements to the Java class path. @@ -150,7 +190,7 @@ def add_classpath(*path): _jpype.addClassPath(p) -def find_jars(directory): +def find_jars(directory: Path | str) -> list[str]: """ Find .jar files beneath a given directory. @@ -166,11 +206,14 @@ def find_jars(directory): return jars -def get_classpath(): +def get_classpath() -> str: + """ + Get the classpath to be passed to the JVM at startup. + """ return _jpype.getClassPath() -def set_heap_min(mb: int = None, gb: int = None): +def set_heap_min(mb: int = None, gb: int = None) -> None: """ Set the initial amount of memory to allocate to the Java heap. @@ -187,7 +230,7 @@ def set_heap_min(mb: int = None, gb: int = None): add_option(f"-Xms{_mem_value(mb, gb)}") -def set_heap_max(mb: int = None, gb: int = None): +def set_heap_max(mb: int = None, gb: int = None) -> None: """ Shortcut for passing -Xmx###m or -Xmx###g to Java. @@ -210,7 +253,7 @@ def _mem_value(mb: int = None, gb: int = None) -> str: raise ValueError("Exactly one of mb or gb must be given.") -def enable_headless_mode(): +def enable_headless_mode() -> None: """ Enable headless mode, for running Java without a display. This mode prevents any graphical elements from popping up. @@ -239,12 +282,29 @@ def enable_remote_debugging(port: int = 8000, suspend: bool = False): add_option(f"-agentlib:jdwp={arg_string}") -def add_option(option): +def add_option(option: str) -> None: + """ + Add an option to pass at JVM startup. Examples: + + -Djava.awt.headless=true + -Xmx10g + --add-opens=java.base/java.lang=ALL-UNNAMED + -XX:+UnlockExperimentalVMOptions + + :param option: + The option to add. + """ global _options _options.append(option) -def add_options(options): +def add_options(options: str | Sequence) -> None: + """ + Add one or more options to pass at JVM startup. + + :param options: + Sequence of options to add, or single string to pass as an individual option. + """ global _options if isinstance(options, str): _options.append(options) @@ -252,16 +312,27 @@ def add_options(options): _options.extend(options) -def get_options(): +def get_options() -> list[str]: + """ + Get the list of options to be passed at JVM startup. + """ global _options return _options -def add_shortcut(k, v): +def add_shortcut(k: str, v: str): + """ + Add a shortcut key/value to be used by jgo for evaluating endpoints. + See the jgo documentation for details. + """ global _shortcuts _shortcuts[k] = v -def get_shortcuts(): +def get_shortcuts() -> dict[str, str]: + """ + Get the dictionary of shorts that jgo will use for evaluating endpoints. + See the jgo documentation for details. + """ global _shortcuts return _shortcuts From 02913c5e902bfc9e656bc0a540749b12aceceace Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 14:20:36 -0500 Subject: [PATCH 451/505] Let scyjava.config accept JPype keyword arguments Closes #79. --- src/scyjava/_jvm.py | 3 ++- src/scyjava/config.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index c9117d0b..a7e6889e 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -198,7 +198,8 @@ def start_jvm(options=None, *, fetch_java: bool = True) -> None: _logger.debug("Starting JVM") if options is None: options = scyjava.config.get_options() - jpype.startJVM(*options, interrupt=True) + kwargs = scyjava.config.get_kwargs() + jpype.startJVM(*options, **kwargs) # replace JPype/JVM shutdown handling with our own jpype.config.onexit = False diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 17075972..dda4e700 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -20,6 +20,7 @@ _cache_dir = Path.home() / ".jgo" _m2_repo = Path.home() / ".m2" / "repository" _options = [] +_kwargs = {"interrupt": True} _shortcuts = {} @@ -320,6 +321,27 @@ def get_options() -> list[str]: return _options +def add_kwargs(**kwargs) -> None: + """ + Add keyword arguments to be passed to JPype at JVM startup. Examples: + + jvmpath = "/path/to/my_jvm" + ignoreUnrecognized = True + convertStrings = True + interrupt = True + """ + global _kwargs + _kwargs.update(kwargs) + + +def get_kwargs() -> dict[str, str]: + """ + Get the keyword arguments to be passed to JPype at JVM startup. + """ + global _kwargs + return _kwargs + + def add_shortcut(k: str, v: str): """ Add a shortcut key/value to be used by jgo for evaluating endpoints. From 6f2d4f171563b00e7404d74d118b79b78a3d6327 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 14:28:37 -0500 Subject: [PATCH 452/505] Avoid variable name shadowing --- src/scyjava/_jvm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index a7e6889e..3c21b2d5 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -55,12 +55,12 @@ def jvm_version() -> tuple[int, ...]: assert mode == Mode.JPYPE - jvm_version = jpype.getJVMVersion() - if jvm_version and jvm_version[0]: + jvm_ver = jpype.getJVMVersion() + if jvm_ver and jvm_ver[0]: # JPype already knew the version. # JVM is probably already started. # Or JPype got smarter since 1.3.0. - return jvm_version + return jvm_ver # JPype was clueless, which means the JVM has probably not started yet. # Let's look for a java executable, and ask via 'java -version'. From fdbda7e913f3d0af3f64a8f088fb3116a9001db1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 15:15:08 -0500 Subject: [PATCH 453/505] Add a "see also" for start_jvm options --- src/scyjava/_jvm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 3c21b2d5..c4c5e57c 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -117,6 +117,7 @@ def start_jvm(options=None, *, fetch_java: bool = True) -> None: :param options: List of options to pass to the JVM. For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] + See also scyjava.config.add_options. :param fetch_java: If True (default), when a JVM/or maven cannot be located on the system, [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download From dad2b2a5ac449eb3ca2aa2c147e7fc0b654b22ac Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Tue, 29 Apr 2025 15:15:23 -0500 Subject: [PATCH 454/505] Soften annoying start_jvm debug warning Now it only shows up when options were attempted to be passed. --- src/scyjava/_jvm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index c4c5e57c..484439f5 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -134,7 +134,8 @@ def start_jvm(options=None, *, fetch_java: bool = True) -> None: """ # if JVM is already running -- break if jvm_started(): - _logger.debug("The JVM is already running.") + if options is not None and len(options) > 0: + _logger.debug(f"Options ignored due to already running JVM: {options}") return assert mode == Mode.JPYPE From 69ff2c39cb49eaa6e1ccf7ae900cb137e362bfcc Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 09:55:03 -0500 Subject: [PATCH 455/505] Move deprecated functions to bottom of source file --- src/scyjava/config.py | 54 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index dda4e700..2a4c6925 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -37,33 +37,6 @@ class Mode(_enum.Enum): mode = Mode.JPYPE -def add_endpoints(*new_endpoints): - """ - DEPRECATED since v1.2.1 - Please modify the endpoints field directly instead. - """ - _logger.warning( - "Deprecated method call: scyjava.config.add_endpoints(). " - "Please modify scyjava.config.endpoints directly instead." - ) - global endpoints - _logger.debug("Adding endpoints %s to %s", new_endpoints, endpoints) - endpoints.extend(new_endpoints) - - -def get_endpoints(): - """ - DEPRECATED since v1.2.1 - Please access the endpoints field directly instead. - """ - _logger.warning( - "Deprecated method call: scyjava.config.get_endpoints(). " - "Please access scyjava.config.endpoints directly instead." - ) - global endpoints - return endpoints - - def add_repositories(*args, **kwargs) -> None: """ Add one or more Maven repositories to be used by jgo for downloading dependencies. @@ -358,3 +331,30 @@ def get_shortcuts() -> dict[str, str]: """ global _shortcuts return _shortcuts + + +def add_endpoints(*new_endpoints): + """ + DEPRECATED since v1.2.1 + Please modify the endpoints field directly instead. + """ + _logger.warning( + "Deprecated method call: scyjava.config.add_endpoints(). " + "Please modify scyjava.config.endpoints directly instead." + ) + global endpoints + _logger.debug("Adding endpoints %s to %s", new_endpoints, endpoints) + endpoints.extend(new_endpoints) + + +def get_endpoints(): + """ + DEPRECATED since v1.2.1 + Please access the endpoints field directly instead. + """ + _logger.warning( + "Deprecated method call: scyjava.config.get_endpoints(). " + "Please access scyjava.config.endpoints directly instead." + ) + global endpoints + return endpoints From 0b2e964b65ee771caf2a27b35c0fa7bfa96c4aef Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 09:55:38 -0500 Subject: [PATCH 456/505] Fix tiny issues in scyjava.config source --- src/scyjava/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 2a4c6925..17a1bf3e 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -52,7 +52,7 @@ def add_repositories(*args, **kwargs) -> None: def get_repositories() -> dict[str, str]: """ - Gets the Maven repositories jgo will use for downloading dependencies. + Get the Maven repositories jgo will use for downloading dependencies. See the jgo documentation for details. """ global _repositories @@ -118,7 +118,7 @@ def get_cache_dir() -> Path: return _cache_dir -def set_m2_repo(repo_dir : Path | str) -> None: +def set_m2_repo(repo_dir: Path | str) -> None: """ Set the location to use for the local Maven repository cache. """ From a29e6740390a7c43f3d9b213e40d11842469d493 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 10:55:55 -0500 Subject: [PATCH 457/505] Move cjdk fetch settings into scyjava.config --- src/scyjava/_cjdk_fetch.py | 48 +++++++++------- src/scyjava/_jvm.py | 23 ++------ src/scyjava/config.py | 110 +++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 38 deletions(-) diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py index 27c8035f..378a3733 100644 --- a/src/scyjava/_cjdk_fetch.py +++ b/src/scyjava/_cjdk_fetch.py @@ -1,3 +1,7 @@ +""" +Utility functions for fetching JDK/JRE and Maven. +""" + from __future__ import annotations import logging @@ -9,21 +13,23 @@ import cjdk import jpype +import scyjava.config + if TYPE_CHECKING: from pathlib import Path _logger = logging.getLogger(__name__) -_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 -_DEFAULT_MAVEN_SHA = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 -_DEFAULT_JAVA_VENDOR = "zulu-jre" -_DEFAULT_JAVA_VERSION = "11" def ensure_jvm_available() -> None: """Ensure that the JVM is available and Maven is installed.""" - if not is_jvm_available(): + fetch = scyjava.config.get_fetch_java() + if fetch == "never": + # Not allowed to use cjdk. + return + if fetch == "always" or not is_jvm_available(): cjdk_fetch_java() - if not shutil.which("mvn"): + if fetch == "always" or not shutil.which("mvn"): cjdk_fetch_maven() @@ -47,27 +53,27 @@ def _silent_check_output(*args, **kwargs): return True -def cjdk_fetch_java(vendor: str = "", version: str = "") -> None: +def cjdk_fetch_java(vendor: str | None = None, version: str | None = None) -> None: """Fetch java using cjdk and add it to the PATH.""" - if not vendor: - vendor = os.getenv("JAVA_VENDOR", _DEFAULT_JAVA_VENDOR) - version = os.getenv("JAVA_VERSION", _DEFAULT_JAVA_VERSION) + if vendor is None: + vendor = scyjava.config.get_java_vendor() + if version is None: + version = scyjava.config.get_java_version() - _logger.info(f"No JVM found, fetching {vendor}:{version} using cjdk...") - home = cjdk.java_home(vendor=vendor, version=version) - _add_to_path(str(home / "bin")) - os.environ["JAVA_HOME"] = str(home) + _logger.info(f"Fetching {vendor}:{version} using cjdk...") + java_home = cjdk.java_home(vendor=vendor, version=version) + _logger.debug(f"java_home -> {java_home}") + _add_to_path(str(java_home / "bin"), front=True) + os.environ["JAVA_HOME"] = str(java_home) def cjdk_fetch_maven(url: str = "", sha: str = "") -> None: """Fetch Maven using cjdk and add it to the PATH.""" - # if url was passed as an argument, or env_var, use it with provided sha + # if url was passed as an argument, use it with provided sha # otherwise, use default values for both - if url := url or os.getenv("MAVEN_URL", ""): - sha = sha or os.getenv("MAVEN_SHA", "") - else: - url = _DEFAULT_MAVEN_URL - sha = _DEFAULT_MAVEN_SHA + if not url: + url = scyjava.config.get_maven_url() + sha = scyjava.config.get_maven_sha() # fix urls to have proper prefix for cjdk if url.startswith("http"): @@ -88,7 +94,9 @@ def cjdk_fetch_maven(url: str = "", sha: str = "") -> None: ) kwargs = {sha_lengths[sha_len]: sha} + _logger.info("Fetching Maven using cjdk...") maven_dir = cjdk.cache_package("Maven", url, **kwargs) + _logger.debug(f"maven_dir -> {maven_dir}") if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None): _add_to_path(maven_bin.parent, front=True) else: # pragma: no cover diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 484439f5..224ac618 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -11,6 +11,7 @@ from functools import lru_cache from importlib import import_module from pathlib import Path +from typing import Sequence import jpype import jpype.config @@ -18,6 +19,7 @@ import scyjava.config from scyjava.config import Mode, mode +from scyjava._cjdk_fetch import ensure_jvm_available _logger = logging.getLogger(__name__) @@ -106,7 +108,7 @@ def jvm_version() -> tuple[int, ...]: return tuple(map(int, m.group(1).split("."))) -def start_jvm(options=None, *, fetch_java: bool = True) -> None: +def start_jvm(options: Sequence[str] = None) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling @@ -118,19 +120,6 @@ def start_jvm(options=None, *, fetch_java: bool = True) -> None: List of options to pass to the JVM. For example: ['-Dfoo=bar', '-XX:+UnlockExperimentalVMOptions'] See also scyjava.config.add_options. - :param fetch_java: - If True (default), when a JVM/or maven cannot be located on the system, - [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download - a JRE distribution and set up the JVM. The following environment variables - may be used to configure the JRE and Maven distributions to download: - * `JAVA_VENDOR`: The vendor of the JRE distribution to download. - Defaults to "zulu-jre". - * `JAVA_VERSION`: The version of the JRE distribution to download. - Defaults to "11". - * `MAVEN_URL`: The URL of the Maven distribution to download. - Defaults to https://dlcdn.apache.org/maven/maven-3/3.9.9/ - * `MAVEN_SHA`: The SHA512 hash of the Maven distribution to download, if - providing a custom MAVEN_URL. """ # if JVM is already running -- break if jvm_started(): @@ -147,10 +136,8 @@ def start_jvm(options=None, *, fetch_java: bool = True) -> None: # use the logger to notify user that endpoints are being added _logger.debug("Adding jars from endpoints {0}".format(endpoints)) - if fetch_java: - from scyjava._cjdk_fetch import ensure_jvm_available - - ensure_jvm_available() + # download JDK/JRE and Maven as appropriate + ensure_jvm_available() # get endpoints and add to JPype class path if len(endpoints) > 0: diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 17a1bf3e..e284fc65 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -12,6 +12,13 @@ _logger = _logging.getLogger(__name__) +# Constraints on the Java installation to be used. +_fetch_java: str = "auto" +_java_vendor: str = "zulu-jre" +_java_version: str = "11" +_maven_url: str = "tgz+https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 +_maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 + endpoints: list[str] = [] _repositories = {"scijava.public": _scijava_public()} @@ -37,6 +44,109 @@ class Mode(_enum.Enum): mode = Mode.JPYPE +def set_java_constraints( + fetch: str | bool | None = None, + vendor: str | None = None, + version: str | None = None, + maven_url: str | None = None, + maven_sha: str | None = None, +) -> None: + """ + Set constraints on the version of Java to be used. + + :param fetch: + If "auto" (default), when a JVM/or maven cannot be located on the system, + [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download + a JDK/JRE distribution and set up the JVM. + If "always", cjdk will always be used; if "never", cjdk will never be used. + :param vendor: + The vendor of the JDK/JRE distribution for cjdk to download and cache. + Defaults to "zulu-jre". See the cjdk documentation for details. + :param version: + Expression defining the Java version for cjdk to download and cache. + Defaults to "11". See the cjdk documentation for details. + :param maven_url: + URL of the Maven distribution for cjdk to download and cache. + Defaults to the Maven 3.9.9 binary distribution from dlcdn.apache.org. + :param maven_sha: + The SHA512 (or SHA256 or SHA1) hash of the Maven distribution to download, + if providing a custom maven_url. + """ + global _fetch_java, _java_vendor, _java_version, _maven_url, _maven_sha + if fetch is not None: + if isinstance(fetch, bool): + # Be nice and allow boolean values as a convenience. + fetch = "always" if fetch else "never" + expected = ["auto", "always", "never"] + if fetch not in expected: + raise ValueError(f"Fetch mode {fetch} is not one of {expected}") + _fetch_java = fetch + if vendor is not None: + _java_vendor = vendor + if version is not None: + _java_version = version + if maven_url is not None: + _maven_url = maven_url + _maven_sha = "" + if maven_sha is not None: + _maven_sha = maven_sha + + +def get_fetch_java() -> str: + """ + Get whether [`cjdk`](https://github.com/cachedjdk/cjdk) + will be used to download a JDK/JRE distribution and set up the JVM. + To set this value, see set_java_constraints. + + :return: + "always" for cjdk to obtain the JDK/JRE; + "never" for cjdk *not* to obtain a JDK/JRE; + "auto" for cjdk to be used only when a JVM/or Maven is not on the system path. + """ + return _fetch_java + + +def get_java_vendor() -> str: + """ + Get the vendor of the JDK/JRE distribution to download. + Vendor of the Java installation for cjdk to download and cache. + To set this value, see set_java_constraints. + + :return: String defining the desired JDK/JRE vendor for downloaded JDK/JREs. + """ + return _java_vendor + + +def get_java_version() -> str: + """ + Expression defining the Java version for cjdk to download and cache. + To set this value, see set_java_constraints. + + :return: String defining the desired JDK/JRE version for downloaded JDK/JREs. + """ + return _java_version + + +def get_maven_url() -> str: + """ + The URL of the Maven distribution to download. + To set this value, see set_java_constraints. + + :return: URL pointing to the Maven distribution. + """ + return _maven_url + + +def get_maven_sha() -> str: + """ + The SHA512 (or SHA256 or SHA1) hash of the Maven distribution to download, + if providing a custom maven_url. To set this value, see set_java_constraints. + + :return: Hash value of the Maven distribution, or empty string to skip hash check. + """ + return _maven_sha + + def add_repositories(*args, **kwargs) -> None: """ Add one or more Maven repositories to be used by jgo for downloading dependencies. From 630ff9db34113017857d02a5fa1a4dcab53fd3d5 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 12:00:49 -0500 Subject: [PATCH 458/505] Add Java bootstrapping example to documentation --- src/scyjava/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index e19cc790..6d23d097 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -40,6 +40,22 @@ +++oo*OO######O**oo+++++oo*OO######O**oo+++++oo*OO######O**oo+++ +++oo*OO######OO*oo+++++oo*OO######OO*oo+++++oo*OO######OO*oo+++ +Bootstrap a Java installation: + + >>> from scyjava import config, jimport + >>> config.set_java_constraints(fetch=True, vendor='zulu', version='17') + >>> System = jimport('java.lang.System') + cjdk: Installing JDK zulu:17.0.15 to /home/chuckles/.cache/cjdk + Download 100% of 189.4 MiB |##########| Elapsed Time: 0:00:02 Time: 0:00:02 + Extract | | # | 714 Elapsed Time: 0:00:01 + cjdk: Installing Maven to /home/chuckles/.cache/cjdk + Download 100% of 8.7 MiB |##########| Elapsed Time: 0:00:00 Time: 0:00:00 + Extract | |# | 102 Elapsed Time: 0:00:00 + >>> System.getProperty('java.vendor') + 'Azul Systems, Inc.' + >>> System.getProperty('java.version') + '17.0.15' + Convert Java collections to Python: >>> from scyjava import jimport From a8a318458c60c6ba4cc62fcb064d7f77bbce4b68 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 12:47:20 -0500 Subject: [PATCH 459/505] Release version 1.12.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8da9830c..cbe04dc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.11.0.dev0" +version = "1.12.0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 859a78779554a5819ad6ca190c6f6bc70f613979 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 12:48:05 -0500 Subject: [PATCH 460/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cbe04dc9..cd124a40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.0" +version = "1.12.1.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 89f043abb73895e96fd221fb009ee991bd9533d4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 30 Apr 2025 12:49:12 -0500 Subject: [PATCH 461/505] Add Java installation bootstrap example to README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index cedcfcb1..ea8f48d0 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,24 @@ u'1.8.0_152-release' See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven endpoints. +## Bootstrap a Java installation + +```python +>>> from scyjava import config, jimport +>>> config.set_java_constraints(fetch=True, vendor='zulu', version='17') +>>> System = jimport('java.lang.System') +cjdk: Installing JDK zulu:17.0.15 to /home/chuckles/.cache/cjdk +Download 100% of 189.4 MiB |##########| Elapsed Time: 0:00:02 Time: 0:00:02 +Extract | | # | 714 Elapsed Time: 0:00:01 +cjdk: Installing Maven to /home/chuckles/.cache/cjdk +Download 100% of 8.7 MiB |##########| Elapsed Time: 0:00:00 Time: 0:00:00 +Extract | |# | 102 Elapsed Time: 0:00:00 +>>> System.getProperty('java.vendor') +'Azul Systems, Inc.' +>>> System.getProperty('java.version') +'17.0.15' +``` + ## Convert between Python and Java data structures ### Convert Java collections to Python From fe835a19923fc96edd01ac76c42fb8435d899513 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 16 Jun 2025 21:45:16 -0500 Subject: [PATCH 462/505] Fix link to Maven 3.9.9 download Unfortunately, it appears that whenever a new Maven version is released, the previously working link breaks. But old versions are available from a hopefully more permanent link in the release archive, so we'll stick with 3.9.9 in its less volatile home. --- src/scyjava/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index e284fc65..0b85bc82 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -16,7 +16,7 @@ _fetch_java: str = "auto" _java_vendor: str = "zulu-jre" _java_version: str = "11" -_maven_url: str = "tgz+https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 +_maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 _maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 endpoints: list[str] = [] From d6cf1f929b2e7778bd60016212d8a8918965722c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 16 Jun 2025 21:56:57 -0500 Subject: [PATCH 463/505] Stop installing and testing jep * The jep tests do not pass reliably on macOS. * Installing jep can trigger a build from source. * The support for scyjava via jep is incomplete anyway, due to jep's lack of support for class reflection (i.e. the jclass function). And now, on Windows CI with Python 3.13: LINK : fatal error LNK1104: cannot open file 'python313t.lib' error: command 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC\\14.43.34808\\bin\\HostX86\\x64\\link.exe' failed with exit code 1104 [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. ERROR: Failed building wheel for jep ERROR: Failed to build installable wheels for some pyproject.toml based projects (jep) Successfully built scyjava assertpy Failed to build jep The jep mode is probably used by no one. We'll leave it in as-is, but no more worrying about whether it still works, at least for now. :-( --- bin/test.sh | 70 ++++----------------------------------------- dev-environment.yml | 1 - pyproject.toml | 1 - 3 files changed, 6 insertions(+), 66 deletions(-) diff --git a/bin/test.sh b/bin/test.sh index db003331..17b18d59 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -13,9 +13,9 @@ dir=$(dirname "$0") cd "$dir/.." echo -echo "-------------------------------------------" -echo "| Testing JPype mode (Java inside Python) |" -echo "-------------------------------------------" +echo "----------------------" +echo "| Running unit tests |" +echo "----------------------" if [ $# -gt 0 ] then @@ -26,9 +26,9 @@ fi jpypeCode=$? echo -echo "-------------------------------------------" -echo "| Running integration tests (JPype only) |" -echo "-------------------------------------------" +echo "-----------------------------" +echo "| Running integration tests |" +echo "-----------------------------" itCode=0 for t in tests/it/*.py do @@ -44,64 +44,6 @@ do fi done -echo -echo "-------------------------------------------" -echo "| Testing Jep mode (Python inside Java) |" -echo "-------------------------------------------" - -# Discern the Jep installation. -site_packages=$(python -c 'import sys; print(next(p for p in sys.path if p.endswith("site-packages")))') -test -d "$site_packages/jep" || { - echo "[ERROR] Failed to detect Jep installation in current environment!" 1>&2 - exit 1 -} - -# We execute the pytest framework through Jep via jgo, so that -# the surrounding JVM includes scijava-table on the classpath. -# -# Arguments to the shell script are translated into an argument -# list to the pytest.main function. A weak attempt at handling -# special characters, e.g. single quotation marks and backslashes, -# is made, but there are surely other non-working cases. - -if [ $# -gt 0 ] -then - a=$(echo "$@" | sed 's/\\/\\\\/g') # escape backslashes - a=$(echo "$a" | sed 's/'\''/\\'\''/g') # escape single quotes - a=$(echo "$a" | sed 's/ /'\'','\''/g') # replace space with ',' - argString="['-v', '$a']" -else - argString="" -fi -if ! java -version 2>&1 | grep -q '^openjdk version "\(1\.8\|9\|10\|11\|12\|13\|14\|15\|16\)\.' -then - echo "Skipping jep tests due to unsupported Java version:" - java -version || true - jepCode=0 -elif [ "$(uname -s)" = "Darwin" ] -then - echo "Skipping jep tests on macOS due to flakiness" - jepCode=0 -else - echo "# AUTOGENERATED test file for jep; safe to delete. -import logging, sys, pytest, scyjava -scyjava._logger.addHandler(logging.StreamHandler(sys.stderr)) -scyjava._logger.setLevel(logging.INFO) -scyjava.config.set_verbose(2) -result = pytest.main($argString) -if result: - sys.exit(result) -" > jep_test.py - jgo -vv \ - -r scijava.public=https://maven.scijava.org/content/groups/public \ - -Djava.library.path="$site_packages/jep" \ - black.ninia:jep:jep.Run+org.scijava:scijava-table \ - jep_test.py - jepCode=$? - rm -f jep_test.py -fi - test "$jpypeCode" -ne 0 && exit "$jpypeCode" test "$itCode" -ne 0 && exit "$itCode" -test "$jepCode" -ne 0 && exit "$jepCode" exit 0 diff --git a/dev-environment.yml b/dev-environment.yml index a2fb77ea..bd009411 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -37,5 +37,4 @@ dependencies: # Project from source - pip - pip: - - git+https://github.com/ninia/jep.git@cfca63f8b3398daa6d2685428660dc4b2bfab67d - -e . diff --git a/pyproject.toml b/pyproject.toml index cd124a40..cfc1e63e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ dev = [ "assertpy", "build", - "jep", "pytest", "pytest-cov", "numpy", From ebbceeb00b7923258a8eda43b3d202eb7f51f4fa Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 16 Jun 2025 22:07:37 -0500 Subject: [PATCH 464/505] Release version 1.12.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cfc1e63e..43f1e4a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.1.dev0" +version = "1.12.1" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From b71c8b369a3ba0b2c693c96e37f030595d4266e8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 16 Jun 2025 22:12:16 -0500 Subject: [PATCH 465/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 43f1e4a6..393d3c4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.1" +version = "1.12.2.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From ccceaa7000668cd446adb3b48d0755b531608803 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 11:40:21 -0500 Subject: [PATCH 466/505] Switch build system to uv No more parallel dependency lists! \^_^/ --- .github/workflows/build.yml | 74 ++++++------------------------------- .gitignore | 4 ++ Makefile | 14 +------ bin/check.sh | 12 ++---- bin/{setup.sh => dist.sh} | 2 +- bin/fmt.sh | 11 ------ bin/lint.sh | 6 +-- bin/test.sh | 8 ++-- dev-environment.yml | 40 -------------------- environment.yml | 30 --------------- pyproject.toml | 4 +- 11 files changed, 30 insertions(+), 175 deletions(-) rename bin/{setup.sh => dist.sh} (52%) delete mode 100755 bin/fmt.sh delete mode 100644 dev-environment.yml delete mode 100644 environment.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6dbb292..e2f19e5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: - main jobs: - build-cross-platform: + build: name: test ${{matrix.os}} - ${{matrix.python-version}} - ${{matrix.java-version}} runs-on: ${{ matrix.os }} strategy: @@ -33,79 +33,27 @@ jobs: java-version: '' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: python-version: ${{matrix.python-version}} - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 if: matrix.java-version != '' with: java-version: ${{matrix.java-version}} distribution: 'zulu' cache: 'maven' - - name: Install ScyJava + - name: Run tests run: | - python -m pip install --upgrade pip - python -m pip install -e '.[dev]' + bin/test.sh - - name: Test ScyJava + - name: Lint code run: | - bin/test.sh --color=yes + bin/lint.sh - ensure-clean-code: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v3 - - - name: Lint code - run: | - python -m pip install ruff - ruff check - ruff format --check - - - name: Validate pyproject.toml - run: | - python -m pip install validate-pyproject[all] - python -m validate_pyproject pyproject.toml - - conda-dev-test: - name: Conda Setup & Code Coverage - runs-on: ubuntu-latest - defaults: - # Steps that rely on the activated environment must be run with this shell setup. - # See https://github.com/marketplace/actions/setup-miniconda#important - run: - shell: bash -l {0} - steps: - - uses: actions/checkout@v2 - - name: Cache conda - uses: actions/cache@v4 - env: - # Increase this value to reset cache if dev-environment.yml has not changed - CACHE_NUMBER: 0 - with: - path: ~/conda_pkgs_dir - key: - ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('dev-environment.yml') }} - - uses: conda-incubator/setup-miniconda@v3 - with: - # Create env with dev packages - auto-update-conda: true - python-version: 3.9 - miniforge-version: latest - environment-file: dev-environment.yml - # Activate scyjava-dev environment - activate-environment: scyjava-dev - auto-activate-base: false - # Use mamba for faster setup - use-mamba: true - - name: Test scyjava - run: | - bin/test.sh --cov-report=xml --cov=. - # We could do this in its own action, but we'd have to setup the environment again. - - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v2 + - name: Upload coverage + if: matrix.platform == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.java-version == '11' + uses: codecov/codecov-action@v4 diff --git a/.gitignore b/.gitignore index 7fdf6cba..56372b82 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ coverage.xml # IDEA .idea/ *.iml + +# uv +/.venv/ +/uv.lock diff --git a/Makefile b/Makefile index 5827c144..ed19ec6f 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,22 @@ help: @echo "Available targets:\n\ clean - remove build files and directories\n\ - setup - create mamba developer environment\n\ lint - run code formatters and linters\n\ test - run automated test suite\n\ dist - generate release archives\n\ - \n\ - Remember to 'mamba activate scyjava-dev' first!" + " clean: bin/clean.sh -setup: - bin/setup.sh - check: @bin/check.sh lint: check bin/lint.sh -fmt: check - bin/fmt.sh - test: check bin/test.sh dist: check clean - python -m build - -.PHONY: test + bin/dist.sh diff --git a/bin/check.sh b/bin/check.sh index 4a9db614..7ef142cb 100755 --- a/bin/check.sh +++ b/bin/check.sh @@ -1,10 +1,6 @@ #!/bin/sh -case "$CONDA_PREFIX" in - */scyjava-dev) - ;; - *) - echo "Please run 'make setup' and then 'mamba activate scyjava-dev' first." - exit 1 - ;; -esac +if ! command -v uv >/dev/null 2>&1; then + echo "Please install uv (https://docs.astral.sh/uv/getting-started/installation/)." + exit 1 +fi diff --git a/bin/setup.sh b/bin/dist.sh similarity index 52% rename from bin/setup.sh rename to bin/dist.sh index 3c711c75..f21fbf48 100755 --- a/bin/setup.sh +++ b/bin/dist.sh @@ -3,4 +3,4 @@ dir=$(dirname "$0") cd "$dir/.." -mamba env create -f dev-environment.yml +uv run python -m build diff --git a/bin/fmt.sh b/bin/fmt.sh deleted file mode 100755 index cd04d02e..00000000 --- a/bin/fmt.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -dir=$(dirname "$0") -cd "$dir/.." - -exitCode=0 -ruff check --fix -code=$?; test $code -eq 0 || exitCode=$code -ruff format -code=$?; test $code -eq 0 || exitCode=$code -exit $exitCode diff --git a/bin/lint.sh b/bin/lint.sh index 1cf86826..74871c95 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,10 +4,10 @@ dir=$(dirname "$0") cd "$dir/.." exitCode=0 -ruff check +uv run validate-pyproject pyproject.toml code=$?; test $code -eq 0 || exitCode=$code -ruff format --check +uv run ruff check --fix code=$?; test $code -eq 0 || exitCode=$code -validate-pyproject pyproject.toml +uv run ruff format code=$?; test $code -eq 0 || exitCode=$code exit $exitCode diff --git a/bin/test.sh b/bin/test.sh index 17b18d59..fc172376 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Executes the pytest framework in both JPype and Jep modes. +# Runs the unit tests. # # Usage examples: # bin/test.sh @@ -19,9 +19,9 @@ echo "----------------------" if [ $# -gt 0 ] then - python -m pytest -p no:faulthandler $@ + uv run python -m pytest -v -p no:faulthandler $@ else - python -m pytest -p no:faulthandler tests/ + uv run python -m pytest -v -p no:faulthandler tests/ fi jpypeCode=$? @@ -32,7 +32,7 @@ echo "-----------------------------" itCode=0 for t in tests/it/*.py do - python "$t" + uv run python "$t" code=$? printf -- "--> %s " "$t" if [ "$code" -eq 0 ] diff --git a/dev-environment.yml b/dev-environment.yml deleted file mode 100644 index bd009411..00000000 --- a/dev-environment.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Use this file to construct an environment -# for developing scyjava from source. -# -# First, install mambaforge: -# -# https://github.com/conda-forge/miniforge#mambaforge -# -# Then run: -# -# mamba env create -f dev-environment.yml -# conda activate scyjava-dev -# -# In addition to the dependencies needed for using scyjava, it -# includes tools for developer-related actions like running -# automated tests (pytest) and linting the code (ruff). If you -# want an environment without these tools, use environment.yml. -name: scyjava-dev -channels: - - conda-forge -dependencies: - - python = 3.9 - # Project dependencies - - jpype1 >= 1.3.0 - - jgo - - cjdk - # Test dependencies - - numpy - - pandas - # Developer tools - - assertpy - - pytest - - pytest-cov - - python-build - - ruff - - toml - - validate-pyproject - # Project from source - - pip - - pip: - - -e . diff --git a/environment.yml b/environment.yml deleted file mode 100644 index bb4bd198..00000000 --- a/environment.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Use this file to construct an environment for -# working with scyjava in a runtime setting. -# -# First, install mambaforge: -# -# https://github.com/conda-forge/miniforge#mambaforge -# -# Then run: -# -# mamba env create -# mamba activate scyjava -# -# It includes the dependencies needed for using scyjava, but not tools -# for developer-related actions like running automated tests (pytest), -# linting the code (ruff), and generating the API documentation (sphinx). -# If you want an environment including these tools, use dev-environment.yml. - -name: scyjava -channels: - - conda-forge -dependencies: - - python >= 3.9 - # Project dependencies - - jpype1 >= 1.3.0 - - jgo - - cjdk - # Project from source - - pip - - pip: - - -e . diff --git a/pyproject.toml b/pyproject.toml index 393d3c4e..fa035711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ classifiers = [ "Topic :: Utilities", ] -# NB: Keep this in sync with environment.yml AND dev-environment.yml! requires-python = ">=3.9" dependencies = [ "jpype1 >= 1.3.0", @@ -38,8 +37,7 @@ dependencies = [ "cjdk", ] -[project.optional-dependencies] -# NB: Keep this in sync with dev-environment.yml! +[dependency-groups] dev = [ "assertpy", "build", From c566a65e1e415741392370d11feece69b85b061f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 12:01:56 -0500 Subject: [PATCH 467/505] Make lint script return non-zero upon any change So that the CI fails when reformatting needed to occur. --- bin/lint.sh | 9 +++++++++ pyproject.toml | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bin/lint.sh b/bin/lint.sh index 74871c95..978c1aa5 100755 --- a/bin/lint.sh +++ b/bin/lint.sh @@ -4,10 +4,19 @@ dir=$(dirname "$0") cd "$dir/.." exitCode=0 + +# Check for errors and capture non-zero exit codes. uv run validate-pyproject pyproject.toml code=$?; test $code -eq 0 || exitCode=$code +uv run ruff check >/dev/null 2>&1 +code=$?; test $code -eq 0 || exitCode=$code +uv run ruff format --check >/dev/null 2>&1 +code=$?; test $code -eq 0 || exitCode=$code + +# Do actual code reformatting. uv run ruff check --fix code=$?; test $code -eq 0 || exitCode=$code uv run ruff format code=$?; test $code -eq 0 || exitCode=$code + exit $exitCode diff --git a/pyproject.toml b/pyproject.toml index fa035711..ccaa9d20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ "pandas", "ruff", "toml", - "validate-pyproject[all]" + "validate-pyproject[all]", ] [project.urls] @@ -65,7 +65,6 @@ include-package-data = false where = ["src"] namespaces = false -# ruff configuration [tool.ruff] line-length = 88 src = ["src", "tests"] From e9f49427a3c7e3d4243fe68e3baeb0740382ff7b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 12:08:17 -0500 Subject: [PATCH 468/505] CI: add missing uv installation step --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2f19e5c..bbaf58e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,11 @@ jobs: distribution: 'zulu' cache: 'maven' + - name: Set up uv + run: | + python -m pip install --upgrade pip + python -m pip install uv + - name: Run tests run: | bin/test.sh From 8a2ec3b624a4003aa33faa2c7ad6c4f5d4806028 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 12:11:16 -0500 Subject: [PATCH 469/505] CI: attempt to fix code coverage upload --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbaf58e0..eabde905 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,5 +60,5 @@ jobs: bin/lint.sh - name: Upload coverage - if: matrix.platform == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.java-version == '11' + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.java-version == '11' uses: codecov/codecov-action@v4 From bca21e607384c7c9be5a275f20fb3153e09d80eb Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 12:14:19 -0500 Subject: [PATCH 470/505] CI: remove code coverage and fix Windows build I'm sick of dealing with Codecov. Latest error message was: Token required because branch is protected It's just not worth the hassle. --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eabde905..a9f51a69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,13 +52,11 @@ jobs: python -m pip install uv - name: Run tests + shell: bash run: | bin/test.sh - name: Lint code + shell: bash run: | bin/lint.sh - - - name: Upload coverage - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && matrix.java-version == '11' - uses: codecov/codecov-action@v4 From afe97d37627beddc4d008767446c4414925e8235 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 20 Jul 2025 12:27:21 -0500 Subject: [PATCH 471/505] Remove obsolete codecov.yml file --- codecov.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index aa84c676..00000000 --- a/codecov.yml +++ /dev/null @@ -1,2 +0,0 @@ -ignore: - - "*/tests/*" From 9e33f2ed350973b4e657634290e7d75824d635e4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 23 Aug 2025 09:07:03 -0400 Subject: [PATCH 472/505] refactor: delay import of pandas until needed --- src/scyjava/_convert.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index af1583b5..c73f8b7a 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -7,8 +7,9 @@ import logging import math from bisect import insort +from importlib.util import find_spec from pathlib import Path -from typing import Any, Callable, Dict, List, NamedTuple +from typing import Any, Callable, Dict, List, NamedTuple, reveal_type from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort @@ -677,7 +678,7 @@ def _stock_py_converters() -> List: priority=Priority.VERY_LOW, ), ] - if _import_pandas(required=False): + if find_spec("pandas"): converters.append( Converter( name="org.scijava.table.Table -> pandas.DataFrame", @@ -716,7 +717,7 @@ def _stock_py_converters() -> List: ), ] ) - if _import_numpy(required=False): + if find_spec("numpy"): converters.append( Converter( name="primitive array -> numpy.ndarray", @@ -803,16 +804,15 @@ def _jarray_shape(jarr): return shape -def _import_numpy(required=True): +def _import_numpy(): try: import numpy as np return np except ImportError as e: - if required: - msg = "The NumPy library is missing (https://numpy.org/). " - msg += "Please install it before using this function." - raise RuntimeError(msg) from e + msg = "The NumPy library is missing (https://numpy.org/). " + msg += "Please install it before using this function." + raise RuntimeError(msg) from e ###################################### @@ -838,16 +838,15 @@ def _convert_table(obj: Any): return None -def _import_pandas(required=True): +def _import_pandas(): try: import pandas as pd return pd except ImportError as e: - if required: - msg = "The Pandas library is missing (http://pandas.pydata.org/). " - msg += "Please install it before using this function." - raise RuntimeError(msg) from e + msg = "The Pandas library is missing (http://pandas.pydata.org/). " + msg += "Please install it before using this function." + raise RuntimeError(msg) from e def _table_to_pandas(table): From 6f89935091be21f25af75214d6d28de1efcd68b2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 23 Aug 2025 09:10:47 -0400 Subject: [PATCH 473/505] remove reveal type --- src/scyjava/_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index c73f8b7a..4a04f279 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -9,7 +9,7 @@ from bisect import insort from importlib.util import find_spec from pathlib import Path -from typing import Any, Callable, Dict, List, NamedTuple, reveal_type +from typing import Any, Callable, Dict, List, NamedTuple from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort From 785e75a23085164dfeba91e49afa102b2800f35d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 22 Sep 2025 13:43:35 -0500 Subject: [PATCH 474/505] Fix JVM version detection for no-dot versions OpenJDK 25 was released very recently, and its version is simply "25" without any period symbols. The regex here was assuming at least one, and so the jvm_version() function was failing with this sort of version string. This commit generalizes the regex to work in such cases. --- src/scyjava/_jvm.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 224ac618..3a5685fb 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -93,6 +93,7 @@ def jvm_version() -> tuple[int, ...]: if java is None: raise RuntimeError(f"No java executable found inside: {p}") + _logger.debug(f"Invoking `{java} -version`...") try: output = subprocess.check_output( [str(java), "-version"], stderr=subprocess.STDOUT @@ -101,11 +102,21 @@ def jvm_version() -> tuple[int, ...]: raise RuntimeError("System call to java failed") from e output = output.replace("\n", " ").replace("\r", "") - m = re.match('.*version "(([0-9]+\\.)+[0-9]+)', output) + m = re.match('.* version "([^"]*)"', output) if not m: - raise RuntimeError(f"Inscrutable java command output:\n{output}") + raise RuntimeError( + "Inscrutable java command output:\n" + + f"$ {java} -version\n" + + output + ) + + v = m.group(1) + _logger.debug(f"Got Java version: {v}") - return tuple(map(int, m.group(1).split("."))) + try: + return tuple(map(int, v.split("."))) + except ValueError: + raise RuntimeError(f"Inscrutable java version: {v}") def start_jvm(options: Sequence[str] = None) -> None: From f4d5784a128ad99cdf6ee3807eeebb3e1e55eed0 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 22 Sep 2025 16:17:16 -0500 Subject: [PATCH 475/505] Make code lint-congruent --- src/scyjava/_jvm.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 3a5685fb..c702623f 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -105,9 +105,7 @@ def jvm_version() -> tuple[int, ...]: m = re.match('.* version "([^"]*)"', output) if not m: raise RuntimeError( - "Inscrutable java command output:\n" + - f"$ {java} -version\n" + - output + f"Inscrutable java command output:\n$ {java} -version\n{output}" ) v = m.group(1) From 6eebe950dbf7f96256d690993f3e1115d18fd261 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 24 Sep 2025 14:30:02 -0500 Subject: [PATCH 476/505] Fix jsource function for no-dot Java versions For the recently released OpenJDK 25, the version is simply "25". --- src/scyjava/_introspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index 42680380..a9ab98a9 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -100,7 +100,7 @@ def jsource(data) -> str: # Discern the Java version. jv_digits = jvm_version() - assert jv_digits is not None and len(jv_digits) > 1 + assert jv_digits is not None and len(jv_digits) > 0 java_version = jv_digits[1] if jv_digits[0] == 1 else jv_digits[0] # Note: some classes (e.g. corba and jaxp) will not be located correctly before From 3b30d08fa702dfdeac1d63bbeb3e9a9383bb2f00 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 24 Sep 2025 14:51:08 -0500 Subject: [PATCH 477/505] Fix remaining OpenJDK-25-related issues * Relax version digit length checks. * Handle wonky JVM paths on macOS. * Fix typo. --- src/scyjava/_cjdk_fetch.py | 2 +- src/scyjava/_jvm.py | 8 +++++++- tests/it/jvm_version.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py index 378a3733..b8fea094 100644 --- a/src/scyjava/_cjdk_fetch.py +++ b/src/scyjava/_cjdk_fetch.py @@ -47,7 +47,7 @@ def _silent_check_output(*args, **kwargs): try: with patch.object(subprocess, "check_output", new=_silent_check_output): jpype.getDefaultJVMPath() - # on Darwin, may raise a CalledProcessError when invoking `/user/libexec/java_home` + # on Darwin, may raise a CalledProcessError when invoking `/usr/libexec/java_home` except (jpype.JVMNotFoundException, subprocess.CalledProcessError): return False return True diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index c702623f..29b499b3 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -70,8 +70,14 @@ def jvm_version() -> tuple[int, ...]: default_jvm_path = jpype.getDefaultJVMPath() if not default_jvm_path: raise RuntimeError("Cannot glean the default JVM path") + print(f"Default JVM path from JPype: {default_jvm_path}") - p = Path(default_jvm_path) + # Good ol' macOS! Nothing beats macOS. + jvm_path = default_jvm_path.replace( + "/Contents/MacOS/libjli.dylib", "/Contents/Home/lib/libjli.dylib" + ) + + p = Path(jvm_path) if not p.exists(): raise RuntimeError(f"Invalid default JVM path: {p}") diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py index a79d20bc..98103bca 100644 --- a/tests/it/jvm_version.py +++ b/tests/it/jvm_version.py @@ -10,7 +10,7 @@ before_version = scyjava.jvm_version() assert_that(before_version).is_not_none() -assert_that(len(before_version)).is_greater_than_or_equal_to(3) +assert_that(len(before_version)).is_greater_than_or_equal_to(1) assert_that(before_version[0]).is_greater_than(0) scyjava.config.enable_headless_mode() @@ -18,7 +18,7 @@ after_version = scyjava.jvm_version() assert_that(after_version).is_not_none() -assert_that(len(after_version)).is_greater_than_or_equal_to(3) +assert_that(len(after_version)).is_greater_than_or_equal_to(1) assert_that(after_version[0]).is_greater_than(0) assert_that(before_version).is_equal_to(after_version) From a2b8bed0a07a87d4c9b715a6dddaad308080f440 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 12 Nov 2025 15:53:11 -0600 Subject: [PATCH 478/505] Fix incorrect type hint in README function list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea8f48d0..35424101 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ FUNCTIONS jvm_started() -> bool Return true iff a Java virtual machine (JVM) has been started. - jvm_version() -> str + jvm_version() -> tuple[int, ...] Gets the version of the JVM as a tuple, with each dot-separated digit as one element. Characters in the version string beyond only numbers and dots are ignored, in line with the java.version system property. From 11deca3291d5b02dd08cc423378a621ffaae35f8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 8 Feb 2026 20:59:25 -0600 Subject: [PATCH 479/505] Change JDK fetch default from "auto" to "always" The "auto" mode can still be used for on-demand fetching, but it is more robust to fetch a JDK with known characteristics rather than relying on whatever the user happens to have installed, regardless of Java version. See tlambert03/bffile#16. --- src/scyjava/_jvm.py | 2 +- src/scyjava/config.py | 10 +++++----- tests/it/jvm_version.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 29b499b3..1bd80170 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -70,7 +70,7 @@ def jvm_version() -> tuple[int, ...]: default_jvm_path = jpype.getDefaultJVMPath() if not default_jvm_path: raise RuntimeError("Cannot glean the default JVM path") - print(f"Default JVM path from JPype: {default_jvm_path}") + _logger.debug(f"Default JVM path from JPype: {default_jvm_path}") # Good ol' macOS! Nothing beats macOS. jvm_path = default_jvm_path.replace( diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 0b85bc82..cfab9d82 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -13,7 +13,7 @@ _logger = _logging.getLogger(__name__) # Constraints on the Java installation to be used. -_fetch_java: str = "auto" +_fetch_java: str = "always" _java_vendor: str = "zulu-jre" _java_version: str = "11" _maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 @@ -55,10 +55,10 @@ def set_java_constraints( Set constraints on the version of Java to be used. :param fetch: - If "auto" (default), when a JVM/or maven cannot be located on the system, - [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download - a JDK/JRE distribution and set up the JVM. - If "always", cjdk will always be used; if "never", cjdk will never be used. + If "always" (default), cjdk will always be used; if "never", cjdk will + never be used. If "auto", when a JVM/or maven cannot be located on the system, + [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download a + JDK/JRE distribution and set up the JVM. :param vendor: The vendor of the JDK/JRE distribution for cjdk to download and cache. Defaults to "zulu-jre". See the cjdk documentation for details. diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py index 98103bca..08331678 100644 --- a/tests/it/jvm_version.py +++ b/tests/it/jvm_version.py @@ -13,6 +13,7 @@ assert_that(len(before_version)).is_greater_than_or_equal_to(1) assert_that(before_version[0]).is_greater_than(0) +scyjava.config.set_java_constraints(fetch="never") scyjava.config.enable_headless_mode() scyjava.start_jvm() From 280b622662c67993da8a8441609f40e8041dee70 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 8 Feb 2026 21:14:01 -0600 Subject: [PATCH 480/505] Fix pandas API usages Co-authored-by: Claude Sonnet 4.5 --- src/scyjava/_convert.py | 78 ++++++++++++++++++++++++---------------- tests/test_introspect.py | 4 ++- tests/test_pandas.py | 12 +++---- 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 4a04f279..6cda7567 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -237,72 +237,90 @@ def _stock_java_converters() -> List[Converter]: ), Converter( name="int -> java.lang.Byte", - predicate=lambda obj, **hints: isinstance(obj, int) - and ("type" in hints and hints["type"] in ("b", "byte", "Byte")) - and _jc.Byte.MIN_VALUE <= obj <= _jc.Byte.MAX_VALUE, + predicate=lambda obj, **hints: ( + isinstance(obj, int) + and ("type" in hints and hints["type"] in ("b", "byte", "Byte")) + and _jc.Byte.MIN_VALUE <= obj <= _jc.Byte.MAX_VALUE + ), converter=_jc.Byte, priority=Priority.HIGH, ), Converter( name="int -> java.lang.Short", - predicate=lambda obj, **hints: isinstance(obj, int) - and ("type" in hints and hints["type"] in ("s", "short", "Short")) - and _jc.Short.MIN_VALUE <= obj <= _jc.Short.MAX_VALUE, + predicate=lambda obj, **hints: ( + isinstance(obj, int) + and ("type" in hints and hints["type"] in ("s", "short", "Short")) + and _jc.Short.MIN_VALUE <= obj <= _jc.Short.MAX_VALUE + ), converter=_jc.Short, priority=Priority.HIGH, ), Converter( name="int -> java.lang.Integer", - predicate=lambda obj, **hints: isinstance(obj, int) - and ("type" not in hints or hints["type"] in ("i", "int", "Integer")) - and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE, + predicate=lambda obj, **hints: ( + isinstance(obj, int) + and ("type" not in hints or hints["type"] in ("i", "int", "Integer")) + and _jc.Integer.MIN_VALUE <= obj <= _jc.Integer.MAX_VALUE + ), converter=_jc.Integer, ), Converter( name="int -> java.lang.Long", - predicate=lambda obj, **hints: isinstance(obj, int) - and ("type" not in hints or hints["type"] in ("j", "l", "long", "Long")) - and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE, + predicate=lambda obj, **hints: ( + isinstance(obj, int) + and ("type" not in hints or hints["type"] in ("j", "l", "long", "Long")) + and _jc.Long.MIN_VALUE <= obj <= _jc.Long.MAX_VALUE + ), converter=_jc.Long, priority=Priority.NORMAL - 1, ), Converter( name="int -> java.math.BigInteger", - predicate=lambda obj, **hints: isinstance(obj, int) - and ( - "type" not in hints or hints["type"] in ("bi", "bigint", "BigInteger") + predicate=lambda obj, **hints: ( + isinstance(obj, int) + and ( + "type" not in hints + or hints["type"] in ("bi", "bigint", "BigInteger") + ) ), converter=lambda obj: _jc.BigInteger(str(obj)), priority=Priority.NORMAL - 2, ), Converter( name="float -> java.lang.Float", - predicate=lambda obj, **hints: isinstance(obj, float) - and ("type" not in hints or hints["type"] in ("f", "float", "Float")) - and ( - math.isinf(obj) - or math.isnan(obj) - or -_jc.Float.MAX_VALUE <= obj <= _jc.Float.MAX_VALUE + predicate=lambda obj, **hints: ( + isinstance(obj, float) + and ("type" not in hints or hints["type"] in ("f", "float", "Float")) + and ( + math.isinf(obj) + or math.isnan(obj) + or -_jc.Float.MAX_VALUE <= obj <= _jc.Float.MAX_VALUE + ) ), converter=_jc.Float, ), Converter( name="float -> java.lang.Double", - predicate=lambda obj, **hints: isinstance(obj, float) - and ("type" not in hints or hints["type"] in ("d", "double", "Double")) - and ( - math.isinf(obj) - or math.isnan(obj) - or -_jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE + predicate=lambda obj, **hints: ( + isinstance(obj, float) + and ("type" not in hints or hints["type"] in ("d", "double", "Double")) + and ( + math.isinf(obj) + or math.isnan(obj) + or -_jc.Double.MAX_VALUE <= obj <= _jc.Double.MAX_VALUE + ) ), converter=_jc.Double, priority=Priority.NORMAL - 1, ), Converter( name="float -> java.math.BigDecimal", - predicate=lambda obj, **hints: isinstance(obj, float) - and ( - "type" not in hints or hints["type"] in ("bd", "bigdec", "BigDecimal") + predicate=lambda obj, **hints: ( + isinstance(obj, float) + and ( + "type" not in hints + or hints["type"] in ("bd", "bigdec", "BigDecimal") + ) ), converter=lambda obj: _jc.BigDecimal(str(obj)), priority=Priority.NORMAL - 2, diff --git a/tests/test_introspect.py b/tests/test_introspect.py index e986dffd..a438ede1 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -53,7 +53,9 @@ def test_jreflect_ctors(self): arraylist_Obj = scyjava.jreflect(ArrayList, "constructors") assert len(str_Obj) == len(arraylist_Obj) == 3 arraylist_Obj.sort( - key=lambda row: f"{row['type']}:{row['name']}:{','.join(str(row['arguments']))}" + key=lambda row: ( + f"{row['type']}:{row['name']}:{','.join(str(row['arguments']))}" + ) ) assert arraylist_Obj == [ { diff --git a/tests/test_pandas.py b/tests/test_pandas.py index c18d2435..1baa5dd9 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -60,11 +60,11 @@ def testPandasToTable(self): df = pd.DataFrame(array, columns=columns) # Convert column 0 to integer - df.iloc[:, 0] = (df.iloc[:, 0] * 100).astype("int") + df[columns[0]] = (df[columns[0]] * 100).astype("int") # Convert column 1 to bool - df.iloc[:, 1] = df.iloc[:, 1] > 0.5 + df[columns[1]] = df[columns[1]] > 0.5 # Convert column 2 to string - df.iloc[:, 2] = df.iloc[:, 2].to_string(index=False).split("\n") + df[columns[2]] = df[columns[2]].to_string(index=False).split("\n") table = to_java(df) @@ -137,11 +137,11 @@ def testTabletoPandas(self): # fill mixed table for i in range(table.getRowCount()): - table.set(0, i, Float(float(array_float[i]))) + table.set(0, i, Float(float(array_float[i].item()))) table.set(1, i, Integer(int(array_int[i].item()))) - table.set(2, i, Boolean(bool(array_bool[i]))) + table.set(2, i, Boolean(bool(array_bool[i].item()))) table.set(3, i, String(array_str[i])) - table.set(4, i, Double(float(array_double[i]))) + table.set(4, i, Double(float(array_double[i].item()))) df = to_python(table) # Table types cannot be the same here, unless we want to cast. From 4c6b83f85669fea578bc2ffba59a087fdcdeb480 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 8 Feb 2026 21:22:43 -0600 Subject: [PATCH 481/505] Also clean tests/.pytest_cache Otherwise, that cache folder makes it into the release tarball. --- bin/clean.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/clean.sh b/bin/clean.sh index 485ed5ef..fcaa9f51 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -6,4 +6,4 @@ cd "$dir/.." find . -name __pycache__ -type d | while read d do rm -rfv "$d" done -rm -rfv .pytest_cache build dist src/*.egg-info +rm -rfv .pytest_cache build dist src/*.egg-info tests/.pytest_cache From 74b6c90075bee5379dc69cb724495765ddd75be4 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 8 Feb 2026 21:20:17 -0600 Subject: [PATCH 482/505] Release version 1.12.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ccaa9d20..60b48697 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.2.dev0" +version = "1.12.2" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 55cb87e2a04a61d31c8282c435659307ed19a218 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 8 Feb 2026 21:23:49 -0600 Subject: [PATCH 483/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60b48697..2fa3933c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.2" +version = "1.12.3.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From cc7e66d97445efd29b69acfcdae7d11ef6597d84 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 Jan 2026 11:21:42 -0600 Subject: [PATCH 484/505] Make deprecation warnings visible during test runs --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2fa3933c..f8aa6ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,11 @@ include-package-data = false where = ["src"] namespaces = false +[tool.pytest.ini_options] +filterwarnings = [ + "default::DeprecationWarning", +] + [tool.ruff] line-length = 88 src = ["src", "tests"] From 16fe82b03a770d518e0c2442a82711c0ae577869 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 14 Jan 2026 11:23:02 -0600 Subject: [PATCH 485/505] Stop using jgo.jgo.maven_scijava_repository() It is deprecated in jgo v2. --- src/scyjava/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scyjava/config.py b/src/scyjava/config.py index cfab9d82..7d046fa3 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -7,7 +7,9 @@ from typing import Sequence import jpype as _jpype -from jgo import maven_scijava_repository as _scijava_public + + +_SCIJAVA_PUBLIC = "https://maven.scijava.org/content/groups/public" _logger = _logging.getLogger(__name__) @@ -21,7 +23,7 @@ endpoints: list[str] = [] -_repositories = {"scijava.public": _scijava_public()} +_repositories = {"scijava.public": _SCIJAVA_PUBLIC} _verbose = 0 _manage_deps = True _cache_dir = Path.home() / ".jgo" From cf6df1465e66129cbec81bf8f832ffe7a568866d Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 28 Jan 2026 12:19:45 -0600 Subject: [PATCH 486/505] Update to jgo v2 --- README.md | 4 +- pyproject.toml | 3 +- src/scyjava/_cjdk_fetch.py | 121 ------------------------------------- src/scyjava/_jdk_fetch.py | 98 ++++++++++++++++++++++++++++++ src/scyjava/_jvm.py | 28 ++++++--- src/scyjava/config.py | 114 ++++++++++++++++++++-------------- 6 files changed, 189 insertions(+), 179 deletions(-) delete mode 100644 src/scyjava/_cjdk_fetch.py create mode 100644 src/scyjava/_jdk_fetch.py diff --git a/README.md b/README.md index 35424101..eb3127b2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Supercharged Java access from Python. Built on [JPype](https://jpype.readthedocs.io/en/latest/) -and [jgo](https://github.com/scijava/jgo). +and [jgo](https://github.com/apposed/jgo). ## Use Java classes from Python @@ -83,7 +83,7 @@ u'1.8.0_152-release' +++oo*OO######OO*oo+++++oo*OO######OO*oo+++++oo*OO######OO*oo+++ ``` -See the [jgo documentation](https://github.com/scijava/jgo) for more about Maven endpoints. +See the [jgo documentation](https://github.com/apposed/jgo) for more about Maven endpoints. ## Bootstrap a Java installation diff --git a/pyproject.toml b/pyproject.toml index f8aa6ae7..e6847762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "jpype1 >= 1.3.0", - "jgo", - "cjdk", + "jgo>=2.0.0", ] [dependency-groups] diff --git a/src/scyjava/_cjdk_fetch.py b/src/scyjava/_cjdk_fetch.py deleted file mode 100644 index b8fea094..00000000 --- a/src/scyjava/_cjdk_fetch.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Utility functions for fetching JDK/JRE and Maven. -""" - -from __future__ import annotations - -import logging -import os -import shutil -import subprocess -from typing import TYPE_CHECKING, Union - -import cjdk -import jpype - -import scyjava.config - -if TYPE_CHECKING: - from pathlib import Path - -_logger = logging.getLogger(__name__) - - -def ensure_jvm_available() -> None: - """Ensure that the JVM is available and Maven is installed.""" - fetch = scyjava.config.get_fetch_java() - if fetch == "never": - # Not allowed to use cjdk. - return - if fetch == "always" or not is_jvm_available(): - cjdk_fetch_java() - if fetch == "always" or not shutil.which("mvn"): - cjdk_fetch_maven() - - -def is_jvm_available() -> bool: - """Return True if the JVM is available, suppressing stderr on macos.""" - from unittest.mock import patch - - subprocess_check_output = subprocess.check_output - - def _silent_check_output(*args, **kwargs): - # also suppress stderr on calls to subprocess.check_output - kwargs.setdefault("stderr", subprocess.DEVNULL) - return subprocess_check_output(*args, **kwargs) - - try: - with patch.object(subprocess, "check_output", new=_silent_check_output): - jpype.getDefaultJVMPath() - # on Darwin, may raise a CalledProcessError when invoking `/usr/libexec/java_home` - except (jpype.JVMNotFoundException, subprocess.CalledProcessError): - return False - return True - - -def cjdk_fetch_java(vendor: str | None = None, version: str | None = None) -> None: - """Fetch java using cjdk and add it to the PATH.""" - if vendor is None: - vendor = scyjava.config.get_java_vendor() - if version is None: - version = scyjava.config.get_java_version() - - _logger.info(f"Fetching {vendor}:{version} using cjdk...") - java_home = cjdk.java_home(vendor=vendor, version=version) - _logger.debug(f"java_home -> {java_home}") - _add_to_path(str(java_home / "bin"), front=True) - os.environ["JAVA_HOME"] = str(java_home) - - -def cjdk_fetch_maven(url: str = "", sha: str = "") -> None: - """Fetch Maven using cjdk and add it to the PATH.""" - # if url was passed as an argument, use it with provided sha - # otherwise, use default values for both - if not url: - url = scyjava.config.get_maven_url() - sha = scyjava.config.get_maven_sha() - - # fix urls to have proper prefix for cjdk - if url.startswith("http"): - if url.endswith(".tar.gz"): - url = url.replace("http", "tgz+http") - elif url.endswith(".zip"): - url = url.replace("http", "zip+http") - - # determine sha type based on length (cjdk requires specifying sha type) - # assuming hex-encoded SHA, length should be 40, 64, or 128 - kwargs = {} - if sha_len := len(sha): # empty sha is fine... we just don't pass it - sha_lengths = {40: "sha1", 64: "sha256", 128: "sha512"} - if sha_len not in sha_lengths: # pragma: no cover - raise ValueError( - "MAVEN_SHA be a valid sha1, sha256, or sha512 hash." - f"Got invalid SHA length: {sha_len}. " - ) - kwargs = {sha_lengths[sha_len]: sha} - - _logger.info("Fetching Maven using cjdk...") - maven_dir = cjdk.cache_package("Maven", url, **kwargs) - _logger.debug(f"maven_dir -> {maven_dir}") - if maven_bin := next(maven_dir.rglob("apache-maven-*/**/mvn"), None): - _add_to_path(maven_bin.parent, front=True) - else: # pragma: no cover - raise RuntimeError( - "Failed to find Maven executable on system " - "PATH, and download via cjdk failed." - ) - - -def _add_to_path(path: Union[Path, str], front: bool = False) -> None: - """Add a path to the PATH environment variable. - - If front is True, the path is added to the front of the PATH. - By default, the path is added to the end of the PATH. - If the path is already in the PATH, it is not added again. - """ - - current_path = os.environ.get("PATH", "") - if (path := str(path)) in current_path: - return - new_path = [path, current_path] if front else [current_path, path] - os.environ["PATH"] = os.pathsep.join(new_path) diff --git a/src/scyjava/_jdk_fetch.py b/src/scyjava/_jdk_fetch.py new file mode 100644 index 00000000..8d2cebf5 --- /dev/null +++ b/src/scyjava/_jdk_fetch.py @@ -0,0 +1,98 @@ +""" +Utility functions for fetching JDK/JRE. +""" + +from __future__ import annotations + +import logging +import os +import subprocess +from typing import TYPE_CHECKING, Union + +import jpype + +from jgo.exec import JavaLocator, JavaSource + +import scyjava.config + +if TYPE_CHECKING: + from pathlib import Path + +_logger = logging.getLogger(__name__) + + +def ensure_jvm_available() -> None: + """ + Ensure that the JVM is available. + """ + fetch = scyjava.config.get_fetch_java() + if fetch == "never": + # Not allowed to fetch Java. + return + if fetch == "always" or not is_jvm_available(): + fetch_java() + + +def is_jvm_available() -> bool: + """Return True if the JVM is available, suppressing stderr on macos.""" + from unittest.mock import patch + + subprocess_check_output = subprocess.check_output + + def _silent_check_output(*args, **kwargs): + # also suppress stderr on calls to subprocess.check_output + kwargs.setdefault("stderr", subprocess.DEVNULL) + return subprocess_check_output(*args, **kwargs) + + try: + with patch.object(subprocess, "check_output", new=_silent_check_output): + jpype.getDefaultJVMPath() + # on Darwin, may raise a CalledProcessError when invoking `/usr/libexec/java_home` + except (jpype.JVMNotFoundException, subprocess.CalledProcessError): + return False + return True + + +def fetch_java(vendor: str | None = None, version: str | None = None) -> None: + """ + Fetch Java and configure PATH/JAVA_HOME. + + Supports cjdk version syntax including "11", "17", "11+", "17+", etc. + See https://pypi.org/project/cjdk for more information. + """ + if vendor is None: + vendor = scyjava.config.get_java_vendor() + if version is None: + version = scyjava.config.get_java_version() + + _logger.info(f"Fetching {vendor}:{version}...") + + locator = JavaLocator( + java_source=JavaSource.AUTO, + java_version=version, # Pass string directly (e.g. "11", "17", "11+", "17+") + java_vendor=vendor, + verbose=True, + ) + + # Locate returns path to java executable (e.g., /path/to/java/bin/java) + java_exe = locator.locate() + java_home = java_exe.parent.parent # Navigate from bin/java to JAVA_HOME + + _logger.debug(f"java_home -> {java_home}") + _add_to_path(str(java_home / "bin"), front=True) + os.environ["JAVA_HOME"] = str(java_home) + + +def _add_to_path(path: Union[Path, str], front: bool = False) -> None: + """Add a path to the PATH environment variable. + + If front is True, the path is added to the front of the PATH. + By default, the path is added to the end of the PATH. + If the path is already in the PATH, it is not added again. + """ + + current_path = os.environ.get("PATH", "") + if (path := str(path)) in current_path: + return + new_path = [path, current_path] if front else [current_path, path] + os.environ["PATH"] = os.pathsep.join(new_path) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 1bd80170..350a8157 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -15,11 +15,11 @@ import jpype import jpype.config -from jgo import jgo +import jgo import scyjava.config from scyjava.config import Mode, mode -from scyjava._cjdk_fetch import ensure_jvm_available +from scyjava._jdk_fetch import ensure_jvm_available _logger = logging.getLogger(__name__) @@ -151,23 +151,31 @@ def start_jvm(options: Sequence[str] = None) -> None: # use the logger to notify user that endpoints are being added _logger.debug("Adding jars from endpoints {0}".format(endpoints)) - # download JDK/JRE and Maven as appropriate + # download Java as appropriate ensure_jvm_available() # get endpoints and add to JPype class path if len(endpoints) > 0: + # sort endpoints list, except for the first one endpoints = endpoints[:1] + sorted(endpoints[1:]) _logger.debug("Using endpoints %s", endpoints) - _, workspace = jgo.resolve_dependencies( - "+".join(endpoints), - m2_repo=scyjava.config.get_m2_repo(), + + # join endpoints list to single concatenated endpoint + endpoint = "+".join(endpoints) + + env = jgo.build( + endpoint=endpoint, + #update=False, cache_dir=scyjava.config.get_cache_dir(), - manage_dependencies=scyjava.config.get_manage_deps(), repositories=repositories, - verbose=scyjava.config.get_verbose(), - shortcuts=scyjava.config.get_shortcuts(), + # The following obsolete arguments are from jgo v1: + #m2_repo=scyjava.config.get_m2_repo(), + #manage_dependencies=scyjava.config.get_manage_deps(), + #verbose=scyjava.config.get_verbose(), + #shortcuts=scyjava.config.get_shortcuts(), ) - jpype.addClassPath(os.path.join(workspace, "*")) + jpype.addClassPath(env.modules_dir / "*") + jpype.addClassPath(env.jars_dir / "*") # HACK: Try to set JAVA_HOME if it isn't already. if ( diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 7d046fa3..70b2467e 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -18,8 +18,6 @@ _fetch_java: str = "always" _java_vendor: str = "zulu-jre" _java_version: str = "11" -_maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 -_maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 endpoints: list[str] = [] @@ -56,25 +54,30 @@ def set_java_constraints( """ Set constraints on the version of Java to be used. + :return: + "always" to download (or retrieve from cache) a suitable JDK/JRE; + "never" to rely only on an existing "system Java" installation + (discovered via the JAVA_HOME environment variable or system path); + "auto" to prefer system Java, but download one if no existing JVM is found. :param fetch: - If "always" (default), cjdk will always be used; if "never", cjdk will - never be used. If "auto", when a JVM/or maven cannot be located on the system, - [`cjdk`](https://github.com/cachedjdk/cjdk) will be used to download a - JDK/JRE distribution and set up the JVM. + If "always" (default), a suitable JDK/JRE will be downloaded (or retrieved from + cache if previously downloaded) ignoring any system Java installations; + if "never", only an already-available JDK/JRE will be used, + discovered via the JAVA_HOME environment variable or system path; + If "auto", a suitable JDK/JRE will be downloaded and cached only when an + existing JDK/JRE cannot be located on the system. :param vendor: - The vendor of the JDK/JRE distribution for cjdk to download and cache. - Defaults to "zulu-jre". See the cjdk documentation for details. + The vendor of the JDK/JRE distribution to download and cache. + Defaults to "zulu-jre". Does not constrain matching of system JDK/JREs. :param version: - Expression defining the Java version for cjdk to download and cache. - Defaults to "11". See the cjdk documentation for details. + Expression defining the Java version to download and cache. + Defaults to "11". Does not constrain matching of system JDK/JREs. :param maven_url: - URL of the Maven distribution for cjdk to download and cache. - Defaults to the Maven 3.9.9 binary distribution from dlcdn.apache.org. + DEPRECATED: scyjava no longer uses Maven to resolve dependencies. :param maven_sha: - The SHA512 (or SHA256 or SHA1) hash of the Maven distribution to download, - if providing a custom maven_url. + DEPRECATED: scyjava no longer uses Maven to resolve dependencies. """ - global _fetch_java, _java_vendor, _java_version, _maven_url, _maven_sha + global _fetch_java, _java_vendor, _java_version if fetch is not None: if isinstance(fetch, bool): # Be nice and allow boolean values as a convenience. @@ -88,30 +91,38 @@ def set_java_constraints( if version is not None: _java_version = version if maven_url is not None: + _logger.warning( + "Deprecated argument: scyjava.config.set_java_constraints(maven_url). " + "scyjava no longer uses Maven to resolve dependencies." + ) _maven_url = maven_url - _maven_sha = "" if maven_sha is not None: + _logger.warning( + "Deprecated argument: scyjava.config.set_java_constraints(maven_sha). " + "scyjava no longer uses Maven to resolve dependencies." + ) _maven_sha = maven_sha def get_fetch_java() -> str: """ - Get whether [`cjdk`](https://github.com/cachedjdk/cjdk) - will be used to download a JDK/JRE distribution and set up the JVM. + Get whether to download (or retrieve from local cache if previously downloaded) + a JDK/JRE distribution and set up the JVM. To set this value, see set_java_constraints. :return: - "always" for cjdk to obtain the JDK/JRE; - "never" for cjdk *not* to obtain a JDK/JRE; - "auto" for cjdk to be used only when a JVM/or Maven is not on the system path. + "always" to download (or retrieve from cache) a suitable JDK/JRE; + "never" to fully rely on an existing installation + (discovered via the JAVA_HOME environment variable or system path); + "auto" to prefer system Java, but download one if no existing JVM is found. """ return _fetch_java def get_java_vendor() -> str: """ - Get the vendor of the JDK/JRE distribution to download. - Vendor of the Java installation for cjdk to download and cache. + Vendor of the Java installation to download and cache. Does not + constrain matching of system JDK/JREs, only those fetched and cached. To set this value, see set_java_constraints. :return: String defining the desired JDK/JRE vendor for downloaded JDK/JREs. @@ -121,7 +132,8 @@ def get_java_vendor() -> str: def get_java_version() -> str: """ - Expression defining the Java version for cjdk to download and cache. + Expression defining the Java version to download and cache. Does not + constrain matching of system JDK/JREs, only those fetched and cached. To set this value, see set_java_constraints. :return: String defining the desired JDK/JRE version for downloaded JDK/JREs. @@ -129,26 +141,6 @@ def get_java_version() -> str: return _java_version -def get_maven_url() -> str: - """ - The URL of the Maven distribution to download. - To set this value, see set_java_constraints. - - :return: URL pointing to the Maven distribution. - """ - return _maven_url - - -def get_maven_sha() -> str: - """ - The SHA512 (or SHA256 or SHA1) hash of the Maven distribution to download, - if providing a custom maven_url. To set this value, see set_java_constraints. - - :return: Hash value of the Maven distribution, or empty string to skip hash check. - """ - return _maven_sha - - def add_repositories(*args, **kwargs) -> None: """ Add one or more Maven repositories to be used by jgo for downloading dependencies. @@ -470,3 +462,37 @@ def get_endpoints(): ) global endpoints return endpoints + + +_maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 +_maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 + + +def get_maven_url() -> str: + """ + DEPRECATED since v1.12.3 + scyjava no longer uses Maven to resolve dependencies, + but rather jgo v2's pure-Python dependency resolver. + + :return: Path to Maven 3.9.9 download (for backwards compatibility). + """ + _logger.warning( + "Deprecated method call: scyjava.config.get_maven_url(). " + "scyjava no longer uses Maven to resolve dependencies." + ) + return _maven_url + + +def get_maven_sha() -> str: + """ + DEPRECATED since v1.12.3 + scyjava no longer uses Maven to resolve dependencies, + but rather jgo v2's pure-Python dependency resolver. + + :return: Hash of Maven 3.9.9 download (for backwards compatibility). + """ + _logger.warning( + "Deprecated method call: scyjava.config.get_maven_sha(). " + "scyjava no longer uses Maven to resolve dependencies." + ) + return _maven_sha From 1f2a326fb46912921538358c9e19cd2c2a2b2d4e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 13 Mar 2026 17:00:59 -0500 Subject: [PATCH 487/505] Fix string syntax --- tests/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inspect.py b/tests/test_inspect.py index d308307d..f265b8c0 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -31,7 +31,7 @@ def test_inspect_members(self): ] pattern = ( r"(https://github.com/openjdk/jdk/blob/)" - "[^ ]*(/share/classes/java/lang/Iterable\.java)" + r"[^ ]*(/share/classes/java/lang/Iterable\.java)" ) members_string = re.sub(pattern, r"\1...\2", "".join(members)) assert members_string.split("\n") == expected From ada8393ab0a012e700af1757292f0d6d0b061eb8 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 13 Mar 2026 16:57:03 -0500 Subject: [PATCH 488/505] Release version 1.12.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e6847762..2771867b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.3.dev0" +version = "1.12.3" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 50006f01251e3f44d77d616f74a7f3443ff9959e Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Fri, 13 Mar 2026 17:02:19 -0500 Subject: [PATCH 489/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2771867b..ddaf9239 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.3" +version = "1.12.4.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 4f1aa5b172062ade10c0754c8d7981478e91860c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 13:26:30 -0500 Subject: [PATCH 490/505] Fail fast with clear error for Java < 11 JPype 1.6+ dropped Java 8 support, so surface a clear RuntimeError in start_jvm() rather than a cryptic JPype failure. Also improve macOS JVM path resolution to handle Java 8's different dylib layout (jre/lib/jli/libjli.dylib), with a further fallback to $JAVA_HOME/bin/java. Co-Authored-By: Claude Sonnet 4.6 --- src/scyjava/_jvm.py | 68 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 350a8157..0aad6f80 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -78,26 +78,49 @@ def jvm_version() -> tuple[int, ...]: ) p = Path(jvm_path) + java = None + if not p.exists(): - raise RuntimeError(f"Invalid default JVM path: {p}") + # Try Java 8 macOS dylib path (jre/lib/jli/libjli.dylib vs lib/libjli.dylib). + p8 = Path( + default_jvm_path.replace( + "/Contents/MacOS/libjli.dylib", + "/Contents/Home/jre/lib/jli/libjli.dylib", + ) + ) + if p8.exists(): + p = p8 - java = None - for _ in range(3): # The bin folder is always <=3 levels up from libjvm. - p = p.parent - if p.name == "lib": - java = p.parent / "bin" / "java" - elif p.name == "bin": - java = p / "java" - - if java is not None: + if not p.exists(): + # Fall back to JAVA_HOME if the dylib path resolution failed. + java_home = os.environ.get("JAVA_HOME") + if java_home: + candidate = Path(java_home) / "bin" / "java" if os.name == "nt": - # Good ol' Windows! Nothing beats Windows. - java = java.with_suffix(".exe") - if not java.is_file(): - raise RuntimeError(f"No ../bin/java found at: {p}") - break + candidate = candidate.with_suffix(".exe") + if candidate.is_file(): + java = candidate + + if java is None: + raise RuntimeError(f"Invalid default JVM path: {p}") + if java is None: - raise RuntimeError(f"No java executable found inside: {p}") + for _ in range(3): # The bin folder is always <=3 levels up from libjvm. + p = p.parent + if p.name == "lib": + java = p.parent / "bin" / "java" + elif p.name == "bin": + java = p / "java" + + if java is not None: + if os.name == "nt": + # Good ol' Windows! Nothing beats Windows. + java = java.with_suffix(".exe") + if not java.is_file(): + raise RuntimeError(f"No ../bin/java found at: {p}") + break + if java is None: + raise RuntimeError(f"No java executable found inside: {p}") _logger.debug(f"Invoking `{java} -version`...") try: @@ -154,6 +177,19 @@ def start_jvm(options: Sequence[str] = None) -> None: # download Java as appropriate ensure_jvm_available() + # Fail fast if Java version is too old. JPype 1.6+ dropped Java 8 support. + try: + ver = jvm_version() + if ver < (11,): + raise RuntimeError( + f"Java {'.'.join(str(v) for v in ver)} is not supported. " + "scyjava requires Java 11 or later." + ) + except RuntimeError as e: + if "not supported" in str(e): + raise + _logger.debug(f"Could not determine JVM version before start: {e}") + # get endpoints and add to JPype class path if len(endpoints) > 0: # sort endpoints list, except for the first one From 5000d739d9a192fd3591678fa78fe38cf9727ea7 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 13:42:32 -0500 Subject: [PATCH 491/505] Set jgo's lenient mode when resolving components Aside from generally being friendlier and more likely to succeed, the specific motivation for this change is sc.fiji:fiji:2.17.0 on macos-arm64, which has a flaw in its POM hierarchy leading to: ValueError: No version available for dependency org.jogamp.gluegen:gluegen-rt:jar:natives-macosx-aarch64 (The proper classifier is natives-macosx-universal.) Setting lenient mode avoids the flaw and lets Fiji work with scyjava. Of course, the proper fix will be in pom-scijava-base + pom-scijava. --- pyproject.toml | 2 +- src/scyjava/_jvm.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ddaf9239..a192f594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "jpype1 >= 1.3.0", - "jgo>=2.0.0", + "jgo>=2.1.0", ] [dependency-groups] diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 0aad6f80..1625237e 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -204,6 +204,7 @@ def start_jvm(options: Sequence[str] = None) -> None: #update=False, cache_dir=scyjava.config.get_cache_dir(), repositories=repositories, + resolver=jgo.maven.PythonResolver(lenient=True), # The following obsolete arguments are from jgo v1: #m2_repo=scyjava.config.get_m2_repo(), #manage_dependencies=scyjava.config.get_manage_deps(), From f05e3054944af0086d8a3da8df324b552b8f0c1b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 13:54:15 -0500 Subject: [PATCH 492/505] Release version 1.12.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a192f594..f03f2832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.4.dev0" +version = "1.12.4" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 25a49841e29f7d1a8f3390fe99ad758c1838387a Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 13:55:40 -0500 Subject: [PATCH 493/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f03f2832..eec9a851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.4" +version = "1.12.5.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 179dcbef7c2f15841ce63aa8420bf516870c2e3f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 21:52:42 -0500 Subject: [PATCH 494/505] Require Python 3.10+ The rich-click >=1.9.5 from jgo, at least from conda-forge, requires Python 3.10+ anyway, so it's a losing battle trying to stick to the already-EOL Python 3.9 as minimum. --- .github/workflows/build.yml | 6 +++--- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9f51a69..d3b77674 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,14 +22,14 @@ jobs: macos-latest ] python-version: [ - '3.9', - '3.13' + '3.10', + '3.14' ] java-version: ['11'] include: # one test without java to test cjdk fallback - os: ubuntu-latest - python-version: '3.9' + python-version: '3.10' java-version: '' steps: diff --git a/pyproject.toml b/pyproject.toml index eec9a851..e32995ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,11 @@ classifiers = [ "Intended Audience :: Education", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: Microsoft :: Windows", "Operating System :: Unix", "Operating System :: MacOS", @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Utilities", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "jpype1 >= 1.3.0", "jgo>=2.1.0", From 2e879d9ae1169817d0d2ffe6673803665412e5c1 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 22:24:24 -0500 Subject: [PATCH 495/505] Lint the code --- src/scyjava/_jvm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 1625237e..e385a37b 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -201,15 +201,15 @@ def start_jvm(options: Sequence[str] = None) -> None: env = jgo.build( endpoint=endpoint, - #update=False, + # update=False, cache_dir=scyjava.config.get_cache_dir(), repositories=repositories, resolver=jgo.maven.PythonResolver(lenient=True), # The following obsolete arguments are from jgo v1: - #m2_repo=scyjava.config.get_m2_repo(), - #manage_dependencies=scyjava.config.get_manage_deps(), - #verbose=scyjava.config.get_verbose(), - #shortcuts=scyjava.config.get_shortcuts(), + # m2_repo=scyjava.config.get_m2_repo(), + # manage_dependencies=scyjava.config.get_manage_deps(), + # verbose=scyjava.config.get_verbose(), + # shortcuts=scyjava.config.get_shortcuts(), ) jpype.addClassPath(env.modules_dir / "*") jpype.addClassPath(env.jars_dir / "*") From 4047c8130b3d8a6a19632c2d114a6fd897a66bef Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Sun, 15 Mar 2026 22:42:53 -0500 Subject: [PATCH 496/505] Relax Java object hash assertion --- tests/test_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 76e2229c..65e13d0b 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -34,7 +34,7 @@ def test_jimport(self): assert str(Object) o = Object() assert scyjava.jinstance(o, "java.lang.Object") - assert re.match("java.lang.Object@[0-9a-f]{7}", str(o.toString())) + assert re.match("java.lang.Object@[0-9a-f]+", str(o.toString())) def test_jinstance(self): """ From c6fc2b3e73092dcd7d72a2bece60b7423e11786f Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 2 Apr 2026 11:42:11 -0500 Subject: [PATCH 497/505] Fix type hint declaration --- src/scyjava/_jvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index e385a37b..aa3f8dd8 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -146,7 +146,7 @@ def jvm_version() -> tuple[int, ...]: raise RuntimeError(f"Inscrutable java version: {v}") -def start_jvm(options: Sequence[str] = None) -> None: +def start_jvm(options: Sequence[str] | None = None) -> None: """ Explicitly connect to the Java virtual machine (JVM). Only one JVM can be active; does nothing if the JVM has already been started. Calling From 127f20317fd4160c2eb0a56f97d74a2bdeb22a23 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 2 Apr 2026 11:45:14 -0500 Subject: [PATCH 498/505] Make "always" fetch mode actually *always* fetch The JavaSource.AUTO to jgo was hardcoded. And the reason was that jgo did not actually have a JavaSource.DOWNLOAD to force cjdk-based resolution. But as of jgo 2.2.0, it now does, so we can use it here. We now fully lean on jgo's JDK/JRE resolution mechanism in all cases, and stop trying to be smarter than jgo downstream here in scyjava. We also support "download" and "system" for fetch_java now, since that is the terminology used in the jgo project, and it would be confusing (to me, at least) if those values mapped silently to AUTO. --- pyproject.toml | 2 +- src/scyjava/_jdk_fetch.py | 57 +++++++++++++-------------------------- src/scyjava/_jvm.py | 4 +-- 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e32995ed..d83e51e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "jpype1 >= 1.3.0", - "jgo>=2.1.0", + "jgo>=2.2.0", ] [dependency-groups] diff --git a/src/scyjava/_jdk_fetch.py b/src/scyjava/_jdk_fetch.py index 8d2cebf5..ef08c57a 100644 --- a/src/scyjava/_jdk_fetch.py +++ b/src/scyjava/_jdk_fetch.py @@ -6,11 +6,8 @@ import logging import os -import subprocess from typing import TYPE_CHECKING, Union -import jpype - from jgo.exec import JavaLocator, JavaSource import scyjava.config @@ -21,41 +18,11 @@ _logger = logging.getLogger(__name__) -def ensure_jvm_available() -> None: - """ - Ensure that the JVM is available. +def resolve_java(vendor: str | None = None, version: str | None = None) -> None: """ - fetch = scyjava.config.get_fetch_java() - if fetch == "never": - # Not allowed to fetch Java. - return - if fetch == "always" or not is_jvm_available(): - fetch_java() - - -def is_jvm_available() -> bool: - """Return True if the JVM is available, suppressing stderr on macos.""" - from unittest.mock import patch - - subprocess_check_output = subprocess.check_output - - def _silent_check_output(*args, **kwargs): - # also suppress stderr on calls to subprocess.check_output - kwargs.setdefault("stderr", subprocess.DEVNULL) - return subprocess_check_output(*args, **kwargs) - - try: - with patch.object(subprocess, "check_output", new=_silent_check_output): - jpype.getDefaultJVMPath() - # on Darwin, may raise a CalledProcessError when invoking `/usr/libexec/java_home` - except (jpype.JVMNotFoundException, subprocess.CalledProcessError): - return False - return True - - -def fetch_java(vendor: str | None = None, version: str | None = None) -> None: - """ - Fetch Java and configure PATH/JAVA_HOME. + Resolve JDK installation location and configure PATH/JAVA_HOME. + Might download Java or use the system Java, depending on the + scyjava.config.fetch_java setting. Supports cjdk version syntax including "11", "17", "11+", "17+", etc. See https://pypi.org/project/cjdk for more information. @@ -67,8 +34,22 @@ def fetch_java(vendor: str | None = None, version: str | None = None) -> None: _logger.info(f"Fetching {vendor}:{version}...") + fetch = scyjava.config.get_fetch_java() + + # Map scyjava fetch mode to jgo JavaSource strategy. + # "always" -> DOWNLOAD: always use cjdk-managed Java, ignoring system Java. + # "never" -> SYSTEM: always use system Java, never downloading via cjdk. + # "auto" -> AUTO: prefer system Java, fall back to cjdk if absent/too old. + _FETCH_MODES = { + "always": JavaSource.DOWNLOAD, + "download": JavaSource.DOWNLOAD, + "never": JavaSource.SYSTEM, + "system": JavaSource.SYSTEM, + } + java_source = _FETCH_MODES.get(fetch, JavaSource.AUTO) + locator = JavaLocator( - java_source=JavaSource.AUTO, + java_source=java_source, java_version=version, # Pass string directly (e.g. "11", "17", "11+", "17+") java_vendor=vendor, verbose=True, diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index aa3f8dd8..95b2ad1f 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -19,7 +19,7 @@ import scyjava.config from scyjava.config import Mode, mode -from scyjava._jdk_fetch import ensure_jvm_available +from scyjava._jdk_fetch import resolve_java _logger = logging.getLogger(__name__) @@ -175,7 +175,7 @@ def start_jvm(options: Sequence[str] | None = None) -> None: _logger.debug("Adding jars from endpoints {0}".format(endpoints)) # download Java as appropriate - ensure_jvm_available() + resolve_java() # Fail fast if Java version is too old. JPype 1.6+ dropped Java 8 support. try: From 23f3862ea42fcd438fa371ec4a4b15b7554c5cae Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 2 Apr 2026 12:29:37 -0500 Subject: [PATCH 499/505] Release version 1.12.5 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d83e51e1..2710455c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.5.dev0" +version = "1.12.5" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 2c701986d39cb35ab1c455c089ddb7da065c9487 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 2 Apr 2026 12:30:23 -0500 Subject: [PATCH 500/505] Bump to next development cycle --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2710455c..c6a81646 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" -version = "1.12.5" +version = "1.12.6.dev0" description = "Supercharged Java access from Python" license = "Unlicense" authors = [{name = "SciJava developers", email = "ctrueden@wisc.edu"}] From 086b9a62d07339e82c5bca76d46354b44dad6c70 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 17 Jun 2026 14:08:59 -0500 Subject: [PATCH 501/505] Update README badges --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb3127b2..2b509f21 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -[![build status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/scijava/scyjava/branch/main/graph/badge.svg?token=NLK3ADZUCU)](https://codecov.io/gh/scijava/scyjava) +[![License](https://img.shields.io/pypi/l/scyjava.svg)](https://github.com/scijava/scyjava/raw/main/UNLICENSE) +[![PyPI](https://img.shields.io/pypi/v/scyjava.svg)](https://pypi.org/project/scyjava) +[![Python Version](https://img.shields.io/pypi/pyversions/scyjava.svg)](https://python.org) +[![Build Status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) Supercharged Java access from Python. From 4f16009bb8bd7dd73dd19606644a25a12c44233b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 17 Jun 2026 14:19:22 -0500 Subject: [PATCH 502/505] Add title to README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b509f21..fcbaaaac 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ +# scyjava + [![License](https://img.shields.io/pypi/l/scyjava.svg)](https://github.com/scijava/scyjava/raw/main/UNLICENSE) [![PyPI](https://img.shields.io/pypi/v/scyjava.svg)](https://pypi.org/project/scyjava) [![Python Version](https://img.shields.io/pypi/pyversions/scyjava.svg)](https://python.org) [![Build Status](https://github.com/scijava/scyjava/actions/workflows/build.yml/badge.svg)](https://github.com/scijava/scyjava/actions/workflows/build.yml) -Supercharged Java access from Python. +*Supercharged Java access from Python.* Built on [JPype](https://jpype.readthedocs.io/en/latest/) and [jgo](https://github.com/apposed/jgo). From 7a193cc0a64042d99f58ea5d958d4a98d15e5c1b Mon Sep 17 00:00:00 2001 From: Nils Christian Date: Thu, 13 Aug 2026 11:04:31 +0200 Subject: [PATCH 503/505] Extract jvm_version parsing into a testable helper function Split the version-string-to-tuple conversion out of jvm_version() into _jvm_version_str_to_tuple() and add unit tests. --- src/scyjava/_jvm.py | 10 +++++++--- tests/test_jvm_version.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/test_jvm_version.py diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 95b2ad1f..42bed354 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -130,11 +130,15 @@ def jvm_version() -> tuple[int, ...]: except subprocess.CalledProcessError as e: raise RuntimeError("System call to java failed") from e - output = output.replace("\n", " ").replace("\r", "") - m = re.match('.* version "([^"]*)"', output) + return _jvm_version_str_to_tuple(output, java) + + +def _jvm_version_str_to_tuple(java_version_output: str, java: str) -> tuple[int, ...]: + java_version_output = java_version_output.replace("\n", " ").replace("\r", "") + m = re.match('.* version "([^"]*)"', java_version_output) if not m: raise RuntimeError( - f"Inscrutable java command output:\n$ {java} -version\n{output}" + f"Inscrutable java command output:\n$ {java} -version\n{java_version_output}" ) v = m.group(1) diff --git a/tests/test_jvm_version.py b/tests/test_jvm_version.py new file mode 100644 index 00000000..1aafeda1 --- /dev/null +++ b/tests/test_jvm_version.py @@ -0,0 +1,15 @@ +""" +Tests for functions in _versions submodule. +""" + +from scyjava._jvm import _jvm_version_str_to_tuple + + +def test_jvm_version(): + assert _jvm_version_str_to_tuple(' version "17.0.1"', "java") == (17, 0, 1) + assert _jvm_version_str_to_tuple(' version "17.0.18-internal"', "java") == ( + 17, 0, 18) + assert _jvm_version_str_to_tuple(' version "11.0.9.1-internal"', "java") == ( + 11, 0, 9, 1) + assert _jvm_version_str_to_tuple(' version "1.8.0_312"', "java") == (1, 8, 0) + assert _jvm_version_str_to_tuple(' version "25"', "java") == (25,) From 70b8d359ca6c59bc10f7f0edd28879d4e3684c8f Mon Sep 17 00:00:00 2001 From: Nils Christian Date: Thu, 13 Aug 2026 11:55:30 +0200 Subject: [PATCH 504/505] Fix jvm_version parsing of non-numeric version suffixes The old regex captured the entire quoted version string, including non-numeric suffixes like "-internal" or "_312", which broke int() conversion downstream. Now only the leading dot-separated digit groups are captured. These examples now work as expected: * OpenJDK 11.0.9.1-internal -> (11, 0, 9, 1) * OpenJDK 1.8.0_312 -> (1, 8, 0) --- src/scyjava/_jvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index 42bed354..d760ef9a 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -135,7 +135,7 @@ def jvm_version() -> tuple[int, ...]: def _jvm_version_str_to_tuple(java_version_output: str, java: str) -> tuple[int, ...]: java_version_output = java_version_output.replace("\n", " ").replace("\r", "") - m = re.match('.* version "([^"]*)"', java_version_output) + m = re.match(r'.*version "(\d+(?:\.\d+)*)', java_version_output) if not m: raise RuntimeError( f"Inscrutable java command output:\n$ {java} -version\n{java_version_output}" From 9e201570d47c03b388d97a35df7c3a0145803405 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 13 Aug 2026 09:50:32 -0500 Subject: [PATCH 505/505] Make the code linter happy Co-authored-by: Claude Sonnet 5 --- src/scyjava/__init__.py | 7 ++-- src/scyjava/_convert.py | 79 +++++++++++++++++++------------------- src/scyjava/_introspect.py | 8 ++-- src/scyjava/_jdk_fetch.py | 4 +- src/scyjava/_jvm.py | 18 ++++----- src/scyjava/_script.py | 14 +++---- src/scyjava/_types.py | 13 ++++--- src/scyjava/config.py | 32 ++++----------- src/scyjava/inspect.py | 17 ++++---- tests/it/awt.py | 4 +- tests/it/headless.py | 4 +- tests/it/java_heap.py | 4 +- tests/it/jvm_version.py | 4 +- tests/it/script_scope.py | 6 +-- tests/it/scripting.py | 6 +-- tests/test_arrays.py | 2 +- tests/test_basics.py | 2 +- tests/test_convert.py | 10 ++--- tests/test_inspect.py | 10 +++-- tests/test_introspect.py | 2 +- tests/test_jvm_version.py | 11 +++++- tests/test_pandas.py | 2 +- tests/test_types.py | 2 +- 23 files changed, 127 insertions(+), 134 deletions(-) diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 6d23d097..a30ec14e 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -84,8 +84,9 @@ """ import logging +from collections.abc import Callable from functools import lru_cache -from typing import Any, Callable, Dict +from typing import Any from . import config, inspect from ._arrays import is_arraylike, is_memoryarraylike, is_xarraylike @@ -112,7 +113,7 @@ jreflect, jsource, ) -from ._jvm import ( # noqa: F401 +from ._jvm import ( available_processors, gc, is_awt_initialized, @@ -161,7 +162,7 @@ _logger = logging.getLogger(__name__) # Set of module properties -_CONSTANTS: Dict[str, Callable] = {} +_CONSTANTS: dict[str, Callable] = {} def constant(func: Callable[[], Any], cache=True) -> Callable[[], Any]: diff --git a/src/scyjava/_convert.py b/src/scyjava/_convert.py index 6cda7567..2ba7d024 100644 --- a/src/scyjava/_convert.py +++ b/src/scyjava/_convert.py @@ -7,9 +7,10 @@ import logging import math from bisect import insort +from collections.abc import Callable from importlib.util import find_spec from pathlib import Path -from typing import Any, Callable, Dict, List, NamedTuple +from typing import Any, NamedTuple from jpype import JBoolean, JByte, JChar, JDouble, JFloat, JInt, JLong, JShort @@ -52,14 +53,14 @@ class Converter(NamedTuple): priority: float = Priority.NORMAL name: str = "" - def supports(self, obj: Any, **hints: Dict) -> bool: + def supports(self, obj: Any, **hints: dict) -> bool: return ( self.predicate(obj, **hints) if _has_kwargs(self.predicate) else self.predicate(obj) ) - def convert(self, obj: Any, **hints: Dict) -> Any: + def convert(self, obj: Any, **hints: dict) -> Any: return ( self.converter(obj, **hints) if _has_kwargs(self.converter) @@ -82,7 +83,7 @@ def __str__(self): return self.name -def _convert(obj: Any, converters: List[Converter], **hints: Dict) -> Any: +def _convert(obj: Any, converters: list[Converter], **hints: dict) -> Any: # NB: The given converters are assumed to be sorted ascending by priority, # meaning lower-priority items appear earlier than higher-priority ones. # But we want to try the higher priority converters first, so we @@ -132,7 +133,7 @@ def _convertIterable(obj: collections.abc.Iterable): return jlist -java_converters: List[Converter] = [] +java_converters: list[Converter] = [] def add_java_converter(converter: Converter) -> None: @@ -143,7 +144,7 @@ def add_java_converter(converter: Converter) -> None: insort(java_converters, converter) -def to_java(obj: Any, **hints: Dict) -> Any: +def to_java(obj: Any, **hints: dict) -> Any: """ Recursively convert a Python object to a Java object. @@ -197,7 +198,7 @@ def to_java(obj: Any, **hints: Dict) -> Any: return _convert(obj, java_converters, **hints) -def _stock_java_converters() -> List[Converter]: +def _stock_java_converters() -> list[Converter]: """ Construct the Python-to-Java converters supported out of the box. :return: A list of Converters @@ -366,7 +367,7 @@ def _jstr(data): if isinstance(data, JavaObject): return str(data) # NB: We want Python strings to render in single quotes. - return "{!r}".format(data) + return f"{data!r}" class JavaObject: @@ -526,7 +527,7 @@ def __str__(self): return "{" + ", ".join(_jstr(v) for v in self) + "}" -py_converters: List[Converter] = [] +py_converters: list[Converter] = [] def add_py_converter(converter: Converter) -> None: @@ -566,13 +567,13 @@ def to_python(data: Any, gentle: bool = False) -> Any: start_jvm() try: return _convert(data, py_converters) - except TypeError as exc: + except TypeError: if gentle: return data - raise exc + raise -def _stock_py_converters() -> List: +def _stock_py_converters() -> list: """ Construct the Java-to-Python converters supported out of the box. :return: A list of Converters @@ -842,7 +843,7 @@ def _is_table(obj: Any) -> bool: """Check if obj is a table.""" try: return jinstance(obj, "org.scijava.table.Table") - except BaseException: + except BaseException: # noqa: BLE001 # No worries if scijava-table is not available. return False @@ -851,7 +852,7 @@ def _convert_table(obj: Any): """Convert obj to a table.""" try: return _table_to_pandas(obj) - except BaseException: + except BaseException: # noqa: BLE001 # No worries if scijava-table is not available. return None @@ -894,8 +895,8 @@ def _pandas_to_table(df): elif table_type.name.startswith("bool"): TableClass = jimport("org.scijava.table.DefaultBoolTable") else: - msg = "The type '{}' is not supported.".format(table_type.name) - raise Exception(msg) + msg = f"The type '{table_type.name}' is not supported." + raise ValueError(msg) table = TableClass(*df.shape[::-1]) @@ -913,51 +914,51 @@ def _pandas_to_table(df): # fmt: off class _JavaClasses(JavaClasses): @JavaClasses.java_import - def Boolean(self): return "java.lang.Boolean" # noqa: E272 + def Boolean(self): return "java.lang.Boolean" @JavaClasses.java_import - def Byte(self): return "java.lang.Byte" # noqa: E272 + def Byte(self): return "java.lang.Byte" @JavaClasses.java_import - def Character(self): return "java.lang.Character" # noqa: E272 + def Character(self): return "java.lang.Character" @JavaClasses.java_import - def Double(self): return "java.lang.Double" # noqa: E272 + def Double(self): return "java.lang.Double" @JavaClasses.java_import - def Float(self): return "java.lang.Float" # noqa: E272 + def Float(self): return "java.lang.Float" @JavaClasses.java_import - def Integer(self): return "java.lang.Integer" # noqa: E272 + def Integer(self): return "java.lang.Integer" @JavaClasses.java_import - def Iterable(self): return "java.lang.Iterable" # noqa: E272 + def Iterable(self): return "java.lang.Iterable" @JavaClasses.java_import - def Long(self): return "java.lang.Long" # noqa: E272 + def Long(self): return "java.lang.Long" @JavaClasses.java_import - def Object(self): return "java.lang.Object" # noqa: E272 + def Object(self): return "java.lang.Object" @JavaClasses.java_import - def Short(self): return "java.lang.Short" # noqa: E272 + def Short(self): return "java.lang.Short" @JavaClasses.java_import - def String(self): return "java.lang.String" # noqa: E272 + def String(self): return "java.lang.String" @JavaClasses.java_import - def BigDecimal(self): return "java.math.BigDecimal" # noqa: E272 + def BigDecimal(self): return "java.math.BigDecimal" @JavaClasses.java_import - def BigInteger(self): return "java.math.BigInteger" # noqa: E272 + def BigInteger(self): return "java.math.BigInteger" @JavaClasses.java_import - def Path(self): return "java.nio.file.Path" # noqa: E272 + def Path(self): return "java.nio.file.Path" @JavaClasses.java_import - def Paths(self): return "java.nio.file.Paths" # noqa: E272 + def Paths(self): return "java.nio.file.Paths" @JavaClasses.java_import - def ArrayList(self): return "java.util.ArrayList" # noqa: E272 + def ArrayList(self): return "java.util.ArrayList" @JavaClasses.java_import - def Collection(self): return "java.util.Collection" # noqa: E272 + def Collection(self): return "java.util.Collection" @JavaClasses.java_import - def Iterator(self): return "java.util.Iterator" # noqa: E272 + def Iterator(self): return "java.util.Iterator" @JavaClasses.java_import - def LinkedHashMap(self): return "java.util.LinkedHashMap" # noqa: E272 + def LinkedHashMap(self): return "java.util.LinkedHashMap" @JavaClasses.java_import - def LinkedHashSet(self): return "java.util.LinkedHashSet" # noqa: E272 + def LinkedHashSet(self): return "java.util.LinkedHashSet" @JavaClasses.java_import - def List(self): return "java.util.List" # noqa: E272 + def List(self): return "java.util.List" @JavaClasses.java_import - def Map(self): return "java.util.Map" # noqa: E272 + def Map(self): return "java.util.Map" @JavaClasses.java_import - def Set(self): return "java.util.Set" # noqa: E272 + def Set(self): return "java.util.Set" # fmt: on diff --git a/src/scyjava/_introspect.py b/src/scyjava/_introspect.py index a9ab98a9..4d657815 100644 --- a/src/scyjava/_introspect.py +++ b/src/scyjava/_introspect.py @@ -3,13 +3,13 @@ class methods, fields, and source code URL. """ -from typing import Any, Dict, List +from typing import Any from scyjava._jvm import jimport, jvm_version -from scyjava._types import isjava, jinstance, jclass +from scyjava._types import isjava, jclass, jinstance -def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: +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. @@ -91,7 +91,7 @@ def jsource(data) -> str: try: data = jimport(data) # check if data can be imported except Exception as err: - raise ValueError(f"Not a Java object {err}") + raise ValueError(f"Not a Java object {err}") from err jcls = data if jinstance(data, "java.lang.Class") else jclass(data) if jcls.getClassLoader() is None: diff --git a/src/scyjava/_jdk_fetch.py b/src/scyjava/_jdk_fetch.py index ef08c57a..6c0f36d1 100644 --- a/src/scyjava/_jdk_fetch.py +++ b/src/scyjava/_jdk_fetch.py @@ -6,7 +6,7 @@ import logging import os -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from jgo.exec import JavaLocator, JavaSource @@ -64,7 +64,7 @@ def resolve_java(vendor: str | None = None, version: str | None = None) -> None: os.environ["JAVA_HOME"] = str(java_home) -def _add_to_path(path: Union[Path, str], front: bool = False) -> None: +def _add_to_path(path: Path | str, front: bool = False) -> None: """Add a path to the PATH environment variable. If front is True, the path is added to the front of the PATH. diff --git a/src/scyjava/_jvm.py b/src/scyjava/_jvm.py index d760ef9a..c3b75541 100644 --- a/src/scyjava/_jvm.py +++ b/src/scyjava/_jvm.py @@ -8,18 +8,18 @@ import re import subprocess import sys -from functools import lru_cache +from collections.abc import Sequence +from functools import cache from importlib import import_module from pathlib import Path -from typing import Sequence +import jgo import jpype import jpype.config -import jgo import scyjava.config -from scyjava.config import Mode, mode from scyjava._jdk_fetch import resolve_java +from scyjava.config import Mode, mode _logger = logging.getLogger(__name__) @@ -176,7 +176,7 @@ def start_jvm(options: Sequence[str] | None = None) -> None: repositories = scyjava.config.get_repositories() # use the logger to notify user that endpoints are being added - _logger.debug("Adding jars from endpoints {0}".format(endpoints)) + _logger.debug(f"Adding jars from endpoints {endpoints}") # download Java as appropriate resolve_java() @@ -297,7 +297,7 @@ def shutdown_jvm() -> None: for callback in _shutdown_callbacks: try: callback() - except Exception as e: + except Exception as e: # noqa: BLE001 _logger.error(f"Exception during shutdown callback: {e}") # dispose AWT resources if applicable @@ -309,7 +309,7 @@ def shutdown_jvm() -> None: # okay to shutdown JVM try: jpype.shutdownJVM() - except Exception as e: + except Exception as e: # noqa: BLE001 _logger.error(f"Exception during JVM shutdown: {e}") @@ -444,7 +444,6 @@ def when_jvm_starts(f) -> None: f() else: # Add function to the list of callbacks to invoke upon start_jvm(). - global _startup_callbacks _startup_callbacks.append(f) @@ -458,11 +457,10 @@ def when_jvm_stops(f) -> None: :param f: Function to invoke when scyjava.shutdown_jvm() is called. """ - global _shutdown_callbacks _shutdown_callbacks.append(f) -@lru_cache(maxsize=None) +@cache def jimport(class_name: str): """ Import a class from Java to Python. diff --git a/src/scyjava/_script.py b/src/scyjava/_script.py index a371b2bb..5302a667 100644 --- a/src/scyjava/_script.py +++ b/src/scyjava/_script.py @@ -67,7 +67,7 @@ class PythonScriptRunner: def apply(self, arg): # Copy script bindings/vars into script locals. script_locals = {} - for key in arg.vars.keys(): + for key in arg.vars: script_locals[key] = arg.vars[key] stdoutContextWriter.addScriptContext( @@ -100,7 +100,7 @@ def apply(self, arg): # See: https://docs.python.org/3/library/functions.html#exec _globals = script_locals - exec( + exec( # noqa: S102 compile(block, "", mode="exec"), _globals, script_locals ) if last is not None: @@ -109,7 +109,7 @@ def apply(self, arg): _globals, script_locals, ) - except Exception: + except Exception: # noqa: BLE001 error_message = traceback.format_exc() error_writer = arg.scriptContext.getErrorWriter() if error_writer is None: @@ -123,11 +123,11 @@ def apply(self, arg): stdoutContextWriter.removeScriptContext(threading.currentThread()) # Copy script locals back into script bindings/vars. - for key in script_locals.keys(): + for key, value in script_locals.items(): try: - arg.vars[key] = to_java(script_locals[key]) - except Exception: - arg.vars[key] = PythonObjectSupplier(script_locals[key]) + arg.vars[key] = to_java(value) + except Exception: # noqa: BLE001 + arg.vars[key] = PythonObjectSupplier(value) return to_java(return_value) diff --git a/src/scyjava/_types.py b/src/scyjava/_types.py index ef0318ad..f49d75fa 100644 --- a/src/scyjava/_types.py +++ b/src/scyjava/_types.py @@ -2,7 +2,8 @@ Utility functions for working with and reasoning about Java types. """ -from typing import Any, Callable, Sequence, Tuple, Union +from collections.abc import Callable, Sequence +from typing import Any import jpype @@ -50,7 +51,7 @@ def java_import(func: Callable[[], str]) -> property: @property def inner(self): if not jvm_started(): - raise Exception() + raise RuntimeError("The JVM has not been started yet.") try: return jimport(func(self)) except TypeError: @@ -128,7 +129,7 @@ def jstacktrace(exc) -> str: sw = StringWriter() exc.printStackTrace(PrintWriter(sw, True)) return str(sw) - except BaseException: + except BaseException: # noqa: BLE001 return "" @@ -138,7 +139,7 @@ def isjava(data) -> bool: return jinstance(data, "java.lang.Object") assert mode == Mode.JPYPE - return isinstance(data, jpype.JClass) or isinstance(data, jpype.JObject) + return isinstance(data, (jpype.JClass, jpype.JObject)) def is_jbyte(the_type: type) -> bool: @@ -227,7 +228,7 @@ def jarray(kind, lengths: Sequence): arraytype = kind if mode == Mode.JEP: - import jep # noqa: F401 + import jep if len(lengths) == 1: # Fast case: 1-d array (we can use primitives) @@ -286,7 +287,7 @@ def jarray(kind, lengths: Sequence): def numeric_bounds( the_type: type, -) -> Union[Tuple[int, int], Tuple[float, float], Tuple[None, None]]: +) -> tuple[int, int] | tuple[float, float] | tuple[None, None]: """ Get the minimum and maximum values for the given numeric type. For example, a Java long returns (int(Long.MIN_VALUE), int(Long.MAX_VALUE)), diff --git a/src/scyjava/config.py b/src/scyjava/config.py index 70b2467e..76271739 100644 --- a/src/scyjava/config.py +++ b/src/scyjava/config.py @@ -3,12 +3,11 @@ import enum as _enum import logging as _logging import os as _os +from collections.abc import Sequence from pathlib import Path -from typing import Sequence import jpype as _jpype - _SCIJAVA_PUBLIC = "https://maven.scijava.org/content/groups/public" @@ -146,7 +145,6 @@ def add_repositories(*args, **kwargs) -> None: Add one or more Maven repositories to be used by jgo for downloading dependencies. See the jgo documentation for details. """ - global _repositories for arg in args: _logger.debug("Adding repositories %s to %s", arg, _repositories) _repositories.update(arg) @@ -159,7 +157,6 @@ def get_repositories() -> dict[str, str]: Get the Maven repositories jgo will use for downloading dependencies. See the jgo documentation for details. """ - global _repositories return _repositories @@ -179,7 +176,6 @@ def get_verbose() -> int: """ Get the level of verbosity for logging environment construction details. """ - global _verbose _logger.debug("Getting verbose level: %d", _verbose) return _verbose @@ -199,7 +195,6 @@ def get_manage_deps() -> bool: Get whether jgo will resolve dependencies in managed mode. See the jgo documentation for details. """ - global _manage_deps return _manage_deps @@ -218,7 +213,6 @@ def get_cache_dir() -> Path: Get the location to use for the jgo environment cache. See the jgo documentation for details. """ - global _cache_dir return _cache_dir @@ -235,7 +229,6 @@ def get_m2_repo() -> Path: """ Get the location to use for the local Maven repository cache. """ - global _m2_repo return _m2_repo @@ -291,7 +284,7 @@ def get_classpath() -> str: return _jpype.getClassPath() -def set_heap_min(mb: int = None, gb: int = None) -> None: +def set_heap_min(mb: int | None = None, gb: int | None = None) -> None: """ Set the initial amount of memory to allocate to the Java heap. @@ -308,7 +301,7 @@ def set_heap_min(mb: int = None, gb: int = None) -> None: add_option(f"-Xms{_mem_value(mb, gb)}") -def set_heap_max(mb: int = None, gb: int = None) -> None: +def set_heap_max(mb: int | None = None, gb: int | None = None) -> None: """ Shortcut for passing -Xmx###m or -Xmx###g to Java. @@ -323,10 +316,10 @@ def set_heap_max(mb: int = None, gb: int = None) -> None: add_option(f"-Xmx{_mem_value(mb, gb)}") -def _mem_value(mb: int = None, gb: int = None) -> str: +def _mem_value(mb: int | None = None, gb: int | None = None) -> str: # fmt: off - if mb is not None and gb is None: return f"{mb}m" # noqa: E701 - if gb is not None and mb is None: return f"{gb}g" # noqa: E701 + if mb is not None and gb is None: return f"{mb}m" + if gb is not None and mb is None: return f"{gb}g" # fmt: on raise ValueError("Exactly one of mb or gb must be given.") @@ -372,7 +365,6 @@ def add_option(option: str) -> None: :param option: The option to add. """ - global _options _options.append(option) @@ -383,7 +375,6 @@ def add_options(options: str | Sequence) -> None: :param options: Sequence of options to add, or single string to pass as an individual option. """ - global _options if isinstance(options, str): _options.append(options) else: @@ -394,7 +385,6 @@ def get_options() -> list[str]: """ Get the list of options to be passed at JVM startup. """ - global _options return _options @@ -407,7 +397,6 @@ def add_kwargs(**kwargs) -> None: convertStrings = True interrupt = True """ - global _kwargs _kwargs.update(kwargs) @@ -415,7 +404,6 @@ def get_kwargs() -> dict[str, str]: """ Get the keyword arguments to be passed to JPype at JVM startup. """ - global _kwargs return _kwargs @@ -424,7 +412,6 @@ def add_shortcut(k: str, v: str): Add a shortcut key/value to be used by jgo for evaluating endpoints. See the jgo documentation for details. """ - global _shortcuts _shortcuts[k] = v @@ -433,7 +420,6 @@ def get_shortcuts() -> dict[str, str]: Get the dictionary of shorts that jgo will use for evaluating endpoints. See the jgo documentation for details. """ - global _shortcuts return _shortcuts @@ -446,7 +432,6 @@ def add_endpoints(*new_endpoints): "Deprecated method call: scyjava.config.add_endpoints(). " "Please modify scyjava.config.endpoints directly instead." ) - global endpoints _logger.debug("Adding endpoints %s to %s", new_endpoints, endpoints) endpoints.extend(new_endpoints) @@ -460,12 +445,11 @@ def get_endpoints(): "Deprecated method call: scyjava.config.get_endpoints(). " "Please access scyjava.config.endpoints directly instead." ) - global endpoints return endpoints -_maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" # noqa: E501 -_maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" # noqa: E501 +_maven_url: str = "tgz+https://archive.apache.org/dist/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz" +_maven_sha: str = "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5678835887ab404556bfaf78dcfe03ba76fa2508649dca8531c74bca4d5846513522404d48e8c4ac8b" def get_maven_url() -> str: diff --git a/src/scyjava/inspect.py b/src/scyjava/inspect.py index 3058822e..a726d86c 100644 --- a/src/scyjava/inspect.py +++ b/src/scyjava/inspect.py @@ -144,7 +144,7 @@ def _print_data( return # Print source code - offset = max(list(map(lambda entry: len(entry["returns"] or "void"), table))) + offset = max(len(entry["returns"] or "void") for entry in table) all_methods = "" if source or source is None: try: @@ -162,14 +162,13 @@ def _print_data( entry["returns"] = _map_syntax(entry["returns"]) if entry["arguments"]: entry["arguments"] = [_map_syntax(e) for e in entry["arguments"]] - if static is None: - entry_string = _pretty_string(entry, offset) - all_methods += entry_string - - elif static and "static" in entry["mods"]: - entry_string = _pretty_string(entry, offset) - all_methods += entry_string - elif not static and "static" not in entry["mods"]: + if ( + static is None + or static + and "static" in entry["mods"] + or not static + and "static" not in entry["mods"] + ): entry_string = _pretty_string(entry, offset) all_methods += entry_string else: diff --git a/tests/it/awt.py b/tests/it/awt.py index 9e746715..4d0bd4b5 100644 --- a/tests/it/awt.py +++ b/tests/it/awt.py @@ -5,10 +5,10 @@ import platform import sys -import scyjava - from assertpy import assert_that +import scyjava + if platform.system() == "Darwin": # NB: This test would hang on macOS, due to AWT threading issues. sys.exit(0) diff --git a/tests/it/headless.py b/tests/it/headless.py index 6f21f376..abe37e59 100644 --- a/tests/it/headless.py +++ b/tests/it/headless.py @@ -2,10 +2,10 @@ Test scyjava headless mode. """ -import scyjava - from assertpy import assert_that +import scyjava + scyjava.config.enable_headless_mode() assert_that(scyjava.jvm_started()).is_false() diff --git a/tests/it/java_heap.py b/tests/it/java_heap.py index 77267ae3..5b14939f 100644 --- a/tests/it/java_heap.py +++ b/tests/it/java_heap.py @@ -2,10 +2,10 @@ Test scyjava JVM memory-related functions. """ -import scyjava - from assertpy import assert_that +import scyjava + mb_initial = 50 # initial MB of memory to snarf up mb_tolerance = 10 # ceiling of expected MB in use diff --git a/tests/it/jvm_version.py b/tests/it/jvm_version.py index 08331678..f488c7cf 100644 --- a/tests/it/jvm_version.py +++ b/tests/it/jvm_version.py @@ -2,10 +2,10 @@ Test the jvm_version() function. """ -import scyjava - from assertpy import assert_that +import scyjava + assert_that(scyjava.jvm_started()).is_false() before_version = scyjava.jvm_version() diff --git a/tests/it/script_scope.py b/tests/it/script_scope.py index fc751fd8..9d99e16e 100644 --- a/tests/it/script_scope.py +++ b/tests/it/script_scope.py @@ -4,10 +4,10 @@ import sys -import scyjava - from assertpy import assert_that +import scyjava + scyjava.config.endpoints.extend( ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] ) @@ -59,7 +59,7 @@ def calculate_cbrt(age): trace = scyjava.jstacktrace(e) if trace: sys.stderr.write(f"{trace}\n") - raise e + raise assert_that(statement).is_equal_to("2") assert_that(return_value).is_equal_to("The rounded cube root of my age is 2") diff --git a/tests/it/scripting.py b/tests/it/scripting.py index 48d24b5a..6026d663 100644 --- a/tests/it/scripting.py +++ b/tests/it/scripting.py @@ -7,10 +7,10 @@ import sys -import scyjava - from assertpy import assert_that +import scyjava + scyjava.config.endpoints.extend( ["org.scijava:scijava-common:2.94.2", "org.scijava:scripting-python:MANAGED"] ) @@ -56,7 +56,7 @@ trace = scyjava.jstacktrace(e) if trace: sys.stderr.write(f"{trace}\n") - raise e + raise assert_that(statement).is_equal_to( "Hello, Chuckles! In one year you will be 14 years old." diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 80f18911..b419066d 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -8,7 +8,7 @@ from scyjava.config import Mode, mode -class TestArrays(object): +class TestArrays: def test_non_primitive_jarray(self): pass diff --git a/tests/test_basics.py b/tests/test_basics.py index 65e13d0b..00aa98a9 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -10,7 +10,7 @@ from scyjava.config import Mode, mode -class TestBasics(object): +class TestBasics: """ Test basic scyjava functions. """ diff --git a/tests/test_convert.py b/tests/test_convert.py index fcfabe10..128975df 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -28,7 +28,7 @@ config.enable_headless_mode() -class TestConvert(object): +class TestConvert: def testClass(self): """ Test class detection from Java objects. @@ -167,7 +167,7 @@ def testString(self): assert ostring == pstring def testList(self): - olist = "The quick brown fox jumps over the lazy dogs".split() + olist = ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dogs"] jlist = to_java(olist) for e, a in zip(olist, jlist): assert e == to_python(a) @@ -179,7 +179,7 @@ def testList(self): assert "The quick brown fox jumps over the silly dogs" == " ".join(plist) def testSet(self): - s = set(["orange", "apple", "pineapple", "plum"]) + s = {"orange", "apple", "pineapple", "plum"} js = to_java(s) assert len(s) == js.size() for e in s: @@ -262,7 +262,7 @@ def testPath(self): def testMixed(self): test_dict = {"a": "b", "c": "d"} test_list = ["e", "f", "g", "h"] - test_set = set(["i", "j", "k"]) + test_set = {"i", "j", "k"} # mixed types in a dictionary mixed_dict = {"d": test_dict, "l": test_list, "s": test_set, "str": "hello"} @@ -303,7 +303,7 @@ def testGentle(self): bad_conversion = None try: bad_conversion = to_python(unknown_thing) - except BaseException: + except TypeError: # NB: Failure is expected here. pass assert bad_conversion is None diff --git a/tests/test_inspect.py b/tests/test_inspect.py index f265b8c0..0a314f8f 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -5,10 +5,10 @@ import re from scyjava import inspect -from scyjava.config import mode, Mode +from scyjava.config import Mode, mode -class TestInspect(object): +class TestInspect: """ Test scyjava.inspect convenience functions. """ @@ -20,8 +20,10 @@ def test_inspect_members(self): members = [] inspect.members("java.lang.Iterable", writer=members.append) expected = [ - "Source code URL: https://github.com/openjdk/jdk/blob/" - ".../share/classes/java/lang/Iterable.java", + ( + "Source code URL: https://github.com/openjdk/jdk/blob/" + ".../share/classes/java/lang/Iterable.java" + ), " * indicates static modifier", "java.util.Iterator = iterator()", "java.util.Spliterator = spliterator()", diff --git a/tests/test_introspect.py b/tests/test_introspect.py index a438ede1..cfc895ef 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -13,7 +13,7 @@ ) -class TestIntrospection(object): +class TestIntrospection: """ Test introspection functionality. """ diff --git a/tests/test_jvm_version.py b/tests/test_jvm_version.py index 1aafeda1..6d60bedf 100644 --- a/tests/test_jvm_version.py +++ b/tests/test_jvm_version.py @@ -8,8 +8,15 @@ def test_jvm_version(): assert _jvm_version_str_to_tuple(' version "17.0.1"', "java") == (17, 0, 1) assert _jvm_version_str_to_tuple(' version "17.0.18-internal"', "java") == ( - 17, 0, 18) + 17, + 0, + 18, + ) assert _jvm_version_str_to_tuple(' version "11.0.9.1-internal"', "java") == ( - 11, 0, 9, 1) + 11, + 0, + 9, + 1, + ) assert _jvm_version_str_to_tuple(' version "1.8.0_312"', "java") == (1, 8, 0) assert _jvm_version_str_to_tuple(' version "25"', "java") == (25,) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 1baa5dd9..a2f28308 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -22,7 +22,7 @@ def assert_same_table(table, df): assert table.getColumnHeader(i) == df.columns[i] -class TestPandas(object): +class TestPandas: def testPandasToTable(self): columns = ["header1", "header2", "header3", "header4", "header5"] diff --git a/tests/test_types.py b/tests/test_types.py index e4bdbc92..b302665b 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -6,7 +6,7 @@ from scyjava.config import Mode, mode -class TestTypes(object): +class TestTypes: """ Test Java-type-related functions. """