Python: Getting AppData folder in a cross-platform way - python

I'd like a code snippet that gets the proper directory for app data (config files, etc) on all platforms (Win/Mac/Linux at least). For example: %APPDATA%/ on Windows.

If you don't mind using the appdirs module, it should solve your problem. (cost = you either need to install the module or include it directly in your Python application.)

Qt's QStandardPaths documentation lists paths like this.
Using Python 3.8
import sys
import pathlib
def get_datadir() -> pathlib.Path:
"""
Returns a parent directory path
where persistent application data can be stored.
# linux: ~/.local/share
# macOS: ~/Library/Application Support
# windows: C:/Users/<USER>/AppData/Roaming
"""
home = pathlib.Path.home()
if sys.platform == "win32":
return home / "AppData/Roaming"
elif sys.platform == "linux":
return home / ".local/share"
elif sys.platform == "darwin":
return home / "Library/Application Support"
# create your program's directory
my_datadir = get_datadir() / "program-name"
try:
my_datadir.mkdir(parents=True)
except FileExistsError:
pass
The Python documentation recommends the sys.platform.startswith('linux') "idiom" for compatibility with older versions of Python that returned things like "linux2" or "linux3".

You can use the following function to get user data dir, tested in linux and w10 (returning AppData/Local dir) it's adapted from the appdirs package:
import sys
from pathlib import Path
from os import getenv
def get_user_data_dir(appname):
if sys.platform == "win32":
import winreg
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
dir_,_ = winreg.QueryValueEx(key, "Local AppData")
ans = Path(dir_).resolve(strict=False)
elif sys.platform == 'darwin':
ans = Path('~/Library/Application Support/').expanduser()
else:
ans=Path(getenv('XDG_DATA_HOME', "~/.local/share")).expanduser()
return ans.joinpath(appname)

I recommend researching the locations of 'appdata' in the operating systems that you want to use this program on. Once you know the locations you could simple use if statements to detect the os and do_something().
import sys
if sys.platform == "platform_value":
do_something()
elif sys.platform == "platform_value":
do_something()
System: platform_value
Linux (2.x and 3.x): 'linux2'
Windows: 'win32'
Windows/Cygwin: 'cygwin'
Mac OS X: 'darwin'
OS/2: 'os2'
OS/2 EMX: 'os2emx'
RiscOS: 'riscos'
AtheOS: 'atheos'
List is from the official Python docs. (Search for 'sys.platform')

You can use module called appdata:
pip install appdata
from appdata import AppDataPaths
app_paths = AppDataPaths()
app_paths.app_data_path # cross-platform path to AppData folder
Original question - How can I get the path to the %APPDATA% directory in Python?
Original answer - https://stackoverflow.com/a/70411576/9543830

I came across a similar problem and I wanted to dynamically resolve all of the Windows % paths without knowing about them prior. You can use os.path.expandvars to resolve the paths dynamically. Something like this:
from os import path
appdatapath = '%APPDATA%\MyApp'
if '%' in appdatapath:
appdatapath = path.expandvars(appdatapath)
print(appdatapath)
The line at the end will print: C:\Users\\{user}\AppData\Roaming\MyApp
This works for windows however I have not tested on Linux. So long as the paths are defined by the environment than expandvars should be able to find it. You can read more about expand vars here.

If you are after the config directory here is a solution that defaults to ~/.config unless a platform specific (sys.platform) entry is available in the method's local platforms: dict
from sys import platform
from os.path import expandvars, join
def platform_config_directory() -> str:
'''
Platform config directory
Entries available in local platforms dict use the current
"best practice" location for config directories.
Default: $HOME/.config (XDG Base Directory Specification)
https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
'''
home: str = expandvars('$HOME')
platforms: dict = {
"win32": expandvars('%AppData%'),
"darwin": join(home, 'Library', 'Application Support'),
}
if platform in platforms:
return platforms[platform]
return join(home, '.config')
This works on windows, mac and linux, however allows for easier expansion given the need.

Related

How should I create and read a user editable configuration file in ~/.config or similar?

