Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 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
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
Prev Previous commit
Next Next commit
update docs
  • Loading branch information
tlambert03 committed Aug 22, 2025
commit ed0add6b06a71dd729fe502c6c8ec16588e9e5e1
10 changes: 9 additions & 1 deletion src/scyjava/_stubs/_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
"""The scyjava-stubs executable."""
"""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

Expand Down
29 changes: 29 additions & 0 deletions src/scyjava/_stubs/_dynamic_import.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
"""Logic for using generated type stubs as runtime importable, with lazy JVM startup.

Most often, the functionality here will be used as follows:

```
from scyjava._stubs import setup_java_imports

__all__, __getattr__ = setup_java_imports(
__name__,
__file__,
endpoints=["org.scijava:parsington:3.1.0"],
base_prefix="org"
)
```

...and that little snippet is written into the generated stubs modules by the
`scyjava._stubs.generate_stubs` function.

See docstring of `setup_java_imports` for details on how it works.
"""

import ast
from logging import warning
from pathlib import Path
Expand All @@ -21,11 +42,14 @@ def setup_java_imports(
:param module_file: The path to the module file (usually `__file__` in the calling
module).
:param endpoints: A list of Java endpoints to add to the scyjava configuration.
(Note that `scyjava._stubs.generate_stubs` will automatically add the necessary
endpoints for the generated stubs.)
:param base_prefix: The base prefix for the Java package name. This is used when
determining the Java class path for the requested class. The java class path
will be truncated to only the part including the base_prefix and after. This
makes it possible to embed a module in a subpackage (like `scyjava.types`) and
still have the correct Java class path.

:return: A 2-tuple containing:
- A list of all classes in the module (as defined in the stub file), to be
assigned to `__all__`.
Expand Down Expand Up @@ -57,6 +81,7 @@ def setup_java_imports(
if ep not in scyjava.config.endpoints:
scyjava.config.endpoints.append(ep)

# list intended to be assigned to `__all__` in the generated module.
module_all = []
try:
my_stub = Path(module_file).with_suffix(".pyi")
Expand All @@ -75,6 +100,7 @@ def setup_java_imports(
)

def module_getattr(name: str, mod_name: str = module_name) -> Any:
"""Function intended to be assigned to __getattr__ in the generate module."""
if module_all and name not in module_all:
raise AttributeError(f"module {module_name!r} has no attribute {name!r}")

Expand All @@ -84,6 +110,9 @@ def module_getattr(name: str, mod_name: str = module_name) -> Any:

class_path = f"{mod_name}.{name}"

# Generate a proxy type (with a nice repr) that
# delays the call to `jimport` until the last moment when type.__new__ is called

class ProxyMeta(type):
def __repr__(self) -> str:
return f"<scyjava class {class_path!r}>"
Expand Down
27 changes: 21 additions & 6 deletions src/scyjava/_stubs/_genstubs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
"""Type stub generation utilities using stubgen.

This module provides utilities for generating type stubs for Java classes
using the stubgenj library. `stubgenj` must be installed for this to work
(it, in turn, only depends on JPype).

See `generate_stubs` for most functionality. For the command-line tool,
see `scyjava._stubs.cli`, which provides a CLI interface for the `generate_stubs`
function.
"""

from __future__ import annotations

import ast
Expand Down Expand Up @@ -42,9 +53,11 @@ def generate_stubs(
The prefixes to generate stubs for. This should be a list of Java class
prefixes that you expect to find in the endpoints. For example,
["org.apache.commons"]. If not provided, the prefixes will be
automatically determined from the jar files provided by endpoints.
automatically determined from the jar files provided by endpoints (see the
`_list_top_level_packages` helper function).
output_dir : str | Path, optional
The directory to write the generated stubs to. Defaults to "stubs".
The directory to write the generated stubs to. Defaults to "stubs" in the
current working directory.
convert_strings : bool, optional
Whether to cast Java strings to Python strings in the stubs. Defaults to True.
NOTE: This leads to type stubs that may not be strictly accurate at runtime.
Expand All @@ -58,8 +71,10 @@ def generate_stubs(
Whether to include Javadoc in the generated stubs. Defaults to True.
add_runtime_imports : bool, optional
Whether to add runtime imports to the generated stubs. Defaults to True.
This is useful if you want to use the stubs as a runtime package with type
safety.
This is useful if you want to actually import the stubs as a runtime package
with type safety. The runtime import "magic" depends on the
`scyjava._stubs.setup_java_imports` function. See its documentation for
more details.
remove_namespace_only_stubs : bool, optional
Whether to remove stubs that export no names beyond a single
`__module_protocol__`. This leaves some folders as PEP420 implicit namespace
Expand Down Expand Up @@ -95,7 +110,7 @@ def _patched_start(*args: Any, **kwargs: Any) -> None:
ep_artifacts = tuple(ep.split(":")[1] for ep in endpoints)
for j in cp.split(os.pathsep):
if Path(j).name.startswith(ep_artifacts):
_prefixes.update(list_top_level_packages(j))
_prefixes.update(_list_top_level_packages(j))

prefixes = sorted(_prefixes)
logger.info(f"Using endpoints: {scyjava.config.endpoints!r}")
Expand Down Expand Up @@ -189,7 +204,7 @@ def ruff_check(output: Path, select: str = "E,W,F,I,UP,C4,B,RUF,TC,TID") -> None
subprocess.run(["ruff", "format", *py_files, "--quiet"])


def list_top_level_packages(jar_path: str) -> set[str]:
def _list_top_level_packages(jar_path: str) -> set[str]:
"""Inspect a JAR file and return the set of top-level Java package names."""
packages: set[str] = set()
with ZipFile(jar_path, "r") as jar:
Expand Down
Loading