Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0f70b03
ignore types folder
tlambert03 Apr 23, 2025
459a999
Add scyjava-stubs CLI and dynamic import functionality
tlambert03 Apr 23, 2025
a8b2da7
remove import
tlambert03 Apr 23, 2025
686739b
add test
tlambert03 Apr 23, 2025
7fe31af
add comment to clarify stubgen command execution in test
tlambert03 Apr 23, 2025
afcc7a7
refactor: clean up jpype imports in stubgen test and main module
tlambert03 Apr 23, 2025
a5cacc8
remove unused jpype import from _genstubs.py
tlambert03 Apr 23, 2025
ab1bc2d
fix: add future annotations import to _cli.py
tlambert03 Apr 23, 2025
11649fa
refactor: enhance dynamic_import function to accept base_prefix and i…
tlambert03 Apr 23, 2025
2cb4836
refactor: rename dynamic_import to setup_java_imports and update usag…
tlambert03 Apr 23, 2025
71f761e
reword
tlambert03 Apr 23, 2025
65cc471
feat: add Hatchling build hook for generating Java stubs
tlambert03 Apr 25, 2025
6e4181e
wip
tlambert03 Apr 25, 2025
6e92b13
fix inclusion
tlambert03 Apr 25, 2025
0d231cc
add docs
tlambert03 Apr 25, 2025
79524b0
setuptools plugin stub
tlambert03 Apr 25, 2025
9bdba53
Merge branch 'main' into stubs
tlambert03 Apr 30, 2025
51937b5
remove repr test
tlambert03 May 1, 2025
192be35
skip in jep
tlambert03 May 1, 2025
e714abb
remove setuptools plugin
tlambert03 May 1, 2025
e7bc894
Merge branch 'main' into stubs
tlambert03 Aug 20, 2025
a18d9d9
remove hatch plugin
tlambert03 Aug 22, 2025
b53e795
newlines
tlambert03 Aug 22, 2025
ed0add6
update docs
tlambert03 Aug 22, 2025
43cb06d
initial
tlambert03 Aug 22, 2025
b181bd7
working principle
tlambert03 Aug 23, 2025
76d9bfa
remove readme
tlambert03 Aug 23, 2025
c579490
remove x
tlambert03 Aug 23, 2025
9e33f2e
refactor: delay import of pandas until needed
tlambert03 Aug 23, 2025
6f89935
remove reveal type
tlambert03 Aug 23, 2025
a519d35
Merge branch 'delay-import' into stubs-metafinder
tlambert03 Aug 23, 2025
93e4e51
cleaner
tlambert03 Aug 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "scyjava"
version = "1.12.2.dev0"
description = "Supercharged Java access from Python"
license = "Unlicense"
authors = [{name = "SciJava developers", email = "[email protected]"}]
authors = [{ name = "SciJava developers", email = "[email protected]" }]
readme = "README.md"
keywords = ["java", "maven", "cross-language"]
classifiers = [
Expand Down Expand Up @@ -35,6 +35,7 @@ dependencies = [
"jpype1 >= 1.3.0",
"jgo",
"cjdk",
"stubgenj",
]

[dependency-groups]
Expand All @@ -50,6 +51,9 @@ dev = [
"validate-pyproject[all]",
]

[project.scripts]
scyjava-stubgen = "scyjava._stubs._cli:main"

[project.urls]
homepage = "https://github.com/scijava/scyjava"
documentation = "https://github.com/scijava/scyjava/blob/main/README.md"
Expand All @@ -58,7 +62,7 @@ download = "https://pypi.org/project/scyjava/"
tracker = "https://github.com/scijava/scyjava/issues"

[tool.setuptools]
package-dir = {"" = "src"}
package-dir = { "" = "src" }
include-package-data = false

[tool.setuptools.packages.find]
Expand Down
23 changes: 11 additions & 12 deletions src/scyjava/_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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


######################################
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/scyjava/_jvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def is_awt_initialized() -> bool:
return False
Thread = scyjava.jimport("java.lang.Thread")
threads = Thread.getAllStackTraces().keySet()
return any(t.getName().startsWith("AWT-") for t in threads)
return any(str(t.getName()).startswith("AWT-") for t in threads)


def when_jvm_starts(f) -> None:
Expand Down
4 changes: 4 additions & 0 deletions src/scyjava/_stubs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._dynamic_import import setup_java_imports
from ._genstubs import generate_stubs

__all__ = ["generate_stubs", "setup_java_imports"]
162 changes: 162 additions & 0 deletions src/scyjava/_stubs/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""The scyjava-stubs executable.

Provides cli access to the `scyjava._stubs.generate_stubs` function.

The only interesting additional things going on here is the choice of *where* the stubs
go by default. When using the CLI, they land in `scyjava.types` by default; see the
`_get_ouput_dir` helper function for details on how the output directory is resolved
from the CLI arguments.
"""

from __future__ import annotations

import argparse
import importlib
import importlib.util
import logging
import sys
from pathlib import Path

from ._genstubs import generate_stubs


def main() -> None:
"""The main entry point for the scyjava-stubs executable."""
logging.basicConfig(level="INFO")
parser = argparse.ArgumentParser(
description="Generate Python Type Stubs for Java classes."
)
parser.add_argument(
"endpoints",
type=str,
nargs="+",
help="Maven endpoints to install and use (e.g. org.myproject:myproject:1.0.0)",
)
parser.add_argument(
"--prefix",
type=str,
help="package prefixes to generate stubs for (e.g. org.myproject), "
"may be used multiple times. If not specified, prefixes are gleaned from the "
"downloaded artifacts.",
action="append",
default=[],
metavar="PREFIX",
dest="prefix",
)
path_group = parser.add_mutually_exclusive_group()
path_group.add_argument(
"--output-dir",
type=str,
default=None,
help="Filesystem path to write stubs to.",
)
path_group.add_argument(
"--output-python-path",
type=str,
default=None,
help="Python path to write stubs to (e.g. 'scyjava.types').",
)
parser.add_argument(
"--convert-strings",
dest="convert_strings",
action="store_true",
default=False,
help="convert java.lang.String to python str in return types. "
"consult the JPype documentation on the convertStrings flag for details",
)
parser.add_argument(
"--no-javadoc",
dest="with_javadoc",
action="store_false",
default=True,
help="do not generate docstrings from JavaDoc where available",
)

rt_group = parser.add_mutually_exclusive_group()
rt_group.add_argument(
"--runtime-imports",
dest="runtime_imports",
action="store_true",
default=True,
help="Add runtime imports to the generated stubs. ",
)
rt_group.add_argument(
"--no-runtime-imports", dest="runtime_imports", action="store_false"
)

parser.add_argument(
"--remove-namespace-only-stubs",
dest="remove_namespace_only_stubs",
action="store_true",
default=False,
help="Remove stubs that export no names beyond a single __module_protocol__. "
"This leaves some folders as PEP420 implicit namespace folders.",
)

if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)

args = parser.parse_args()
output_dir = _get_ouput_dir(args.output_dir, args.output_python_path)
if not output_dir.exists():
output_dir.mkdir(parents=True, exist_ok=True)

generate_stubs(
endpoints=args.endpoints,
prefixes=args.prefix,
output_dir=output_dir,
convert_strings=args.convert_strings,
include_javadoc=args.with_javadoc,
add_runtime_imports=args.runtime_imports,
remove_namespace_only_stubs=args.remove_namespace_only_stubs,
)


def _get_ouput_dir(output_dir: str | None, python_path: str | None) -> Path:
if out_dir := output_dir:
return Path(out_dir)
if pp := python_path:
return _glean_path(pp)
try:
import scyjava

return Path(scyjava.__file__).parent / "types"
except ImportError:
return Path("stubs")


def _glean_path(pp: str) -> Path:
try:
importlib.import_module(pp.split(".")[0])
except ModuleNotFoundError:
# the top level module doesn't exist:
raise ValueError(f"Module {pp} does not exist. Cannot install stubs there.")

try:
spec = importlib.util.find_spec(pp)
except ModuleNotFoundError as e:
# at least one of the middle levels doesn't exist:
raise NotImplementedError(f"Cannot install stubs to {pp}: {e}")

new_ns = None
if not spec:
# if we get here, it means everything but the last level exists:
parent, new_ns = pp.rsplit(".", 1)
spec = importlib.util.find_spec(parent)

if not spec:
# if we get here, it means the last level doesn't exist:
raise ValueError(f"Module {pp} does not exist. Cannot install stubs there.")

search_locations = spec.submodule_search_locations
if not spec.loader and search_locations:
# namespace package with submodules
return Path(search_locations[0])
if spec.origin:
return Path(spec.origin).parent
if new_ns and search_locations:
# namespace package with submodules
return Path(search_locations[0]) / new_ns

raise ValueError(f"Error finding module {pp}. Cannot install stubs there.")
Loading
Loading