test(tools): pin underscore-leading tool names to subscript access
This commit is contained in:
@@ -136,9 +136,13 @@ const MAX_CLASS_NAME_BASE = 120
|
|||||||
* nested parentheses`), so an array chain deeper than that would render an SDK
|
* nested parentheses`), so an array chain deeper than that would render an SDK
|
||||||
* block that is not valid Python at all — the same failure the docstring
|
* block that is not valid Python at all — the same failure the docstring
|
||||||
* escaping in {@link docLines} exists to prevent. 180 leaves headroom for the
|
* escaping in {@link docLines} exists to prevent. 180 leaves headroom for the
|
||||||
* few brackets an annotation can add around the chain: `NotRequired[…]`, a
|
* few brackets an annotation can add around the chain, all of which count
|
||||||
* `Literal[…]` item, and the `def` parameter list an argument annotation sits
|
* toward the same limit: a `Literal[…]` item, plus exactly one of `NotRequired[…]`
|
||||||
* inside, for a worst case of 182.
|
* (a chain in a TypedDict field, whose class-body line has no other open
|
||||||
|
* bracket) or the `def` parameter list still open around a chain in a method's
|
||||||
|
* RETURN annotation — the two are mutually exclusive, so the worst case is 182.
|
||||||
|
* An argument annotation is always a bare TypedDict class name and opens
|
||||||
|
* nothing.
|
||||||
*
|
*
|
||||||
* A CPython grammar limit, not a deployment choice, so it is fixed rather than
|
* A CPython grammar limit, not a deployment choice, so it is fixed rather than
|
||||||
* configurable. The sibling `ts-types` renderer needs no counterpart: nothing
|
* configurable. The sibling `ts-types` renderer needs no counterpart: nothing
|
||||||
@@ -557,10 +561,15 @@ export function renderToolsSdkPy(schemas: ToolSdkSchema[]): string {
|
|||||||
members.push(...doc)
|
members.push(...doc)
|
||||||
statements += 1
|
statements += 1
|
||||||
} else {
|
} else {
|
||||||
// Not a legal attribute name — the model reaches it via ``tools[name]``.
|
// Not reachable as ``tools.name`` — the model reaches it via
|
||||||
// The stub lists it as a subscript comment (referencing the named
|
// ``tools[name]``. Exotic names and hard keywords are not legal
|
||||||
// TypedDicts too) so a reader sees what is accessible; runtime resolution
|
// attributes at all; an underscore-leading name (``_foo``) IS a legal
|
||||||
// goes through the proxy's __getitem__.
|
// attribute and is routed here anyway, so one rule covers every
|
||||||
|
// underscore form rather than singling out the dunders that would
|
||||||
|
// name-mangle or resolve on ``object`` ahead of the proxy hook (see
|
||||||
|
// {@link RESERVED}). The stub lists it as a subscript comment
|
||||||
|
// (referencing the named TypedDicts too) so a reader sees what is
|
||||||
|
// accessible; runtime resolution goes through the proxy's __getitem__.
|
||||||
members.push(`${pad(1)}# tools[${JSON.stringify(schema.name)}](args: ${argType}) -> ${outputType}`)
|
members.push(`${pad(1)}# tools[${JSON.stringify(schema.name)}](args: ${argType}) -> ${outputType}`)
|
||||||
const description = describe(schema)
|
const description = describe(schema)
|
||||||
if (description !== undefined) members.push(`${pad(1)}# ${description}`)
|
if (description !== undefined) members.push(`${pad(1)}# ${description}`)
|
||||||
|
|||||||
@@ -465,7 +465,8 @@ describe('renderToolsSdkPy', () => {
|
|||||||
output: { type: 'string' },
|
output: { type: 'string' },
|
||||||
}
|
}
|
||||||
const text = renderToolsSdkPy([undescribedIdentifier, undescribedExotic])
|
const text = renderToolsSdkPy([undescribedIdentifier, undescribedExotic])
|
||||||
// Identifier method appears without a docstring line above it.
|
// Identifier method appears without a docstring in its body — hence the
|
||||||
|
// `: ...` stub, which a documented method replaces with the docstring.
|
||||||
expect(text).toContain('async def plain(self, args: dict[str, Any]) -> str: ...')
|
expect(text).toContain('async def plain(self, args: dict[str, Any]) -> str: ...')
|
||||||
expect(text).not.toContain('"""')
|
expect(text).not.toContain('"""')
|
||||||
// Subscript entry appears without the "# ..." description follow-up.
|
// Subscript entry appears without the "# ..." description follow-up.
|
||||||
@@ -660,6 +661,27 @@ describe('renderToolsSdkPy', () => {
|
|||||||
expect(text).not.toContain('__debug__')
|
expect(text).not.toContain('__debug__')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('routes every underscore-leading tool name to subscript access', () => {
|
||||||
|
// `_foo` is a legal Python attribute, unlike an exotic name or a hard
|
||||||
|
// keyword, but the whole underscore family goes to `tools[name]` under one
|
||||||
|
// rule: `__meta__` resolves on `object` before the proxy's __getattr__ ever
|
||||||
|
// runs, and `__token` name-mangles at the CALL SITE inside the model's own
|
||||||
|
// class. `_foo` follows them so the rule needs no per-form exception.
|
||||||
|
const make = (name: string): ToolSdkSchema => ({
|
||||||
|
name,
|
||||||
|
description: 'Leading underscore.',
|
||||||
|
parameters: parameterSchemaSpecToJsonSchema({}) as unknown as Record<string, unknown>,
|
||||||
|
output: { type: 'string' },
|
||||||
|
})
|
||||||
|
const text = renderToolsSdkPy([make('_foo'), make('__meta__'), make('__token')])
|
||||||
|
for (const name of ['_foo', '__meta__', '__token']) {
|
||||||
|
expect(text).toContain(`# tools[${JSON.stringify(name)}](args: dict[str, Any]) -> str`)
|
||||||
|
expect(text).not.toContain(`async def ${name}(`)
|
||||||
|
}
|
||||||
|
// No method emitted at all, so the class body needs the explicit `pass`.
|
||||||
|
expect(text).toContain(' pass\n')
|
||||||
|
})
|
||||||
|
|
||||||
it('escapes quotes and backslashes in descriptions so the docstring stays valid Python', () => {
|
it('escapes quotes and backslashes in descriptions so the docstring stays valid Python', () => {
|
||||||
// A description ending in `"` or an odd backslash would otherwise merge
|
// A description ending in `"` or an odd backslash would otherwise merge
|
||||||
// with (or escape) the closing triple quote — and this block is Code
|
// with (or escape) the closing triple quote — and this block is Code
|
||||||
|
|||||||
Reference in New Issue
Block a user