How to create a shortcut in startmenu using setuptools windows installer - python

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"]},

Related

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)

Python: Getting AppData folder in a cross-platform way

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.

How do I get Plone to read my pythonrc?

I am using the collective.python buildout.
I have the following .pythonrc (configured with export PYTHONSTARTUP=~/.pythonrc):
import readline
import rlcompleter
readline.parse_and_bind('tab: complete')
When I run Python in the shell, tab completion works. When I run Plone in debug mode it does not. Unless, I paste the contents of my .pythonrc into the Plone debug Python prompt. What am I missing here?
Note: Pasting the contents of my .pythonrc only works when I install Plone via python bootstrap.py (i.e. bootstrapping Plone buildout with collective.python Python). If I install Plone inside a virtualenv, nothing works. But at least in that scenario, the missing functionality makes sense to me (i.e. something is probably missing from the virtualenv that is required to make tab completion work.)
The instance controller uses two command-line switches; -i for interactive mode, and -c to load the Zope configuration and set up the app variable. The -c switch is what disables the PYTHONSTARTUP environment variable.
You could modify the plone.recipe.zope2instance package to run the script anyway.
In plone.recipe.zope2instance, find the plone/recipe/zope2instance/ctl.py file, alter the do_debug() method to:
def do_debug(self, arg):
interactive_startup = ("import os;"
"os.path.exists(os.environ.get('PYTHONSTARTUP', '')) "
"and execfile(os.environ['PYTHONSTARTUP']); del os;"
'import Zope2; app=Zope2.app()')
cmdline = self.get_startup_cmd(self.options.python,
interactive_startup,
pyflags = '-i', )
In fact, I like the idea of supporting PYTHONSTARTUP so much I committed that change to the recipe already, see rev 536f8fc1c4!
I do import user. This reads ~/.pythonrc.py. Note the .py extension. I have set that file as my PYTHONSTARTUP
I'll paste that file for good measure. I have cobbled it together a few years ago. Not sure if it is still the best, as I see comments about 2006 and python2.3. It does the trick though.
$ cat ~/.pythonrc.py
# See http://blog.partecs.com/2006/02/27/source-inspector/
#import pydoc
import inspect
import rlcompleter, readline
readline.parse_and_bind('tab: complete')
# def source(obj):
# """source of the obj."""
# try:
# pydoc.pipepager(inspect.getsource(obj), 'less')
# except IOError:
# pass
# From /usr/local/lib/python2.3/user.py
import os
home = os.curdir # Default
if 'HOME' in os.environ:
home = os.environ['HOME']
elif os.name == 'posix':
home = os.path.expanduser("~/")
# Make sure home always ends with a directory separator:
home = os.path.realpath(home) + os.sep
# From http://wiki.python.org/moin/PdbRcIdea
# Command line history:
histfile = home + '.pyhist'
try:
readline.read_history_file(histfile)
except IOError:
pass
import atexit
atexit.register(readline.write_history_file, histfile)
readline.set_history_length(200)
# Cleanup namespace
# del atexit
# del home
# del histfile
# del os
# del readline
# del rlcompleter

Copying a file in python : Permission denied [duplicate]

