# Copyright (C) 2009 Daniel Harding
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
r"""Executes a Python script on Windows, using heuristics to determine which
Python interpreter to use.
If the specified script doesn't start with #!, executes the script using the
same interpreter that is running this script (i.e. sys.executable).
If the the script does start with #!, attempts to determine which Python
interpreter to use based on the contents of the first line of the script.
Examples:
#! C:\Python26\python.exe
uses the interpreter specified on the #! line - in this case
C:\Python26\python.exe
#! /usr/bin/env python
uses the same interpreter that is running this script (i.e.
sys.executable)
#! /usr/bin/env python3
searches the Windows registry for the latest installed version of
Python 3.x, and uses the interpreter from that installation
#! /usr/bin/env python25
looks in the Windows registry to determine the installation path of
Python 2.5 and uses the interpreter from that installation
#! /usr/bin/env python2.5
same as the previous example
If there are arguments specified on the #! line, they are passed to the Python
interpreter along with the script plus any arguments for the script itself.
"""
import re
import os
import os.path
import shlex
import sys
try:
from optparse import OptionParser
except ImportError:
sys.stdout.write("Error: %s requires Python 2.3 or later\n" %
os.path.basename(sys.argv[0]))
sys.exit(1)
try:
import _winreg as winreg
except ImportError:
import winreg
ERROR_FILE_NOT_FOUND = 2
class RunnerException(Exception):
pass
# utility function because of differences between Python 2.x and Python 3.x
# exception handling syntax
def get_current_exception():
return sys.exc_info()[1]
def set_registry_entry(file_type, command, icon_file, user_only):
if user_only:
file_type_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER,
r'Software\CLASSES\%s' % file_type)
else:
file_type_key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, file_type)
try:
command_key = winreg.CreateKey(file_type_key, r'shell\open\command')
try:
winreg.SetValue(command_key, None, winreg.REG_SZ, command)
finally:
winreg.CloseKey(command_key)
if icon_file is not None:
icon_key = winreg.CreateKey(file_type_key, 'DefaultIcon')
try:
winreg.SetValue(icon_key, None, winreg.REG_SZ, icon_file)
finally:
winreg.CloseKey(icon_key)
else:
try:
winreg.DeleteKey(file_type_key, 'DefaultIcon')
except WindowsError:
# if the DefaultIcon key doesn't exist, ignore the error when
# trying to delete it
if get_current_exception().errno != ERROR_FILE_NOT_FOUND:
raise
finally:
winreg.CloseKey(file_type_key)
def find_icon_file(python_dir, file_name):
icon_path = os.path.join(python_dir, file_name)
if not os.path.exists(icon_path):
icon_path = os.path.join(python_dir, 'DLLs', file_name)
if not os.path.exists(icon_path):
icon_path = None
return icon_path
def substitute(source, substitutions):
for old, new in substitutions:
source = source.replace(old, new)
return source
def set_registry_entries(python_file_command, python_noconfile_command,
python_compiledfile_command, user_only):
try:
python_dir = os.path.dirname(sys.executable)
py_icon_path = find_icon_file(python_dir, 'py.ico')
if py_icon_path is None:
sys.stderr.write('Warning: Could not find icon for python'
' files\n')
pyc_icon_path = find_icon_file(python_dir, 'pyc.ico')
if pyc_icon_path is None:
sys.stderr.write('Warning: Could not find icon for compiled'
' python files\n')
substitutions = (
('${python_exe}', os.path.join(python_dir, 'python.exe')),
('${pythonw_exe}', os.path.join(python_dir, 'pythonw.exe')),
)
set_registry_entry('Python.File', substitute(python_file_command,
substitutions), py_icon_path, user_only)
set_registry_entry('Python.NoConFile',
substitute(python_noconfile_command, substitutions), py_icon_path,
user_only)
set_registry_entry('Python.CompiledFile',
substitute(python_compiledfile_command, substitutions),
pyc_icon_path, user_only)
except WindowsError:
raise RunnerException('Could not write to the Windows registry: %s'
% get_current_exception())
def install(user_only):
script_path = os.path.normpath(os.path.abspath(sys.argv[0]))
set_registry_entries(
python_file_command='"${python_exe}" "%s" "%%1" %%*' % script_path,
python_noconfile_command='"${pythonw_exe}" "%s" "%%1" %%*'
% script_path,
python_compiledfile_command='"${python_exe}" "%1" %*',
user_only=user_only)
def uninstall(user_only):
set_registry_entries(
python_file_command='"${python_exe}" "%1" %*',
python_noconfile_command='"${pythonw_exe}" "%1" %*',
python_compiledfile_command='"${python_exe}" "%1" %*',
user_only=user_only)
def get_interpreter_name(script):
extension = os.path.splitext(script)[1]
if extension == '.pyw':
executable = 'pythonw.exe'
else:
executable = 'python.exe'
return executable
def get_interpreter(path, script):
return os.path.join(path, get_interpreter_name(script))
def get_default_interpreter(script):
return get_interpreter(os.path.dirname(sys.executable), script)
def get_major_version_install_path(python_keys, major_version):
install_path = None
best_version = (0, 0)
for python_key in python_keys:
subkey_count = winreg.QueryInfoKey(python_key)[0]
for i in range(subkey_count):
version_key = winreg.EnumKey(python_key, i)
if re.match(r'^\d\.\d$', version_key) == None:
sys.stderr.write("Warning: Ignoring incorrectly formatted"
" version key '%s'.\n" % version_key)
continue
registry_version = tuple(map(int, version_key.split('.')))
if registry_version[0] == major_version \
and registry_version > best_version:
try:
install_path = winreg.QueryValue(python_key,
'%s\InstallPath' % version_key)
except WindowsError:
# sometimes the registry key for a version of Python will
# still exist in the registry even after that version of
# Python has been uninstalled, so ignore versions that
# don't have an InstallPath subkey
if get_current_exception().errno != ERROR_FILE_NOT_FOUND:
raise
else:
best_version = registry_version
if install_path is None:
raise RunnerException('Could not locate any installations for'
' Python %d.x using the Windows registry.' % major_version)
return install_path
def get_specific_version_install_path(python_keys, version):
install_path = None
registry_path = '%s\InstallPath' % '.'.join(map(str, version))
for python_key in python_keys:
try:
install_path = winreg.QueryValue(python_key, registry_path)
except WindowsError:
if get_current_exception().errno != ERROR_FILE_NOT_FOUND:
raise
else:
break
if install_path is None:
raise RunnerException('Could not find an installation of Python %d.%d'
' using the Windows registry.' % version)
return install_path
def find_best_interpreter(version, script):
try:
python_keys = []
for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
try:
python_keys.append(winreg.OpenKey(key,
'SOFTWARE\Python\PythonCore'))
except WindowsError:
if get_current_exception().errno != ERROR_FILE_NOT_FOUND:
raise
if not python_keys:
raise RunnerException('Could not locate any Python installations'
' using the Windows registry.')
try:
if len(version) == 1:
install_path = get_major_version_install_path(python_keys,
version[0])
else:
install_path = get_specific_version_install_path(python_keys,
version)
return get_interpreter(install_path, script)
finally:
for python_key in python_keys:
winreg.CloseKey(python_key)
except WindowsError:
raise RunnerException('Could not read from the Windows registry: %s'
% get_current_exception())
# escape arguments so that they will be parsed properly by Microsoft C/C++
# startup code as documented at
#
def escape_args(args):
new_args = []
for arg in args:
# escape quotation marks, doubling up any preceeding backslashes
arg = re.sub(r'(\\*)"', r'\1\1\\"', arg)
# wrap string in quotes if it contains spaces or tabs, doubling up
# backslashes that occur at the end of the argument
if ' ' in arg or '\t' in arg:
arg = '"%s"' % re.sub(r'(\\+)$', r'\1\1', arg)
new_args.append(arg)
return new_args
def execute_script(python, python_args, script, script_args):
try:
# must use os.spawnv with os.P_WAIT because on windows os.execv does
# not actually overlay the current process as it does on POSIX
# platforms - this results in two instances in the Python interpreter
# in memory, but it can't be helped
sys.exit(os.spawnv(os.P_WAIT, python,
escape_args([python] + python_args + [script] + script_args)))
except OSError:
raise RunnerException('Could not start Python excutable "%s": %s'
% (python, get_current_exception()))
def run_script(script, script_args):
python = get_default_interpreter(script)
python_args = []
try:
f = open(script, 'r')
line = f.readline()
f.close()
except IOError:
raise RunnerException('Could not read from script "%s": %s' %
(script, get_current_exception()))
if line.startswith('#!'):
line = line[2:]
parts = shlex.split(line)
if not parts:
raise RunnerException('Script "%s" starts with \'#!\' but does not'
' specify an executable.' % script)
executable = parts[0]
if os.path.exists(executable):
if not os.path.isabs(executable):
raise RunnerException('Script "%s" specifies an executable'
'using a relative path (%s).' % (script, executable))
python = executable
python_args = parts[1:]
else:
# match the following forms (where X and Y are major and minor
# version numbers, respectively):
# python
# pythonX
# pythonXY
# pythonX.Y
m = re.search(r'\bpython((?:\d\.\d)|\d{,2})\b', line)
if m:
python_args = shlex.split(line[m.end():])
version = tuple(map(int, m.group(1).replace('.', '')))
if version:
python = find_best_interpreter(version, script)
else:
raise RunnerException('Script "%s" starts with \'#!\' but does'
'not specify a Python executable to use.')
execute_script(python, python_args, script, script_args)
def main():
try:
parser = OptionParser()
parser.set_usage("""usage: python %prog script args
python %prog --install [options]
python %prog --uninstall [options]""")
parser.add_option('--install', action='store_true',
help='update the Windows registry to use this script to run'
' Python files, defaulting to the current Python'
' interpreter (Python %d.%d)' % sys.version_info[0:2])
parser.add_option('--uninstall', action='store_true', default=False,
help='update the Windows registry to use the current Python'
' interpreter (Python %d.%d) to run Python files'
% sys.version_info[0:2])
parser.add_option('--all', action='store_false', dest='user_only',
help='update the Windows registry for all users (requires'
' administrative rights) (default)')
parser.add_option('--user', action='store_true', dest='user_only',
help='update the Windows registry for only for the current'
' user')
parser.set_defaults(install=False, uninstall=False, user_only=None)
parser.disable_interspersed_args()
options, args = parser.parse_args()
if options.install:
if options.uninstall:
parser.error('--install and --uninstall are mutually'
' exclusive')
if args:
parser.error('--install cannot be specified along with a'
' script')
install(options.user_only)
elif options.uninstall:
if args:
parser.error('--uninstall cannot be specified along with a'
' script')
uninstall(options.user_only)
else:
if not args:
parser.error('script required')
if options.user_only is True:
parser.error('--user cannot be specified along with a script')
if options.user_only is False:
parser.error('--all cannot be specified along with a script')
run_script(args[0], args[1:])
except RunnerException:
sys.stderr.write('Error: %s' % get_current_exception())
sys.exit(1)
if __name__ == '__main__':
main()