I am planning a command line Python application that I intend to distribute through PyPi.
When the application is installed with pip, I want to create a user-editable configuration file in the appropriate location on the user's filesystem.
For example, in Ubuntu, the file would be something like ~/.config/foo/config.ini
On installation I want to create the file (if possible) and be able to specify another config file to use instead with a command line parameter.
What is the usual scheme for getting this done?
I think appdirs package on PyPI is what you need, isn’t it?
You should use the appdirs module for this as it reliably handles differences across different platforms:
>>> from appdirs import user_config_dir
>>> appname = "SuperApp"
>>> appauthor = "Acme"
>>> user_config_dir(appname)
'/home/trentm/.config/SuperApp'
A project such as platformdirs can help with such a task. It implements Freedesktop's "XDG Base Directory Specification".
Don't create the file at installation time though. It is preferable to create the file when it is actually needed, this might be during the first run of the application for example.
Something like the following should get you started with the configuration directory:
>>> import platformdirs
>>> platformdirs.user_config_dir('foo')
'/home/sinoroc/.config/foo'
Or without external dependency, it could roughly look like this:
#!/usr/bin/env python3
import argparse
import os
import pathlib
import platform
def get_config_file_path(project_name, file_name):
path = None
config_path = None
platform_system = platform.system()
if platform_system == 'Windows':
if 'APPDATA' in os.environ:
config_path = pathlib.Path(os.environ['APPDATA'])
else:
config_path = pathlib.Path.home().joinpath('AppData', 'Roaming')
elif platform_system == 'Linux':
if 'XDG_CONFIG_HOME' in os.environ:
config_path = pathlib.Path(os.environ['XDG_CONFIG_HOME'])
else:
config_path = pathlib.Path.home().joinpath('.config')
if config_path:
path = config_path.joinpath(project_name, file_name)
return path
def main():
default_config_file_path = get_config_file_path('foo', 'config.ini')
args_parser = argparse.ArgumentParser()
args_parser.add_argument(
'--config', '-c',
default=str(default_config_file_path),
type=argparse.FileType('r'),
)
args = args_parser.parse_args()
if __name__ == '__main__':
main()

python - Finding the user's "Downloads" folder

I already found this question that suggests to use os.path.expanduser(path) to get the user's home directory.
I would like to achieve the same with the "Downloads" folder. I know that this is possible in C#, yet I'm new to Python and don't know if this is possible here too, preferable platform-independent (Windows, Ubuntu).
I know that I just could do download_folder = os.path.expanduser("~")+"/Downloads/", yet (at least in Windows) it is possible to change the Default download folder.
from pathlib import Path
downloads_path = str(Path.home() / "Downloads")
This fairly simple solution (expanded from this reddit post) worked for me
import os
def get_download_path():
"""Returns the default downloads path for linux or windows"""
if os.name == 'nt':
import winreg
sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key:
location = winreg.QueryValueEx(key, downloads_guid)[0]
return location
else:
return os.path.join(os.path.expanduser('~'), 'downloads')
The GUID can be obtained from Microsoft's KNOWNFOLDERID docs
This can be expanded to work more generically other directories
For python3+ mac or linux
from pathlib import Path
path_to_download_folder = str(os.path.join(Path.home(), "Downloads"))
Correctly locating Windows folders is somewhat of a chore in Python. According to answers covering Microsoft development technologies, such as this one, they should be obtained using the Vista Known Folder API. This API is not wrapped by the Python standard library (though there is an issue from 2008 requesting it), but one can use the ctypes module to access it anyway.
Adapting the above answer to use the folder id for downloads shown here and combining it with your existing Unix code should result in code that looks like this:
import os
if os.name == 'nt':
import ctypes
from ctypes import windll, wintypes
from uuid import UUID
# ctypes GUID copied from MSDN sample code
class GUID(ctypes.Structure):
_fields_ = [
("Data1", wintypes.DWORD),
("Data2", wintypes.WORD),
("Data3", wintypes.WORD),
("Data4", wintypes.BYTE * 8)
]
def __init__(self, uuidstr):
uuid = UUID(uuidstr)
ctypes.Structure.__init__(self)
self.Data1, self.Data2, self.Data3, \
self.Data4[0], self.Data4[1], rest = uuid.fields
for i in range(2, 8):
self.Data4[i] = rest>>(8-i-1)*8 & 0xff
SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath
SHGetKnownFolderPath.argtypes = [
ctypes.POINTER(GUID), wintypes.DWORD,
wintypes.HANDLE, ctypes.POINTER(ctypes.c_wchar_p)
]
def _get_known_folder_path(uuidstr):
pathptr = ctypes.c_wchar_p()
guid = GUID(uuidstr)
if SHGetKnownFolderPath(ctypes.byref(guid), 0, 0, ctypes.byref(pathptr)):
raise ctypes.WinError()
return pathptr.value
FOLDERID_Download = '{374DE290-123F-4565-9164-39C4925E467B}'
def get_download_folder():
return _get_known_folder_path(FOLDERID_Download)
else:
def get_download_folder():
home = os.path.expanduser("~")
return os.path.join(home, "Downloads")
A more complete module for retrieving known folders from Python is available on github.
Some linux distributions localize the name of the Downloads folder. E.g. after changing my locale to zh_TW, the Downloads folder became /home/user/下載. The correct way on linux distributions (using xdg-utils from freedesktop.org) is to call xdg-user-dir:
import subprocess
# Copy windows part from other answers here
try:
folder = subprocess.run(["xdg-user-dir", "DOWNLOAD"],
capture_output=True, text=True).stdout.strip("\n")
except FileNotFoundError: # if the command is missing
import os.path
folder = os.path.expanduser("~/Downloads") # fallback
Note that the use of capture_output requires Python ≥3.7.
If you already use GLib or don't mind adding more dependencies, see also
these approaches using packages.
For python3 on windows try:
import os
folder = os.path.join(os.path.join(os.environ['USERPROFILE']), 'folder_name')
print(folder)

