Rhet Turnbullshared thisshort script for looking up the named timezone for a given location from Python on macOS usingobjc
and theCoreLocation
framework. It uses theobjc
package andpyobjc-framework-CoreLocation.
This piqued my interest, so Iconversed with Claude about other things I could do with that same framework. Here's the script we came up with, for geocoding an address passed to it using Core Location'sCLGeocoder.geocodeAddressString()
method:
# /// script# requires-python = ">=3.12"# dependencies = [# "pyobjc-core",# "pyobjc-framework-CoreLocation",# "click"# ]# ///"""Basic geocoding using CoreLocation on macOS."""importclickimportobjcfromCoreLocationimportCLGeocoderfromFoundationimportNSRunLoop,NSDatedefforward_geocode(address:str)->list[dict]:withobjc.autorelease_pool():geocoder=CLGeocoder.alloc().init()results= {"placemarks": [],"error":None}completed=Falsedefcompletion(placemarks,error):nonlocalcompletediferror:results["error"]=error.localizedDescription()elifplacemarks:results["placemarks"]=placemarkscompleted=Truegeocoder.geocodeAddressString_completionHandler_(address,completion)whilenotcompleted:NSRunLoop.currentRunLoop().runMode_beforeDate_("NSDefaultRunLoopMode",NSDate.dateWithTimeIntervalSinceNow_(0.1) )ifresults["error"]:raiseException(f"Geocoding error:{results['error']}")return [{"latitude":pm.location().coordinate().latitude,"longitude":pm.location().coordinate().longitude,"name":pm.name(),"locality":pm.locality(),"country":pm.country() }forpminresults["placemarks"]]@click.command()@click.argument('address')defmain(address):try:locations=forward_geocode(address)forlocinlocations:click.echo("\nLocation found:")forkey,valueinloc.items():ifvalue:click.echo(f"{key}:{value}")exceptExceptionase:click.echo(f"Error:{e}",err=True)raiseclick.Abort()if__name__=="__main__":main()
This can be run usinguv run
like this:
uv run geocode.py'500 Grove St, San Francisco, CA'
Example output:
Location found:latitude: 37.777717longitude: -122.42504name: 500 Grove Stlocality: San Franciscocountry: United States
I tried this without a network connection and it failed, demonstrating that Core Location uses some form of network-based API to geocode addresses.
There are a few new-to-me tricks in this script.
with objc.autorelease_pool()
is a neatmemory management pattern provided by PyObjC for establishing an autorelease memory pool for the duration of a Pythonwith
block. Everything allocated by Objective C should be automatically cleaned up at the end of that block.
ThegeocodeAddressString
method takes a completion handler. In this code we're setting that to a Python function that communicates state using shared variables:
results= {"placemarks": [],"error":None}completed=Falsedefcompletion(placemarks,error):nonlocalcompletediferror:results["error"]=error.localizedDescription()elifplacemarks:results["placemarks"]=placemarkscompleted=True
We start that running like so:
geocoder=CLGeocoder.alloc().init()geocoder.geocodeAddressString_completionHandler_(address,completion)
Then the clever bit:
whilenotcompleted:NSRunLoop.currentRunLoop().runMode_beforeDate_("NSDefaultRunLoopMode",NSDate.dateWithTimeIntervalSinceNow_(0.1) )
Where did this code come from? It turns out Claude lifted that from the Rhet Turnbull script I fed into it earlier. Here'sthat code with Rhet's comments:
WAIT_FOR_COMPLETION=0.01# wait time for async completion in seconds# ...# reverseGeocodeLocation_completionHandler_ is async so run the event loop until completion# I usually use threading.Event for this type of thing in pyobjc but the the thread blocked foreverwaiting=0whilenotcompleted:NSRunLoop.currentRunLoop().runMode_beforeDate_("NSDefaultRunLoopMode",NSDate.dateWithTimeIntervalSinceNow_(WAIT_FOR_COMPLETION), )waiting+=WAIT_FOR_COMPLETIONifwaiting>=COMPLETION_TIMEOUT:raiseTimeoutError(f"Timeout waiting for completion of reverseGeocodeLocation_completionHandler_:{waiting} seconds" )
Is this the best pattern for my own, simpler script? I don't know for sure, but it works. Approach with caution!
Since my script has inline script dependencies and I'vepublished it to a Gist you can run it directly withuv run
without first installing anything else like this:
uv run https://gist.githubusercontent.com/simonw/178ea93ac035293744bde97270d6a7a0/raw/88c817e4103034579ec7523d8591bf60aa11fa67/geocode.py \'500 Grove St, San Francisco, CA'
Created 2025-01-26T09:25:48-08:00 ·Edit