I want my Python script to copy files on Vista. When I run it from a normal cmd.exe window, no errors are generated, yet the files are NOT copied. If I run cmd.exe "as administator" and then run my script, it works fine.
This makes sense since User Account Control (UAC) normally prevents many file system actions.
Is there a way I can, from within a Python script, invoke a UAC elevation request (those dialogs that say something like "such and such app needs admin access, is this OK?")
If that's not possible, is there a way my script can at least detect that it is not elevated so it can fail gracefully?
As of 2017, an easy method to achieve this is the following:
import ctypes, sys
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
if is_admin():
# Code of your program here
else:
# Re-run the program with admin rights
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1)
If you are using Python 2.x, then you should replace the last line for:
ctypes.windll.shell32.ShellExecuteW(None, u"runas", unicode(sys.executable), unicode(" ".join(sys.argv)), None, 1)
Also note that if you converted you python script into an executable file (using tools like py2exe, cx_freeze, pyinstaller) then you should use sys.argv[1:] instead of sys.argv in the fourth parameter.
Some of the advantages here are:
No external libraries required. It only uses ctypes and sys from standard library.
Works on both Python 2 and Python 3.
There is no need to modify the file resources nor creating a manifest file.
If you don't add code below if/else statement, the code won't ever be executed twice.
You can get the return value of the API call in the last line and take an action if it fails (code <= 32). Check possible return values here.
You can change the display method of the spawned process modifying the sixth parameter.
Documentation for the underlying ShellExecute call is here.
It took me a little while to get dguaraglia's answer working, so in the interest of saving others time, here's what I did to implement this idea:
import os
import sys
import win32com.shell.shell as shell
ASADMIN = 'asadmin'
if sys.argv[-1] != ASADMIN:
script = os.path.abspath(sys.argv[0])
params = ' '.join([script] + sys.argv[1:] + [ASADMIN])
shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params)
sys.exit(0)
It seems there's no way to elevate the application privileges for a while for you to perform a particular task. Windows needs to know at the start of the program whether the application requires certain privileges, and will ask the user to confirm when the application performs any tasks that need those privileges. There are two ways to do this:
Write a manifest file that tells Windows the application might require some privileges
Run the application with elevated privileges from inside another program
This two articles explain in much more detail how this works.
What I'd do, if you don't want to write a nasty ctypes wrapper for the CreateElevatedProcess API, is use the ShellExecuteEx trick explained in the Code Project article (Pywin32 comes with a wrapper for ShellExecute). How? Something like this:
When your program starts, it checks if it has Administrator privileges, if it doesn't it runs itself using the ShellExecute trick and exits immediately, if it does, it performs the task at hand.
As you describe your program as a "script", I suppose that's enough for your needs.
Cheers.
Just adding this answer in case others are directed here by Google Search as I was.
I used the elevate module in my Python script and the script executed with Administrator Privileges in Windows 10.
https://pypi.org/project/elevate/
The following example builds on MARTIN DE LA FUENTE SAAVEDRA's excellent work and accepted answer. In particular, two enumerations are introduced. The first allows for easy specification of how an elevated program is to be opened, and the second helps when errors need to be easily identified. Please note that if you want all command line arguments passed to the new process, sys.argv[0] should probably be replaced with a function call: subprocess.list2cmdline(sys.argv).
#! /usr/bin/env python3
import ctypes
import enum
import subprocess
import sys
# Reference:
# msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
# noinspection SpellCheckingInspection
class SW(enum.IntEnum):
HIDE = 0
MAXIMIZE = 3
MINIMIZE = 6
RESTORE = 9
SHOW = 5
SHOWDEFAULT = 10
SHOWMAXIMIZED = 3
SHOWMINIMIZED = 2
SHOWMINNOACTIVE = 7
SHOWNA = 8
SHOWNOACTIVATE = 4
SHOWNORMAL = 1
class ERROR(enum.IntEnum):
ZERO = 0
FILE_NOT_FOUND = 2
PATH_NOT_FOUND = 3
BAD_FORMAT = 11
ACCESS_DENIED = 5
ASSOC_INCOMPLETE = 27
DDE_BUSY = 30
DDE_FAIL = 29
DDE_TIMEOUT = 28
DLL_NOT_FOUND = 32
NO_ASSOC = 31
OOM = 8
SHARE = 26
def bootstrap():
if ctypes.windll.shell32.IsUserAnAdmin():
main()
else:
# noinspection SpellCheckingInspection
hinstance = ctypes.windll.shell32.ShellExecuteW(
None,
'runas',
sys.executable,
subprocess.list2cmdline(sys.argv),
None,
SW.SHOWNORMAL
)
if hinstance <= 32:
raise RuntimeError(ERROR(hinstance))
def main():
# Your Code Here
print(input('Echo: '))
if __name__ == '__main__':
bootstrap()
Recognizing this question was asked years ago, I think a more elegant solution is offered on github by frmdstryr using his module pywinutils:
Excerpt:
import pythoncom
from win32com.shell import shell,shellcon
def copy(src,dst,flags=shellcon.FOF_NOCONFIRMATION):
""" Copy files using the built in Windows File copy dialog
Requires absolute paths. Does NOT create root destination folder if it doesn't exist.
Overwrites and is recursive by default
#see http://msdn.microsoft.com/en-us/library/bb775799(v=vs.85).aspx for flags available
"""
# #see IFileOperation
pfo = pythoncom.CoCreateInstance(shell.CLSID_FileOperation,None,pythoncom.CLSCTX_ALL,shell.IID_IFileOperation)
# Respond with Yes to All for any dialog
# #see http://msdn.microsoft.com/en-us/library/bb775799(v=vs.85).aspx
pfo.SetOperationFlags(flags)
# Set the destionation folder
dst = shell.SHCreateItemFromParsingName(dst,None,shell.IID_IShellItem)
if type(src) not in (tuple,list):
src = (src,)
for f in src:
item = shell.SHCreateItemFromParsingName(f,None,shell.IID_IShellItem)
pfo.CopyItem(item,dst) # Schedule an operation to be performed
# #see http://msdn.microsoft.com/en-us/library/bb775780(v=vs.85).aspx
success = pfo.PerformOperations()
# #see sdn.microsoft.com/en-us/library/bb775769(v=vs.85).aspx
aborted = pfo.GetAnyOperationsAborted()
return success is None and not aborted
This utilizes the COM interface and automatically indicates that admin privileges are needed with the familiar dialog prompt that you would see if you were copying into a directory where admin privileges are required and also provides the typical file progress dialog during the copy operation.
This may not completely answer your question but you could also try using the Elevate Command Powertoy in order to run the script with elevated UAC privileges.
http://technet.microsoft.com/en-us/magazine/2008.06.elevation.aspx
I think if you use it it would look like 'elevate python yourscript.py'
You can make a shortcut somewhere and as the target use:
python yourscript.py
then under properties and advanced select run as administrator.
When the user executes the shortcut it will ask them to elevate the application.
A variation on Jorenko's work above allows the elevated process to use the same console (but see my comment below):
def spawn_as_administrator():
""" Spawn ourself with administrator rights and wait for new process to exit
Make the new process use the same console as the old one.
Raise Exception() if we could not get a handle for the new re-run the process
Raise pywintypes.error() if we could not re-spawn
Return the exit code of the new process,
or return None if already running the second admin process. """
#pylint: disable=no-name-in-module,import-error
import win32event, win32api, win32process
import win32com.shell.shell as shell
if '--admin' in sys.argv:
return None
script = os.path.abspath(sys.argv[0])
params = ' '.join([script] + sys.argv[1:] + ['--admin'])
SEE_MASK_NO_CONSOLE = 0x00008000
SEE_MASK_NOCLOSE_PROCESS = 0x00000040
process = shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params, fMask=SEE_MASK_NO_CONSOLE|SEE_MASK_NOCLOSE_PROCESS)
hProcess = process['hProcess']
if not hProcess:
raise Exception("Could not identify administrator process to install drivers")
# It is necessary to wait for the elevated process or else
# stdin lines are shared between 2 processes: they get one line each
INFINITE = -1
win32event.WaitForSingleObject(hProcess, INFINITE)
exitcode = win32process.GetExitCodeProcess(hProcess)
win32api.CloseHandle(hProcess)
return exitcode
This is mostly an upgrade to Jorenko's answer, that allows to use parameters with spaces in Windows, but should also work fairly well on Linux :)
Also, will work with cx_freeze or py2exe since we don't use __file__ but sys.argv[0] as executable
[EDIT]
Disclaimer: The code in this post is outdated.
I have published the elevation code as a python package.
Install with pip install command_runner
Usage:
from command_runner.elevate import elevate
def main():
"""My main function that should be elevated"""
print("Who's the administrator, now ?")
if __name__ == '__main__':
elevate(main)
[/EDIT]
import sys,ctypes,platform
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
raise False
if __name__ == '__main__':
if platform.system() == "Windows":
if is_admin():
main(sys.argv[1:])
else:
# Re-run the program with admin rights, don't use __file__ since py2exe won't know about it
# Use sys.argv[0] as script path and sys.argv[1:] as arguments, join them as lpstr, quoting each parameter or spaces will divide parameters
lpParameters = ""
# Litteraly quote all parameters which get unquoted when passed to python
for i, item in enumerate(sys.argv[0:]):
lpParameters += '"' + item + '" '
try:
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, lpParameters , None, 1)
except:
sys.exit(1)
else:
main(sys.argv[1:])
For one-liners, put the code to where you need UAC.
Request UAC, if failed, keep running:
import ctypes, sys
ctypes.windll.shell32.IsUserAnAdmin() or ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1) > 32 and exit()
Request UAC, if failed, exit:
import ctypes, sys
ctypes.windll.shell32.IsUserAnAdmin() or (ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1) > 32, exit())
Function style:
# Created by BaiJiFeiLong#gmail.com at 2022/6/24
import ctypes
import sys
def request_uac_or_skip():
ctypes.windll.shell32.IsUserAnAdmin() or ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1) > 32 and sys.exit()
def request_uac_or_exit():
ctypes.windll.shell32.IsUserAnAdmin() or (ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1) > 32, sys.exit())
If your script always requires an Administrator's privileges then:
runas /user:Administrator "python your_script.py"