How to create a shortcut in startmenu using setuptools windows installer

I want to create a start menu or Desktop shortcut for my Python windows installer package. I am trying to follow https://docs.python.org/3.4/distutils/builtdist.html#the-postinstallation-script
Here is my script;
import sys
from os.path import dirname, join, expanduser
pyw_executable = sys.executable.replace('python.exe','pythonw.exe')
script_file = join(dirname(pyw_executable), 'Scripts', 'tklsystem-script.py')
w_dir = expanduser(join('~','lsf_files'))
print(sys.argv)
if sys.argv[1] == '-install':
print('Creating Shortcut')
create_shortcut(
target=pyw_executable,
description='A program to work with L-System Equations',
filename='L-System Tool',
arguments=script_file,
workdir=wdir
)
I also specified this script in scripts setup option, as indicated by aforementioned docs.
Here is the command I use to create my installer;
python setup.py bdist_wininst --install-script tklsystem-post-install.py
After I install my package using created windows installer, I can't find where my shorcut is created, nor I can confirm whether my script run or not?
How can I make setuptools generated windows installer to create desktop or start menu shortcuts?
Like others have commented here and elsewhere the support functions don't seem to work at all (at least not with setuptools). After a good day's worth of searching through various resources I found a way to create at least the Desktop shortcut. I'm sharing my solution (basically an amalgam of code I found here and here). I should add that my case is slightly different from yasar's, because it creates a shortcut to an installed package (i.e. an .exe file in Python's Scripts directory) instead of a script.
In short, I added a post_install function to my setup.py, and then used the Python extensions for Windows to create the shortcut. The location of the Desktop folder is read from the Windows registry (there are other methods for this, but they can be unreliable if the Desktop is at a non-standard location).
#!/usr/bin/env python
import os
import sys
import sysconfig
if sys.platform == 'win32':
from win32com.client import Dispatch
import winreg
def get_reg(name,path):
# Read variable from Windows Registry
# From https://stackoverflow.com/a/35286642
try:
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
winreg.KEY_READ)
value, regtype = winreg.QueryValueEx(registry_key, name)
winreg.CloseKey(registry_key)
return value
except WindowsError:
return None
def post_install():
# Creates a Desktop shortcut to the installed software
# Package name
packageName = 'mypackage'
# Scripts directory (location of launcher script)
scriptsDir = sysconfig.get_path('scripts')
# Target of shortcut
target = os.path.join(scriptsDir, packageName + '.exe')
# Name of link file
linkName = packageName + '.lnk'
# Read location of Windows desktop folder from registry
regName = 'Desktop'
regPath = r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
desktopFolder = os.path.normpath(get_reg(regName,regPath))
# Path to location of link file
pathLink = os.path.join(desktopFolder, linkName)
shell = Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(pathLink)
shortcut.Targetpath = target
shortcut.WorkingDirectory = scriptsDir
shortcut.IconLocation = target
shortcut.save()
setup(name='mypackage',
...,
...)
if sys.argv[1] == 'install' and sys.platform == 'win32':
post_install()
Here's a link to a full setup script in which I used this:
https://github.com/KBNLresearch/iromlab/blob/master/setup.py
If you want to confirm whether the script is running or not, you can print to a file instead of the console. Looks like text you print to console in the post-install script won't show up.
Try this:
import sys
from os.path import expanduser, join
pyw_executable = join(sys.prefix, "pythonw.exe")
shortcut_filename = "L-System Toolsss.lnk"
working_dir = expanduser(join('~','lsf_files'))
script_path = join(sys.prefix, "Scripts", "tklsystem-script.py")
if sys.argv[1] == '-install':
# Log output to a file (for test)
f = open(r"C:\test.txt",'w')
print('Creating Shortcut', file=f)
# Get paths to the desktop and start menu
desktop_path = get_special_folder_path("CSIDL_COMMON_DESKTOPDIRECTORY")
startmenu_path = get_special_folder_path("CSIDL_COMMON_STARTMENU")
# Create shortcuts.
for path in [desktop_path, startmenu_path]:
create_shortcut(pyw_executable,
"A program to work with L-System Equations",
join(path, shortcut_filename),
script_path,
working_dir)
At least with Python 3.6.5, 32bit on Windows, setuptools does work for this. But based on the accepted answer, by trial and error I found some issues that may have caused your script to fail to do what you wanted.
create_shortcut does not accept keyword arguments, only positional, so its usage in your code is invalid
You must add a .lnk extension for Windows to recognise the shortcut
I found sys.executable will be the name of the installer executable, not the python executable
As mentioned, you can't see stdout or stderr so you might want to log to a text file. I would suggest also redirecting sys.stdout and sys.stderr to the log file.
(Maybe not relevant) as mentioned in this question there appears to be a bug with the version string generated by bdist_wininst. I used the hexediting hack from an answer there to work around this. The location in the answer is not the same, you have to find the -32 yourself.
Full example script:
import sys
import os
import datetime
global datadir
datadir = os.path.join(get_special_folder_path("CSIDL_APPDATA"), "mymodule")
def main(argv):
if "-install" in argv:
desktop = get_special_folder_path("CSIDL_DESKTOPDIRECTORY")
print("Desktop path: %s" % repr(desktop))
if not os.path.exists(datadir):
os.makedirs(datadir)
dir_created(datadir)
print("Created data directory: %s" % repr(datadir))
else:
print("Data directory already existed at %s" % repr(datadir))
shortcut = os.path.join(desktop, "MyModule.lnk")
if os.path.exists(shortcut):
print("Remove existing shortcut at %s" % repr(shortcut))
os.unlink(shortcut)
print("Creating shortcut at %s...\n" % shortcut)
create_shortcut(
r'C:\Python36\python.exe',
"MyModuleScript",
shortcut,
"",
datadir)
file_created(shortcut)
print("Successfull!")
elif "-remove" in sys.argv:
print("Removing...")
pass
if __name__ == "__main__":
logfile = r'C:\mymodule_install.log' # Fallback location
if os.path.exists(datadir):
logfile = os.path.join(datadir, "install.log")
elif os.environ.get("TEMP") and os.path.exists(os.environ.get("TEMP"),""):
logfile = os.path.join(os.environ.get("TEMP"), "mymodule_install.log")
with open(logfile, 'a+') as f:
f.write("Opened\r\n")
f.write("Ran %s %s at %s" % (sys.executable, " ".join(sys.argv), datetime.datetime.now().isoformat()))
sys.stdout = f
sys.stderr = f
try:
main(sys.argv)
except Exception as e:
raise
f.close()
sys.exit(0)
UPD: on an off chance that the client machine has pywin32 installed, we try in-process creation first. Somewhat cleaner that way.
Here is another take. This assumes the package is called myapp, and that also becomes the executable that you want a shortcut to. Substitute your own package name and your own shortcut text.
Uses a Windows Scripting Host COM class - in process if possible, inside a Powershell command line as a subprocess if not. Tested on Python 3.6+.
from setuptools import setup
from setuptools.command.install import install
import platform, sys, os, site
from os import path, environ
def create_shortcut_under(root, exepath):
# Root is an env variable name -
# either ALLUSERSPROFILE for the all users' Start menu,
# or APPDATA for the current user specific one
profile = environ[root]
linkpath = path.join(profile, "Microsoft", "Windows", "Start Menu", "Programs", "My Python app.lnk")
try:
from win32com.client import Dispatch
from pywintypes import com_error
try:
sh = Dispatch('WScript.Shell')
link = sh.CreateShortcut(linkpath)
link.TargetPath = exepath
link.Save()
return True
except com_error:
return False
except ImportError:
import subprocess
s = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + linkpath + "');$s.TargetPath='" + exepath + "';$s.Save()"
return subprocess.call(['powershell', s], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) == 0
def create_shortcut(inst):
try:
exepath = path.join(path.dirname(sys.executable), "Scripts", "myapp.exe")
if not path.exists(exepath):
# Support for "pip install --user"
exepath = path.join(path.dirname(site.getusersitepackages()), "Scripts", "myapp.exe")
# If can't modify the global menu, fall back to the
# current user's one
if not create_shortcut_under('ALLUSERSPROFILE', exepath):
create_shortcut_under('APPDATA', exepath)
except:
pass
class my_install(install):
def run(self):
install.run(self)
if platform.system() == 'Windows':
create_shortcut(self)
#...
setup(
#...
cmdclass={'install': my_install},
entry_points={"gui_scripts": ["myapp = myapp.__main__:main"]},

Symlinks on windows

I'm trying to check is the path symlink hardlink or junction point on windows
How can I do it? os.path.islink() not work. It always returns False
I create symlinks by next method:
mklink /d linkPath targetDir
mklink /h linkPath targetDir
mklink /j linkPath targetDir
I've used command line because os.link and os.symlink available only on Unix systems
Maybe there are any command line tools for it?
Thanks
The os.path.islink() docstring states:
Test for symbolic link.
On WindowsNT/95 and OS/2 always returns false
In Windows the links are ending with .lnk, for files and folders, so you could create a function adding this extension and checking with os.path.isfile() and os.path.isfolder(), like:
mylink = lambda path: os.path.isfile(path + '.lnk') or os.path.isdir(path + '.lnk')
This works on Python 3.3 on Windows 8.1 using an NTFS filesystem.
islink() returns True for a symlink (as created with mklink) and False for a normal file.
Taken from https://eklausmeier.wordpress.com/2015/10/27/working-with-windows-junctions-in-python/
(see also: Having trouble implementing a readlink() function)
from ctypes import WinDLL, WinError
from ctypes.wintypes import DWORD, LPCWSTR
kernel32 = WinDLL('kernel32')
GetFileAttributesW = kernel32.GetFileAttributesW
GetFileAttributesW.restype = DWORD
GetFileAttributesW.argtypes = (LPCWSTR,) #lpFileName In
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
def islink(path):
result = GetFileAttributesW(path)
if result == INVALID_FILE_ATTRIBUTES:
raise WinError()
return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
if __name__ == '__main__':
path = "C:\\Programme" # "C:\\Program Files" on a German Windows.
b = islink(path)
print path, 'is link:', b

Python: Opening a folder in Explorer/Nautilus/Finder

I'm in Python, and I have the path of a certain folder. I want to open it using the default folder explorer for that system. For example, if it's a Windows computer, I want to use Explorer, if it's Linux, I want to use Nautilus or whatever is the default there, if it's Mac, I want to use Finder.
How can I do that?
I am surprised no one has mentioned using xdg-open for *nix which will work for both files and folders:
import os
import platform
import subprocess
def open_file(path):
if platform.system() == "Windows":
os.startfile(path)
elif platform.system() == "Darwin":
subprocess.Popen(["open", path])
else:
subprocess.Popen(["xdg-open", path])
You can use subprocess.
import subprocess
import sys
if sys.platform == 'darwin':
def openFolder(path):
subprocess.check_call(['open', '--', path])
elif sys.platform == 'linux2':
def openFolder(path):
subprocess.check_call(['xdg-open', '--', path])
elif sys.platform == 'win32':
def openFolder(path):
subprocess.check_call(['explorer', path])
The following works on Macintosh.
import webbrowser
webbrowser.open('file:///Users/test/test_folder')
On GNU/Linux, use the absolute path of the folder. (Make sure the folder exists)
import webbrowser
webbrowser.open('/home/test/test_folder')
As pointed out in the other answer, it works on Windows, too.
I think you may have to detect the operating system, and then launch the relevant file explorer accordingly.
This could come in userful for OSX's Finder: Python "show in finder"
(The below only works for windows unfortunately)
import webbrowser as wb
wb.open('C:/path/to/folder')
This works on Windows. I assume it would work across other platforms. Can anyone confirm? Confirmed windows only :(
One approach to something like this is maybe to prioritize readability, and prepare the code in such a manner that extracting abstractions is easy. You could take advantage of python higher order functions capabilities and go along these lines, throwing an exception if the proper function assignment cannot be made when a specific platform is not supported.
import subprocess
import sys
class UnsupportedPlatformException(Exception):
pass
def _show_file_darwin():
subprocess.check_call(["open", "--", path])
def _show_file_linux():
subprocess.check_call(["xdg-open", "--", path])
def _show_file_win32():
subprocess.check_call(["explorer", "/select", path])
_show_file_func = {'darwin': _show_file_darwin,
'linux': _show_file_linux,
'win32': _show_file_win32}
try:
show_file = _show_file_func[sys.platform]
except KeyError:
raise UnsupportedPlatformException
# then call show_file() as usual
For Mac OS, you can use
import subprocess
subprocess.run["open", "your/path"])

Categories