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