Skip to content

Commit 93e4e51

Browse files
committed
cleaner
1 parent a519d35 commit 93e4e51

File tree

5 files changed

+87
-125
lines changed

5 files changed

+87
-125
lines changed

src/scyjava/_stubs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from ._dynamic_import import setup_java_imports
22
from ._genstubs import generate_stubs
33

4-
__all__ = ["setup_java_imports", "generate_stubs"]
4+
__all__ = ["generate_stubs", "setup_java_imports"]

src/scyjava/_stubs/_genstubs.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ def generate_stubs(
6565
called with the `convertStrings` argument set to True or False. By setting
6666
this `convert_strings` argument to true, the type stubs will be generated as if
6767
`convertStrings` is set to True: that is, all string types will be listed as
68-
`str` rather than `java.lang.String | str`. This is a safer default (as `str`)
69-
is a subtype of `java.lang.String`), but may lead to type errors in some cases.
68+
`str` rather than `java.lang.String | str`. This is a safer default (as `str`
69+
is a base of `java.lang.String`), but may lead to type errors in some cases.
7070
include_javadoc : bool, optional
7171
Whether to include Javadoc in the generated stubs. Defaults to True.
7272
add_runtime_imports : bool, optional
@@ -90,9 +90,13 @@ def generate_stubs(
9090
"stubgenj is not installed, but is required to generate java stubs. "
9191
"Please install it with `pip/conda install stubgenj`."
9292
) from e
93-
print("GENERATE")
9493
import jpype
9594

95+
# if jpype.isJVMStarted():
96+
# raise RuntimeError(
97+
# "Generating type stubs after the JVM has started is not supported."
98+
# )
99+
96100
startJVM = jpype.startJVM
97101

98102
scyjava.config.endpoints.extend(endpoints)
@@ -117,23 +121,24 @@ def _patched_start(*args: Any, **kwargs: Any) -> None:
117121
logger.info(f"Generating stubs for: {prefixes}")
118122
logger.info(f"Writing stubs to: {output_dir}")
119123

120-
metapath = sys.meta_path
124+
metapath = sys.meta_path.copy()
121125
try:
122126
import jpype.imports
123127

124128
jmodules = [import_module(prefix) for prefix in prefixes]
129+
130+
stubgenj.generateJavaStubs(
131+
jmodules,
132+
useStubsSuffix=False,
133+
outputDir=str(output_dir),
134+
jpypeJPackageStubs=False,
135+
includeJavadoc=include_javadoc,
136+
)
137+
125138
finally:
126-
# remove the jpype.imports magic from the import system
127-
# if it wasn't there to begin with
128-
sys.meta_path = metapath
129-
130-
stubgenj.generateJavaStubs(
131-
jmodules,
132-
useStubsSuffix=False,
133-
outputDir=str(output_dir),
134-
jpypeJPackageStubs=False,
135-
includeJavadoc=include_javadoc,
136-
)
139+
# restore sys.metapath
140+
# (remove the jpype.imports magic if it wasn't there to begin with)
141+
sys.meta_path[:] = metapath
137142

138143
output_dir = Path(output_dir)
139144
if add_runtime_imports:

src/scyjava/py.typed

Whitespace-only changes.

src/scyjava/types/__init__.py

Lines changed: 60 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -9,136 +9,96 @@
99

1010
from __future__ import annotations
1111

12-
import importlib.util
12+
import os
1313
import sys
1414
import threading
15-
import types
16-
from ast import mod
17-
from importlib.abc import Loader, MetaPathFinder
18-
from importlib.machinery import SourceFileLoader
1915
from pathlib import Path
2016
from typing import TYPE_CHECKING
2117

22-
import scyjava
23-
from scyjava._stubs import generate_stubs
24-
2518
if TYPE_CHECKING:
19+
import types
2620
from collections.abc import Sequence
2721
from importlib.machinery import ModuleSpec
2822

2923

24+
# where generated stubs should land (defaults to this dir: `scyjava.types`)
25+
STUBS_DIR = os.getenv("SCYJAVA_STUBS_DIR", str(Path(__file__).parent))
26+
# namespace under which generated stubs will be placed
27+
STUBS_NAMESPACE = __name__
28+
# module lock to prevent concurrent stub generation
3029
_STUBS_LOCK = threading.Lock()
31-
TYPES_DIR = Path(__file__).parent
3230

