• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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