from Guide to Hacking on Jun 18, 2023
How a Python-only deeplink works
Setting up deeplinks in a desktop application is generally no easy task, especially for these newer classes of browser-based apps. Some popular frameworks such as Electron have a commonly-used extension for deeplinks, but others may not. How do they work? And how can you implement one yourself, for Python?
Deeplinks, also called custom URL protocols, allow anyone to link to specific locations within an application. For example, by clicking on a "Launch" button in the browser, you can launch a specific workspace in your Slack desktop app or a specific project in your Figma desktop app.
In this post, we'll cover how deeplinks work. To simplify implementation, we'll use Python instead of Swift and Xcode to implement deep linking. For our demos in this post, we'll specifically implement this on a Mac, but the concepts well discuss generalize to other operating systems and languages as well.
Deeplink to launch your application
Start with the usual project initialization steps. Create and navigate to a directory for your project.
mkdir -p ~/Desktop/deeplink/webview
cd ~/Desktop/deeplink
Next, create a virtual environment in this directory.
python3 -m venv env
source env/bin/activate
Navigate to the webview
subdirectory, where we'll start our pywebview-based application.
cd ~/Desktop/deeplink/webview
Here, we'll initialize a "Hello World" desktop application, just like we did in How a Python-only desktop app works. Install a few prerequisite libraries.
pip install pywebview py2app
Create an app.py
.
import webview
if __name__ == '__main__':
window = webview.create_window('Hello world', 'https://alvinwan.com/blog')
webview.start()
Let's now specify the deeplink for our application. On a Mac, deeplinks require only three properties:
- The custom URL protocol (a.k.a., deeplink prefix) for your application must be specified in your application's
Info.plist
. - The application must be located in the
/Applications
folder on your computer1. - The application must have been opened at least once, normally, without deeplinks.
Let's handle the first requirement, by specifying a custom URL protocol in py2app
's build configuration. Modify your setup.py
script to match the following.
from setuptools import setup
OPTIONS = {
'plist': {
'CFBundleURLTypes': [ # for custom URL schemes (i.e., deeplinks)
{
'CFBundleURLName': 'MyApplication', # arbitrary
'CFBundleURLSchemes': ['myapp'], # deeplink will be myapp://
},
],
},
}
setup(
app=['app.py'], # your main application
options={'py2app': OPTIONS},
setup_requires=['py2app', 'pywebview'], # add other dependencies here
)
Let's now build our desktop application. Optionally check with our reference implementation. To build the desktop application, use the py2app
module. We'll add back the -A
flag for now, so that building runs much faster.
python setup.py py2app -A
Next, move the built application to the ~/Applications
folder.
mv ./dist/app.app /Applications
Open the application once first, without the deeplink.
open /Applications/app.app
Now, you can test your deeplink by opening myapp://whatever_you_like_here
. You can open this in your browser, or by typing the following in your terminal on Mac.
open myapp://whatever_you_like_here
Either way, your desktop application should now pop open, with the following preview.
Right now, although the deeplink does open our application, our application doesn't have access to the contents of our deeplink. Let's now modify our desktop application so that it can read and use the deeplink itself.
Start by modifying the configuration in setup.py
, so that deeplinks are passed to the application as though they were arguments.
OPTIONS = {
'argv_emulation': True, # pass sys.argv to the app
Then, from your app.py
, display the deeplink in the window title, by reading and displaying file arguments.
import webview
import sys
if __name__ == '__main__':
window = webview.create_window(str(sys.argv[1:]), 'https://alvinwan.com/blog')
webview.start()
Optionally check with our reference implementation. Repeat the same process to build your application.
python setup.py py2app -A
rm -rf /Applications/app.app
mv dist/app.app /Applications
Open the deeplink.
open myapp://whatever_you_want_here
This should open in your desktop application, with the deeplink now displayed in your window title.
You can now use this deeplink to open a specific page or perform a particular action. With that said, since the deeplink is passed as command line arguments, there's no clear way to access a deeplink that's opened after the application launches. To support this, we'll need a slightly deeper understanding of how deep links work.
How deeplinks work
Deeplinks are just one of several possible events3 sent to the application. The particular event we care about is called a "File Open" event, because at its core, this event is what enables deeplinking.
In particular, the "File Open" event can be sent to our application for one of two reasons — to open a resource with a custom protocol or to open a file with a custom file extension:
- protocols: A protocol is the prefix for a Uniform Resource Identifier (URI). For example,
https://
is a very familiar prefix and protocol, that specifies a resource on the internet; your operating system knows how to handle such a protocol, by default. You can also configure how to handle custom protocols, such asmyapp://
. Any request to open a URI that starts with this custom protocol would be sent to your application. - file extensions: A file extension is the suffix for a filename. For example,
.jpg
is a familiar suffix and extension that specifies a JPEG-encoded image — again, your operating system knows how to handle such a file extension, by default. You can configure how to handle custom file extensions such.myapp
. Any request to open a file that ends in this custom extension would be sent to your application.
To deeplink, you would register your application with your operating system to handle a custom protocol like myapp://
or a custom file extension like .myapp
. Then, once such a resource is accessed, your application will receive a "File Open" event with either a URI or a path.
Above, these details are abstracted away from us by py2app
. However, to handle deeplinks even after the application has launched, we now need to handle this incoming "File Open" event.
Deeplink after launch
As we discussed in How a Python-only desktop app works, the library pywebview that we're using is powered by several engines under the hood. One of those engines is pyqt, which we'll use directly, for a simple starting demo. pyqt supports event handlers by default.
Create and navigate to a subdirectory for this version of your project.
mkdir -p ~/Desktop/deeplink/qt
cd ~/Desktop/deeplink/qt
Install the GUI library PyQt.
pip install qtpy==2.3.1 pyqt6==6.5.1
Copy your last setup.py
file, except drop the argv_emulation
flag. We will now rely on pyqt instead of our packaging tool py2app to handle system events. This way, our pyqt event handler will receive the "File open" event.
from setuptools import setup
OPTIONS = { # notice argv_emulation has been removed
'plist': {
'CFBundleURLTypes': [ # for custom URL schemes (i.e., deeplinks)
{
'CFBundleURLName': 'MyApplication', # arbitrary
'CFBundleURLSchemes': ['myapp'], # deeplink will be myapp://
},
],
},
}
setup(
app=['app.py'],
options={'py2app': OPTIONS},
setup_requires=['py2app', 'qtpy', 'pyqt6'], # add pyqt to dependencies
)
Then, create a new file app.py
for our initial test. This is a brand new desktop application written in raw pyqt.
from qtpy.QtWidgets import QApplication
from qtpy.QtCore import QEvent
class MyApp(QApplication): # subclass application
def event(self, event: QEvent): # custom event handler
if event.type() == QEvent.Type.FileOpen: # handle "File Open" event
print(event.url()) # log the deeplink to console
return super().event(event)
if __name__ == "__main__":
app = MyApp([])
app.exec()
Check with our reference implementation. Run through the familiar steps to build and move the application.
python setup.py py2app -A
rm -rf /Applications/app.app
mv dist/app.app /Applications
This time, open the application's packaged binary directly, so that we can see the outputs printed to console.
/Applications/app.app/Contents/MacOS/app
Note that our application doesn't have a window; it's just a minimal GUI-less "application" that is listening for deeplink opens. Then, open a second terminal window. In this new session, access the deeplink.
open myapp://whatever_you_want_here
In your first terminal window, you should see the following printed.
output
PyQt5.QtCore.QUrl('myapp://whatever_you_want_here')
Success! You can now handle the deeplink in the above event handler to perform whatever action you wish. However, this implementation uses pyqt; we'd still like to retain the benefit of a browser-based desktop application. To achieve that, we'll now add this event handler to pywebview.
Deeplink into a browser-based application
We'll now implement the same manual event handling from How a Python-only desktop app works. This will enable us to override PyQt's default event handling while still benefiting from pywebview.
Navigate back to your pywebview-based project in webview/
.
cd ~/Desktop/deeplink/webview
Install the PyQt web engine for pywebview to use.
pip install PyQt6-WebEngine==6.5.0
Finally, update your setup.py
script to include the new GUI packages.
setup_requires=['py2app', 'pywebview', 'qtpy', 'pyqt6', 'PyQt6-WebEngine'], # add pyqt to dependencies
In app.py
, update your imports to include PyQt and friends.
import sys
import webview
from qtpy.QtWidgets import QApplication
from qtpy.QtCore import QEvent
import webview.platforms.qt # needed for us to define a custom application
Still in app.py
, Create a new application that contains a custom event handler for the "File Open" event.
class MyApp(QApplication):
def event(self, event: QEvent):
if event.type() == QEvent.Type.FileOpen:
print(event.url())
return super(QApplication, self).event(event)
Instantiate your custom application, and change the GUI engine to Qt.
if __name__ == '__main__':
app = MyApp(sys.argv)
webview.create_window('Hello world', 'https://alvinwan.com/blog')
webview.start(gui='qt')
Check against our reference implementation. We'll now test this in the same way we did above: Build, move, and open the application.
python setup.py py2app -A
rm -rf /Applications/app.app
mv dist/app.app /Applications
/Applications/app.app/Content/MacOS/app
In a second terminal window, open the deeplink.
open myapp://whatever_you_want_here
In your first terminal window, you should see the following printed.
output
PyQt6.QtCore.QUrl('myapp://whatever_you_want_here')
Success again! You now have proper deeplink support for your browser-based desktop application; your application can now handle deeplinks both at launch and after launch.
Conclusion
In this post, we discussed how deeplinking works and implemented a minimal, Python-only implementation: We started with a simple demo that only supported deeplinks to launch the application, then built up to a usable form of deeplinks that work even after the application has launched. Your takeaways are two-fold:
-
First, there are three requirements for a deeplink to take effect.
- The custom URL protocol (a.k.a., deeplink prefix) for your application must be specified in your application's
Info.plist
. - The application must be located in the
/Applications
folder on your computer. - The application must have been opened at least once, normally, without deeplinks.
- The custom URL protocol (a.k.a., deeplink prefix) for your application must be specified in your application's
-
Deeplinks work by registering your application as an event handler for a custom protocol. This terminology is useful to know, when looking for the right documentation and tooling.
That's a wrap on Python-only desktop applications with deeplinking support. You can find the source code for this tutorial, as well as the finished code, on my "Guide to Hacking" repository.
-
Technically, the application doesn't need to be in the
/Applications
folder. According to SO, the application simply needs to be moved. ↩ -
There's a small caveat here. If you modify the configuration for the desktop application, then you'll need to rebuild the application. For example, if you want to rename the deeplink, you'd need to rebuild. ↩
-
An event is more formally an interprocess message. In this case, another application sends this message, and the operating system determines that your application should receive this message. You can learn more from official Apple documentation for Apple events. ↩
Want more tips? Drop your email, and I'll keep you in the loop.