Reducing Assumptions, Exploding Your Code | Rye blog
Table of Contents
Elegant scripts
Our nice, worldly example
Python Version
Rye version
What are we assuming?
Adding Basic Validation (Step 2)
Python Version
Rye Version
Full Error Handling (Step 3)
Python Version
Rye Version
But … magic
The point
Elegant scripts#
We’ve all written them, they fall together perfectly, they are readable, but they assume the happy paths. And world can be a happy place, but it’s also deeply flawed, imperfect and<br>for your code to function in such world, even add value … it must handle the imperfection.
Our nice, worldly example#
Our script will accept an ID as an argument. It will find an API token in setup.json and make a request to download a PDF from a remote server. The name of the downloaded file is determined by the server.
It’s simple, but a little realistic-ly messy, but hey - we are programmers, this is what we do, this is what we thrive at, right :) … right :I (thinks of all the vibeco…)
Python Version#
Python is the lingua franca of programming. Let’s go!
import sys, json, requests, re<br>from requests.auth import HTTPBasicAuth
id = int(sys.argv[1])
with open('setup.json') as f:<br>setup = json.load(f)
url = f"https://www.example.com/pdf-api?id={id}"<br>resp = requests.get(url, auth=HTTPBasicAuth(setup['token'], 'x'))
pattern = re.compile(r"filename\*?=[f']?(.*?)[']?(?:;?$)")<br>content_disp = resp.headers['Content-Disposition']<br>filename = pattern.search(content_disp).group(1)
with open(filename, 'wb') as f:<br>f.write(resp.content)
It’s a perfect little script. Each block of code does one thing, each one is few lines, there is no unneded structure or boilerplate really - I like it.
Rye version#
Since this is a Rye language blog, we will also write this in Rye :)
rye .Args? .load .first :id<br>Load %setup.rye |context :setup
re: regexp "filename\*?=[f']?(.*?)[']?(?:;?$)"
format id https://www.example.com/pdf-api?id=%d<br>|Request 'GET ""<br>|Basic-auth! setup/token "x"<br>|Call :resp<br>|Header? "Content-Disposition" |Submatch?* re<br>|file .Create<br>|Copy* Reader resp
Ok, a little different, but similar.
What are we assuming?#
script always gets one integer argument
setup file exists and has correct content
HTTP request never fails
Content-Disposition header is always present with a filename
We can always create a new file
As Eugene Lewis Fordsworthe would say - that’s a lot of …, assumptions :(
Adding Basic Validation (Step 2)#
I can be like watered down version of Eugene:
“User input is the source of many problems”.
No user input, no problems - but we need them users. So let’s validate those inputs.
Python Version#
We will now:
check the number of arguments
check if ID is integer
check if setup has token value defined
import sys, json, requests, re<br>from requests.auth import HTTPBasicAuth
if len(sys.argv) != 2:<br>raise ValueError("script argument id - expected exactly one integer")
try:<br>id = int(sys.argv[1])<br>except ValueError:<br>raise ValueError("script argument id - must be an integer")
with open('setup.json') as f:<br>setup = json.load(f)
if 'token' not in setup or not isinstance(setup['token'], str):<br>raise ValueError("loading setup - token field required as string")
url = f"https://www.example.com/pdf-api?id={id}"<br>resp = requests.get(url, auth=HTTPBasicAuth(setup['token'], 'x'))
pattern = re.compile(r"filename\*?=[f']?(.*?)[']?(?:;?$)")<br>content_disp = resp.headers['Content-Disposition']<br>filename = pattern.search(content_disp).group(1)
with open(filename, 'wb') as f:<br>f.write(resp.content)
We added those few checks and if you ask me (I am partial to this), the elegant, readable script is already gone. That’s one of the reasons I loathe try/catch approach. It adds structure that disrupts the flow of code.
Rye Version#
rye .Args? .validate { integer }<br>|check "script argument id" |first :id
Load %setup.rye |context |validate { token: required string }<br>|check "setup file" :setup
re: regexp "filename\*?=[f']?(.*?)[']?(?:;?$)"
format id https://www.example.com/pdf-api?id=%d<br>|Request 'GET ""<br>|Basic-auth! setup/token "x"<br>|Call :resp<br>|Header? "Content-Disposition" |Submatch?* re<br>|file .Create .defer\ 'Close<br>|Copy* resp .Reader .defer\ 'Close
We used validation dialect for the arguments and config. And .defer\ 'Close to ensure resources (the file writer and HTTP stream reader - no copying to memory btw) are cleaned up.
Script got a little more complex, but structure and flow of it didn’t change.
Full Error Handling (Step 3)#
Now let’s handle all the failures, and provide helpful feedback to the user in case it fails. Our initially elegant script, exploded into this … :o
Python Version#
We now also check for:
does the setup.json exist
can we parse the setup.json’s JSON
did the HTTP request succeed
we provide the default filename if there is no Content-Disposition
can we create a new file
can we write PDF to...