Skip to content

Commit 41fa4ec

Browse files
err h
1 parent e3590eb commit 41fa4ec

File tree

1 file changed

+388
-0
lines changed

1 file changed

+388
-0
lines changed
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Part 1: 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\n",
132+
"\n",
133+
"First, let's fix the root cause and catch exceptions. Now, what do we do in the `except` block? \n",
134+
"\n",
135+
"Well, we can reraise, but that brings us right back to the previous case and doesn't achieve anything helpful. \n",
136+
"\n",
137+
"Instead, notice what happens if we return None and type hint the function accordingly (Optional[...])."
138+
]
139+
},
140+
{
141+
"cell_type": "code",
142+
"execution_count": 6,
143+
"metadata": {},
144+
"outputs": [
145+
{
146+
"name": "stdout",
147+
"output_type": "stream",
148+
"text": [
149+
"\n",
150+
"[Pyright] Object of type \"None\" is not subscriptable\n",
151+
"PylancereportOptionalSubscript\n",
152+
"(variable) aiconfig: dict[str, Any] | None\n",
153+
"\n"
154+
]
155+
}
156+
],
157+
"source": [
158+
"from typing import Any, Optional\n",
159+
"\n",
160+
"\n",
161+
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n",
162+
" try:\n",
163+
" with open(path, \"r\") as f:\n",
164+
" return json.loads(f.read())\n",
165+
" except Exception as e:\n",
166+
" return None\n",
167+
" \n",
168+
"\n",
169+
"def start_app(path: str):\n",
170+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
171+
" aiconfig = read_json_from_file(path)\n",
172+
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n",
173+
"\n",
174+
"print(\"\"\"\n",
175+
"[Pyright] Object of type \"None\" is not subscriptable\n",
176+
"PylancereportOptionalSubscript\n",
177+
"(variable) aiconfig: dict[str, Any] | None\n",
178+
"\"\"\")"
179+
]
180+
},
181+
{
182+
"cell_type": "markdown",
183+
"metadata": {},
184+
"source": [
185+
"# Aha!\n",
186+
"\n",
187+
"Now, Pyright immediately tells us that `None` is a possibility, and we have to handle this case. Let's do that.\n"
188+
]
189+
},
190+
{
191+
"cell_type": "code",
192+
"execution_count": 31,
193+
"metadata": {},
194+
"outputs": [
195+
{
196+
"name": "stdout",
197+
"output_type": "stream",
198+
"text": [
199+
"Loaded AIConfig: NYC Trip Planner\n",
200+
"\n",
201+
"Loaded AIConfig: \n",
202+
"\n"
203+
]
204+
}
205+
],
206+
"source": [
207+
"from typing import Optional\n",
208+
"from aiconfig.Config import AIConfigRuntime\n",
209+
"\n",
210+
"\n",
211+
"\n",
212+
"def read_json_from_file(path: str) -> Optional[dict[str, Any]]:\n",
213+
" try:\n",
214+
" with open(path, \"r\") as f:\n",
215+
" return json.loads(f.read())\n",
216+
" except Exception:\n",
217+
" return None\n",
218+
"\n",
219+
"def start_app(path: str):\n",
220+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
221+
" aiconfig = read_json_from_file(path)\n",
222+
" if aiconfig is None:\n",
223+
" print(f\"Could not load AIConfig from path: {path}. Creating and saving.\")\n",
224+
" aiconfig = json.dumps(AIConfigRuntime.create())\n",
225+
" # [save the aiconfig to the path] \n",
226+
" print(f\"Loaded and saved new AIConfig\\n\")\n",
227+
" else:\n",
228+
" print(f\"Loaded AIConfig: {aiconfig}\\n\")\n",
229+
"\n",
230+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n",
231+
"start_app(\"i-dont-exist-yet-please-create-me.json\")"
232+
]
233+
},
234+
{
235+
"cell_type": "markdown",
236+
"metadata": {},
237+
"source": [
238+
"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."
239+
]
240+
},
241+
{
242+
"cell_type": "markdown",
243+
"metadata": {},
244+
"source": [
245+
"# V3: Result"
246+
]
247+
},
248+
{
249+
"cell_type": "markdown",
250+
"metadata": {},
251+
"source": [
252+
"The result library (https://github.com/rustedpy/result) provides a neat type\n",
253+
"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",
254+
"\n",
255+
"We can use it like optional, but store an arbitrary value with information about what went wrong."
256+
]
257+
},
258+
{
259+
"cell_type": "code",
260+
"execution_count": 52,
261+
"metadata": {},
262+
"outputs": [
263+
{
264+
"name": "stdout",
265+
"output_type": "stream",
266+
"text": [
267+
"Loaded AIConfig: NYC Trip Planner\n",
268+
"\n",
269+
"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",
270+
"Created and saved new AIConfig: \n",
271+
"\n"
272+
]
273+
}
274+
],
275+
"source": [
276+
"from aiconfig.Config import AIConfigRuntime\n",
277+
"from result import Result, Ok, Err\n",
278+
"from typing import Any\n",
279+
"\n",
280+
"from json import JSONDecodeError\n",
281+
"\n",
282+
"\n",
283+
"def read_json_from_file(path: str) -> Result[dict[str, Any], str]:\n",
284+
" \"\"\"Use `str` in the error case to contain a helpful error message.\"\"\"\n",
285+
" try:\n",
286+
" with open(path, \"r\") as f:\n",
287+
" return Ok(json.loads(f.read()))\n",
288+
" except FileNotFoundError:\n",
289+
" return Err(f\"File not found at path: {path}\")\n",
290+
" except OSError as e:\n",
291+
" return Err(f\"Could not read file at path: {path}: {e}\")\n",
292+
" except JSONDecodeError as e:\n",
293+
" return Err(f\"Could not parse JSON at path: {path}: {e}\")\n",
294+
"\n",
295+
"def start_app(path: str):\n",
296+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
297+
" file_contents = read_json_from_file(path)\n",
298+
" match file_contents:\n",
299+
" case Ok(aiconfig_ok):\n",
300+
" print(f\"Loaded AIConfig: {aiconfig_ok['name']}\\n\")\n",
301+
" case Err(e):\n",
302+
" print(f\"Could not load AIConfig from path: {path} ({e}). Creating and saving.\")\n",
303+
" aiconfig = AIConfigRuntime.create().model_dump(exclude=\"callback_manager\")\n",
304+
" # [Save to file path]\n",
305+
" # aiconfig.save(path)\n",
306+
" print(f\"Created and saved new AIConfig: {aiconfig['name']}\\n\")\n",
307+
"\n",
308+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")\n",
309+
"start_app(\"i-dont-exist-yet-please-create-me.json\")"
310+
]
311+
},
312+
{
313+
"cell_type": "markdown",
314+
"metadata": {},
315+
"source": [
316+
"There are several nice things about this pattern:\n",
317+
"* If you fail to check for the error case, you get static errors similar to the `None` Optional case\n",
318+
"* You also get specific, useful error information unlike Optional\n",
319+
"* Structural pattern matching: When matching the cases, you can elegantly and safely unbox the data inside the result.\n",
320+
"* 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."
321+
]
322+
},
323+
{
324+
"cell_type": "markdown",
325+
"metadata": {},
326+
"source": [
327+
"# Part 2: Composition"
328+
]
329+
},
330+
{
331+
"cell_type": "markdown",
332+
"metadata": {},
333+
"source": [
334+
"Cool, so we have a very basic example of better error handling. What about a more realistic level of complexity involving a sequence of chained operations? Consider this variant of the previous app example:"
335+
]
336+
},
337+
{
338+
"cell_type": "code",
339+
"execution_count": null,
340+
"metadata": {},
341+
"outputs": [],
342+
"source": [
343+
"import json\n",
344+
"from typing import Any\n",
345+
"\n",
346+
"def read_file(path: str) -> str:\n",
347+
" with open(path, \"r\") as f:\n",
348+
" return f.read()\n",
349+
" \n",
350+
" \n",
351+
"\n",
352+
"def read_json_from_file(path: str) -> dict[str, Any]:\n",
353+
" with open(path, \"r\") as f:\n",
354+
" return json.loads(f.read())\n",
355+
" \n",
356+
"\n",
357+
"def start_app(path: str):\n",
358+
" \"\"\"Load an AIConfig from a local path and do something with it.\"\"\"\n",
359+
" aiconfig = read_json_from_file(path)\n",
360+
" print(f\"Loaded AIConfig: {aiconfig['name']}\\n\")\n",
361+
"\n",
362+
"\n",
363+
"start_app(\"cookbooks/Getting-Started/travel.aiconfig.json\")"
364+
]
365+
}
366+
],
367+
"metadata": {
368+
"kernelspec": {
369+
"display_name": "aiconfig",
370+
"language": "python",
371+
"name": "python3"
372+
},
373+
"language_info": {
374+
"codemirror_mode": {
375+
"name": "ipython",
376+
"version": 3
377+
},
378+
"file_extension": ".py",
379+
"mimetype": "text/x-python",
380+
"name": "python",
381+
"nbconvert_exporter": "python",
382+
"pygments_lexer": "ipython3",
383+
"version": "3.10.13"
384+
}
385+
},
386+
"nbformat": 4,
387+
"nbformat_minor": 2
388+
}

0 commit comments

Comments
 (0)