Practical Business Python

Taking care of business, one python script at a time

Mon 02 December 2019

Building a Windows Shortcut with Python

Posted by Chris Moffitt in articles   

article header image

Introduction

I prefer to use miniconda for installing a lightweight python environment on Windows. I also like to create and customize Windows shortcuts for launching different conda environments in specific working directories. This is an especially useful tip for new users that are not as familiar with the command line on Windows.

After spending way too much time trying to get the shortcuts setup properly on multiple Windows machines, I spent some time automating the link creation process. This article will discuss how to use python to create custom Windows shortcuts to launch conda environments.

Launching Windows Environments

miniconda is great for streamlining the install of packages on Windows and using conda for environment management.

By default, miniconda tries to have as minimal an impact on your system as possible. For example, a default install will not add any python information to your default path, nor will it require admin privileges for installation. This is “a good thing” but it means that you need to do a couple of extra steps to get your python environment working from a standard Windows prompt. For new users this is just one more step in the python installation process.

Fortunately, Anaconda (fka Continuum) provides all the foundations to launch a powershell or command prompt with everything setup for your environment. In fact, the default install will create some shortcuts to do exactly that.

However, I had a hard time modifying these shortcuts to customize the working directory. Additionally, It’s really useful to automate a new user setup instead of trying to walk someone through this tedious process by hand. Hence, the need for this script to automate the process.

For the purposes of this article, I am only going to discuss using the command prompt approach to launching python. There is also a powershell option which is a little more complex but the same principals apply to both.

Once miniconda is installed, the preferred way to launch a python shell is to use miniconda’s activate.bat file to configure the shell environment. On my system (with a default miniconda install), the file is stored here: C:/Users/CMoffitt/AppData/Local/Continuum/miniconda3/Scripts/activate.bat

In addition, I recommend that you keep your conda base environment relatively lightweight and use another environment for your actual work. On my system, I have a work environment that I want to start up with this shortcut.

When conda creates a new environment on windows, the default directory location for the environment looks like this: C:/Users/CMoffitt/AppData/Local/Continuum/miniconda3/envs/work . You can pass this full path to the activate.bat file and it will launch for you and automatically start with the work environment activated.

The final piece of the launch puzzle is to use cmd.exe /K to run a command shell and return to a prompt once the shell is active.

The full command, if you were to type it, would look something like this:

cmd.exe /K C:/Users/CMoffitt/AppData/Local/Continuum/miniconda3/Scripts/activate.bat C:/Users/CMoffitt/AppData/Local/Continuum/miniconda3/envs/work

The overall concept is pretty straightforward. The challenge is that the paths get pretty long and we want to be smart about making sure we make this as future-proof and portable as possible.

Special Folders

The winshell module makes the process of working with Windows shortcuts a lot easier. This module has been around for a while and has not been updated recently but it worked just fine for me. Since it is a relatively thin wrapper over pywin32 there’s not much need to keep updating winshell.

For the purposes of this article, I used winshell to access special folders, create shortcuts and read shortcuts. The documentation is straightforward but still uses os.path for file path manipulations so I decided to update my examples to use pathlib. You can refer to my previous post for an intro to pathlib.

One of the useful aspects of winshell is that it gives you shortcuts to access special directories on Windows. It’s a best practice not to hard code paths but use the aliases that Windows provides. This way, your scripts should work seamlessly on someone else’s machine and work across different versions of Windows.

As shown above, the paths to our miniconda files are buried pretty deep and are dependent on the logged in user’s profile. Trying to hard code all this would be problematic. Talking a new user through the process can be challenging as well.

In order to demonstrate winshell, let’s get the imports in place:

import winshell
from pathlib import Path

If we want to get the user’s profile directory, we can use the folder function:

profile = winshell.folder('profile')

Which automatically figures out that it is:

'C:\\Users\\CMoffitt`

Winshell offers access to many different folders that can be accessed via their CSIDL(Constant Special ID List). Here is a list of CSIDLs for reference. As a side note, it looks like the CSIDL has been replaced with KNOWNFOLDERID but in my limited testing, the CSIDLs I’m using in this article are supported for backwards compatibility.

One of the things I like to do is use Pathlib to make some of the needed manipulations a little bit easier. In the example above, the profile variable is a string. I can pass the string to Path() which will make subsequent operations easier when building up our paths.

Let’s illustrate by getting the full path to my desktop using the convenience function available for the desktop folder:

desktop = Path(winshell.desktop())

Which looks like this now:

WindowsPath('C:/Users/CMoffitt/OneDrive-Desktop')

We can combine these folder approaches to get a location of the miniconda base directory.

miniconda_base = Path(winshell.folder('CSIDL_LOCAL_APPDATA')) / 'Continuum' / 'miniconda3')

If we want to validate that this is a valid directory:

miniconda_base.is_dir()
True

