Skip to content

Commit 93922a1

Browse files
committed
Docs(feat[autodoc]): Implement sidecar introspection schema
why: Provide real runtime introspection data for API docs and docstring rendering. what: - add sidecar introspection helpers with signatures, members, and docstring HTML - expand introspection schema and wire annotation format options - update sidecar CLI and schema tests; add docutils dependency
1 parent e8a1379 commit 93922a1

6 files changed

Lines changed: 801 additions & 4 deletions

File tree

astro/packages/py-introspect/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type PyIntrospectPayload, PyIntrospectPayloadSchema } from '@libtmux/sc
66
export type IntrospectOptions = {
77
root?: string
88
includePrivate?: boolean
9+
annotationFormat?: 'string' | 'value'
910
pythonCommand?: PythonCommand
1011
env?: NodeJS.ProcessEnv
1112
cwd?: string
@@ -37,6 +38,10 @@ export const introspectModule = async (
3738
args.push('--include-private')
3839
}
3940

41+
if (options.annotationFormat) {
42+
args.push('--annotation-format', options.annotationFormat)
43+
}
44+
4045
const payload = await runPythonJson(args, {
4146
pythonCommand: options.pythonCommand,
4247
cwd: options.cwd ?? sidecarRoot,
@@ -62,6 +67,10 @@ export const introspectPackage = async (
6267
args.push('--include-private')
6368
}
6469

70+
if (options.annotationFormat) {
71+
args.push('--annotation-format', options.annotationFormat)
72+
}
73+
6574
const payload = await runPythonJson(args, {
6675
pythonCommand: options.pythonCommand,
6776
cwd: options.cwd ?? sidecarRoot,
Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,88 @@
11
import { z } from 'zod'
22
import { ProtocolVersionSchema } from './common.ts'
3+
import { PyParameterKindSchema } from './py-parse.ts'
34

4-
export const PyIntrospectModuleSchema = z.object({}).passthrough()
5+
const DocstringFormatSchema = z.enum(['rst', 'markdown', 'plain', 'unknown'])
6+
7+
const DocstringFieldsSchema = z.object({
8+
docstringRaw: z.string().nullable(),
9+
docstringFormat: DocstringFormatSchema,
10+
docstringHtml: z.string().nullable(),
11+
summary: z.string().nullable(),
12+
})
13+
14+
const AnnotationFieldsSchema = z.object({
15+
annotationText: z.string().nullable(),
16+
annotationValue: z.string().nullable(),
17+
})
18+
19+
export const PyIntrospectParameterSchema = z
20+
.object({
21+
name: z.string(),
22+
kind: PyParameterKindSchema,
23+
default: z.string().nullable(),
24+
})
25+
.merge(AnnotationFieldsSchema)
26+
27+
export const PyIntrospectFunctionSchema = z
28+
.object({
29+
kind: z.enum(['function', 'method']),
30+
name: z.string(),
31+
qualname: z.string(),
32+
module: z.string(),
33+
signature: z.string(),
34+
parameters: z.array(PyIntrospectParameterSchema),
35+
returns: AnnotationFieldsSchema,
36+
isAsync: z.boolean(),
37+
isPrivate: z.boolean(),
38+
})
39+
.merge(DocstringFieldsSchema)
40+
41+
export const PyIntrospectVariableSchema = z
42+
.object({
43+
kind: z.literal('variable'),
44+
name: z.string(),
45+
qualname: z.string(),
46+
module: z.string(),
47+
value: z.string().nullable(),
48+
isPrivate: z.boolean(),
49+
})
50+
.merge(AnnotationFieldsSchema)
51+
.merge(DocstringFieldsSchema)
52+
53+
export const PyIntrospectClassSchema = z
54+
.object({
55+
kind: z.literal('class'),
56+
name: z.string(),
57+
qualname: z.string(),
58+
module: z.string(),
59+
bases: z.array(z.string()),
60+
methods: z.array(PyIntrospectFunctionSchema),
61+
attributes: z.array(PyIntrospectVariableSchema),
62+
isPrivate: z.boolean(),
63+
})
64+
.merge(DocstringFieldsSchema)
65+
66+
export const PyIntrospectModuleSchema = z
67+
.object({
68+
kind: z.literal('module'),
69+
name: z.string(),
70+
qualname: z.string(),
71+
classes: z.array(PyIntrospectClassSchema),
72+
functions: z.array(PyIntrospectFunctionSchema),
73+
variables: z.array(PyIntrospectVariableSchema),
74+
isPrivate: z.boolean(),
75+
})
76+
.merge(DocstringFieldsSchema)
577

678
export const PyIntrospectPayloadSchema = z.object({
779
protocolVersion: ProtocolVersionSchema,
880
modules: z.array(PyIntrospectModuleSchema),
981
})
1082

83+
export type PyIntrospectParameter = z.infer<typeof PyIntrospectParameterSchema>
84+
export type PyIntrospectFunction = z.infer<typeof PyIntrospectFunctionSchema>
85+
export type PyIntrospectVariable = z.infer<typeof PyIntrospectVariableSchema>
86+
export type PyIntrospectClass = z.infer<typeof PyIntrospectClassSchema>
1187
export type PyIntrospectModule = z.infer<typeof PyIntrospectModuleSchema>
1288
export type PyIntrospectPayload = z.infer<typeof PyIntrospectPayloadSchema>

astro/packages/schema/tests/py-introspect.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,27 @@ describe('PyIntrospectPayloadSchema', () => {
1010

1111
expect(() => PyIntrospectPayloadSchema.parse(payload)).not.toThrow()
1212
})
13+
14+
it('accepts a minimal module payload', () => {
15+
const payload = {
16+
protocolVersion: 1,
17+
modules: [
18+
{
19+
kind: 'module',
20+
name: 'demo',
21+
qualname: 'demo',
22+
isPrivate: false,
23+
classes: [],
24+
functions: [],
25+
variables: [],
26+
docstringRaw: null,
27+
docstringFormat: 'unknown',
28+
docstringHtml: null,
29+
summary: null,
30+
},
31+
],
32+
}
33+
34+
expect(() => PyIntrospectPayloadSchema.parse(payload)).not.toThrow()
35+
})
1336
})

astro/python/pyautodoc_sidecar/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ name = "pyautodoc-sidecar"
33
version = "0.0.1"
44
description = "Python sidecar for the libtmux autodoc pipeline."
55
requires-python = ">=3.10"
6-
dependencies = []
6+
dependencies = [
7+
"docutils",
8+
]
79

810
[build-system]
911
requires = ["hatchling"]

astro/python/pyautodoc_sidecar/src/pyautodoc_sidecar/cli.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pathlib
88
import typing as t
99

10+
from pyautodoc_sidecar.introspect import introspect_module, introspect_package
1011
from pyautodoc_sidecar.parse import scan_paths
1112

1213
PROTOCOL_VERSION = 1
@@ -60,13 +61,23 @@ def main(argv: list[str] | None = None) -> int:
6061
intro_module.add_argument("--module", required=True)
6162
intro_module.add_argument("--root")
6263
intro_module.add_argument("--include-private", action="store_true")
64+
intro_module.add_argument(
65+
"--annotation-format",
66+
choices=["string", "value"],
67+
default="string",
68+
)
6369

6470
intro_package = subparsers.add_parser(
6571
"introspect-package", help="Introspect a package."
6672
)
6773
intro_package.add_argument("--package", required=True)
6874
intro_package.add_argument("--root")
6975
intro_package.add_argument("--include-private", action="store_true")
76+
intro_package.add_argument(
77+
"--annotation-format",
78+
choices=["string", "value"],
79+
default="string",
80+
)
7081

7182
args = parser.parse_args(argv)
7283
payload: dict[str, t.Any]
@@ -76,8 +87,26 @@ def main(argv: list[str] | None = None) -> int:
7687
paths = [pathlib.Path(item) for item in args.paths]
7788
modules = scan_paths(root, paths, args.include_private)
7889
payload = {"protocolVersion": PROTOCOL_VERSION, "modules": modules}
79-
elif args.command in {"introspect-module", "introspect-package"}:
80-
payload = {"protocolVersion": PROTOCOL_VERSION, "modules": []}
90+
elif args.command == "introspect-module":
91+
root = pathlib.Path(args.root) if args.root else None
92+
modules = [
93+
introspect_module(
94+
args.module,
95+
root=root,
96+
include_private=args.include_private,
97+
annotation_format=args.annotation_format,
98+
)
99+
]
100+
payload = {"protocolVersion": PROTOCOL_VERSION, "modules": modules}
101+
elif args.command == "introspect-package":
102+
root = pathlib.Path(args.root) if args.root else None
103+
modules = introspect_package(
104+
args.package,
105+
root=root,
106+
include_private=args.include_private,
107+
annotation_format=args.annotation_format,
108+
)
109+
payload = {"protocolVersion": PROTOCOL_VERSION, "modules": modules}
81110
else:
82111
raise ValueError(f"Unsupported command: {args.command}")
83112

0 commit comments

Comments
 (0)