3331

34-
class ScyJavaTypesMetaFinder(MetaPathFinder):
35-
"""Meta path finder for scyjava.types that delegates to our loader."""
32+
class ScyJavaTypesMetaFinder:
33+
"""Meta path finder for scyjava.types that generates stubs on demand."""
3634

3735
def find_spec(
3836
self,
3937
fullname: str,
4038
path: Sequence[str] | None,
4139
target: types.ModuleType | None = None,
42-
/,
4340
) -> ModuleSpec | None:
4441
"""Return a spec for names under scyjava.types (except the base)."""
45-
base_package = __name__
46-
47-
if not fullname.startswith(base_package + ".") or fullname == base_package:
48-
return None
49-
50-
return importlib.util.spec_from_loader(
51-
fullname,
52-
ScyJavaTypesLoader(fullname),
53-
origin="dynamic",
54-
)
55-
56-
57-
class ScyJavaTypesLoader(Loader):
58-
"""Loader that lazily generates stubs and loads/synthesizes modules."""
59-
60-
def __init__(self, fullname: str) -> None:
61-
self.fullname = fullname
62-
63-
def create_module(self, spec: ModuleSpec) -> types.ModuleType | None:
64-
"""Load an existing module/package or lazily generate stubs then load."""
65-
pkg_dir, pkg_init, mod_file = _paths_for(spec.name, TYPES_DIR)
66-
67-
def _load_module() -> types.ModuleType | None:
68-
# Fast paths: concrete module file or package present
69-
if pkg_init.exists() or mod_file.exists():
70-
return _load_generated_module(spec.name, TYPES_DIR)
71-
if pkg_dir.is_dir():
72-
return _namespace_package(spec, pkg_dir)
73-
return None
74-
75-
if module := _load_module():
76-
return module
77-
78-
# Nothing exists for this name: generate once under a lock
79-
with _STUBS_LOCK:
80-
# Re-check under the lock to avoid duplicate work
81-
if not (pkg_init.exists() or mod_file.exists() or pkg_dir.exists()):
82-
endpoints = ["org.scijava:parsington:3.1.0"] # TODO
83-
generate_stubs(endpoints, output_dir=TYPES_DIR)
84-
85-
# Retry after generation (or if another thread created it)
86-
if module := _load_module():
87-
return module
42+
# if this is an import from this module ('scyjava.types.<name>')
43+
# check if the module exists, and if not, call generation routines
44+
if fullname.startswith(f"{__name__}."):
45+
with _STUBS_LOCK:
46+
# check if the spec already exists
47+
# under the module lock to avoid duplicate work
48+
if not _find_spec(fullname, path, target, skip=self):
49+
_generate_stubs()
8850

89-
raise ImportError(f"Generated module not found: {spec.name} under {pkg_dir}")
51+
return None
9052

91-
def exec_module(self, module: types.ModuleType) -> None:
92-
pass
9353

