|
330 | 330 | "outputs": [], |
331 | 331 | "source": [ |
332 | 332 | "#| export\n", |
333 | | - "def _handle_type(t, defs):\n", |
| 333 | + "def handle_type(t, defs):\n", |
334 | 334 | " \"Convert a type annotation to JSON Schema\"\n", |
| 335 | + " if t is empty: raise TypeError(\"Missing type annotation\")\n", |
| 336 | + " if t in (object, Any): raise TypeError(f\"Can't make a schema for {t!r}\")\n", |
335 | 337 | " ot = ifnone(get_origin(t), t)\n", |
336 | 338 | " if t is NoneType: return {'type': 'null'}\n", |
337 | | - " if ot in (Union, UnionType): return {\"anyOf\": [_handle_type(arg, defs) for arg in get_args(t)]}\n", |
| 339 | + " if ot in (Union, UnionType): return union_schema(t, defs)\n", |
338 | 340 | " if t in custom_types: return {'type': 'string', 'format': t.__name__}\n", |
339 | 341 | " if ot is dict:\n", |
340 | 342 | " args = get_args(t)\n", |
341 | | - " return {'type': 'object', 'additionalProperties': _handle_type(args[1], defs)} if args else {'type': 'object'}\n", |
| 343 | + " return {'type': 'object', 'additionalProperties': handle_type(args[1], defs)} if args else {'type': 'object'}\n", |
342 | 344 | " if ot is tuple:\n", |
343 | 345 | " args = get_args(t)\n", |
344 | 346 | " if not args: return {'type': 'array', 'items': {}}\n", |
345 | | - " if args[-1] is Ellipsis: return {'type': 'array', 'items': _handle_type(args[0], defs)}\n", |
346 | | - " prefix = [_handle_type(a, defs) for a in args]\n", |
| 347 | + " if args[-1] is Ellipsis: return {'type': 'array', 'items': handle_type(args[0], defs)}\n", |
| 348 | + " prefix = [handle_type(a, defs) for a in args]\n", |
347 | 349 | " items = prefix[0] if all(p == prefix[0] for p in prefix) else {'anyOf': prefix}\n", |
348 | 350 | " return {'type': 'array', 'prefixItems': prefix, 'items': items, 'minItems': len(args), 'maxItems': len(args)}\n", |
349 | 351 | " if ot in (list, set):\n", |
350 | 352 | " args = get_args(t)\n", |
351 | | - " schema = {'type': 'array', 'items': _handle_type(args[0], defs) if args else {'type': 'string'}}\n", |
| 353 | + " schema = {'type': 'array', 'items': handle_type(args[0], defs) if args else {'type': 'string'}}\n", |
352 | 354 | " if ot is set: schema['uniqueItems'] = True\n", |
353 | 355 | " return schema\n", |
354 | 356 | " if isinstance(t, type) and not issubclass(t, (int, float, str, bool)) and t.__module__ != 'builtins' or inspect.isfunction(t):\n", |
355 | 357 | " defs[t.__name__] = _get_nested_schema(t)\n", |
356 | 358 | " return {'$ref': f'#/$defs/{t.__name__}'}\n", |
357 | | - " return {'type': _type_str(t)}" |
| 359 | + " return {'type': _type_str(t)}\n", |
| 360 | + "\n", |
| 361 | + "def _schemable(t, defs=None):\n", |
| 362 | + " \"JSON Schema for type `t`, or None if `t` has no schema representation\"\n", |
| 363 | + " try: return handle_type(t, defs)\n", |
| 364 | + " except TypeError: return None\n", |
| 365 | + "\n", |
| 366 | + "def union_schema(t, defs=None):\n", |
| 367 | + " \"Schema for union type `t`: its schema-representable members, unwrapped if only one, `anyOf` otherwise\"\n", |
| 368 | + " args = list(filter(None, (_schemable(a, defs) for a in get_args(t))))\n", |
| 369 | + " if not args: raise TypeError(f\"No schema-representable member in {t!r}\")\n", |
| 370 | + " return args[0] if len(args)==1 else {\"anyOf\": args}" |
| 371 | + ] |
| 372 | + }, |
| 373 | + { |
| 374 | + "cell_type": "markdown", |
| 375 | + "id": "c320ab26", |
| 376 | + "metadata": {}, |
| 377 | + "source": [ |
| 378 | + "Not every Python type has a JSON Schema representation: bare `object` and `Any` say nothing a model could act on, and a missing annotation is treated the same way; `handle_type` raises `TypeError` for them all. `union_schema` builds a union's schema from just its schema-representable members, unwrapping when only one remains, so an annotation like `str|object` (meaning \"pass a string; other objects accepted at runtime\") collapses to a plain string schema:\n" |
| 379 | + ] |
| 380 | + }, |
| 381 | + { |
| 382 | + "cell_type": "code", |
| 383 | + "execution_count": null, |
| 384 | + "id": "7999acf7", |
| 385 | + "metadata": {}, |
| 386 | + "outputs": [], |
| 387 | + "source": [ |
| 388 | + "test_eq(union_schema(str|object), {'type': 'string'})\n", |
| 389 | + "test_eq(union_schema(int|str), {'anyOf': [{'type': 'integer'}, {'type': 'string'}]})\n", |
| 390 | + "test_fail(lambda: union_schema(object|Any), contains=\"No schema-representable member\")\n", |
| 391 | + "test_is(_schemable(object), None)\n" |
358 | 392 | ] |
359 | 393 | }, |
360 | 394 | { |
|
375 | 409 | } |
376 | 410 | ], |
377 | 411 | "source": [ |
378 | | - "_handle_type(int, None), _handle_type(Path, None)" |
| 412 | + "handle_type(int, None), handle_type(Path, None)" |
379 | 413 | ] |
380 | 414 | }, |
381 | 415 | { |
|
411 | 445 | ], |
412 | 446 | "source": [ |
413 | 447 | "# gemini expect `items` to be defined for arrays\n", |
414 | | - "_handle_type(list, None), _handle_type(tuple[str], None), _handle_type(set[str], None)" |
| 448 | + "handle_type(list, None), handle_type(tuple[str], None), handle_type(set[str], None)" |
| 449 | + ] |
| 450 | + }, |
| 451 | + { |
| 452 | + "cell_type": "markdown", |
| 453 | + "id": "9cae3817", |
| 454 | + "metadata": {}, |
| 455 | + "source": [ |
| 456 | + "Tool parameters must be fully annotated: a missing annotation, or a type with no schema representation (bare `object` or `Any`), raises rather than producing a junk schema. Unions are filtered to just their schema-representable members, so `str|object` means \"a string, but any object accepted at runtime\", and collapses to a plain string schema. A union with no representable member raises:" |
| 457 | + ] |
| 458 | + }, |
| 459 | + { |
| 460 | + "cell_type": "code", |
| 461 | + "execution_count": null, |
| 462 | + "id": "dc4231d0", |
| 463 | + "metadata": {}, |
| 464 | + "outputs": [], |
| 465 | + "source": [ |
| 466 | + "test_fail(lambda: handle_type(object, None), contains=\"Can't make a schema\")\n", |
| 467 | + "test_eq(handle_type(str|object, None), {'type': 'string'})\n", |
| 468 | + "test_eq(handle_type(int|str, None), {'anyOf': [{'type': 'integer'}, {'type': 'string'}]})\n", |
| 469 | + "test_fail(lambda: handle_type(object|Any, None), contains=\"No schema-representable member\")" |
415 | 470 | ] |
416 | 471 | }, |
417 | 472 | { |
|
433 | 488 | } |
434 | 489 | ], |
435 | 490 | "source": [ |
436 | | - "_handle_type(dict, None), _handle_type(dict[str,str], None)" |
| 491 | + "handle_type(dict, None), handle_type(dict[str,str], None)" |
437 | 492 | ] |
438 | 493 | }, |
439 | 494 | { |
|
501 | 556 | "metadata": {}, |
502 | 557 | "outputs": [], |
503 | 558 | "source": [ |
504 | | - "test_eq(_handle_type(list, {}), {'type': 'array', 'items': {'type': 'string'}})\n", |
505 | | - "test_eq(_handle_type(set, {}), {'type': 'array', 'items': {'type': 'string'}, 'uniqueItems': True})" |
| 559 | + "test_eq(handle_type(list, {}), {'type': 'array', 'items': {'type': 'string'}})\n", |
| 560 | + "test_eq(handle_type(set, {}), {'type': 'array', 'items': {'type': 'string'}, 'uniqueItems': True})" |
506 | 561 | ] |
507 | 562 | }, |
508 | 563 | { |
|
518 | 573 | " p = _param(obj, evalable=evalable)\n", |
519 | 574 | " props[name] = p\n", |
520 | 575 | " if obj.default is empty: req[name] = True\n", |
521 | | - " p.update(_handle_type(obj.anno, defs))\n", |
| 576 | + " try: p.update(handle_type(obj.anno, defs))\n", |
| 577 | + " except TypeError as e: raise TypeError(f\"Parameter {name!r}: {e}\") from None\n", |
522 | 578 | " if 'anyOf' in p: p.pop('type', None)" |
523 | 579 | ] |
524 | 580 | }, |
|
556 | 612 | "source": [ |
557 | 613 | "# Test primitive types\n", |
558 | 614 | "defs = {}\n", |
559 | | - "assert _handle_type(int, defs) == {'type': 'integer'}\n", |
560 | | - "assert _handle_type(str, defs) == {'type': 'string'}\n", |
561 | | - "assert _handle_type(bool, defs) == {'type': 'boolean'}\n", |
562 | | - "assert _handle_type(float, defs) == {'type': 'number'}\n", |
| 615 | + "assert handle_type(int, defs) == {'type': 'integer'}\n", |
| 616 | + "assert handle_type(str, defs) == {'type': 'string'}\n", |
| 617 | + "assert handle_type(bool, defs) == {'type': 'boolean'}\n", |
| 618 | + "assert handle_type(float, defs) == {'type': 'number'}\n", |
563 | 619 | "\n", |
564 | 620 | "# Test custom class\n", |
565 | 621 | "class TestClass:\n", |
566 | 622 | " def __init__(self, x: int, y: int): store_attr()\n", |
567 | 623 | "\n", |
568 | | - "result = _handle_type(TestClass, defs)\n", |
| 624 | + "result = handle_type(TestClass, defs)\n", |
569 | 625 | "assert result == {'$ref': '#/$defs/TestClass'}\n", |
570 | 626 | "assert 'TestClass' in defs\n", |
571 | 627 | "assert defs['TestClass']['type'] == 'object'\n", |
572 | 628 | "assert 'properties' in defs['TestClass']\n", |
573 | 629 | "\n", |
574 | 630 | "# tuple[int, ...] should produce array with items, not prefixItems\n", |
575 | | - "test_eq(_handle_type(tuple[int, ...], {}), {'type': 'array', 'items': {'type': 'integer'}})" |
| 631 | + "test_eq(handle_type(tuple[int, ...], {}), {'type': 'array', 'items': {'type': 'integer'}})" |
576 | 632 | ] |
577 | 633 | }, |
578 | 634 | { |
|
583 | 639 | "outputs": [], |
584 | 640 | "source": [ |
585 | 641 | "# Test primitive types in containers\n", |
586 | | - "test_eq(_handle_type(list[int], defs), {'type': 'array', 'items': {'type': 'integer'}})\n", |
587 | | - "test_eq(_handle_type(tuple[str], defs), {'type': 'array', 'prefixItems': [{'type': 'string'}], 'items': {'type': 'string'}, 'minItems': 1, 'maxItems': 1})\n", |
588 | | - "test_eq(_handle_type(set[str], defs), dict(type='array', items={'type': 'string'}, uniqueItems=True))\n", |
589 | | - "test_eq(_handle_type(dict[str,bool], defs), {'type': 'object', 'additionalProperties': {'type': 'boolean'}})" |
| 642 | + "test_eq(handle_type(list[int], defs), {'type': 'array', 'items': {'type': 'integer'}})\n", |
| 643 | + "test_eq(handle_type(tuple[str], defs), {'type': 'array', 'prefixItems': [{'type': 'string'}], 'items': {'type': 'string'}, 'minItems': 1, 'maxItems': 1})\n", |
| 644 | + "test_eq(handle_type(set[str], defs), dict(type='array', items={'type': 'string'}, uniqueItems=True))\n", |
| 645 | + "test_eq(handle_type(dict[str,bool], defs), {'type': 'object', 'additionalProperties': {'type': 'boolean'}})" |
590 | 646 | ] |
591 | 647 | }, |
592 | 648 | { |
|
596 | 652 | "metadata": {}, |
597 | 653 | "outputs": [], |
598 | 654 | "source": [ |
599 | | - "result = _handle_type(list[TestClass], defs)\n", |
| 655 | + "result = handle_type(list[TestClass], defs)\n", |
600 | 656 | "assert result == {'type': 'array', 'items': {'$ref': '#/$defs/TestClass'}}\n", |
601 | 657 | "assert 'TestClass' in defs\n", |
602 | 658 | "\n", |
603 | 659 | "# Test complex nested structure\n", |
604 | 660 | "ComplexType = dict[str, list[TestClass]]\n", |
605 | | - "result = _handle_type(dict[str, list[TestClass]], defs)\n", |
| 661 | + "result = handle_type(dict[str, list[TestClass]], defs)\n", |
606 | 662 | "assert result == {'type': 'object', 'additionalProperties': {'type': 'array', 'items': {'$ref': '#/$defs/TestClass'}}}" |
607 | 663 | ] |
608 | 664 | }, |
|
726 | 782 | "outputs": [], |
727 | 783 | "source": [ |
728 | 784 | "def f(\n", |
729 | | - " o:object, # the o\n", |
| 785 | + " o:dict, # the o\n", |
730 | 786 | " q:tuple[int,str],\n", |
731 | 787 | " p:str|list[str] = 'a',\n", |
732 | | - "): \"object function\"" |
| 788 | + "): \"dict function\"" |
733 | 789 | ] |
734 | 790 | }, |
735 | 791 | { |
|
773 | 829 | "s" |
774 | 830 | ] |
775 | 831 | }, |
| 832 | + { |
| 833 | + "cell_type": "code", |
| 834 | + "execution_count": null, |
| 835 | + "id": "6037a5ed", |
| 836 | + "metadata": {}, |
| 837 | + "outputs": [], |
| 838 | + "source": [ |
| 839 | + "def _noanno(x, y:int=0):\n", |
| 840 | + " \"Docs\"\n", |
| 841 | + " return x\n", |
| 842 | + "test_fail(lambda: get_schema(_noanno), contains=\"Parameter 'x': Missing type annotation\")" |
| 843 | + ] |
| 844 | + }, |
776 | 845 | { |
777 | 846 | "cell_type": "code", |
778 | 847 | "execution_count": null, |
|
1086 | 1155 | " \"A conversation between two speakers\"\n", |
1087 | 1156 | " def __init__(\n", |
1088 | 1157 | " self,\n", |
1089 | | - " turns:dict[str,object], # dictionary of topics and the Turns of the conversation\n", |
| 1158 | + " turns:dict[str,Turn], # dictionary of topics and the Turns of the conversation\n", |
1090 | 1159 | " ): store_attr()\n", |
1091 | 1160 | "\n", |
1092 | 1161 | "get_schema(DictConversation)" |
|
0 commit comments