| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- """
- Python Script Wrapper for Windows
- =================================
- setuptools includes wrappers for Python scripts that allows them to be
- executed like regular windows programs. There are 2 wrappers, one
- for command-line programs, cli.exe, and one for graphical programs,
- gui.exe. These programs are almost identical, function pretty much
- the same way, and are generated from the same source file. The
- wrapper programs are used by copying them to the directory containing
- the script they are to wrap and with the same name as the script they
- are to wrap.
- """
- import pathlib
- import platform
- import subprocess
- import sys
- import textwrap
- import pytest
- from setuptools._importlib import resources
- pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only")
- class WrapperTester:
- @classmethod
- def prep_script(cls, template):
- python_exe = subprocess.list2cmdline([sys.executable])
- return template % locals()
- @classmethod
- def create_script(cls, tmpdir):
- """
- Create a simple script, foo-script.py
- Note that the script starts with a Unix-style '#!' line saying which
- Python executable to run. The wrapper will use this line to find the
- correct Python executable.
- """
- script = cls.prep_script(cls.script_tmpl)
- with (tmpdir / cls.script_name).open('w') as f:
- f.write(script)
- # also copy cli.exe to the sample directory
- with (tmpdir / cls.wrapper_name).open('wb') as f:
- w = resources.files('setuptools').joinpath(cls.wrapper_source).read_bytes()
- f.write(w)
- def win_launcher_exe(prefix):
- """A simple routine to select launcher script based on platform."""
- assert prefix in ('cli', 'gui')
- if platform.machine() == "ARM64":
- return f"{prefix}-arm64.exe"
- else:
- return f"{prefix}-32.exe"
- class TestCLI(WrapperTester):
- script_name = 'foo-script.py'
- wrapper_name = 'foo.exe'
- wrapper_source = win_launcher_exe('cli')
- script_tmpl = textwrap.dedent(
- """
- #!%(python_exe)s
- import sys
- input = repr(sys.stdin.read())
- print(sys.argv[0][-14:])
- print(sys.argv[1:])
- print(input)
- if __debug__:
- print('non-optimized')
- """
- ).lstrip()
- def test_basic(self, tmpdir):
- """
- When the copy of cli.exe, foo.exe in this example, runs, it examines
- the path name it was run with and computes a Python script path name
- by removing the '.exe' suffix and adding the '-script.py' suffix. (For
- GUI programs, the suffix '-script.pyw' is added.) This is why we
- named out script the way we did. Now we can run out script by running
- the wrapper:
- This example was a little pathological in that it exercised windows
- (MS C runtime) quoting rules:
- - Strings containing spaces are surrounded by double quotes.
- - Double quotes in strings need to be escaped by preceding them with
- back slashes.
- - One or more backslashes preceding double quotes need to be escaped
- by preceding each of them with back slashes.
- """
- self.create_script(tmpdir)
- cmd = [
- str(tmpdir / 'foo.exe'),
- 'arg1',
- 'arg 2',
- 'arg "2\\"',
- 'arg 4\\',
- 'arg5 a\\\\b',
- ]
- proc = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- text=True,
- encoding="utf-8",
- )
- stdout, _stderr = proc.communicate('hello\nworld\n')
- actual = stdout.replace('\r\n', '\n')
- expected = textwrap.dedent(
- r"""
- \foo-script.py
- ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']
- 'hello\nworld\n'
- non-optimized
- """
- ).lstrip()
- assert actual == expected
- def test_symlink(self, tmpdir):
- """
- Ensure that symlink for the foo.exe is working correctly.
- """
- script_dir = tmpdir / "script_dir"
- script_dir.mkdir()
- self.create_script(script_dir)
- symlink = pathlib.Path(tmpdir / "foo.exe")
- symlink.symlink_to(script_dir / "foo.exe")
- cmd = [
- str(tmpdir / 'foo.exe'),
- 'arg1',
- 'arg 2',
- 'arg "2\\"',
- 'arg 4\\',
- 'arg5 a\\\\b',
- ]
- proc = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- text=True,
- encoding="utf-8",
- )
- stdout, _stderr = proc.communicate('hello\nworld\n')
- actual = stdout.replace('\r\n', '\n')
- expected = textwrap.dedent(
- r"""
- \foo-script.py
- ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']
- 'hello\nworld\n'
- non-optimized
- """
- ).lstrip()
- assert actual == expected
- def test_with_options(self, tmpdir):
- """
- Specifying Python Command-line Options
- --------------------------------------
- You can specify a single argument on the '#!' line. This can be used
- to specify Python options like -O, to run in optimized mode or -i
- to start the interactive interpreter. You can combine multiple
- options as usual. For example, to run in optimized mode and
- enter the interpreter after running the script, you could use -Oi:
- """
- self.create_script(tmpdir)
- tmpl = textwrap.dedent(
- """
- #!%(python_exe)s -Oi
- import sys
- input = repr(sys.stdin.read())
- print(sys.argv[0][-14:])
- print(sys.argv[1:])
- print(input)
- if __debug__:
- print('non-optimized')
- sys.ps1 = '---'
- """
- ).lstrip()
- with (tmpdir / 'foo-script.py').open('w') as f:
- f.write(self.prep_script(tmpl))
- cmd = [str(tmpdir / 'foo.exe')]
- proc = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- encoding="utf-8",
- )
- stdout, _stderr = proc.communicate()
- actual = stdout.replace('\r\n', '\n')
- expected = textwrap.dedent(
- r"""
- \foo-script.py
- []
- ''
- ---
- """
- ).lstrip()
- assert actual == expected
- class TestGUI(WrapperTester):
- """
- Testing the GUI Version
- -----------------------
- """
- script_name = 'bar-script.pyw'
- wrapper_source = win_launcher_exe('gui')
- wrapper_name = 'bar.exe'
- script_tmpl = textwrap.dedent(
- """
- #!%(python_exe)s
- import sys
- f = open(sys.argv[1], 'wb')
- bytes_written = f.write(repr(sys.argv[2]).encode('utf-8'))
- f.close()
- """
- ).strip()
- def test_basic(self, tmpdir):
- """Test the GUI version with the simple script, bar-script.py"""
- self.create_script(tmpdir)
- cmd = [
- str(tmpdir / 'bar.exe'),
- str(tmpdir / 'test_output.txt'),
- 'Test Argument',
- ]
- proc = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- encoding="utf-8",
- )
- stdout, stderr = proc.communicate()
- assert not stdout
- assert not stderr
- with (tmpdir / 'test_output.txt').open('rb') as f_out:
- actual = f_out.read().decode('ascii')
- assert actual == repr('Test Argument')
|