Related
I am trying to understand a script found on the Python documentation page.
I do not understand exactly, how to introduce the arguments, I know that I must put the name of the directory where the virtual environment will be installed so that the argument dirs stores the tuple of directories, however I do not understand how to modify the parameters of the other arguments such as nopip.
Code:
import os
import os.path
from subprocess import Popen, PIPE
import sys
from threading import Thread
from urllib.parse import urlparse
from urllib.request import urlretrieve
import venv
class ExtendedEnvBuilder(venv.EnvBuilder):
"""
This builder installs setuptools and pip so that you can pip or
easy_install other packages into the created virtual environment.
:param nodist: If true, setuptools and pip are not installed into the
created virtual environment.
:param nopip: If true, pip is not installed into the created
virtual environment.
:param progress: If setuptools or pip are installed, the progress of the
installation can be monitored by passing a progress
callable. If specified, it is called with two
arguments: a string indicating some progress, and a
context indicating where the string is coming from.
The context argument can have one of three values:
'main', indicating that it is called from virtualize()
itself, and 'stdout' and 'stderr', which are obtained
by reading lines from the output streams of a subprocess
which is used to install the app.
If a callable is not specified, default progress
information is output to sys.stderr.
"""
def __init__(self, *args, **kwargs):
self.nodist = kwargs.pop('nodist', False)
self.nopip = kwargs.pop('nopip', False)
self.progress = kwargs.pop('progress', None)
self.verbose = kwargs.pop('verbose', False)
super().__init__(*args, **kwargs)
def post_setup(self, context):
"""
Set up any packages which need to be pre-installed into the
virtual environment being created.
:param context: The information for the virtual environment
creation request being processed.
"""
os.environ['VIRTUAL_ENV'] = context.env_dir
if not self.nodist:
self.install_setuptools(context)
# Can't install pip without setuptools
if not self.nopip and not self.nodist:
self.install_pip(context)
def reader(self, stream, context):
"""
Read lines from a subprocess' output stream and either pass to a progress
callable (if specified) or write progress information to sys.stderr.
"""
progress = self.progress
while True:
s = stream.readline()
if not s:
break
if progress is not None:
progress(s, context)
else:
if not self.verbose:
sys.stderr.write('.')
else:
sys.stderr.write(s.decode('utf-8'))
sys.stderr.flush()
stream.close()
def install_script(self, context, name, url):
_, _, path, _, _, _ = urlparse(url)
fn = os.path.split(path)[-1]
binpath = context.bin_path
distpath = os.path.join(binpath, fn)
# Download script into the virtual environment's binaries folder
urlretrieve(url, distpath)
progress = self.progress
if self.verbose:
term = '\n'
else:
term = ''
if progress is not None:
progress('Installing %s ...%s' % (name, term), 'main')
else:
sys.stderr.write('Installing %s ...%s' % (name, term))
sys.stderr.flush()
# Install in the virtual environment
args = [context.env_exe, fn]
p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=binpath)
t1 = Thread(target=self.reader, args=(p.stdout, 'stdout'))
t1.start()
t2 = Thread(target=self.reader, args=(p.stderr, 'stderr'))
t2.start()
p.wait()
t1.join()
t2.join()
if progress is not None:
progress('done.', 'main')
else:
sys.stderr.write('done.\n')
# Clean up - no longer needed
os.unlink(distpath)
def install_setuptools(self, context):
"""
Install setuptools in the virtual environment.
:param context: The information for the virtual environment
creation request being processed.
"""
url = 'https://bitbucket.org/pypa/setuptools/downloads/ez_setup.py'
self.install_script(context, 'setuptools', url)
# clear up the setuptools archive which gets downloaded
pred = lambda o: o.startswith('setuptools-') and o.endswith('.tar.gz')
files = filter(pred, os.listdir(context.bin_path))
for f in files:
f = os.path.join(context.bin_path, f)
os.unlink(f)
def install_pip(self, context):
"""
Install pip in the virtual environment.
:param context: The information for the virtual environment
creation request being processed.
"""
url = 'https://raw.github.com/pypa/pip/master/contrib/get-pip.py'
self.install_script(context, 'pip', url)
def main(args=None):
compatible = True
if sys.version_info < (3, 3):
compatible = False
elif not hasattr(sys, 'base_prefix'):
compatible = False
if not compatible:
raise ValueError('This script is only for use with '
'Python 3.3 or later')
else:
import argparse
parser = argparse.ArgumentParser(prog=__name__,
description='Creates virtual Python '
'environments in one or '
'more target '
'directories.')
parser.add_argument('dirs', metavar='ENV_DIR', nargs='+ Pruebas',
help='A directory in which to create the'
'virtual environment.')
parser.add_argument('--no-setuptools', default=False,
action='store_true', dest='nodist',
help="Don't install setuptools or pip in the "
"virtual environment.")
parser.add_argument('--no-pip', default=False,
action='store_true', dest='nopip',
help="Don't install pip in the virtual "
"environment.")
parser.add_argument('--system-site-packages', default=False,
action='store_true', dest='system_site',
help='Give the virtual environment access to the '
'system site-packages dir.')
if os.name == 'nt':
use_symlinks = False
else:
use_symlinks = True
parser.add_argument('--symlinks', default=use_symlinks,
action='store_true', dest='symlinks',
help='Try to use symlinks rather than copies, '
'when symlinks are not the default for '
'the platform.')
parser.add_argument('--clear', default=False, action='store_true',
dest='clear', help='Delete the contents of the '
'virtual environment '
'directory if it already '
'exists, before virtual '
'environment creation.')
parser.add_argument('--upgrade', default=False, action='store_true',
dest='upgrade', help='Upgrade the virtual '
'environment directory to '
'use this version of '
'Python, assuming Python '
'has been upgraded '
'in-place.')
parser.add_argument('--verbose', default=False, action='store_true',
dest='verbose', help='Display the output '
'from the scripts which '
'install setuptools and pip.')
options = parser.parse_args(args)
if options.upgrade and options.clear:
raise ValueError('you cannot supply --upgrade and --clear together.')
builder = ExtendedEnvBuilder(system_site_packages=options.system_site,
clear=options.clear,
symlinks=options.symlinks,
upgrade=options.upgrade,
nodist=options.nodist,
nopip=options.nopip,
verbose=options.verbose)
for d in options.dirs:
builder.create(d)
if __name__ == '__main__':
rc = 1
try:
main()
rc = 0
except Exception as e:
print('Error: %s' % e, file=sys.stderr)
sys.exit(rc)
All the other options could be modified through command line arguments. You could see available options if run script with --help flag:
python3 pyvenvex.py --help
Output:
Usage: __main__ [-h] [--no-setuptools] [--no-pip] [--system-site-packages]
[--symlinks] [--clear] [--upgrade] [--verbose]
ENV_DIR [ENV_DIR ...]
Creates virtual Python environments in one or more target directories.
positional arguments:
ENV_DIR A directory to create the environment in.
optional arguments:
-h, --help show this help message and exit
--no-setuptools Don't install setuptools or pip in the virtual
environment.
--no-pip Don't install pip in the virtual environment.
--system-site-packages
Give the virtual environment access to the system
site-packages dir.
--symlinks Try to use symlinks rather than copies, when symlinks
are not the default for the platform.
--clear Delete the contents of the environment directory if it
already exists, before environment creation.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
--verbose Display the output from the scripts which install
setuptools and pip.
This arguments handled with argparse module from standard library. You could see a lot of boilerplate code like this in the script:
parser.add_argument('--no-pip', default=False,
action='store_true', dest='nopip',
help="Don't install pip in the virtual "
"environment.")
The only tricky thing here is the keyword argument dest='nopip', which means "parse --no-pip argument and put result into nopip attribute".
Without getting confused, there are tons of questions about installing packages, how to import the resulting modules, and listing what packages are available. But there doesn't seem to be the equivalent of a "--what-provides" option for pip, if you don't have a requirements.txt or pipenv. This question is similar to a previous question, but asks for the parent package, and not additional metadata. That said, these other questions did not get a lot of attention or many accepted answers - eg. How do you find python package metadata information given a module. So forging ahead... .
By way of example, there are two packages (to name a few) that will install a module called "serial" - namely "pyserial" and "serial". So assuming that one of the packages was installed, we might find it by using pip list:
python3 -m pip list | grep serial
However, the problem comes in if the name of the package does not match the name of the module, or if you just want to find out what package to install, working on a legacy server or dev machine.
You can check the path of the imported module - which can give you a clue. But continuing the example...
>>> import serial
>>> print(serial.__file__)
/usr/lib/python3.6/site-packages/serial/__init__.py
It is in a "serial" directory, but only pyserial is in fact installed, not serial:
> python3 -m pip list | grep serial
pyserial 3.4
The closest I can come is to generate a requirements.txt via "pipreqs ./" which may fail on a dependent child file (as it does with me), or to reverse check dependencies via pipenv (which brings a whole set of new issues along to get it all setup):
> pipenv graph --reverse
cymysql==0.9.15
ftptool==0.7.1
netifaces==0.10.9
pip==20.2.2
PyQt5-sip==12.8.1
- PyQt5==5.15.0 [requires: PyQt5-sip>=12.8,<13]
setuptools==50.3.0
wheel==0.35.1
Does anyone know of a command that I have missed for a simple solution to finding what pip package provides a particular module?
Use the packages_distributions() function from importlib.metadata (or importlib-metadata). So for example, in your case where serial is the name of the "import package":
import importlib.metadata # or: `import importlib_metadata`
importlib.metadata.packages_distributions()['serial']
This should return a list containing pyserial, which is the name of the "distribution package" (the name that should be used to pip-install).
References
https://importlib-metadata.readthedocs.io/en/stable/using.html#package-distributions
https://github.com/python/importlib_metadata/pull/287/files
For older Python versions and/or older versions of importlib-metadata...
I believe something like the following should work:
#!/usr/bin/env python3
import importlib.util
import pathlib
import importlib_metadata
def get_distribution(file_name):
result = None
for distribution in importlib_metadata.distributions():
try:
relative = (
pathlib.Path(file_name)
.relative_to(distribution.locate_file(''))
)
except ValueError:
pass
else:
if distribution.files and relative in distribution.files:
result = distribution
break
return result
def alpha():
file_name = importlib.util.find_spec('serial').origin
distribution = get_distribution(file_name)
print("alpha", distribution.metadata['Name'])
def bravo():
import serial
file_name = serial.__file__
distribution = get_distribution(file_name)
print("bravo", distribution.metadata['Name'])
if __name__ == '__main__':
alpha()
bravo()
This is just an example of code showing how to get the metadata of the installed project a specific module belongs to.
The important bit is the get_distribution function, it takes a file name as an argument. It could be the file name of a module or package data. If that file name belongs to a project installed in the environment (via pip install for example) then the importlib.metadata.Distribution object is returned.
Edit 2023/01/31: This issue is now solved via the importlib_metadata library. See Provide mapping from "Python packages" to "distribution packages", specifically "Note 2" deals with this exact issue. As such, see comments by #sinoroc, you can locate the package (eg. package "pyserial" providing module "serial") with something like this:
>>> import importlib_metadata
>>> print(importlib_metadata.packages_distributions()['serial'])
['pyserial']
Building on #sinoroc's much-published answer, I came up with the following code (incorporating the mentioned importlib.util.find_spec method, but with a bash-based search against the RECORD file in the path returned). I also tried to implement #sinoroc's version - but was not successful. Both methods are included to demonstrate.
Run as "python3 python_find-module-package.py -m [module-name-here] -d", which will also print debug. Leave off the "-d" switch to get just the package name returned (and errors).
TLDR: Code on github.
#!/usr/bin/python3
import sys
import os.path
import importlib.util
import importlib_metadata
import pathlib
import subprocess
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--module", help="Find matching package for the specified Python module",
type=str)
#parser.add_argument("-u", "--username", help="Database username",
# type=str)
#parser.add_argument("-p", "--password", help="Database password",
# type=str)
parser.add_argument("-d", "--debug", help="Debug messages are enabled",
action="store_true")
args = parser.parse_args()
TESTMODULE='serial'
def debugPrint (message="Nothing"):
if args.debug:
print ("[DEBUG] %s" % str(message))
class application ():
def __init__(self, argsPassed):
self.argsPassed = argsPassed
debugPrint("Got these arguments:\n%s" % (argsPassed))
def run (self):
#debugPrint("Running with args:\n%s" % (self.argsPassed))
try:
if self.argsPassed.module is not None:
self.moduleName=self.argsPassed.module #i.e. the module that you're trying to match with a package.
else:
self.moduleName=TESTMODULE
print("[WARN] No module name supplied - defaulting to %s!" % (TESTMODULE))
self.location=importlib.util.find_spec(self.moduleName).origin
debugPrint(self.location)
except:
print("[ERROR] Parsing module name!")
exit(1)
try:
self.getPackage()
except Exception as e:
print ("[ERROR] getPackage failed: %s" % str(e))
try:
distResult=self.getDistribution(self.location)
self.packageStrDist=distResult.metadata['Name']
print(self.packageStrDist)
except Exception as e:
print ("[ERROR] getDistribution failed: %s" % str(e))
debugPrint("Parent package for \"%s\" is: \"%s\"" % (self.moduleName, self.packageStr))
return self.packageStr
def getPackage (self):
locationStr=self.location.split("site-packages/",1)[1]
debugPrint(locationStr)
#serial/__init__.py
locationDir=self.location.split(locationStr,1)[0]
debugPrint(locationDir)
#/usr/lib/python3.6/site-packages
cmd='find \"' + locationDir + '\" -type f -iname \'RECORD\' -printf \'\"%p\"\\n\' | xargs grep \"' + locationStr + '\" -l -Z'
debugPrint(cmd)
#find "/usr/lib/python3.6/site-packages" -type f -iname 'RECORD' -printf '"%p"\n' | xargs grep "serial/__init__.py" -l -Z
#return_code = os.system(cmd)
#return_code = subprocess.run([cmd], stdout=subprocess.PIPE, universal_newlines=True, shell=False)
#findResultAll = return_code.stdout
findResultAll = subprocess.check_output(cmd, shell=True) # Returns stdout as byte array, null terminated.
findResult = str(findResultAll.decode('ascii').strip().strip('\x00'))
debugPrint(findResult)
#/usr/lib/python3.6/site-packages/pyserial-3.4.dist-info/RECORD
findDir = os.path.split(findResult)
self.packageStr=findDir[0].replace(locationDir,"")
debugPrint(self.packageStr)
def getDistribution(self, fileName=TESTMODULE):
result = None
for distribution in importlib_metadata.distributions():
try:
relative = (pathlib.Path(fileName).relative_to(distribution.locate_file('')))
#except ValueError:
#except AttributeError:
except:
pass
else:
if relative in distribution.files:
result = distribution
return result
if __name__ == '__main__':
result=1
try:
prog = application(args)
result = prog.run()
except Exception as E:
print ("[ERROR] Prog Exception: %s" % str(E))
finally:
sys.exit(result)
# exit the program if we haven't already
print ("Shouldn't get here.")
sys.exit(result)
I've built some debian packages and put them on my host. I want to install these packages in a python script. So I've written the install function using apt_pkg as follows :
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import apt
import apt_pkg
import sys
class my_pkg_manager:
def __init__(self):
apt_pkg.init()
self.cache = apt_pkg.Cache()
self.sources = apt_pkg.SourceList()
self.pkg_records = apt_pkg.PackageRecords(self.cache)
self.depcache = apt_pkg.DepCache(self.cache)
self.pkg_manager = apt_pkg.PackageManager(self.depcache)
self.fetcher = apt_pkg.Acquire()
def find_my_package(self):
for pkg in self.cache.packages:
if len(pkg.version_list)>0:
package_site = pkg.version_list[0].file_list[0][0].site
if package_site == 'my_host_address.com':
if pkg.name == 'my_pkg_name':
return pkg
return None
def install_package(self, pkg):
self.depcache.mark_install(pkg)
self.sources.read_main_list()
self.pkg_manager.get_archives(self.fetcher, self.sources,
self.pkg_records)
log_file = open('install_log','w')
res = self.pkg_manager.do_install(log_file.fileno())
if res == self.pkg_manager.RESULT_COMPLETED:
print('result completed!')
elif res == self.pkg_manager.RESULT_INCOMPLETE:
print('result incomplete!')
else:
print('result failed!')
def run(self):
my_pkg = self.find_my_package()
if my_pkg != None:
self.install_package(my_pkg)
else:
print('Can't find package!')
if __name__ == '__main__':
my_package_manager = my_pkg_manager()
my_package_manager.run()
If I use apt-get install, packages will be installed without any problem, but with this script the following error occurs :
res = self.pkg_manager.do_install(log_file.fileno()) apt_pkg.Error:
E:Internal Error, Pathname to install is not absolute
'myPackage_1.0.0_all.deb'
I've placed .deb file and Packages.gz in a directory named 'debian' in my host and added the following line to my sources.list :
deb http://my_host_address.com debian/
I don't know what is wrong with my script?
deb http://my_host_address.com debian/
should be:
deb http://my_host_address.com /debian/
Absolute path needs to start with a slash.
Could any one help me to check in Linux to meet rpm version
Below command I used to get package version.
rpm -qi libstdc++6 | awk -F': ' '/Version/ {print $2}'
using sub-process, I get below out put
6.2.1+r239768
Now I want to compare 6.2.1+r239768 is greater than 5.1.1
Below is results I am expected
output = 6.2.1+r239768
print "This rpm version is supported"
output = 6.3+r23
print "This rpm version is supported"
output = 7.1.1+r57678
print "This rpm version is supported"
output = 5.1.1+r23677
print "This rpm version is not supported"
output = 4.1+r56888
print "This rpm version is not supported"
You can use pkg_resources module which you should already have as it is part of setuptools:
In []:
from pkg_resources import parse_version
parse_version('6.2.1+r239768') > parse_version('5.1.1')
Out[]:
True
So to get your output:
In []:
versions = ['6.2.1+r239768', '6.3+r23', '7.1.1+r57678', '5.1.1+r23677', '4.1+r56888']
base = parse_version('6.2.1+r239768')
for v in versions:
print("Output = {}".format(v))
print("This rpm version is {}supported".format('not ' if parse_version(v) < base else ''))
Out[]:
Output = 6.2.1+r239768
This rpm version is supported
Output = 6.3+r23
This rpm version is supported
Output = 7.1.1+r57678
This rpm version is supported
Output = 5.1.1+r23677
This rpm version is not supported
Output = 4.1+r56888
This rpm version is not supported
I feels like you should be better of using the python API for rpm.
As I did not have RPM based system on hand right now, could not come up with code.
Following code, taken from Section 16.5, can be adapted accordingly
#!/usr/bin/python
# Reads in package header, compares to installed package.
# Usage:
# python vercompare.py libstdc++6.rpm
#
import os
import sys
import rpm
def readRpmHeader(ts, filename):
""" Read an rpm header. """
fd = os.open(filename, os.O_RDONLY)
try:
h = ts.hdrFromFdno(fd)
finally:
os.close(fd)
return h
ts = rpm.TransactionSet()
h = readRpmHeader( ts, sys.argv[1] )
pkg_ds = h.dsOfHeader()
for inst_h in ts.dbMatch('name', h['name']):
inst_ds = inst_h.dsOfHeader()
if pkg_ds.EVR() >= inst_ds.EVR():
print "Package file is same or newer, OK to upgrade."
else:
print "Package file is older than installed version."
$ rpmdev-vercmp 6.2.1+r239768 6.3+r23
6.2.1+r239768 < 6.3+r23
You can read the exit code:
rpmdev-vercmp <EVR1> <EVR2>
Exit status is 0 if the EVR's are equal, 11 if EVR1 is newer, and 12 if EVR2
is newer. Other exit statuses indicate problems.
The python rpm library gives you a way to compare a EVR (Epoch, Version, Release), but doesn't seem to give you the code to split a string into those parts. It's trivial, though. Here's a little working program.
#! /usr/bin/env python
import re
import sys
import rpm
evr_re = re.compile(r'^(?:([^:]*):)?([^-]+)(?:-(.*))?$')
def str_to_evr(s):
mo = evr_re.match(s)
if not mo:
raise ValueError("not a valid version string")
return mo.groups()
def main():
if len(sys.argv) != 3:
raise SystemExit("usage: vercmp VER1 VER2")
try:
evr_a = str_to_evr(sys.argv[1].strip())
evr_b = str_to_evr(sys.argv[2].strip())
result = rpm.labelCompare(evr_a, evr_b)
except Exception as exc:
raise SystemExit("Problem converting or comparing versions: %s %s" % (
type(exc).__name__, exc))
print (result)
if __name__ == "__main__":
main()
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"]},