How can I change my desktop background with python?

How can I change my desktop background with python?
I want to do it in both Windows and Linux.
On Windows with python2.5 or higher, use ctypes to load user32.dll and call SystemParametersInfo() with SPI_SETDESKWALLPAPER action.
For example:
import ctypes
SPI_SETDESKWALLPAPER = 20
ctypes.windll.user32.SystemParametersInfoA(SPI_SETDESKWALLPAPER, 0, "image.jpg" , 0)
For Python3.5, SystemParametersInfoA doesn't work. Use SystemParametersInfoW.
import ctypes
ctypes.windll.user32.SystemParametersInfoW(20, 0, "absolute path" , 0)
I use the following method in one of my initial projects:
def set_wallpaper(self,file_loc, first_run):
# Note: There are two common Linux desktop environments where
# I have not been able to set the desktop background from
# command line: KDE, Enlightenment
desktop_env = self.get_desktop_environment()
try:
if desktop_env in ["gnome", "unity", "cinnamon"]:
uri = "'file://%s'" % file_loc
try:
SCHEMA = "org.gnome.desktop.background"
KEY = "picture-uri"
gsettings = Gio.Settings.new(SCHEMA)
gsettings.set_string(KEY, uri)
except:
args = ["gsettings", "set", "org.gnome.desktop.background", "picture-uri", uri]
subprocess.Popen(args)
elif desktop_env=="mate":
try: # MATE >= 1.6
# info from http://wiki.mate-desktop.org/docs:gsettings
args = ["gsettings", "set", "org.mate.background", "picture-filename", "'%s'" % file_loc]
subprocess.Popen(args)
except: # MATE < 1.6
# From https://bugs.launchpad.net/variety/+bug/1033918
args = ["mateconftool-2","-t","string","--set","/desktop/mate/background/picture_filename",'"%s"' %file_loc]
subprocess.Popen(args)
elif desktop_env=="gnome2": # Not tested
# From https://bugs.launchpad.net/variety/+bug/1033918
args = ["gconftool-2","-t","string","--set","/desktop/gnome/background/picture_filename", '"%s"' %file_loc]
subprocess.Popen(args)
## KDE4 is difficult
## see http://blog.zx2c4.com/699 for a solution that might work
elif desktop_env in ["kde3", "trinity"]:
# From http://ubuntuforums.org/archive/index.php/t-803417.html
args = 'dcop kdesktop KBackgroundIface setWallpaper 0 "%s" 6' % file_loc
subprocess.Popen(args,shell=True)
elif desktop_env=="xfce4":
#From http://www.commandlinefu.com/commands/view/2055/change-wallpaper-for-xfce4-4.6.0
if first_run:
args0 = ["xfconf-query", "-c", "xfce4-desktop", "-p", "/backdrop/screen0/monitor0/image-path", "-s", file_loc]
args1 = ["xfconf-query", "-c", "xfce4-desktop", "-p", "/backdrop/screen0/monitor0/image-style", "-s", "3"]
args2 = ["xfconf-query", "-c", "xfce4-desktop", "-p", "/backdrop/screen0/monitor0/image-show", "-s", "true"]
subprocess.Popen(args0)
subprocess.Popen(args1)
subprocess.Popen(args2)
args = ["xfdesktop","--reload"]
subprocess.Popen(args)
elif desktop_env=="razor-qt": #TODO: implement reload of desktop when possible
if first_run:
desktop_conf = configparser.ConfigParser()
# Development version
desktop_conf_file = os.path.join(self.get_config_dir("razor"),"desktop.conf")
if os.path.isfile(desktop_conf_file):
config_option = r"screens\1\desktops\1\wallpaper"
else:
desktop_conf_file = os.path.join(self.get_home_dir(),".razor/desktop.conf")
config_option = r"desktops\1\wallpaper"
desktop_conf.read(os.path.join(desktop_conf_file))
try:
if desktop_conf.has_option("razor",config_option): #only replacing a value
desktop_conf.set("razor",config_option,file_loc)
with codecs.open(desktop_conf_file, "w", encoding="utf-8", errors="replace") as f:
desktop_conf.write(f)
except:
pass
else:
#TODO: reload desktop when possible
pass
elif desktop_env in ["fluxbox","jwm","openbox","afterstep"]:
#http://fluxbox-wiki.org/index.php/Howto_set_the_background
# used fbsetbg on jwm too since I am too lazy to edit the XML configuration
# now where fbsetbg does the job excellent anyway.
# and I have not figured out how else it can be set on Openbox and AfterSTep
# but fbsetbg works excellent here too.
try:
args = ["fbsetbg", file_loc]
subprocess.Popen(args)
except:
sys.stderr.write("ERROR: Failed to set wallpaper with fbsetbg!\n")
sys.stderr.write("Please make sre that You have fbsetbg installed.\n")
elif desktop_env=="icewm":
# command found at http://urukrama.wordpress.com/2007/12/05/desktop-backgrounds-in-window-managers/
args = ["icewmbg", file_loc]
subprocess.Popen(args)
elif desktop_env=="blackbox":
# command found at http://blackboxwm.sourceforge.net/BlackboxDocumentation/BlackboxBackground
args = ["bsetbg", "-full", file_loc]
subprocess.Popen(args)
elif desktop_env=="lxde":
args = "pcmanfm --set-wallpaper %s --wallpaper-mode=scaled" % file_loc
subprocess.Popen(args,shell=True)
elif desktop_env=="windowmaker":
# From http://www.commandlinefu.com/commands/view/3857/set-wallpaper-on-windowmaker-in-one-line
args = "wmsetbg -s -u %s" % file_loc
subprocess.Popen(args,shell=True)
## NOT TESTED BELOW - don't want to mess things up ##
#elif desktop_env=="enlightenment": # I have not been able to make it work on e17. On e16 it would have been something in this direction
# args = "enlightenment_remote -desktop-bg-add 0 0 0 0 %s" % file_loc
# subprocess.Popen(args,shell=True)
#elif desktop_env=="windows": #Not tested since I do not run this on Windows
# #From https://stackoverflow.com/questions/1977694/change-desktop-background
# import ctypes
# SPI_SETDESKWALLPAPER = 20
# ctypes.windll.user32.SystemParametersInfoA(SPI_SETDESKWALLPAPER, 0, file_loc , 0)
#elif desktop_env=="mac": #Not tested since I do not have a mac
# #From https://stackoverflow.com/questions/431205/how-can-i-programatically-change-the-background-in-mac-os-x
# try:
# from appscript import app, mactypes
# app('Finder').desktop_picture.set(mactypes.File(file_loc))
# except ImportError:
# #import subprocess
# SCRIPT = """/usr/bin/osascript<<END
# tell application "Finder" to
# set desktop picture to POSIX file "%s"
# end tell
# END"""
# subprocess.Popen(SCRIPT%file_loc, shell=True)
else:
if first_run: #don't spam the user with the same message over and over again
sys.stderr.write("Warning: Failed to set wallpaper. Your desktop environment is not supported.")
sys.stderr.write("You can try manually to set Your wallpaper to %s" % file_loc)
return False
return True
except:
sys.stderr.write("ERROR: Failed to set wallpaper. There might be a bug.\n")
return False
def get_config_dir(self, app_name=APP_NAME):
if "XDG_CONFIG_HOME" in os.environ:
confighome = os.environ['XDG_CONFIG_HOME']
elif "APPDATA" in os.environ: # On Windows
confighome = os.environ['APPDATA']
else:
try:
from xdg import BaseDirectory
confighome = BaseDirectory.xdg_config_home
except ImportError: # Most likely a Linux/Unix system anyway
confighome = os.path.join(self.get_home_dir(),".config")
configdir = os.path.join(confighome,app_name)
return configdir
def get_home_dir(self):
if sys.platform == "cygwin":
home_dir = os.getenv('HOME')
else:
home_dir = os.getenv('USERPROFILE') or os.getenv('HOME')
if home_dir is not None:
return os.path.normpath(home_dir)
else:
raise KeyError("Neither USERPROFILE or HOME environment variables set.")
The get_desktop_environment method has been posted in another thread.
On a gnome desktop, you usually do this with gconf, either directly calling gconftool or using the gconf python module. The latter is in the link given by unutbu. The first method could be done like this.
import commands
command = "gconftool-2 --set /desktop/gnome/background/picture_filename --type string '/path/to/file.jpg'"
status, output = commands.getstatusoutput(command) # status=0 if success
In gnome, it is probably preferable to use the python binding of gconf directly:
import gconf
conf = gconf.client_get_default()
conf.set_string('/desktop/gnome/background/picture_filename','/path/to/filename.jpg')
On windows, you will need some trickery with pywin32, and the windows API, on 'linux' the answer will depend on which desktop is running - KDE, Gnome, or something more exotic. Under KDE (and maybe Gnome) you can probably send a message using D-Bus, which you could do without including any new libraries by using the command line tool dbus-send.
The other option would be to set the desktop wallpaper to a file which you then edit / replace from python - but this will probably only result in a change when the user logs in.
Firstly, import ctypes: it gives you access to windows components such as the screensaver, wallpapers, etc.
Then call
ctypes.windll.user32.SystemParametersInfoA(20, 0, the_complete_path_of_your_image, 0)
Make sure the path is the complete path of your image, not just the path from the active directory
There is a difference what SystemParametersInfo method to be called based on what if you are running on 64 bit or 32 bit OS. For 64 bit you have to use SystemParametersInfoW (Unicode) and for 32 bit SystemParametersInfoA (ANSI)
import struct
import ctypes
SPI_SETDESKWALLPAPER = 20
WALLPAPER_PATH = 'C:\\your_file_name.jpg'
def is_64_windows():
"""Find out how many bits is OS. """
return struct.calcsize('P') * 8 == 64
def get_sys_parameters_info():
"""Based on if this is 32bit or 64bit returns correct version of SystemParametersInfo function. """
return ctypes.windll.user32.SystemParametersInfoW if is_64_windows() \
else ctypes.windll.user32.SystemParametersInfoA
def change_wallpaper():
sys_parameters_info = get_sys_parameters_info()
r = sys_parameters_info(SPI_SETDESKWALLPAPER, 0, WALLPAPER_PATH, 3)
# When the SPI_SETDESKWALLPAPER flag is used,
# SystemParametersInfo returns TRUE
# unless there is an error (like when the specified file doesn't exist).
if not r:
print(ctypes.WinError())
change_wallpaper()
import ctypes,win32con
def getWallpaper():
ubuf = ctypes.create_unicode_buffer(512)
ctypes.windll.user32.SystemParametersInfoW(win32con.SPI_GETDESKWALLPAPER,len(ubuf),ubuf,0)
return ubuf.value
def setWallpaper(path):
changed = win32con.SPIF_UPDATEINIFILE | win32con.SPIF_SENDCHANGE
ctypes.windll.user32.SystemParametersInfoW(win32con.SPI_SETDESKWALLPAPER,0,path,changed)
Alternatively: (with SystemParametersInfoA)
def getWallpaper():
sbuf = ctypes.create_string_buffer(512) # ctypes.c_buffer(512)
ctypes.windll.user32.SystemParametersInfoA(win32con.SPI_GETDESKWALLPAPER,len(sbuf),sbuf,0)
return sbuf.value
def setWallpaper(path):
changed = win32con.SPIF_UPDATEINIFILE | win32con.SPIF_SENDCHANGE
ctypes.windll.user32.SystemParametersInfoA(win32con.SPI_SETDESKWALLPAPER,0,path.encode(),changed) # "".encode() = b""
Arguments are:
SystemParametersInfo(SetOrGet, GetBufferSize, SetBufferOrGetBuffer, SetChange)
The path has to be absolute, so if you're using something relative to your script, do:
path = os.path.abspath(path)
To see more stuff you can do with SystemParametersInfo, see the docs.
(near the bottom there's an example to change the mouse speed)
P.S. There are many answers already here, but they're leaving out the broadcasting you're supposed to do. Sure it works without it, but it's bad practice not to use it properly.
P.P.S And they only gave hard coded values, rather than the variables they come from.
Also note, i use 512 characters for the buffer size when getting the path, just to be more safe since paths might exceed 256. I doubt anyone will have paths as long as that though.
One more note. I've only tested the above examples in Python 3, but i don't think SystemParametersInfoA needs the .encode() in Python 2. (they updated strings in Python 3 to unicode i believe) The string in SystemParametersInfoW may need converting for Python 2.
I read all the answers and after searching for a while i found a easier solution.
Install the module named py-wallpaper.
pip install py-wallpaper
Import the module.
from wallpaper import set_wallpaper, get_wallpaper
set the wallpaper using set walpaper
set_wallpaper("location/to/image.jpg")
get the current wallpaper's path using get wallpaper
print(get_wallpaper())
thanks.
changing the background image of desktop
import ctypes
import os
SPI_SETDESKWALLPAPER = 20
ctypes.windll.user32.SystemParametersInfoA(SPI_SETDESKWALLPAPER, 0, 'your image path', 3)
#'C:\\Users\\Public\\Pictures\\abc.jpg'
it worked fine for me. windows10, python27
On Windows with python2.5 or higher, use ctypes to load user32.dll and call
import ctypes
ctypes.windll.user32.SystemParametersInfoW(20,0,"Path_wallpaper", 0)
speak("Background changed succesfully")
Just adding a small precision to ShivaGuntuku 's post :
In python 3 you should replace the 'A' by a 'W' in SytemParametersInfoA. Small exemple to change your desktop background in windows10 with python 3 :
import ctypes
import os
SPI_SETDESKWALLPAPER = 20
ctypes.windll.user32.SystemParametersInfoW(
SPI_SETDESKWALLPAPER, 0, 'C:\\Users\\godet\\OneDrive\\Images\\breaker_wall.jpg', 0)
this works for me
import ctypes
ctypes.windll.user32.SystemParametersInfoW(20,0,path:os.PathLike,3)
You can use this library PyWallpaper, worked for me on mac also.
To install type pip install PyWallpaper.
And then to change/set your wallpaper -
from PyWallpaper import change_wallpaper
change_wallpaper("/some_path/sample.jpg")

Categories