Skip to content

Commit f1cc504

Browse files
committed
experimental path support
1 parent 479452e commit f1cc504

File tree

3 files changed

+170
-45
lines changed

3 files changed

+170
-45
lines changed

01_funccall.ipynb

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -338,15 +338,39 @@
338338
"outputs": [],
339339
"source": [
340340
"#| export\n",
341+
"custom_types = {Path}\n",
342+
"\n",
341343
"def _handle_type(t, defs):\n",
342344
" \"Handle a single type, creating nested schemas if necessary\"\n",
343345
" if t is NoneType: return {'type': 'null'}\n",
344-
" if isinstance(t, type) and not issubclass(t, (int, float, str, bool)):\n",
346+
" if t in custom_types: return {'type':'string', 'format':t.__name__}\n",
347+
" if isinstance(t, type) and not issubclass(t, (int, float, str, bool)) or inspect.isfunction(t):\n",
345348
" defs[t.__name__] = _get_nested_schema(t)\n",
346349
" return {'$ref': f'#/$defs/{t.__name__}'}\n",
347350
" return {'type': _types(t)[0]}"
348351
]
349352
},
353+
{
354+
"cell_type": "code",
355+
"execution_count": null,
356+
"id": "16dbf080",
357+
"metadata": {},
358+
"outputs": [
359+
{
360+
"data": {
361+
"text/plain": [
362+
"({'type': 'integer'}, {'type': 'string', 'format': 'Path'})"
363+
]
364+
},
365+
"execution_count": null,
366+
"metadata": {},
367+
"output_type": "execute_result"
368+
}
369+
],
370+
"source": [
371+
"_handle_type(int, None), _handle_type(Path, None)"
372+
]
373+
},
350374
{
351375
"cell_type": "code",
352376
"execution_count": null,
@@ -500,11 +524,11 @@
500524
{
501525
"cell_type": "code",
502526
"execution_count": null,
503-
"id": "bfaa1712",
527+
"id": "38b0f97e",
504528
"metadata": {},
505529
"outputs": [],
506530
"source": [
507-
"#| export\n",
531+
"# exports\n",
508532
"def _get_nested_schema(obj):\n",
509533
" \"Generate nested JSON schema for a class or function\"\n",
510534
" d = docments(obj, full=True)\n",
@@ -616,7 +640,7 @@
616640
{
617641
"cell_type": "code",
618642
"execution_count": null,
619-
"id": "700298e3",
643+
"id": "23f54386",
620644
"metadata": {},
621645
"outputs": [],
622646
"source": [
@@ -643,7 +667,7 @@
643667
{
644668
"cell_type": "code",
645669
"execution_count": null,
646-
"id": "23c75a4a",
670+
"id": "e7311af9",
647671
"metadata": {},
648672
"outputs": [
649673
{
@@ -696,7 +720,7 @@
696720
{
697721
"cell_type": "code",
698722
"execution_count": null,
699-
"id": "0e6f7789",
723+
"id": "80203962",
700724
"metadata": {},
701725
"outputs": [
702726
{
@@ -730,7 +754,7 @@
730754
"id": "e3f36f8a",
731755
"metadata": {},
732756
"source": [
733-
"This also works with class methods:"
757+
"This also works with instance methods:"
734758
]
735759
},
736760
{
@@ -783,7 +807,7 @@
783807
{
784808
"cell_type": "code",
785809
"execution_count": null,
786-
"id": "89664bc9",
810+
"id": "ce3be915",
787811
"metadata": {},
788812
"outputs": [
789813
{
@@ -799,9 +823,9 @@
799823
" 'required': ['turns'],\n",
800824
" '$defs': {'Turn': {'type': 'object',\n",
801825
" 'properties': {'speaker_a': {'type': 'string',\n",
802-
" 'description': \"First speaker to speak's message\"},\n",
826+
" 'description': \"First speaker's message\"},\n",
803827
" 'speaker_b': {'type': 'string',\n",
804-
" 'description': \"Second speaker to speak's message\"}},\n",
828+
" 'description': \"Second speaker's message\"}},\n",
805829
" 'title': 'Turn',\n",
806830
" 'required': ['speaker_a', 'speaker_b']}}}}"
807831
]
@@ -816,8 +840,8 @@
816840
" \"Turn between two speakers\"\n",
817841
" def __init__(\n",
818842
" self,\n",
819-
" speaker_a:str, # First speaker to speak's message\n",
820-
" speaker_b:str, # Second speaker to speak's message\n",
843+
" speaker_a:str, # First speaker's message\n",
844+
" speaker_b:str, # Second speaker's message\n",
821845
" ): store_attr()\n",
822846
"\n",
823847
"class Conversation:\n",
@@ -850,9 +874,9 @@
850874
" 'required': ['turns'],\n",
851875
" '$defs': {'Turn': {'type': 'object',\n",
852876
" 'properties': {'speaker_a': {'type': 'string',\n",
853-
" 'description': \"First speaker to speak's message\"},\n",
877+
" 'description': \"First speaker's message\"},\n",
854878
" 'speaker_b': {'type': 'string',\n",
855-
" 'description': \"Second speaker to speak's message\"}},\n",
879+
" 'description': \"Second speaker's message\"}},\n",
856880
" 'title': 'Turn',\n",
857881
" 'required': ['speaker_a', 'speaker_b']}}}}"
858882
]
@@ -893,9 +917,9 @@
893917
" 'required': ['turns'],\n",
894918
" '$defs': {'Turn': {'type': 'object',\n",
895919
" 'properties': {'speaker_a': {'type': 'string',\n",
896-
" 'description': \"First speaker to speak's message\"},\n",
920+
" 'description': \"First speaker's message\"},\n",
897921
" 'speaker_b': {'type': 'string',\n",
898-
" 'description': \"Second speaker to speak's message\"}},\n",
922+
" 'description': \"Second speaker's message\"}},\n",
899923
" 'title': 'Turn',\n",
900924
" 'required': ['speaker_a', 'speaker_b']}}}}"
901925
]
@@ -916,6 +940,114 @@
916940
"get_schema(SetConversation)"
917941
]
918942
},
943+
{
944+
"cell_type": "code",
945+
"execution_count": null,
946+
"id": "8cf3f35c",
947+
"metadata": {},
948+
"outputs": [],
949+
"source": [
950+
"#| exports\n",
951+
"def PathArg(\n",
952+
" path: str # A filesystem path\n",
953+
"): return Path(path)"
954+
]
955+
},
956+
{
957+
"cell_type": "markdown",
958+
"id": "169212a6",
959+
"metadata": {},
960+
"source": [
961+
"Paths are a special case, since they only take `*args` and `**kwargs` as params, but normally we'd use them in a schema by just passing a str. So we create a custom param type for that."
962+
]
963+
},
964+
{
965+
"cell_type": "code",
966+
"execution_count": null,
967+
"id": "e9135dfa",
968+
"metadata": {},
969+
"outputs": [
970+
{
971+
"data": {
972+
"text/plain": [
973+
"{'name': 'path_test',\n",
974+
" 'description': 'Mandatory docstring',\n",
975+
" 'input_schema': {'type': 'object',\n",
976+
" 'properties': {'a': {'type': 'object',\n",
977+
" 'description': 'a type hint',\n",
978+
" '$ref': '#/$defs/PathArg'},\n",
979+
" 'b': {'type': 'object',\n",
980+
" 'description': 'b type hint',\n",
981+
" '$ref': '#/$defs/PathArg'}},\n",
982+
" 'title': None,\n",
983+
" 'required': ['a', 'b'],\n",
984+
" '$defs': {'PathArg': {'type': 'object',\n",
985+
" 'properties': {'path': {'type': 'string',\n",
986+
" 'description': 'A filesystem path'}},\n",
987+
" 'title': None,\n",
988+
" 'required': ['path']}}}}"
989+
]
990+
},
991+
"execution_count": null,
992+
"metadata": {},
993+
"output_type": "execute_result"
994+
}
995+
],
996+
"source": [
997+
"def path_test(\n",
998+
" a: PathArg, # a type hint\n",
999+
" b: PathArg # b type hint\n",
1000+
"):\n",
1001+
" \"Mandatory docstring\"\n",
1002+
" return a/b\n",
1003+
"\n",
1004+
"get_schema(path_test)"
1005+
]
1006+
},
1007+
{
1008+
"cell_type": "markdown",
1009+
"id": "c6d1d0c8",
1010+
"metadata": {},
1011+
"source": [
1012+
"Alternatively, use `Path` as usual, and handle the `format` key in the json to use that as a callable:"
1013+
]
1014+
},
1015+
{
1016+
"cell_type": "code",
1017+
"execution_count": null,
1018+
"id": "bdb69462",
1019+
"metadata": {},
1020+
"outputs": [
1021+
{
1022+
"data": {
1023+
"text/plain": [
1024+
"{'name': 'path_test2',\n",
1025+
" 'description': 'Mandatory docstring',\n",
1026+
" 'input_schema': {'type': 'object',\n",
1027+
" 'properties': {'a': {'type': 'string',\n",
1028+
" 'description': 'a type hint',\n",
1029+
" 'format': 'Path'},\n",
1030+
" 'b': {'type': 'string', 'description': 'b type hint', 'format': 'Path'}},\n",
1031+
" 'title': None,\n",
1032+
" 'required': ['a', 'b']}}"
1033+
]
1034+
},
1035+
"execution_count": null,
1036+
"metadata": {},
1037+
"output_type": "execute_result"
1038+
}
1039+
],
1040+
"source": [
1041+
"def path_test2(\n",
1042+
" a: Path, # a type hint\n",
1043+
" b: Path # b type hint\n",
1044+
"):\n",
1045+
" \"Mandatory docstring\"\n",
1046+
" return a/b\n",
1047+
"\n",
1048+
"get_schema(path_test2)"
1049+
]
1050+
},
9191051
{
9201052
"cell_type": "code",
9211053
"execution_count": null,

toolslm/_modidx.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
'toolslm.download.read_html': ('download.html#read_html', 'toolslm/download.py'),
1515
'toolslm.download.read_md': ('download.html#read_md', 'toolslm/download.py'),
1616
'toolslm.download.split_url': ('download.html#split_url', 'toolslm/download.py')},
17-
'toolslm.funccall': { 'toolslm.funccall._copy_loc': ('funccall.html#_copy_loc', 'toolslm/funccall.py'),
18-
'toolslm.funccall._get_nested_schema': ('funccall.html#_get_nested_schema', 'toolslm/funccall.py'),
17+
'toolslm.funccall': { 'toolslm.funccall.PathArg': ('funccall.html#patharg', 'toolslm/funccall.py'),
18+
'toolslm.funccall._copy_loc': ('funccall.html#_copy_loc', 'toolslm/funccall.py'),
1919
'toolslm.funccall._handle_container': ('funccall.html#_handle_container', 'toolslm/funccall.py'),
2020
'toolslm.funccall._handle_type': ('funccall.html#_handle_type', 'toolslm/funccall.py'),
2121
'toolslm.funccall._is_container': ('funccall.html#_is_container', 'toolslm/funccall.py'),

toolslm/funccall.py

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: ../01_funccall.ipynb.
22

33
# %% auto 0
4-
__all__ = ['empty', 'get_schema', 'python', 'mk_ns', 'call_func']
4+
__all__ = ['empty', 'custom_types', 'get_schema', 'PathArg', 'python', 'mk_ns', 'call_func']
55

66
# %% ../01_funccall.ipynb 2
77
import inspect
@@ -41,15 +41,18 @@ def _param(name, info):
4141
return pschema
4242

4343
# %% ../01_funccall.ipynb 21
44+
custom_types = {Path}
45+
4446
def _handle_type(t, defs):
4547
"Handle a single type, creating nested schemas if necessary"
4648
if t is NoneType: return {'type': 'null'}
47-
if isinstance(t, type) and not issubclass(t, (int, float, str, bool)):
49+
if t in custom_types: return {'type':'string', 'format':t.__name__}
50+
if isinstance(t, type) and not issubclass(t, (int, float, str, bool)) or inspect.isfunction(t):
4851
defs[t.__name__] = _get_nested_schema(t)
4952
return {'$ref': f'#/$defs/{t.__name__}'}
5053
return {'type': _types(t)[0]}
5154

52-
# %% ../01_funccall.ipynb 22
55+
# %% ../01_funccall.ipynb 23
5356
def _is_container(t):
5457
"Check if type is a container (list, dict, tuple, set, Union)"
5558
origin = get_origin(t)
@@ -59,7 +62,7 @@ def _is_parameterized(t):
5962
"Check if type has arguments (e.g. list[int] vs list, dict[str, int] vs dict)"
6063
return _is_container(t) and (get_args(t) != ())
6164

62-
# %% ../01_funccall.ipynb 28
65+
# %% ../01_funccall.ipynb 29
6366
def _handle_container(origin, args, defs):
6467
"Handle container types like dict, list, tuple, set, and Union"
6568
if origin is Union or origin is UnionType:
@@ -80,7 +83,7 @@ def _handle_container(origin, args, defs):
8083
return schema
8184
return None
8285

83-
# %% ../01_funccall.ipynb 29
86+
# %% ../01_funccall.ipynb 30
8487
def _process_property(name, obj, props, req, defs):
8588
"Process a single property of the schema"
8689
p = _param(name, obj)
@@ -93,22 +96,7 @@ def _process_property(name, obj, props, req, defs):
9396
# Non-container type or container without arguments
9497
p.update(_handle_type(obj.anno, defs))
9598

96-
# %% ../01_funccall.ipynb 30
97-
def _get_nested_schema(obj):
98-
"Generate nested JSON schema for a class or function"
99-
d = docments(obj, full=True)
100-
props, req, defs = {}, {}, {}
101-
102-
for n, o in d.items():
103-
if n != 'return' and n != 'self':
104-
_process_property(n, o, props, req, defs)
105-
106-
schema = dict(type='object', properties=props, title=obj.__name__ if isinstance(obj, type) else None)
107-
if req: schema['required'] = list(req)
108-
if defs: schema['$defs'] = defs
109-
return schema
110-
111-
# %% ../01_funccall.ipynb 34
99+
# %% ../01_funccall.ipynb 35
112100
def get_schema(f:callable, pname='input_schema')->dict:
113101
"Generate JSON schema for a class, function, or method"
114102
schema = _get_nested_schema(f)
@@ -119,11 +107,16 @@ def get_schema(f:callable, pname='input_schema')->dict:
119107
if ret.anno is not empty: desc += f'\n\nReturns:\n- type: {_types(ret.anno)[0]}'
120108
return {"name": f.__name__, "description": desc, pname: schema}
121109

122-
# %% ../01_funccall.ipynb 55
110+
# %% ../01_funccall.ipynb 46
111+
def PathArg(
112+
path: str # A filesystem path
113+
): return Path(path)
114+
115+
# %% ../01_funccall.ipynb 61
123116
import ast, time, signal, traceback
124117
from fastcore.utils import *
125118

126-
# %% ../01_funccall.ipynb 56
119+
# %% ../01_funccall.ipynb 62
127120
def _copy_loc(new, orig):
128121
"Copy location information from original node to new node and all children."
129122
new = ast.copy_location(new, orig)
@@ -132,7 +125,7 @@ def _copy_loc(new, orig):
132125
elif isinstance(o, list): setattr(new, field, [_copy_loc(value, orig) for value in o])
133126
return new
134127

135-
# %% ../01_funccall.ipynb 58
128+
# %% ../01_funccall.ipynb 64
136129
def _run(code:str ):
137130
"Run `code`, returning final expression (similar to IPython)"
138131
tree = ast.parse(code)
@@ -155,7 +148,7 @@ def _run(code:str ):
155148
if _result is not None: return _result
156149
return stdout_buffer.getvalue().strip()
157150

158-
# %% ../01_funccall.ipynb 63
151+
# %% ../01_funccall.ipynb 69
159152
def python(code, # Code to execute
160153
timeout=5 # Maximum run time in seconds before a `TimeoutError` is raised
161154
): # Result of last node, if it's an expression, or `None` otherwise
@@ -168,7 +161,7 @@ def handler(*args): raise TimeoutError()
168161
except Exception as e: return traceback.format_exc()
169162
finally: signal.alarm(0)
170163

171-
# %% ../01_funccall.ipynb 70
164+
# %% ../01_funccall.ipynb 76
172165
def mk_ns(*funcs_or_objs):
173166
merged = {}
174167
for o in funcs_or_objs:
@@ -177,7 +170,7 @@ def mk_ns(*funcs_or_objs):
177170
if callable(o) and hasattr(o, '__name__'): merged |= {o.__name__: o}
178171
return merged
179172

180-
# %% ../01_funccall.ipynb 79
173+
# %% ../01_funccall.ipynb 85
181174
def call_func(fc_name, fc_inputs, ns):
182175
"Call the function `fc_name` with the given `fc_inputs` using namespace `ns`."
183176
if not isinstance(ns, abc.Mapping): ns = mk_ns(*ns)

0 commit comments

Comments
 (0)