codekingpro/portable-devtools
114k
1"""2Python Script Wrapper for Windows3=================================4 5setuptools includes wrappers for Python scripts that allows them to be6executed like regular windows programs. There are 2 wrappers, one7for command-line programs, cli.exe, and one for graphical programs,8gui.exe. These programs are almost identical, function pretty much9the same way, and are generated from the same source file. The10wrapper programs are used by copying them to the directory containing11the script they are to wrap and with the same name as the script they12are to wrap.13"""14 15import pathlib16import platform17import subprocess18import sys19import textwrap20 21import pytest22 23from setuptools._importlib import resources24 25pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only")26 27 28class WrapperTester:29 @classmethod30 def prep_script(cls, template):31 python_exe = subprocess.list2cmdline([sys.executable])32 return template % locals()33 34 @classmethod35 def create_script(cls, tmpdir):36 """37 Create a simple script, foo-script.py38 39 Note that the script starts with a Unix-style '#!' line saying which40 Python executable to run. The wrapper will use this line to find the41 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 directory50 with (tmpdir / cls.wrapper_name).open('wb') as f:51 w = resources.files('setuptools').joinpath(cls.wrapper_source).read_bytes()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 f"{prefix}-arm64.exe"60 else:61 return f"{prefix}-32.exe"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 """71 #!%(python_exe)s72 import sys73 input = repr(sys.stdin.read())74 print(sys.argv[0][-14:])75 print(sys.argv[1:])76 print(input)77 if __debug__:78 print('non-optimized')79 """80 ).lstrip()81 82 def test_basic(self, tmpdir):83 """84 When the copy of cli.exe, foo.exe in this example, runs, it examines85 the path name it was run with and computes a Python script path name86 by removing the '.exe' suffix and adding the '-script.py' suffix. (For87 GUI programs, the suffix '-script.pyw' is added.) This is why we88 named out script the way we did. Now we can run out script by running89 the wrapper:90 91 This example was a little pathological in that it exercised windows92 (MS C runtime) quoting rules:93 94 - Strings containing spaces are surrounded by double quotes.95 96 - Double quotes in strings need to be escaped by preceding them with97 back slashes.98 99 - One or more backslashes preceding double quotes need to be escaped100 by preceding each of them with back slashes.101 """102 self.create_script(tmpdir)103 cmd = [104 str(tmpdir / 'foo.exe'),105 'arg1',106 'arg 2',107 'arg "2\\"',108 'arg 4\\',109 'arg5 a\\\\b',110 ]111 proc = subprocess.Popen(112 cmd,113 stdout=subprocess.PIPE,114 stdin=subprocess.PIPE,115 text=True,116 encoding="utf-8",117 )118 stdout, _stderr = proc.communicate('hello\nworld\n')119 actual = stdout.replace('\r\n', '\n')120 expected = textwrap.dedent(121 r"""122 \foo-script.py123 ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']124 'hello\nworld\n'125 non-optimized126 """127 ).lstrip()128 assert actual == expected129 130 def test_symlink(self, tmpdir):131 """132 Ensure that symlink for the foo.exe is working correctly.133 """134 script_dir = tmpdir / "script_dir"135 script_dir.mkdir()136 self.create_script(script_dir)137 symlink = pathlib.Path(tmpdir / "foo.exe")138 symlink.symlink_to(script_dir / "foo.exe")139 140 cmd = [141 str(tmpdir / 'foo.exe'),142 'arg1',143 'arg 2',144 'arg "2\\"',145 'arg 4\\',146 'arg5 a\\\\b',147 ]148 proc = subprocess.Popen(149 cmd,150 stdout=subprocess.PIPE,151 stdin=subprocess.PIPE,152 text=True,153 encoding="utf-8",154 )155 stdout, _stderr = proc.communicate('hello\nworld\n')156 actual = stdout.replace('\r\n', '\n')157 expected = textwrap.dedent(158 r"""159 \foo-script.py160 ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']161 'hello\nworld\n'162 non-optimized163 """164 ).lstrip()165 assert actual == expected166 167 def test_with_options(self, tmpdir):168 """169 Specifying Python Command-line Options170 --------------------------------------171 172 You can specify a single argument on the '#!' line. This can be used173 to specify Python options like -O, to run in optimized mode or -i174 to start the interactive interpreter. You can combine multiple175 options as usual. For example, to run in optimized mode and176 enter the interpreter after running the script, you could use -Oi:177 """178 self.create_script(tmpdir)179 tmpl = textwrap.dedent(180 """181 #!%(python_exe)s -Oi182 import sys183 input = repr(sys.stdin.read())184 print(sys.argv[0][-14:])185 print(sys.argv[1:])186 print(input)187 if __debug__:188 print('non-optimized')189 sys.ps1 = '---'190 """191 ).lstrip()192 with (tmpdir / 'foo-script.py').open('w') as f:193 f.write(self.prep_script(tmpl))194 cmd = [str(tmpdir / 'foo.exe')]195 proc = subprocess.Popen(196 cmd,197 stdout=subprocess.PIPE,198 stdin=subprocess.PIPE,199 stderr=subprocess.STDOUT,200 text=True,201 encoding="utf-8",202 )203 stdout, _stderr = proc.communicate()204 actual = stdout.replace('\r\n', '\n')205 expected = textwrap.dedent(206 r"""207 \foo-script.py208 []209 ''210 ---211 """212 ).lstrip()213 assert actual == expected214 215 216class TestGUI(WrapperTester):217 """218 Testing the GUI Version219 -----------------------220 """221 222 script_name = 'bar-script.pyw'223 wrapper_source = win_launcher_exe('gui')224 wrapper_name = 'bar.exe'225 226 script_tmpl = textwrap.dedent(227 """228 #!%(python_exe)s229 import sys230 f = open(sys.argv[1], 'wb')231 bytes_written = f.write(repr(sys.argv[2]).encode('utf-8'))232 f.close()233 """234 ).strip()235 236 def test_basic(self, tmpdir):237 """Test the GUI version with the simple script, bar-script.py"""238 self.create_script(tmpdir)239 240 cmd = [241 str(tmpdir / 'bar.exe'),242 str(tmpdir / 'test_output.txt'),243 'Test Argument',244 ]245 proc = subprocess.Popen(246 cmd,247 stdout=subprocess.PIPE,248 stdin=subprocess.PIPE,249 stderr=subprocess.STDOUT,250 text=True,251 encoding="utf-8",252 )253 stdout, stderr = proc.communicate()254 assert not stdout255 assert not stderr256 with (tmpdir / 'test_output.txt').open('rb') as f_out:257 actual = f_out.read().decode('ascii')258 assert actual == repr('Test Argument')259 