In my opionion this is much cleaner than trying to do a lot of os.path.join to build up the directory structure.

The other location we need is cmd.exe which we can get with CSIDL_SYSTEM .

win32_cmd = str(Path(winshell.folder('CSIDL_SYSTEM')) / 'cmd.exe')

You will notice that I converted the Path to a string by using str . I did this because winshell expects all of its inputs to be strings. It does not know how to handle a pathlib object directly. This is important to keep in mind when creating the actual shortcut in the code below.

Working with Shortcuts

When working with shortcuts on windows, you can right click on the shortcut icon and view the properties. Most people have probably seen something like this:

Properties

As you get really long command strings, it can be difficult to view in the GUI. Editing them can also get a little challenging when it comes to making sure quotes and escape characters are used correctly.

Winshell provides a dump function to make the actual shortcut properties easier to review.

For example, if we want to look at the existing shortcut in our start menu, we need to get the full path to the .lnk file, then create a shortcut object and display the values using dump .

lnk = Path(winshell.programs()) / "Anaconda3 (64-bit)" / "Anaconda Prompt (miniconda3).lnk"
shortcut = winshell.shortcut(str(lnk))
shortcut.dump()
{
C:\Users\CMoffitt\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Anaconda3 (64-bit)\Anaconda Prompt (miniconda3).lnk -> C:\Windows\System32\cmd.exe

arguments: "/K" C:\Users\CMoffitt\AppData\Local\Continuum\miniconda3\Scripts\activate.bat C:\Users\CMoffitt\AppData\Local\Continuum\miniconda3
description: Anaconda Prompt (miniconda3)
hotkey: 0
icon_location: ('C:\\Users\\CMoffitt\\AppData\\Local\\Continuum\\miniconda3\\Menu\\Iconleak-Atrous-Console.ico', 0)
path: C:\Windows\System32\cmd.exe
show_cmd: normal
working_directory: %HOMEPATH%
}

This is a simple representation of all the information we need to use to create a new shortcut link. In my experience this view can make it much easier to understand how to create your own.

Now that we know the information we need, we can create our own shortcut.

We will create our full argument string which includes cmd.exe /K followed by the activate.bat then the environment we want to start in:

arg_str = "/K " + str(miniconda_base / "Scripts" / "activate.bat") + " " + str(miniconda_base / "envs" / "work")

We also have the option of passing in an icon which needs to include a full path as well as the index for the icon.

For this example, I’m using the default icon that miniconda uses. Feel free to modify for your own usage.

icon = str(miniconda_base / "Menu" / "Iconleak-Atrous-Console.ico")

The final portion is to start in a specified working directory.

In my case, I have a My Documents/py_work directory that contains all my python code. We can use CSIDL_PERSONAL to access My Documents and build the full path to py_work .

my_working = str(Path(winshell.folder('CSIDL_PERSONAL')) / "py_work")

Now that all the variables are defined, we create a shortcut link on the desktop:

link_filepath = str(desktop / "python_working.lnk")
    with winshell.shortcut(link_filepath) as link:
        link.path = win32_cmd
        link.description = "Python(work)"
        link.arguments = arg_str
        link.icon_location = (icon, 0)
        link.working_directory = my_working

You should now see something like this on your desktop:

Properties

You can easily customize it to use your own directories and environments. It’s a short bit of code but in my opinion it is a lot easier to understand and customize than dealing with Windows shortcut files by hand.

Summary

Here is the full example for a creating a simple shortcut on your desktop that activates a working conda environment and starts in a specific working directory.

import winshell
from pathlib import Path

# Define all the file paths needed for the shortcut
# Assumes default miniconda install
desktop = Path(winshell.desktop())
miniconda_base = Path(
    winshell.folder('CSIDL_LOCAL_APPDATA')) / 'Continuum' / 'miniconda3'
win32_cmd = str(Path(winshell.folder('CSIDL_SYSTEM')) / 'cmd.exe')
icon = str(miniconda_base / "Menu" / "Iconleak-Atrous-Console.ico")

# This will point to My Documents/py_work. Adjust to your preferences
my_working = str(Path(winshell.folder('CSIDL_PERSONAL')) / "py_work")
link_filepath = str(desktop / "python_working.lnk")

# Build up all the arguments to cmd.exe
# Use /K so that the command prompt will stay open
arg_str = "/K " + str(miniconda_base / "Scripts" / "activate.bat") + " " + str(
    miniconda_base / "envs" / "work")

# Create the shortcut on the desktop
with winshell.shortcut(link_filepath) as link:
    link.path = win32_cmd
    link.description = "Python(work)"
    link.arguments = arg_str
    link.icon_location = (icon, 0)
    link.working_directory = my_working

I hope this script will save you just a little bit of time when you are trying to get your Windows system setup to run various conda environments. If you have any other favorite tips you use, let me know in the comments.

Comments