Skip to content

Commit 2664a97

Browse files
mbastianellidesruisseaux
authored andcommitted
Add Python bridge tests: verify calls from Python to the Java Metadata API.
1 parent 8b0f80d commit 2664a97

8 files changed

Lines changed: 354 additions & 3 deletions

File tree

geoapi-java-python/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Test execution
2+
3+
It is recommended to create a dedicated virtual environment:
4+
5+
```bash
6+
python -m venv .venv
7+
source .venv/bin/activate
8+
```
9+
10+
Ensure that the `JAVA_HOME` environment variable is set to the path to valid java JDK.
11+
Then ensure that the project is installed:
12+
13+
```bash
14+
pip install -e ".[test,dev]"
15+
```
16+
17+
Then run test. By default, `pytest` searches for tests in the `*/tests` directory.
18+
19+
```bash
20+
pytest
21+
```
22+
23+
Note: in order to run, the tests of the Java-Python bridge require Python to start a JVM
24+
with classes of the module and its dependencies referenced in the `classpath`.
25+
Currently, the classpath is set manually in the `src/test/python/tests/__init__.py` file.
26+
27+
If classes are not found when trying to access a java class from Python,
28+
check that the requested class **and** its imported dependencies are present on the JVM's classpath.
29+
30+
[//]: # (export JVM_DLL=$JAVA_HOME/lib/server/libjvm.so)

geoapi-java-python/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@
7474
<version>${project.version}</version>
7575
<scope>test</scope>
7676
</dependency>
77+
<dependency>
78+
<groupId>org.opengis.example</groupId>
79+
<artifactId>geoapi-examples</artifactId>
80+
<version>${project.version}</version>
81+
<scope>test</scope>
82+
</dependency>
7783
</dependencies>
7884

7985

geoapi-java-python/pyproject.toml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[build-system]
2+
requires = [
3+
"setuptools >= 74.1",
4+
]
5+
build-backend = "setuptools.build_meta"
6+
7+
[project]
8+
name = "geoapi-python"
9+
dynamic = ["version"]
10+
dependencies = [
11+
"jpy >= 1.1.0"
12+
]
13+
requires-python = ">=3.10"
14+
authors = [
15+
{name = "Matthieu Bastianelli", email = "matthieu.bastianelli@geomatys.com"},
16+
{name = "David Meaux", email = "david.meaux@geomatys.com"},
17+
{name = "Martin Desruisseaux", email = "martin.desruisseaux@geomatys.com"},
18+
]
19+
maintainers = [
20+
{name = "Matthieu Bastianelli", email = "matthieu.bastianelli@geomatys.com"},
21+
{name = "David Meaux", email = "david.meaux@geomatys.com"},
22+
{name = "Martin Desruisseaux", email = "martin.desruisseaux@geomatys.com"},
23+
]
24+
description = "Java - Python binding of GeoAPI - abstractions for OGC/ISO standards"
25+
readme = {file = "README.md", content-type = "text/markdown"}
26+
license = {text = "Apache 2.0 License"}
27+
keywords = ["geospatial", "metadata", "referencing"]
28+
classifiers = [
29+
"Development Status :: 3 - Alpha",
30+
"Programming Language :: Python :: 3",
31+
"Intended Audience :: Developers",
32+
"Intended Audience :: Information Technology",
33+
"License :: OSI Approved :: Apache Software License",
34+
"Natural Language :: English",
35+
"Operating System :: OS Independent",
36+
"Topic :: Scientific/Engineering :: GIS",
37+
"Topic :: Software Development :: Libraries"
38+
]
39+
40+
41+
[project.optional-dependencies]
42+
build = [
43+
"build"
44+
]
45+
dev = [
46+
]
47+
doc = [
48+
]
49+
test = [
50+
"pytest >= 7.4"
51+
]
52+
53+
54+
[project.urls]
55+
Homepage = "https://www.geoapi.org/java-python/"
56+
Documentation = "https://www.geoapi.org/java-python/index.html"
57+
#API = "https://www.geoapi.org/snapshot/python/index.html"
58+
Repository = "https://github.com/opengeospatial/geoapi/"
59+
60+
61+
# Set max line length to the PEP 8 standard recommandation.
62+
line-length = "79"
63+
64+
exclude = [
65+
"*.pyi"
66+
]
67+
# pytest
68+
# ----------------------------------------
69+
[tool.pytest.ini_options]
70+
log_cli = "True"
71+
log_level = "INFO"
72+
# ----------------------------------------
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* GeoAPI - Java interfaces for OGC/ISO standards
3+
* Copyright © 2026 Open Geospatial Consortium, Inc.
4+
* http://www.geoapi.org
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.opengis.bridge.python;
19+
20+
import java.util.Map;
21+
import java.util.Set;
22+
import org.opengis.example.metadata.MetadataProxyFactory;
23+
import org.opengis.example.metadata.SimpleCitation;
24+
import org.opengis.metadata.Metadata;
25+
import org.opengis.metadata.citation.Party;
26+
import org.opengis.metadata.citation.Responsibility;
27+
import org.opengis.util.InternationalString;
28+
29+
30+
/**
31+
* Factory of test instances of {@link org.opengis.metadata.Metadata} interfaces.
32+
* This class is invoked from Python code in the {@code java-python/src/test/python} directory.
33+
*
34+
* @author Matthieu Bastianelli (Geomatys)
35+
*/
36+
public final class PythonBridgeHelper {
37+
/**
38+
* Do not allow instantiation of this class.
39+
*/
40+
private PythonBridgeHelper() {
41+
}
42+
43+
/**
44+
* Factory of metadata objects.
45+
*/
46+
private static final MetadataProxyFactory factory = MetadataProxyFactory.INSTANCE;
47+
48+
/**
49+
* Creates an "Aristotle" responsible party.
50+
*
51+
* @return an "Aristotle" responsible party.
52+
*/
53+
public static Responsibility responsibility() {
54+
InternationalString name = new SimpleCitation("Aristotle");
55+
Party party = factory.create(Party.class, Map.of("name", name));
56+
return factory.create(Responsibility.class, Map.of("party", Set.of(party)));
57+
}
58+
59+
/**
60+
* Creates a metadata with only an "Aristotle" responsible party.
61+
*
62+
* @return a metadata with an "Aristotle" responsible party.
63+
*/
64+
public static Metadata metadata() {
65+
return factory.create(Metadata.class, Map.of("contact", Set.of(responsibility())));
66+
}
67+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* GeoAPI - Java interfaces for OGC/ISO standards
3+
* Copyright © 2026 Open Geospatial Consortium, Inc.
4+
* http://www.geoapi.org
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.opengis.bridge.python;
19+
20+
import org.junit.jupiter.api.Assertions;
21+
import org.junit.jupiter.api.Test;
22+
import org.opengis.metadata.Metadata;
23+
import org.opengis.metadata.citation.Responsibility;
24+
import org.opengis.referencing.crs.CoordinateReferenceSystem;
25+
import org.opengis.util.FactoryException;
26+
27+
import static org.junit.jupiter.api.Assertions.assertEquals;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
30+
31+
/**
32+
* Tests {@link PythonBridgeHelper}, the helper class used from Python tests.
33+
*
34+
* @author Matthieu Bastianelli (Geomatys)
35+
*/
36+
public final class PythonBridgeHelperTest {
37+
/**
38+
* Creates a new test case.
39+
*/
40+
public PythonBridgeHelperTest() {
41+
}
42+
43+
/**
44+
* Tests {@link PythonBridgeHelper#responsibility()}.
45+
*/
46+
@Test
47+
public void testResponsibility() {
48+
final Responsibility responsibility = PythonBridgeHelper.responsibility();
49+
assertEquals("CI_Responsibility{party=[CI_Party{name=Aristotle}]}", responsibility.toString());
50+
}
51+
52+
/**
53+
* Tests {@link PythonBridgeHelper#metadata()}.
54+
*/
55+
@Test
56+
public void testMetadata() {
57+
final Metadata md = PythonBridgeHelper.metadata();
58+
assertTrue(md.getSpatialRepresentationInfo().isEmpty(),
59+
"Null value should have been replaced by empty collection.");
60+
assertEquals("MD_Metadata{contact=[CI_Responsibility{party=[CI_Party{name=Aristotle}]}]}", md.toString());
61+
}
62+
63+
/**
64+
* Tests {@link PythonHelper#findCoordinateReferenceSystem(String)}.
65+
*
66+
* @throws FactoryException if an error occurred while fetching the <abbr>CRS</abbr> definition.
67+
*/
68+
@Test
69+
public void testReferencing() throws FactoryException {
70+
final CoordinateReferenceSystem crs = PythonHelper.findCoordinateReferenceSystem("EPSG:3395");
71+
Assertions.assertEquals("WGS 84 / World Mercator", crs.getName().getCode());
72+
}
73+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# GeoAPI - Programming interfaces for OGC/ISO standards
2+
# Copyright © 2026 Open Geospatial Consortium, Inc.
3+
# http://www.geoapi.org
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import jpyutil
18+
19+
jpyutil.init_jvm(jvm_classpath=[
20+
"target/classes",
21+
"target/test-classes",
22+
"../geoapi/target/classes",
23+
"../geoapi-examples/target/classes"
24+
])
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#
2+
# GeoAPI - Programming interfaces for OGC/ISO standards
3+
# Copyright © 2026 Open Geospatial Consortium, Inc.
4+
# http://www.geoapi.org
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
import jpy
20+
21+
import unittest
22+
import logging
23+
24+
logger = logging.getLogger(__name__)
25+
26+
class TestMetadata(unittest.TestCase):
27+
28+
def test_jvm(self):
29+
"""
30+
Test that the JVM is well initialized and that classes can be loaded.
31+
:return:
32+
"""
33+
java_system = jpy.get_type("java.lang.System")
34+
self.assertIsNotNone(java_system, "Java system not loaded")
35+
jRuntime = jpy.get_type("java.lang.Runtime")
36+
self.assertIsNotNone(jRuntime, "Java Runtime class not loaded")
37+
java_version = jRuntime.version()
38+
self.assertIsNotNone(java_version, "Java version of initialized jvm is None.")
39+
40+
# Load java standard Math class
41+
jMath = jpy.get_type("java.lang.Math")
42+
self.assertIsNotNone(jMath, "Java Math class not loaded")
43+
self.assertEqual(jMath.cos(0), 1, "Java Math cos fails to compute cos(0)")
44+
45+
def test_metadata(self):
46+
47+
# Get java ClassLoader to check tested java classes are well configured in running JVM
48+
class_loader = jpy.get_type("java.lang.ClassLoader")
49+
self.assertIsNotNone(class_loader, "Java class loader not loaded.")
50+
# Get the system class loader (the one with your classpath)
51+
system_loader = class_loader.getSystemClassLoader()
52+
self.assertIsNotNone(system_loader, "Java class loader not loaded.")
53+
54+
# Check access to project's java source ; If it fails ensure classpath is well set in ./init.py
55+
cls_url = system_loader.getResource("org/opengis/bridge/python/PythonHelper.class")
56+
self.assertIsNotNone(cls_url, "PythonHelper class not accessible in jvm classpath.")
57+
cls = system_loader.loadClass("org.opengis.bridge.python.PythonHelper")
58+
self.assertIsNotNone(cls, "PythonHelper class could not be loaded.")
59+
logger.info("PythonHelper loaded successfully : " +str(cls))
60+
61+
# Check access to project's java test source ; If it fails ensure classpath is well set in ./init.py
62+
cls_url = system_loader.getResource("org/opengis/bridge/python/PythonBridgeHelper.class")
63+
self.assertIsNotNone(cls_url, "PythonBridgeTestHelper class not accessible in jvm classpath.")
64+
cls = system_loader.loadClass("org.opengis.bridge.python.PythonBridgeHelper")
65+
self.assertIsNotNone(cls, "PythonBridgeTestHelper class could not be loaded.")
66+
logger.info("PythonBridgeTestHelper loaded successfully : " +str(cls))
67+
68+
# Check access to geoapi core module
69+
metadata_class = jpy.get_type('org.opengis.metadata.Metadata')
70+
logger.info("Metadata loaded successfully from geoapi dependency : " +str(metadata_class))
71+
72+
self._test_helper = jpy.get_type('org.opengis.bridge.python.PythonBridgeHelper')
73+
self.assertIsNotNone(self._test_helper, "Failed to get python binding for PythonBridgeHelper class.")
74+
logger.info("Java PythonBridgeTestHelper helper class accessed from python.")
75+
76+
77+
metadata_ = self._test_helper.metadata()
78+
self.assertIsNotNone(metadata_, "Java PythonBridgeHelper#metadata run but returned None.")
79+
logger.info("Python binding instantiate for java metadata :"+str(metadata_))
80+
responsibilities_ = metadata_.getContacts()
81+
self.assertIsNotNone(responsibilities_, "Failed to obtain from python metadata")
82+
logger.info("Contacts obtained from python metadata : "+str(responsibilities_))

geoapi-java-python/src/test/python/test_referencing.py renamed to geoapi-java-python/src/test/python/tests/test_referencing.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@
3333
# python -m unittest discover
3434
#
3535

36-
import jpyutil
37-
jpyutil.init_jvm()
38-
3936
import jpy
4037
import opengis.bridge.java.referencing
4138
import unittest

0 commit comments

Comments
 (0)