Quick Tip: Controlling macOS with Python
In this quick tip, excerpted from Useful Python, Stuart looks at ways to control the Windows OS with Python.
Working on a Mac, we can control almost everything about the system using pyobjc, the Python-to-Objective-C bridge. Apple makes most of its OS controllable via the AppKit module, and pyobjc
gives Python access to all of this. This will be most useful if we already know the AppKit way to do the thing we want, but with a little exploration it’s possible to make our way through the operating system APIs.
Let’s try an example. First, we’ll need pyobjc
, which can be installed with pip install pyobjc
. This will install a whole list of operating system API bridges, allowing access to all sorts of aspects of macOS. For now, we’ll consider AppKit, which is the tool used to build and control running apps on a Mac desktop.
We can list all the applications currently running using AppKit:
Python 3.9.6 (default, Oct 18 2022, 12:41:40)
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from AppKit import NSWorkspace
>>> NSWorkspace.sharedWorkspace().runningApplications()
(
"<NSRunningApplication: 0x60000145c000 (com.apple.loginwindow - 148) LSASN:{hi=0x0;lo=0x6006}>",
"<NSRunningApplication: 0x60000145c080 (com.apple.backgroundtaskmanagement.agent - 475) LSASN:{hi=0x0;lo=0xb00b}>",
"<NSRunningApplication: 0x60000145c100 (com.apple.WindowManager - 474) LSASN:{hi=0x0;lo=0xc00c}>",
"<NSRunningApplication: 0x60000145c180 (com.apple.CoreLocationAgent - 500) LSASN:{hi=0x0;lo=0xe00e}>",
"<NSRunningApplication: 0x60000145c980 (com.apple.Terminal - 1302) LSASN:{hi=0x0;lo=0x24024}>",
"<NSRunningApplication: 0x60000145ca00 (com.apple.Safari - 1303) LSASN:{hi=0x0;lo=0x25025}>",
"<NSRunningApplication: 0x60000145cb80 (com.apple.Spotlight - 1310) LSASN:{hi=0x0;lo=0x28028}>",
"<NSRunningApplication: 0x60000145cc00 (com.apple.finder - 1306) LSASN:{hi=0x0;lo=0x29029}>",
)
>>>
This will give a long list of NSRunningApplication
objects. Each one corresponds to a specific application currently running on the desktop. Many are “invisible” applications (things that are running but aren’t necessarily showing a window), but others are things that we might think of as actual applications that we can see—such as Safari, Terminal, and so on. NSRunningApplication
is documented at developer.apple.com, where its properties can be seen. For example, each application has a localizedName
and a bundleIdentifier
:
>>> for nsapp in NSWorkspace.sharedWorkspace().runningApplications():
... print(f"{nsapp.localizedName()} -> {nsapp.bundleIdentifier()}")
...
loginwindow -> com.apple.loginwindow
BackgroundTaskManagementAgent -> com.apple.backgroundtaskmanagement.agent
WindowManager -> com.apple.WindowManager
CoreLocationAgent -> com.apple.CoreLocationAgent
Terminal -> com.apple.Terminal
Safari -> com.apple.Safari
Spotlight -> com.apple.Spotlight
Finder -> com.apple.finder
We can also see that a NSRunningApplication
object has an activate function, which we can call to activate that app as though we had clicked its icon in the Dock. So, to find Safari and then activate it, we would use that activate function. The call to activate
requires a value for options
, as the documentation describes, and that also needs to be imported from AppKit:
>>> from AppKit import NSWorkspace, NSApplicationActivateIgnoringOtherApps
>>> safari_list = [x for x in NSWorkspace.sharedWorkspace().runningApplications()
if x.bundleIdentifier() == 'com.apple.Safari']
>>> safari = safari_list[0]
>>> safari.activateWithOptions_(NSApplicationActivateIgnoringOtherApps)
Now Safari is activated.
Finding Python Versions of macOS APIs
Finding the name of something in Python that corresponds to the Objective-C name can be a little tricky. As shown in the code above, the Objective-C activate
function is called activateWithOptions_
in Python. There’s a set of rules for this name translation, which the pyobjc documentation explains, but it can sometimes be quicker to use Python’s own dir()
function to show all the properties of an object and then pick out the one that looks most plausible:
>>> print(len(dir(safari)))
452
Ouch! Our safari
instance of an NSRunningApplication
has 452 properties! Well, the one we want is probably called something like “activate”, so:
>>> print([x for x in dir(safari) if "activate" in x.lower()])
['activateWithOptions_', 'activateWithOptions_']
Aha! So activateWithOptions_
is the name of the function we need to call. Similarly, the name of the option we want to pass to that function is in AppKit itself:
>>> [x for x in dir(AppKit) if "ignoringotherapps" in x.lower()]
['NSApplicationActivateIgnoringOtherApps']
This process can feel a little exploratory at times, but it’s possible to do anything that Objective-C can do from Python as well.
This article is excerpted from Useful Python, available on SitePoint Premium and from ebook retailers.