-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy path_dynamic_import.py
More file actions
131 lines (106 loc) · 4.75 KB
/
Copy path_dynamic_import.py
File metadata and controls
131 lines (106 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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
from typing import Any, Callable, Sequence
def setup_java_imports(
module_name: str,
module_file: str,
endpoints: Sequence[str] = (),
base_prefix: str = "",
) -> tuple[list[str], Callable[[str], Any]]:
"""Setup a module to dynamically import Java class names.
This function creates a `__getattr__` function that, when called, will dynamically
import the requested class from the Java namespace corresponding to the calling
module.
:param module_name: The dotted name/identifier of the module that is calling this
function (usually `__name__` in the calling module).
: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__`.
- A callable that takes a class name and returns a proxy for the Java class.
This callable should be assigned to `__getattr__` in the calling module.
The proxy object, when called, will start the JVM, import the Java class,
and return an instance of the class. The JVM will *only* be started when
the object is called.
Example:
If the module calling this function is named `scyjava.types.org.scijava.parsington`,
then it should invoke this function as:
.. code-block:: python
from scyjava._stubs import setup_java_imports
__all__, __getattr__ = setup_java_imports(
__name__,
__file__,
endpoints=["org.scijava:parsington:3.1.0"],
base_prefix="org"
)
"""
import scyjava
import scyjava.config
for ep in endpoints:
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")
stub_ast = ast.parse(my_stub.read_text())
module_all = sorted(
{
node.name
for node in stub_ast.body
if isinstance(node, ast.ClassDef) and not node.name.startswith("__")
}
)
except (OSError, SyntaxError):
warning(
f"Failed to read stub file {my_stub!r}. Falling back to empty __all__.",
stacklevel=3,
)
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}")
# cut the mod_name to only the part including the base_prefix and after
if base_prefix in mod_name:
mod_name = mod_name[mod_name.index(base_prefix) :]
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}>"
class Proxy(metaclass=ProxyMeta):
def __new__(_cls_, *args: Any, **kwargs: Any) -> Any:
cls = scyjava.jimport(class_path)
return cls(*args, **kwargs)
Proxy.__name__ = name
Proxy.__qualname__ = name
Proxy.__module__ = module_name
Proxy.__doc__ = f"Proxy for {class_path}"
return Proxy
return module_all, module_getattr