• 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
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