1""" 2Python Script Wrapper for Windows 3================================= 4 5setuptools includes wrappers for Python scripts that allows them to be 6executed like regular windows programs. There are 2 wrappers, one 7for command-line programs, cli.exe, and one for graphical programs, 8gui.exe. These programs are almost identical, function pretty much 9the same way, and are generated from the same source file. The 10wrapper programs are used by copying them to the directory containing 11the script they are to wrap and with the same name as the script they 12are to wrap. 13""" 14 15from __future__ import absolute_import 16 17import sys 18import textwrap 19import subprocess 20 21import pytest 22 23from setuptools.command.easy_install import nt_quote_arg 24import pkg_resources 25 26pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only") 27 28 29class WrapperTester: 30 @classmethod 31 def prep_script(cls, template): 32 python_exe = nt_quote_arg(sys.executable) 33 return template % locals() 34 35 @classmethod 36 def create_script(cls, tmpdir): 37 """ 38 Create a simple script, foo-script.py 39 40 Note that the script starts with a Unix-style '#!' line saying which 41 Python executable to run. The wrapper will use this line to find the 42 correct Python executable. 43 """ 44 45 script = cls.prep_script(cls.script_tmpl) 46 47 with (tmpdir / cls.script_name).open('w') as f: 48 f.write(script) 49 50 # also copy cli.exe to the sample directory 51 with (tmpdir / cls.wrapper_name).open('wb') as f: 52 w = pkg_resources.resource_string('setuptools', cls.wrapper_source) 53 f.write(w) 54 55 56class TestCLI(WrapperTester): 57 script_name = 'foo-script.py' 58 wrapper_source = 'cli-32.exe' 59 wrapper_name = 'foo.exe' 60 script_tmpl = textwrap.dedent(""" 61 #!%(python_exe)s 62 import sys 63 input = repr(sys.stdin.read()) 64 print(sys.argv[0][-14:]) 65 print(sys.argv[1:]) 66 print(input) 67 if __debug__: 68 print('non-optimized') 69 """).lstrip() 70 71 def test_basic(self, tmpdir): 72 """ 73 When the copy of cli.exe, foo.exe in this example, runs, it examines 74 the path name it was run with and computes a Python script path name 75 by removing the '.exe' suffix and adding the '-script.py' suffix. (For 76 GUI programs, the suffix '-script.pyw' is added.) This is why we 77 named out script the way we did. Now we can run out script by running 78 the wrapper: 79 80 This example was a little pathological in that it exercised windows 81 (MS C runtime) quoting rules: 82 83 - Strings containing spaces are surrounded by double quotes. 84 85 - Double quotes in strings need to be escaped by preceding them with 86 back slashes. 87 88 - One or more backslashes preceding double quotes need to be escaped 89 by preceding each of them with back slashes. 90 """ 91 self.create_script(tmpdir) 92 cmd = [ 93 str(tmpdir / 'foo.exe'), 94 'arg1', 95 'arg 2', 96 'arg "2\\"', 97 'arg 4\\', 98 'arg5 a\\\\b', 99 ] 100 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) 101 stdout, stderr = proc.communicate('hello\nworld\n'.encode('ascii')) 102 actual = stdout.decode('ascii').replace('\r\n', '\n') 103 expected = textwrap.dedent(r""" 104 \foo-script.py 105 ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b'] 106 'hello\nworld\n' 107 non-optimized 108 """).lstrip() 109 assert actual == expected 110 111 def test_with_options(self, tmpdir): 112 """ 113 Specifying Python Command-line Options 114 -------------------------------------- 115 116 You can specify a single argument on the '#!' line. This can be used 117 to specify Python options like -O, to run in optimized mode or -i 118 to start the interactive interpreter. You can combine multiple 119 options as usual. For example, to run in optimized mode and 120 enter the interpreter after running the script, you could use -Oi: 121 """ 122 self.create_script(tmpdir) 123 tmpl = textwrap.dedent(""" 124 #!%(python_exe)s -Oi 125 import sys 126 input = repr(sys.stdin.read()) 127 print(sys.argv[0][-14:]) 128 print(sys.argv[1:]) 129 print(input) 130 if __debug__: 131 print('non-optimized') 132 sys.ps1 = '---' 133 """).lstrip() 134 with (tmpdir / 'foo-script.py').open('w') as f: 135 f.write(self.prep_script(tmpl)) 136 cmd = [str(tmpdir / 'foo.exe')] 137 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) 138 stdout, stderr = proc.communicate() 139 actual = stdout.decode('ascii').replace('\r\n', '\n') 140 expected = textwrap.dedent(r""" 141 \foo-script.py 142 [] 143 '' 144 --- 145 """).lstrip() 146 assert actual == expected 147 148 149class TestGUI(WrapperTester): 150 """ 151 Testing the GUI Version 152 ----------------------- 153 """ 154 script_name = 'bar-script.pyw' 155 wrapper_source = 'gui-32.exe' 156 wrapper_name = 'bar.exe' 157 158 script_tmpl = textwrap.dedent(""" 159 #!%(python_exe)s 160 import sys 161 f = open(sys.argv[1], 'wb') 162 bytes_written = f.write(repr(sys.argv[2]).encode('utf-8')) 163 f.close() 164 """).strip() 165 166 def test_basic(self, tmpdir): 167 """Test the GUI version with the simple scipt, bar-script.py""" 168 self.create_script(tmpdir) 169 170 cmd = [ 171 str(tmpdir / 'bar.exe'), 172 str(tmpdir / 'test_output.txt'), 173 'Test Argument', 174 ] 175 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) 176 stdout, stderr = proc.communicate() 177 assert not stdout 178 assert not stderr 179 with (tmpdir / 'test_output.txt').open('rb') as f_out: 180 actual = f_out.read().decode('ascii') 181 assert actual == repr('Test Argument') 182