Skip to content

Commit 899416e

Browse files
example
1 parent 50c9408 commit 899416e

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Motivation\n",
8+
"Let's say we're building a local editor that allows you to load an AIConfig\n",
9+
"from a local file and then run methods on it.\n",
10+
"\n",
11+
"In the (simplified) code below, we do just that."
12+
]
13+
},
14+
{
15+
"cell_type": "code",
16+
"execution_count": 16,
17+
"metadata": {},
18+
"outputs": [
19+
{
20+
"name": "stdout",
21+
"output_type": "stream",
22+
"text": [
23+
"Loaded AIConfig: NYC Trip Planner\n",
24+
"\n",
25+
"\n"
26+
]
27+
}
28+
],
29+
"source": [
30+
"import json\n",
31+
"from typing import Any\n",
32+
"\n",
33+
"\n",
34+
"def read_json_from_file(path: str) -> dict[str, Any]:\n",
35+
" with open(path, \"r\") as f:\n",
36+
" return json.loads(f.read())\n",
37+
" \n",
38+
"\n",
39+
"def start_app(path: str):\n",
40+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
41+
" aiconfig = read_json_from_file(path)\n",
42+
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n",
43+
"\n",
44+
"\n",
45+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")"
46+
]
47+
},
48+
{
49+
"cell_type": "markdown",
50+
"metadata": {},
51+
"source": [
52+
"## Cool, LGTM, ship it!"
53+
]
54+
},
55+
{
56+
"cell_type": "markdown",
57+
"metadata": {},
58+
"source": [
59+
"# A few hours later..."
60+
]
61+
},
62+
{
63+
"cell_type": "markdown",
64+
"metadata": {},
65+
"source": [
66+
"# Issue #9000 Editor crashes on new file path\n",
67+
"### opened 2 hours ago by lastmile-biggest-fan\n",
68+
"\n",
69+
"Dear LastMile team,\n",
70+
"I really like the editor, but when I give it a new file path, it crashes!\n",
71+
"I was hoping it would create a new AIConfig for me and write it to the file..."
72+
]
73+
},
74+
{
75+
"cell_type": "markdown",
76+
"metadata": {},
77+
"source": [
78+
"# OK, what happened?"
79+
]
80+
},
81+
{
82+
"cell_type": "code",
83+
"execution_count": 17,
84+
"metadata": {},
85+
"outputs": [
86+
{
87+
"ename": "FileNotFoundError",
88+
"evalue": "[Errno 2] No such file or directory: 'i-dont-exist-yet-please-create-me.json'",
89+
"output_type": "error",
90+
"traceback": [
91+
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
92+
"\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
93+
"Cell \u001b[0;32mIn[17], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mstart_app\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mi-dont-exist-yet-please-create-me.json\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n",
94+
"Cell \u001b[0;32mIn[16], line 11\u001b[0m, in \u001b[0;36mstart_app\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mstart_app\u001b[39m(path: \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m 10\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\u001b[39;00m\n\u001b[0;32m---> 11\u001b[0m aiconfig \u001b[38;5;241m=\u001b[39m json\u001b[38;5;241m.\u001b[39mloads(\u001b[43mread_file\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mLoaded AIConfig: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00maiconfig[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mname\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28mprint\u001b[39m()\n",
95+
"Cell \u001b[0;32mIn[16], line 5\u001b[0m, in \u001b[0;36mread_file\u001b[0;34m(path)\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mread_file\u001b[39m(path: \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mr\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m f\u001b[38;5;241m.\u001b[39mread()\n",
96+
"File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/aiconfig/lib/python3.10/site-packages/IPython/core/interactiveshell.py:310\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 303\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 304\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 305\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 306\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 307\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 308\u001b[0m )\n\u001b[0;32m--> 310\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
97+
"\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'i-dont-exist-yet-please-create-me.json'"
98+
]
99+
}
100+
],
101+
"source": [
102+
"start_app(\"i-dont-exist-yet-please-create-me.json\")"
103+
]
104+
},
105+
{
106+
"cell_type": "markdown",
107+
"metadata": {},
108+
"source": [
109+
"# Oops"
110+
]
111+
},
112+
{
113+
"cell_type": "markdown",
114+
"metadata": {},
115+
"source": [
116+
"Ok, let's diagnose the problem here. We forgot to handle the case where the path doesn't exist.\n",
117+
"\n",
118+
"That's understandable. As programmers, we don't always write perfect code.\n",
119+
"Sometimes it's helpful to bring new tools into the workflow to prevent this kind of problem in the future.\n",
120+
"\n",
121+
"\n",
122+
"Hmm, ok. Wouldn't it be nice if we had a static analyzer that could have caught this problem immediately? That way we could have fixed it before the initial PR was merged.\n",
123+
"\n",
124+
"Let's analyze some tools."
125+
]
126+
},
127+
{
128+
"cell_type": "markdown",
129+
"metadata": {},
130+
"source": [
131+
"## V2: Optional"
132+
]
133+
},
134+
{
135+
"cell_type": "code",
136+
"execution_count": 6,
137+
"metadata": {},
138+
"outputs": [
139+
{
140+
"name": "stdout",
141+
"output_type": "stream",
142+
"text": [
143+
"\n",
144+
"[Pyright] Object of type \"None\" is not subscriptable\n",
145+
"PylancereportOptionalSubscript\n",
146+
"(variable) aiconfig: dict[str, Any] | None\n",
147+
"\n"
148+
]
149+
}
150+
],
151+
"source": [
152+
"from typing import Any, Optional\n",
153+
"\n",
154+
"\n",
155+
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n",
156+
" with open(path, \"r\") as f:\n",
157+
" return json.loads(f.read())\n",
158+
" \n",
159+
"\n",
160+
"def start_app(path: str):\n",
161+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
162+
" aiconfig = read_json_from_file(path)\n",
163+
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n",
164+
"\n",
165+
"print(\"\"\"\n",
166+
"[Pyright] Object of type \"None\" is not subscriptable\n",
167+
"PylancereportOptionalSubscript\n",
168+
"(variable) aiconfig: dict[str, Any] | None\n",
169+
"\"\"\")"
170+
]
171+
},
172+
{
173+
"cell_type": "markdown",
174+
"metadata": {},
175+
"source": [
176+
"# Aha!\n",
177+
"\n",
178+
"Now, Pyright immediately tells us that `None` is a possibility, and we have to handle this case. Let's do that.\n"
179+
]
180+
},
181+
{
182+
"cell_type": "code",
183+
"execution_count": 31,
184+
"metadata": {},
185+
"outputs": [
186+
{
187+
"name": "stdout",
188+
"output_type": "stream",
189+
"text": [
190+
"Loaded AIConfig: NYC Trip Planner\n",
191+
"\n",
192+
"Loaded AIConfig: \n",
193+
"\n"
194+
]
195+
}
196+
],
197+
"source": [
198+
"from typing import Optional\n",
199+
"from aiconfig.Config import AIConfigRuntime\n",
200+
"\n",
201+
"\n",
202+
"\n",
203+
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n",
204+
" try:\n",
205+
" with open(path, \"r\") as f:\n",
206+
" return json.loads(f.read())\n",
207+
" except FileNotFoundError:\n",
208+
" return None\n",
209+
"\n",
210+
"def start_app(path: str):\n",
211+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
212+
" aiconfig = read_json_from_file(path)\n",
213+
" if aiconfig is None:\n",
214+
" print(f\"Could not load AIConfig from path: {path}. Creating and saving.\")\n",
215+
" aiconfig = json.dumps(AIConfigRuntime.create())\n",
216+
" # [save the aiconfig to the path] \n",
217+
" print(f\"Loaded and saved new AIConfig\\n\")\n",
218+
" else:\n",
219+
" print(f\"Loaded AIConfig: {aiconfig}\\n\")\n",
220+
"\n",
221+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n",
222+
"start_app(\"i-dont-exist-yet-please-create-me.json\")"
223+
]
224+
},
225+
{
226+
"cell_type": "markdown",
227+
"metadata": {},
228+
"source": [
229+
"Ok, cool, much better. But wait, it would be nice to retain some information about what went wrong. My `None` value doesn't tell me anything about why the AIConfig couldn't be loaded. Does the file not exist? Was it a permission problem, networked filesystem problem? etc."
230+
]
231+
},
232+
{
233+
"cell_type": "markdown",
234+
"metadata": {},
235+
"source": [
236+
"# V3: Result"
237+
]
238+
},
239+
{
240+
"cell_type": "markdown",
241+
"metadata": {},
242+
"source": [
243+
"The result library (https://github.com/rustedpy/result) provides a neat type\n",
244+
"called `Result`, which is a bit like Optional. It's parametrized by the value type just like optional, but also by a second type for the error case.\n",
245+
"\n",
246+
"We can use it like optional, but store an arbitrary value with information about what went wrong."
247+
]
248+
},
249+
{
250+
"cell_type": "code",
251+
"execution_count": 52,
252+
"metadata": {},
253+
"outputs": [
254+
{
255+
"name": "stdout",
256+
"output_type": "stream",
257+
"text": [
258+
"Loaded AIConfig: NYC Trip Planner\n",
259+
"\n",
260+
"Could not load AIConfig from path: i-dont-exist-yet-please-create-me.json (File not found at path: i-dont-exist-yet-please-create-me.json). Creating and saving.\n",
261+
"Created and saved new AIConfig: \n",
262+
"\n"
263+
]
264+
}
265+
],
266+
"source": [
267+
"from aiconfig.Config import AIConfigRuntime\n",
268+
"from result import Result, Ok, Err\n",
269+
"from typing import Any\n",
270+
"\n",
271+
"from json import JSONDecodeError\n",
272+
"\n",
273+
"\n",
274+
"def read_json_from_file(path: str) -> Result[dict[str, Any], str]:\n",
275+
" \"\"\"Use `str` in the error case to contain a helpful error message.\"\"\"\n",
276+
" try:\n",
277+
" with open(path, \"r\") as f:\n",
278+
" return Ok(json.loads(f.read()))\n",
279+
" except FileNotFoundError:\n",
280+
" return Err(f\"File not found at path: {path}\")\n",
281+
" except OSError as e:\n",
282+
" return Err(f\"Could not read file at path: {path}: {e}\")\n",
283+
" except JSONDecodeError as e:\n",
284+
" return Err(f\"Could not parse JSON at path: {path}: {e}\")\n",
285+
"\n",
286+
"def start_app(path: str):\n",
287+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
288+
" file_contents = read_json_from_file(path)\n",
289+
" match file_contents:\n",
290+
" case Ok(aiconfig_ok):\n",
291+
" print(f\"Loaded AIConfig: {aiconfig_ok['name']}\\n\")\n",
292+
" case Err(e):\n",
293+
" print(f\"Could not load AIConfig from path: {path} ({e}). Creating and saving.\")\n",
294+
" aiconfig = AIConfigRuntime.create().model_dump(exclude=\"callback_manager\")\n",
295+
" # [Save to file path]\n",
296+
" # aiconfig.save(path)\n",
297+
" print(f\"Created and saved new AIConfig: {aiconfig['name']}\\n\")\n",
298+
"\n",
299+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n",
300+
"start_app(\"i-dont-exist-yet-please-create-me.json\")"
301+
]
302+
},
303+
{
304+
"cell_type": "markdown",
305+
"metadata": {},
306+
"source": [
307+
"There are several nice things about this pattern:\n",
308+
"* You get static errors similar to the Optional case unless you check for None\n",
309+
"* You also get specific, useful error information\n",
310+
"* Structural pattern matching: When matching the cases, you can elegantly and safely unbox the data inside the result.\n",
311+
"* Because of pyright's ability to check for exhaustive pattern matching, it will yell at you if you don't handle the Err case. Try it! Comment out the Err case."
312+
]
313+
},
314+
{
315+
"cell_type": "markdown",
316+
"metadata": {},
317+
"source": [
318+
"# Part 2: Composition (To be continued)"
319+
]
320+
}
321+
],
322+
"metadata": {
323+
"kernelspec": {
324+
"display_name": "aiconfig",
325+
"language": "python",
326+
"name": "python3"
327+
},
328+
"language_info": {
329+
"codemirror_mode": {
330+
"name": "ipython",
331+
"version": 3
332+
},
333+
"file_extension": ".py",
334+
"mimetype": "text/x-python",
335+
"name": "python",
336+
"nbconvert_exporter": "python",
337+
"pygments_lexer": "ipython3",
338+
"version": "3.10.13"
339+
}
340+
},
341+
"nbformat": 4,
342+
"nbformat_minor": 2
343+
}

0 commit comments

Comments
 (0)