54+
def _generate_stubs() -> None:
55+
"""Install stubs for all endpoints detected in `scyjava.config`.
9456
95-
def _paths_for(fullname: str, out_dir: Path) -> tuple[Path, Path, Path]:
96-
"""Return (pkg_dir, pkg_init, mod_file) for a scyjava.types.* fullname."""
97-
rel = fullname.split("scyjava.types.", 1)[1]
98-
pkg_dir = out_dir / rel.replace(".", "/")
99-
pkg_init = pkg_dir / "__init__.py"
100-
mod_file = out_dir / (rel.replace(".", "/") + ".py")
101-
return pkg_dir, pkg_init, mod_file
102-
103-
104-
def _namespace_package(spec: ModuleSpec, pkg_dir: Path) -> types.ModuleType:
105-
"""Create a simple package module pointing at pkg_dir.
106-
107-
This fills the role of a namespace package, (a folder with no __init__.py).
57+
This could be expanded to include additional endpoints detected in, for example,
58+
python entry-points discovered in packages in the environment.
10859
"""
109-
module = types.ModuleType(spec.name)
110-
module.__package__ = spec.name
111-
module.__path__ = [str(pkg_dir)]
112-
module.__spec__ = spec
113-
return module
114-
115-
116-
def _load_generated_module(fullname: str, out_dir: Path) -> types.ModuleType:
117-
"""Load a just-generated module/package from disk and return it."""
118-
_, pkg_init, mod_file = _paths_for(fullname, out_dir)
119-
path = pkg_init if pkg_init.exists() else mod_file
120-
if not path.exists():
121-
raise ImportError(f"Generated module not found: {fullname} at {path}")
122-
123-
loader = SourceFileLoader(fullname, str(path))
124-
spec = importlib.util.spec_from_loader(fullname, loader, origin=str(path))
125-
if spec is None or spec.loader is None:
126-
raise ImportError(f"Failed to build spec for: {fullname}")
60+
from scyjava import config
61+
from scyjava._stubs import generate_stubs
62+
63+
generate_stubs(
64+
config.endpoints,
65+
output_dir=STUBS_DIR,
66+
add_runtime_imports=True,
67+
remove_namespace_only_stubs=True,
68+
)
69+
70+
71+
def _find_spec(
72+
fullname: str,
73+
path: Sequence[str] | None,
74+
target: types.ModuleType | None = None,
75+
skip: object | None = None,
76+
) -> ModuleSpec | None:
77+
"""Find a module spec, skipping finder `skip` to avoid recursion."""
78+
# if the module is already loaded and has a spec, return it
79+
if module := sys.modules.get(fullname):
80+
try:
81+
if module.__spec__ is not None:
82+
return module.__spec__
83+
except AttributeError:
84+
pass
12785

128-
spec.has_location = True # populate __file__
129-
sys.modules[fullname] = module = importlib.util.module_from_spec(spec)
130-
spec.loader.exec_module(module)
131-
return module
132-
133-
134-
# -----------------------------------------------------------
86+
for finder in sys.meta_path:
87+
if finder is not skip:
88+
try:
89+
spec = finder.find_spec(fullname, path, target)
90+
except AttributeError:
91+
continue
92+
else:
93+
if spec is not None:
94+
return spec
95+
return None
13596

13697

13798
def _install_meta_finder() -> None:
138-
for finder in sys.meta_path:
139-
if isinstance(finder, ScyJavaTypesMetaFinder):
140-
return
141-
99+
"""Install the ScyJavaTypesMetaFinder into sys.meta_path if not already there."""
100+
if any(isinstance(finder, ScyJavaTypesMetaFinder) for finder in sys.meta_path):
101+
return
142102
sys.meta_path.insert(0, ScyJavaTypesMetaFinder())
143103

144104

tests/test_stubgen.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
from pathlib import Path
1515

1616

17-
@pytest.mark.skipif(
18-
scyjava.config.mode != scyjava.config.Mode.JPYPE,
19-
reason="Stubgen not supported in JEP",
20-
)
17+
JEP_MODE = scyjava.config.mode != scyjava.config.Mode.JPYPE
18+
skip_if_jep = pytest.mark.skipif(JEP_MODE, reason="Stubgen not supported in JEP")
19+
20+
21+
@skip_if_jep
2122
def test_stubgen(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
2223
# run the stubgen command as if it was run from the command line
2324
monkeypatch.setattr(
@@ -32,19 +33,15 @@ def test_stubgen(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
3233
)
3334
_cli.main()
3435

35-
# remove the `jpype.imports` magic from the import system if present
36-
mp = [x for x in sys.meta_path if not isinstance(x, jpype.imports._JImportLoader)]
37-
monkeypatch.setattr(sys, "meta_path", mp)
38-
3936
# add tmp_path to the import path
4037
monkeypatch.setattr(sys, "path", [str(tmp_path)])
4138

4239
# first cleanup to make sure we are not importing from the cache
4340
sys.modules.pop("org", None)
4441
sys.modules.pop("org.scijava", None)
4542
sys.modules.pop("org.scijava.parsington", None)
46-
# make sure the stubgen command works and that we can now impmort stuff
4743

44+
# make sure the stubgen command works and that we can now import stuff
4845
with patch.object(scyjava._jvm, "start_jvm") as mock_start_jvm:
4946
from org.scijava.parsington import Function
5047

0 commit comments

Comments
 (0)