Skip to content

Commit 4e7978e

Browse files
andreaTPclaude
andcommitted
Fix review issues: package-private internals, update stale docs
Make UvRunner and PythonProbe package-private — they are internal implementation details used by PythonEnv, not part of the public API. Update api-contracts.md to match the actual implemented API (remove unimplemented load/importModule/PythonSource, show real PythonEngine, PythonEnv, HostFunction, and @ScriptInterface usage). Update implementation-plan.md to mark Phases 1-3 as done. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 467ae6a commit 4e7978e

4 files changed

Lines changed: 68 additions & 59 deletions

File tree

core/src/main/java/io/roastedroot/cpython4j/core/PythonProbe.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import java.nio.file.Path;
88
import java.util.concurrent.TimeUnit;
99

10-
public final class PythonProbe {
10+
final class PythonProbe {
1111
private static final String PROBE_SCRIPT =
1212
"import sys, sysconfig, json; print(json.dumps({"
1313
+ "'version': list(sys.version_info[:3]),"
@@ -21,7 +21,7 @@ public final class PythonProbe {
2121

2222
private PythonProbe() {}
2323

24-
public static Result probe(Path venvDir) {
24+
static Result probe(Path venvDir) {
2525
var pythonBin = resolvePythonBin(venvDir);
2626
if (!Files.isExecutable(pythonBin)) {
2727
throw new RuntimeException(
@@ -99,5 +99,5 @@ private static Result parseResult(String json) {
9999
}
100100
}
101101

102-
public record Result(Path libPythonPath, Path purelib, Path platlib, String version) {}
102+
record Result(Path libPythonPath, Path purelib, Path platlib, String version) {}
103103
}

core/src/main/java/io/roastedroot/cpython4j/core/UvRunner.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import java.util.List;
77
import java.util.concurrent.TimeUnit;
88

9-
public final class UvRunner {
9+
final class UvRunner {
1010
private static final long TIMEOUT_SECONDS = 300;
1111

1212
private UvRunner() {}
1313

14-
public static void sync(Path projectDir) {
14+
static void sync(Path projectDir) {
1515
run(projectDir, "sync", "--python-preference", "managed");
1616
}
1717

docs/api-contracts.md

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,43 @@
11
# Contract-first API Design
22

3-
## Primary Java API
3+
## PythonEngine API
44

55
```java
6-
public interface PythonEngine extends AutoCloseable {
7-
static PythonEngine create(PythonEnv env) { ... }
6+
public final class PythonEngine implements AutoCloseable {
7+
public static PythonEngine create(PythonEnv env);
8+
public static Builder builder();
89

9-
<T> T load(Class<T> guestContract, PythonSource source);
10+
public int exec(String code);
11+
public <T> T invokeFunction(String moduleName, String functionName, List<Object> args, Class<T> returnType);
1012

11-
<T> T importModule(Class<T> guestContract, String moduleName);
12-
13-
<T> void expose(String moduleName, Class<T> hostContract, T implementation);
13+
public void close();
1414
}
1515
```
1616

17-
## PythonSource
17+
## PythonEngine.Builder
1818

1919
```java
20-
public sealed interface PythonSource {
21-
static PythonSource file(Path path) { ... }
22-
static PythonSource code(String code) { ... }
23-
static PythonSource module(String moduleName) { ... }
24-
}
20+
PythonEngine.Builder builder = PythonEngine.builder()
21+
.withEnv(env)
22+
.withObjectMapper(mapper)
23+
.expose("host", hostFunction1, hostFunction2)
24+
.build();
25+
```
26+
27+
## PythonEnv
28+
29+
```java
30+
// Option A: uv project (syncs on first run)
31+
PythonEnv env = PythonEnv.uvProject(Path.of(".")).sync(true).build();
32+
33+
// Option B: existing venv
34+
PythonEnv env = PythonEnv.venv(Path.of(".venv")).build();
35+
36+
// Option C: explicit paths (container / air-gapped)
37+
PythonEnv env = PythonEnv.explicit()
38+
.library(Path.of("/usr/lib/libpython3.13.so"))
39+
.sitePackages(Path.of("/app/.venv/lib/python3.13/site-packages"))
40+
.build();
2541
```
2642

2743
## Guest contract example
@@ -32,12 +48,16 @@ Java defines the contract:
3248
public record Document(String text, String source) {}
3349
public record Analysis(String language, int wordCount) {}
3450

51+
@ScriptInterface(module = "analyzer")
3552
public interface Analyzer {
3653
Analysis analyze(Document document);
3754
List<String> keywords(String text);
55+
int wordCount(String text);
3856
}
3957
```
4058

59+
The annotation processor generates `Analyzer_Proxy` at compile time (no reflection).
60+
4161
Python implements it:
4262

4363
```python
@@ -49,34 +69,39 @@ def analyze(document):
4969

5070
def keywords(text):
5171
return text.lower().split()[:5]
72+
73+
def wordCount(text):
74+
return len(text.split())
5275
```
5376

5477
Java loads it:
5578

5679
```java
57-
Analyzer analyzer = engine.load(
58-
Analyzer.class,
59-
PythonSource.file(Path.of("analyzer.py"))
60-
);
80+
var env = PythonEnv.uvProject(Path.of(".")).sync(true).build();
81+
try (var engine = PythonEngine.create(env)) {
82+
engine.exec("import sys; sys.path.insert(0, '.')");
83+
Analyzer analyzer = new Analyzer_Proxy(engine);
84+
Analysis result = analyzer.analyze(new Document("Hello", "demo"));
85+
}
6186
```
6287

6388
## Host contract example
6489

65-
Java defines a host API:
90+
Java exposes functions to Python:
6691

6792
```java
68-
public interface HostApi {
69-
void log(String message);
70-
User findUser(String id);
93+
try (var engine = PythonEngine.builder()
94+
.withEnv(env)
95+
.expose("host",
96+
new HostFunction("log", List.of(String.class), Void.class,
97+
args -> { System.out.println(args.get(0)); return null; }),
98+
new HostFunction("findUser", List.of(String.class), User.class,
99+
args -> userService.find((String) args.get(0))))
100+
.build()) {
101+
// ...
71102
}
72103
```
73104

74-
Java exposes it:
75-
76-
```java
77-
engine.expose("host", HostApi.class, hostImpl);
78-
```
79-
80105
Python imports it as a normal module:
81106

82107
```python
@@ -88,29 +113,13 @@ def run(id):
88113
return user
89114
```
90115

91-
## Bundled mode example
92-
93-
```java
94-
try (var engine = PythonEngine.create(PythonEnv.bundled().sync(true))) {
95-
Docling docling = engine.load(
96-
Docling.class,
97-
PythonSource.module("docling_bridge")
98-
);
99-
Document doc = docling.convert(Path.of("report.pdf"));
100-
}
101-
```
102-
103-
The `bundled()` mode extracts `pyproject.toml` and `uv.lock` from JAR resources, bootstraps CPython and dependencies on first run via uv, and caches the environment locally for subsequent runs.
104-
105116
## Low-level escape hatch
106117

107-
The low-level API is optional and secondary:
118+
The low-level API is available for tests, diagnostics, and simple scripts:
108119

109120
```java
110-
public interface PythonSession {
111-
void exec(String code);
112-
Object eval(String expression);
121+
try (var engine = PythonEngine.create(env)) {
122+
engine.exec("print('hello')");
123+
int count = engine.invokeFunction("my_module", "count", List.of("hello world"), int.class);
113124
}
114125
```
115-
116-
This is useful for tests, diagnostics, and simple scripts, but documentation should lead with Java contracts.

docs/implementation-plan.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ Many patterns transfer directly from QuickJs4J: the contract-first API (Builtins
88

99
Core functionality (Phases 1–6): ~2–3 weeks.
1010

11-
| Phase | Estimate | QuickJs4J reuse |
11+
| Phase | Status | QuickJs4J reuse |
1212
|---|---|---|
1313
| 0. Docs & design | done ||
14-
| 1. FFM smoke test + callbacks | 2–3 days | (new: FFM, upcalls, PyMethodDef/PyModuleDef via MemorySegment) |
15-
| 2. Env resolver | 2–3 days | (new: uv/venv/libpython discovery) |
16-
| 3. Contract loader with callbacks | 2–3 days | high (proxy, JSON conversion, ObjectMapper, dispatcher) |
17-
| 4. JSON hardening | 1–2 days | high (same Jackson patterns, mostly edge-case testing) |
18-
| 5. Battery-included | 2–3 days | — (new: uv bootstrap, cache) |
14+
| 1. FFM smoke test + callbacks | done ||
15+
| 2. Env resolver | done ||
16+
| 3. Contract loader with callbacks | done | high (AP, JSON conversion, dispatcher) |
17+
| 4. JSON hardening | next | high (same Jackson patterns, edge-case testing) |
18+
| 5. Battery-included | pending | — (new: uv bootstrap, cache) |
1919
| 6. Build plugins | optional | medium (Maven plugin structure) |
20-
| 7. Platform hardening | 3–5 days | low (platform-specific libpython, CI matrix) |
20+
| 7. Platform hardening | pending | low (platform-specific libpython, CI matrix) |
2121
| 8. Optional layers | later | high (annotations, processors) |
2222

2323
## Phase 0: Documentation and design lock

0 commit comments

Comments
 (0)