File: test_pil.py

package info (click to toggle)
pyinstaller 6.18.0%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 11,824 kB
  • sloc: python: 41,828; ansic: 12,123; makefile: 171; sh: 131; xml: 19
file content (167 lines) | stat: -rw-r--r-- 5,739 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Functional tests for the Python Imaging Library (PIL).

Note that the original unmaintained PIL has been obsoleted by the PIL-compatible fork Pillow,
which retains the same Python package `PIL`.
"""

import pathlib

import pytest

from PyInstaller.utils.tests import importorskip
from PyInstaller.utils.hooks import can_import_module

# All tests in this file require PIL
pytestmark = importorskip('PIL')


# Functional test that tries to convert a .tiff image to a .png
def test_pil_image_conversion(pyi_builder, tmp_path):
    pyi_builder.test_source(
        """
        import sys
        import os

        import PIL.Image

        if len(sys.argv) != 3:
            print(f"use: {sys.argv[0]} <input-filename> <output-filename>")
            raise SystemExit(1)

        input_file = sys.argv[1]
        output_file = sys.argv[2]

        image = PIL.Image.open(input_file)
        image.save(output_file)
        """,
        app_args=[
            str(pathlib.Path(__file__).parent / 'data' / 'PIL_images' / 'tinysample.tiff'),
            str(tmp_path / 'converted_tinysample.png'),
        ],
    )


@importorskip('PyQt5')
def test_pil_pyqt5(pyi_builder):
    # hook-PIL is excluding PyQt5, but is must still be included since it is imported elsewhere.
    # Also see issue #1584.
    pyi_builder.test_source("""
        import PyQt5
        import PIL
        import PIL.ImageQt
        """)


def test_pil_plugins(pyi_builder):
    pyi_builder.test_source(
        """
        # Verify packaging of PIL.Image.
        from PIL.Image import frombytes
        print(frombytes)

        # PIL import hook should bundle all available PIL plugins. Verify that plugins are collected.
        from PIL import Image
        Image.init()
        MIN_PLUG_COUNT = 7  # Without all plugins the count is usually 6.
        plugins = list(Image.SAVE.keys())
        plugins.sort()
        if len(plugins) < MIN_PLUG_COUNT:
            raise SystemExit('No PIL image plugins were collected!')
        else:
            print('PIL supported image formats: %s' % plugins)
        """
    )


# The tkinter module may be available for import, but not actually importable due to missing shared libraries.
# Therefore, we need to use `can_import_module`-based skip decorator instead of `@importorskip`.
@pytest.mark.skipif(not can_import_module("tkinter"), reason="tkinter cannot be imported.")
def test_pil_no_tkinter(pyi_builder):
    """
    Ensure that the Tkinter package excluded by `PIL` package hooks is unimportable by frozen applications explicitly
    importing only the latter.
    """

    pyi_builder.test_source(
        """
        import PIL.Image

        # Dynamically importing the Tkinter package should fail with an "ImportError", implying "PIL" package hooks
        # successfully excluded Tkinter. To prevent PyInstaller from parsing this import and thus freezing this
        # extension with this test, this import is dynamic.
        try:
            __import__('tkinter')
            raise SystemExit('ERROR: Module tkinter is bundled.')
        except ImportError:
            pass

        # Dynamically importing the "_tkinter" shared library should also fail.
        try:
            __import__('_tkinter')
            raise SystemExit('ERROR: Module _tkinter is bundled.')
        except ImportError:
            pass
        """
    )


# The tkinter module may be available for import, but not actually importable due to missing shared libraries.
# Therefore, we need to use `can_import_module`-based skip decorator instead of `@importorskip`.
@pytest.mark.skipif(not can_import_module("tkinter"), reason="tkinter cannot be imported.")
def test_pil_tkinter(pyi_builder):
    """
    Ensure that the Tkinter package excluded by `PIL` package hooks is importable by frozen applications explicitly
    importing both.

    == See Also ==

    * PyInstaller [issue #1584](https://github.com/pyinstaller/pyinstaller/issues/1584).
    """

    pyi_builder.test_source(
        """
        import PIL.Image

        # Statically importing the Tkinter package should succeed, implying this importation successfully overrode
        # the exclusion of this package requested by "PIL" package hooks. To ensure PyInstaller parses this import
        # and freezes this package with this test, this import is static.
        try:
            import tkinter
        except ImportError:
            raise SystemExit('ERROR: Module tkinter is NOT bundled.')
        """
    )


@importorskip('matplotlib')
def test_pil_no_matplotlib(pyi_builder):
    """
    Ensure that using PIL.Image does not pull in `matplotlib` when the latter is not explicitly imported by the program.
    The import chain in question,
    PIL.Image -> PIL -> PIL.ImageShow -> IPython -> matplotlib_inline.backend_inline -> matplotlib,
    should be broken by the PIL hook excluding IPython.
    """

    pyi_builder.test_source(
        """
        import PIL.Image

        # Use dynamic import of matplotlib to prevent PyInstaller from picking up the import.
        try:
            __import__('matplotlib')
            raise SystemExit('ERROR: matplotlib is bundled.')
        except ImportError:
            pass
        """
    )