From 52090756c1a599e4f31322171da5a80c730deeb5 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 16:12:08 +0200 Subject: [PATCH 01/34] Remove unnecessary __future__s --- qrcode/image/styledpil.py | 3 --- qrcode/image/styles/colormasks.py | 3 --- qrcode/image/styles/moduledrawers/base.py | 2 -- qrcode/image/styles/moduledrawers/pil.py | 3 --- 4 files changed, 11 deletions(-) diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index 7c9d9995..6291872d 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -1,6 +1,3 @@ -# Needed on case-insensitive filesystems -from __future__ import absolute_import - import qrcode.image.base from qrcode.compat.pil import Image from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 3b9a8084..9a4dc4e7 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -1,6 +1,3 @@ -# Needed on case-insensitive filesystems -from __future__ import absolute_import - import math from qrcode.compat.pil import Image diff --git a/qrcode/image/styles/moduledrawers/base.py b/qrcode/image/styles/moduledrawers/base.py index 8de33059..f1acc97c 100644 --- a/qrcode/image/styles/moduledrawers/base.py +++ b/qrcode/image/styles/moduledrawers/base.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import abc from typing import TYPE_CHECKING diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index 398010c6..f4ff65a0 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -1,6 +1,3 @@ -# Needed on case-insensitive filesystems -from __future__ import absolute_import - from typing import TYPE_CHECKING, List from qrcode.compat.pil import Image, ImageDraw From 3b9b407ad03ea3ad316514360eebf3066a8233cf Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 16:14:34 +0200 Subject: [PATCH 02/34] There is only one way to import Pillow>9.1.0 --- qrcode/compat/pil.py | 12 ------------ qrcode/image/pil.py | 2 +- qrcode/image/styledpil.py | 2 +- qrcode/image/styles/colormasks.py | 2 +- qrcode/image/styles/moduledrawers/pil.py | 2 +- qrcode/tests/test_example.py | 2 +- qrcode/tests/test_qrcode.py | 2 +- qrcode/tests/test_script.py | 2 +- 8 files changed, 7 insertions(+), 19 deletions(-) delete mode 100644 qrcode/compat/pil.py diff --git a/qrcode/compat/pil.py b/qrcode/compat/pil.py deleted file mode 100644 index de6c6cb7..00000000 --- a/qrcode/compat/pil.py +++ /dev/null @@ -1,12 +0,0 @@ -# Try to import PIL in either of the two ways it can be installed. -Image = None -ImageDraw = None - -try: - from PIL import Image, ImageDraw # type: ignore # noqa: F401 -except ImportError: # pragma: no cover - try: - import Image # type: ignore # noqa: F401 - import ImageDraw # type: ignore # noqa: F401 - except ImportError: - pass diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 5767148d..547c9ab0 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,5 +1,5 @@ import qrcode.image.base -from qrcode.compat.pil import Image, ImageDraw +from PIL import Image, ImageDraw class PilImage(qrcode.image.base.BaseImage): diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index 6291872d..d0c3b24e 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -1,5 +1,5 @@ import qrcode.image.base -from qrcode.compat.pil import Image +from PIL import Image from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask from qrcode.image.styles.moduledrawers import SquareModuleDrawer diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 9a4dc4e7..43186b27 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -1,6 +1,6 @@ import math -from qrcode.compat.pil import Image +from PIL import Image class QRColorMask: diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index f4ff65a0..8b9321b9 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, List -from qrcode.compat.pil import Image, ImageDraw +from PIL import Image, ImageDraw from qrcode.image.styles.moduledrawers.base import QRModuleDrawer if TYPE_CHECKING: diff --git a/qrcode/tests/test_example.py b/qrcode/tests/test_example.py index 2e03dc53..a18830da 100644 --- a/qrcode/tests/test_example.py +++ b/qrcode/tests/test_example.py @@ -2,7 +2,7 @@ from unittest import mock from qrcode import run_example -from qrcode.compat.pil import Image +from PIL import Image class ExampleTest(unittest.TestCase): diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 5c1ea35b..c1b66ed6 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -9,7 +9,7 @@ import qrcode import qrcode.util -from qrcode.compat.pil import Image as pil_Image +from PIL import Image as pil_Image from qrcode.exceptions import DataOverflowError from qrcode.image.base import BaseImage from qrcode.image.pure import PyPNGImage diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index 4ae4ccbc..2075bea3 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -5,7 +5,7 @@ from tempfile import mkdtemp from unittest import mock -from qrcode.compat.pil import Image +from PIL import Image from qrcode.console_scripts import commas, main From eec74e081b4eab26e808624f007e9be3e48c8e1b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 17:01:02 +0200 Subject: [PATCH 03/34] Move test constants to a single location --- qrcode/tests/consts.py | 4 ++++ qrcode/tests/test_qrcode.py | 6 +----- qrcode/tests/test_qrcode_svg.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 qrcode/tests/consts.py diff --git a/qrcode/tests/consts.py b/qrcode/tests/consts.py new file mode 100644 index 00000000..b1240139 --- /dev/null +++ b/qrcode/tests/consts.py @@ -0,0 +1,4 @@ +UNICODE_TEXT = "\u03b1\u03b2\u03b3" +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 0, 0) diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 5c1ea35b..7a906ea3 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -15,13 +15,9 @@ from qrcode.image.pure import PyPNGImage from qrcode.image.styledpil import StyledPilImage from qrcode.image.styles import colormasks, moduledrawers +from qrcode.tests.consts import UNICODE_TEXT, WHITE, BLACK, RED from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData -UNICODE_TEXT = "\u03b1\u03b2\u03b3" -WHITE = (255, 255, 255) -BLACK = (0, 0, 0) -RED = (255, 0, 0) - class QRCodeTests(unittest.TestCase): def setUp(self): diff --git a/qrcode/tests/test_qrcode_svg.py b/qrcode/tests/test_qrcode_svg.py index 74b5c553..26ba0aeb 100644 --- a/qrcode/tests/test_qrcode_svg.py +++ b/qrcode/tests/test_qrcode_svg.py @@ -5,8 +5,7 @@ import qrcode from qrcode.image import svg - -UNICODE_TEXT = "\u03b1\u03b2\u03b3" +from qrcode.tests.consts import UNICODE_TEXT class SvgImageWhite(svg.SvgImage): From da16f4a30b4ab28db0c310f39287cb45c5351881 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 17:02:49 +0200 Subject: [PATCH 04/34] Split writer-specific tests out of test_qrcode --- qrcode/tests/test_qrcode.py | 242 +----------------------------- qrcode/tests/test_qrcode_pil.py | 197 ++++++++++++++++++++++++ qrcode/tests/test_qrcode_pypng.py | 32 ++++ 3 files changed, 230 insertions(+), 241 deletions(-) create mode 100644 qrcode/tests/test_qrcode_pil.py create mode 100644 qrcode/tests/test_qrcode_pypng.py diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 7a906ea3..08dc2cb9 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -1,31 +1,16 @@ import io -import os import unittest -import warnings -from tempfile import mkdtemp from unittest import mock -import png - import qrcode import qrcode.util -from qrcode.compat.pil import Image as pil_Image from qrcode.exceptions import DataOverflowError from qrcode.image.base import BaseImage -from qrcode.image.pure import PyPNGImage -from qrcode.image.styledpil import StyledPilImage -from qrcode.image.styles import colormasks, moduledrawers -from qrcode.tests.consts import UNICODE_TEXT, WHITE, BLACK, RED +from qrcode.tests.consts import UNICODE_TEXT from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData class QRCodeTests(unittest.TestCase): - def setUp(self): - self.tmpdir = mkdtemp() - - def tearDown(self): - os.rmdir(self.tmpdir) - def test_basic(self): qr = qrcode.QRCode(version=1) qr.add_data("a") @@ -95,42 +80,6 @@ def test_mode_8bit_newline(self): qr.make() self.assertEqual(qr.data_list[0].mode, MODE_8BIT_BYTE) - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_pil(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - self.assertIsInstance(img.get_image(), pil_Image.Image) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_pil_with_transparent_background(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="TransParent") - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_pil_with_red_background(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="red") - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_pil_with_rgb_color_tuples(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_with_pattern(self): - qr = qrcode.QRCode(mask_pattern=3) - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - def test_make_image_with_wrong_pattern(self): with self.assertRaises(TypeError): qrcode.QRCode(mask_pattern="string pattern") @@ -171,189 +120,6 @@ class MockFactory(BaseImage): self.assertTrue(MockFactory.new_image.called) self.assertTrue(MockFactory.drawrect.called) - def test_render_pypng(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=PyPNGImage) - self.assertIsInstance(img.get_image(), png.Writer) - - print(img.width, img.box_size, img.border) - img.save(io.BytesIO()) - - def test_render_pypng_to_str(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=PyPNGImage) - self.assertIsInstance(img.get_image(), png.Writer) - - mock_open = mock.mock_open() - with mock.patch("qrcode.image.pure.open", mock_open, create=True): - img.save("test_file.png") - mock_open.assert_called_once_with("test_file.png", "wb") - mock_open("test_file.png", "wb").write.assert_called() - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_Image(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_embeded_image(self): - embeded_img = pil_Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_embeded_image_path(self): - tmpfile = os.path.join(self.tmpdir, "test.png") - embeded_img = pil_Image.new("RGB", (10, 10), color="red") - embeded_img.save(tmpfile) - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embeded_image_path=tmpfile) - img.save(io.BytesIO()) - os.remove(tmpfile) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_square_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.SquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_gapped_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.GappedSquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_circle_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.CircleModuleDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_rounded_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.RoundedModuleDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_vertical_bars_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.VerticalBarsDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_horizontal_bars_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.HorizontalBarsDrawer(), - ) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_default_solid_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask() - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_solid_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_color_mask_with_transparency(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask( - back_color=(255, 0, 255, 255), front_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - assert img.mode == "RGBA" - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_radial_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.RadialGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_square_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SquareGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_horizontal_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.HorizontalGradiantColorMask( - back_color=WHITE, left_color=RED, right_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_vertical_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.VerticalGradiantColorMask( - back_color=WHITE, top_color=RED, bottom_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - @unittest.skipIf(not pil_Image, "Requires PIL") - def test_render_styled_with_image_color_mask(self): - img_mask = pil_Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.ImageColorMask(back_color=WHITE, color_mask_image=img_mask) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - def test_optimize(self): qr = qrcode.QRCode() text = "A1abc12345def1HELLOa" @@ -475,9 +241,3 @@ def test_negative_size_at_usage(self): qr = qrcode.QRCode() qr.box_size = -1 self.assertRaises(ValueError, qr.make_image) - - -class ShortcutTest(unittest.TestCase): - @unittest.skipIf(not pil_Image, "Requires PIL") - def runTest(self): - qrcode.make("image") diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py new file mode 100644 index 00000000..397797c1 --- /dev/null +++ b/qrcode/tests/test_qrcode_pil.py @@ -0,0 +1,197 @@ +import io +import os +import unittest + +import pytest + +import qrcode +import qrcode.util +from qrcode.image.styledpil import StyledPilImage +from qrcode.image.styles import colormasks, moduledrawers +from qrcode.tests.consts import UNICODE_TEXT, RED, WHITE, BLACK + +Image = pytest.importorskip("PIL.Image", reason="PIL is not installed") + + +class PILQRCodeTests(unittest.TestCase): + + def test_render_pil(self): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + self.assertIsInstance(img.get_image(), Image.Image) + + def test_render_pil_with_transparent_background(self): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color="TransParent") + img.save(io.BytesIO()) + + def test_render_pil_with_red_background(self): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color="red") + img.save(io.BytesIO()) + + def test_render_pil_with_rgb_color_tuples(self): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) + img.save(io.BytesIO()) + + def test_render_with_pattern(self): + qr = qrcode.QRCode(mask_pattern=3) + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + + def test_render_styled_Image(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage) + img.save(io.BytesIO()) + + def test_render_styled_with_embeded_image(self): + embeded_img = Image.new("RGB", (10, 10), color="red") + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) + img.save(io.BytesIO()) + + def test_render_styled_with_embeded_image_path(self, tmp_path): + tmpfile = os.path.join(str(tmp_path), "test.png") + embeded_img = Image.new("RGB", (10, 10), color="red") + embeded_img.save(tmpfile) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embeded_image_path=tmpfile) + img.save(io.BytesIO()) + + def test_render_styled_with_square_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.SquareModuleDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_gapped_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.GappedSquareModuleDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_circle_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.CircleModuleDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_rounded_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.RoundedModuleDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_vertical_bars_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.VerticalBarsDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_horizontal_bars_module_drawer(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.HorizontalBarsDrawer(), + ) + img.save(io.BytesIO()) + + def test_render_styled_with_default_solid_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask() + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_solid_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_color_mask_with_transparency(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask( + back_color=(255, 0, 255, 255), front_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + assert img.mode == "RGBA" + + def test_render_styled_with_radial_gradient_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.RadialGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_square_gradient_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SquareGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_horizontal_gradient_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.HorizontalGradiantColorMask( + back_color=WHITE, left_color=RED, right_color=BLACK + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_vertical_gradient_color_mask(self): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.VerticalGradiantColorMask( + back_color=WHITE, top_color=RED, bottom_color=BLACK + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + def test_render_styled_with_image_color_mask(self): + img_mask = Image.new("RGB", (10, 10), color="red") + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.ImageColorMask(back_color=WHITE, color_mask_image=img_mask) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +class ShortcutTest(unittest.TestCase): + + def runTest(self): + qrcode.make("image") diff --git a/qrcode/tests/test_qrcode_pypng.py b/qrcode/tests/test_qrcode_pypng.py new file mode 100644 index 00000000..0245b207 --- /dev/null +++ b/qrcode/tests/test_qrcode_pypng.py @@ -0,0 +1,32 @@ +import io +from unittest import mock + +import png + +import qrcode +import qrcode.util +from qrcode.image.pure import PyPNGImage +from qrcode.tests.consts import UNICODE_TEXT + + +def test_render_pypng(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=PyPNGImage) + assert isinstance(img.get_image(), png.Writer) + + print(img.width, img.box_size, img.border) + img.save(io.BytesIO()) + + +def test_render_pypng_to_str(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=PyPNGImage) + assert isinstance(img.get_image(), png.Writer) + + mock_open = mock.mock_open() + with mock.patch("qrcode.image.pure.open", mock_open, create=True): + img.save("test_file.png") + mock_open.assert_called_once_with("test_file.png", "wb") + mock_open("test_file.png", "wb").write.assert_called() From 6ddd6dfc1055f3e299f1f1feb64efec9bfa568e4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 17:08:59 +0200 Subject: [PATCH 05/34] Rewrite assertions to regular/pytest style --- qrcode/tests/test_qrcode.py | 107 +++++++++++++++----------------- qrcode/tests/test_qrcode_pil.py | 2 +- qrcode/tests/test_script.py | 26 ++++---- qrcode/tests/test_util.py | 5 +- 4 files changed, 68 insertions(+), 72 deletions(-) diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 08dc2cb9..1ed870c0 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -1,3 +1,4 @@ +import pytest import io import unittest from unittest import mock @@ -22,15 +23,18 @@ def test_large(self): qr.make(fit=False) def test_invalid_version(self): - self.assertRaises(ValueError, qrcode.QRCode, version=41) + with pytest.raises(ValueError): + qrcode.QRCode(version=42) def test_invalid_border(self): - self.assertRaises(ValueError, qrcode.QRCode, border=-1) + with pytest.raises(ValueError): + qrcode.QRCode(border=-1) def test_overflow(self): qr = qrcode.QRCode(version=1) qr.add_data("abcdefghijklmno") - self.assertRaises(DataOverflowError, qr.make, fit=False) + with pytest.raises(DataOverflowError): + qr.make(fit=False) def test_add_qrdata(self): qr = qrcode.QRCode(version=1) @@ -42,71 +46,71 @@ def test_fit(self): qr = qrcode.QRCode() qr.add_data("a") qr.make() - self.assertEqual(qr.version, 1) + assert qr.version == 1 qr.add_data("bcdefghijklmno") qr.make() - self.assertEqual(qr.version, 2) + assert qr.version == 2 def test_mode_number(self): qr = qrcode.QRCode() qr.add_data("1234567890123456789012345678901234", optimize=0) qr.make() - self.assertEqual(qr.version, 1) - self.assertEqual(qr.data_list[0].mode, MODE_NUMBER) + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_NUMBER def test_mode_alpha(self): qr = qrcode.QRCode() qr.add_data("ABCDEFGHIJ1234567890", optimize=0) qr.make() - self.assertEqual(qr.version, 1) - self.assertEqual(qr.data_list[0].mode, MODE_ALPHA_NUM) + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM def test_regression_mode_comma(self): qr = qrcode.QRCode() qr.add_data(",", optimize=0) qr.make() - self.assertEqual(qr.data_list[0].mode, MODE_8BIT_BYTE) + assert qr.data_list[0].mode == MODE_8BIT_BYTE def test_mode_8bit(self): qr = qrcode.QRCode() qr.add_data("abcABC" + UNICODE_TEXT, optimize=0) qr.make() - self.assertEqual(qr.version, 1) - self.assertEqual(qr.data_list[0].mode, MODE_8BIT_BYTE) + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_8BIT_BYTE def test_mode_8bit_newline(self): qr = qrcode.QRCode() qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0) qr.make() - self.assertEqual(qr.data_list[0].mode, MODE_8BIT_BYTE) + assert qr.data_list[0].mode == MODE_8BIT_BYTE def test_make_image_with_wrong_pattern(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): qrcode.QRCode(mask_pattern="string pattern") - with self.assertRaises(ValueError): + with pytest.raises(ValueError): qrcode.QRCode(mask_pattern=-1) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): qrcode.QRCode(mask_pattern=42) def test_mask_pattern_setter(self): qr = qrcode.QRCode() - with self.assertRaises(TypeError): + with pytest.raises(TypeError): qr.mask_pattern = "string pattern" - with self.assertRaises(ValueError): + with pytest.raises(ValueError): qr.mask_pattern = -1 - with self.assertRaises(ValueError): + with pytest.raises(ValueError): qr.mask_pattern = 8 def test_qrcode_bad_factory(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): qrcode.QRCode(image_factory="not_BaseImage") # type: ignore - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): qrcode.QRCode(image_factory=dict) # type: ignore def test_qrcode_factory(self): @@ -117,44 +121,32 @@ class MockFactory(BaseImage): qr = qrcode.QRCode(image_factory=MockFactory) qr.add_data(UNICODE_TEXT) qr.make_image() - self.assertTrue(MockFactory.new_image.called) - self.assertTrue(MockFactory.drawrect.called) + assert MockFactory.new_image.called + assert MockFactory.drawrect.called def test_optimize(self): qr = qrcode.QRCode() text = "A1abc12345def1HELLOa" qr.add_data(text, optimize=4) qr.make() - self.assertEqual( - [d.mode for d in qr.data_list], - [ - MODE_8BIT_BYTE, - MODE_NUMBER, - MODE_8BIT_BYTE, - MODE_ALPHA_NUM, - MODE_8BIT_BYTE, - ], - ) - self.assertEqual(qr.version, 2) + assert [d.mode for d in qr.data_list] == [MODE_8BIT_BYTE, MODE_NUMBER, MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_8BIT_BYTE] + assert qr.version == 2 def test_optimize_short(self): qr = qrcode.QRCode() text = "A1abc1234567def1HELLOa" qr.add_data(text, optimize=7) qr.make() - self.assertEqual(len(qr.data_list), 3) - self.assertEqual( - [d.mode for d in qr.data_list], - [MODE_8BIT_BYTE, MODE_NUMBER, MODE_8BIT_BYTE], - ) - self.assertEqual(qr.version, 2) + assert len(qr.data_list) == 3 + assert [d.mode for d in qr.data_list] == [MODE_8BIT_BYTE, MODE_NUMBER, MODE_8BIT_BYTE] + assert qr.version == 2 def test_optimize_longer_than_data(self): qr = qrcode.QRCode() text = "ABCDEFGHIJK" qr.add_data(text, optimize=12) - self.assertEqual(len(qr.data_list), 1) - self.assertEqual(qr.data_list[0].mode, MODE_ALPHA_NUM) + assert len(qr.data_list) == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM def test_optimize_size(self): text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5 @@ -162,24 +154,25 @@ def test_optimize_size(self): qr = qrcode.QRCode() qr.add_data(text) qr.make() - self.assertEqual(qr.version, 10) + assert qr.version == 10 qr = qrcode.QRCode() qr.add_data(text, optimize=0) qr.make() - self.assertEqual(qr.version, 11) + assert qr.version == 11 def test_qrdata_repr(self): data = b"hello" data_obj = qrcode.util.QRData(data) - self.assertEqual(repr(data_obj), repr(data)) + assert repr(data_obj) == repr(data) def test_print_ascii_stdout(self): qr = qrcode.QRCode() with mock.patch("sys.stdout") as fake_stdout: fake_stdout.isatty.return_value = None - self.assertRaises(OSError, qr.print_ascii, tty=True) - self.assertTrue(fake_stdout.isatty.called) + with pytest.raises(OSError): + qr.print_ascii(tty=True) + assert fake_stdout.isatty.called def test_print_ascii(self): qr = qrcode.QRCode(border=0) @@ -188,7 +181,7 @@ def test_print_ascii(self): printed = f.getvalue() f.close() expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588" - self.assertEqual(printed[: len(expected)], expected) + assert printed[:len(expected)] == expected f = io.StringIO() f.isatty = lambda: True @@ -198,14 +191,14 @@ def test_print_ascii(self): expected = ( "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0" ) - self.assertEqual(printed[: len(expected)], expected) + assert printed[:len(expected)] == expected def test_print_tty_stdout(self): qr = qrcode.QRCode() with mock.patch("sys.stdout") as fake_stdout: fake_stdout.isatty.return_value = None - self.assertRaises(OSError, qr.print_tty) - self.assertTrue(fake_stdout.isatty.called) + pytest.raises(OSError, qr.print_tty) + assert fake_stdout.isatty.called def test_print_tty(self): qr = qrcode.QRCode() @@ -221,23 +214,25 @@ def test_print_tty(self): expected = ( BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK ) - self.assertEqual(printed[: len(expected)], expected) + assert printed[:len(expected)] == expected def test_get_matrix(self): qr = qrcode.QRCode(border=0) qr.add_data("1") - self.assertEqual(qr.get_matrix(), qr.modules) + assert qr.get_matrix() == qr.modules def test_get_matrix_border(self): qr = qrcode.QRCode(border=1) qr.add_data("1") matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]] - self.assertEqual(matrix, qr.modules) + assert matrix == qr.modules def test_negative_size_at_construction(self): - self.assertRaises(ValueError, qrcode.QRCode, box_size=-1) + with pytest.raises(ValueError): + qrcode.QRCode(box_size=-1) def test_negative_size_at_usage(self): qr = qrcode.QRCode() qr.box_size = -1 - self.assertRaises(ValueError, qr.make_image) + with pytest.raises(ValueError): + qr.make_image() diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index 397797c1..497896c1 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -20,7 +20,7 @@ def test_render_pil(self): qr.add_data(UNICODE_TEXT) img = qr.make_image() img.save(io.BytesIO()) - self.assertIsInstance(img.get_image(), Image.Image) + assert isinstance(img.get_image(), Image.Image) def test_render_pil_with_transparent_background(self): qr = qrcode.QRCode() diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index 4ae4ccbc..c19de76b 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -1,3 +1,4 @@ +import pytest import io import os import sys @@ -38,7 +39,7 @@ def test_piped(self, mock_stdout): def test_stdin(self, mock_stdin, mock_print_ascii): mock_stdin.buffer.read.return_value = "testtext" main([]) - self.assertTrue(mock_stdin.buffer.read.called) + assert mock_stdin.buffer.read.called mock_print_ascii.assert_called_with(tty=True) @mock.patch("os.isatty", lambda *args: True) @@ -49,7 +50,8 @@ def test_stdin_py3_unicodedecodeerror(self, mock_print_ascii): mock_stdin.read.side_effect = bad_read with mock.patch("sys.stdin", mock_stdin): # sys.stdin.read() will raise an error... - self.assertRaises(UnicodeDecodeError, sys.stdin.read) + with pytest.raises(UnicodeDecodeError): + sys.stdin.read() # ... but it won't be used now. main([]) mock_print_ascii.assert_called_with(tty=True) @@ -81,25 +83,23 @@ def test_output(self): @mock.patch("sys.stderr", new_callable=io.StringIO) @unittest.skipIf(not Image, "Requires PIL") def test_factory_drawer_none(self, mock_stderr): - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): main("testtext --factory pil --factory-drawer nope".split()) - self.assertIn( - "The selected factory has no drawer aliases", mock_stderr.getvalue() - ) + assert 'The selected factory has no drawer aliases' in mock_stderr.getvalue() @mock.patch("sys.stderr", new_callable=io.StringIO) def test_factory_drawer_bad(self, mock_stderr): - with self.assertRaises(SystemExit): + with pytest.raises(SystemExit): main("testtext --factory svg --factory-drawer sobad".split()) - self.assertIn("sobad factory drawer not found", mock_stderr.getvalue()) + assert 'sobad factory drawer not found' in mock_stderr.getvalue() @mock.patch("sys.stderr", new_callable=io.StringIO) def test_factory_drawer(self, mock_stderr): main("testtext --factory svg --factory-drawer circle".split()) def test_commas(self): - self.assertEqual(commas([]), "") - self.assertEqual(commas(["A"]), "A") - self.assertEqual(commas("AB"), "A or B") - self.assertEqual(commas("ABC"), "A, B or C") - self.assertEqual(commas("ABC", joiner="and"), "A, B and C") + assert commas([]) == '' + assert commas(['A']) == 'A' + assert commas('AB') == 'A or B' + assert commas('ABC') == 'A, B or C' + assert commas('ABC', joiner='and') == 'A, B and C' diff --git a/qrcode/tests/test_util.py b/qrcode/tests/test_util.py index 1c98e425..de19a103 100644 --- a/qrcode/tests/test_util.py +++ b/qrcode/tests/test_util.py @@ -1,3 +1,4 @@ +import pytest import unittest from qrcode import util @@ -5,8 +6,8 @@ class UtilTests(unittest.TestCase): def test_check_wrong_version(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): util.check_version(0) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): util.check_version(41) From c37646fbe254d01ad990f5531b206a06f90a55e0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 17:20:28 +0200 Subject: [PATCH 06/34] Rewrite tests in py.test format --- qrcode/tests/test_example.py | 16 +- qrcode/tests/test_qrcode.py | 475 +++++++++++++++++--------------- qrcode/tests/test_qrcode_pil.py | 383 ++++++++++++------------- qrcode/tests/test_qrcode_svg.py | 91 +++--- qrcode/tests/test_release.py | 61 ++-- qrcode/tests/test_script.py | 171 ++++++------ qrcode/tests/test_util.py | 12 +- 7 files changed, 623 insertions(+), 586 deletions(-) diff --git a/qrcode/tests/test_example.py b/qrcode/tests/test_example.py index 2e03dc53..7190fe3e 100644 --- a/qrcode/tests/test_example.py +++ b/qrcode/tests/test_example.py @@ -1,13 +1,13 @@ -import unittest from unittest import mock +import pytest + from qrcode import run_example -from qrcode.compat.pil import Image + +pytest.importorskip("PIL", reason="Requires PIL") -class ExampleTest(unittest.TestCase): - @unittest.skipIf(not Image, "Requires PIL") - @mock.patch("PIL.Image.Image.show") - def runTest(self, mock_show): - run_example() - mock_show.assert_called_with() +@mock.patch("PIL.Image.Image.show") +def test_example(mock_show): + run_example() + mock_show.assert_called_with() diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 1ed870c0..4c915ba5 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -1,8 +1,8 @@ -import pytest import io -import unittest from unittest import mock +import pytest + import qrcode import qrcode.util from qrcode.exceptions import DataOverflowError @@ -11,228 +11,265 @@ from qrcode.util import MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_NUMBER, QRData -class QRCodeTests(unittest.TestCase): - def test_basic(self): - qr = qrcode.QRCode(version=1) - qr.add_data("a") - qr.make(fit=False) +def test_basic(): + qr = qrcode.QRCode(version=1) + qr.add_data("a") + qr.make(fit=False) - def test_large(self): - qr = qrcode.QRCode(version=27) - qr.add_data("a") - qr.make(fit=False) - def test_invalid_version(self): - with pytest.raises(ValueError): - qrcode.QRCode(version=42) +def test_large(): + qr = qrcode.QRCode(version=27) + qr.add_data("a") + qr.make(fit=False) - def test_invalid_border(self): - with pytest.raises(ValueError): - qrcode.QRCode(border=-1) - def test_overflow(self): - qr = qrcode.QRCode(version=1) - qr.add_data("abcdefghijklmno") - with pytest.raises(DataOverflowError): - qr.make(fit=False) +def test_invalid_version(): + with pytest.raises(ValueError): + qrcode.QRCode(version=42) - def test_add_qrdata(self): - qr = qrcode.QRCode(version=1) - data = QRData("a") - qr.add_data(data) + +def test_invalid_border(): + with pytest.raises(ValueError): + qrcode.QRCode(border=-1) + + +def test_overflow(): + qr = qrcode.QRCode(version=1) + qr.add_data("abcdefghijklmno") + with pytest.raises(DataOverflowError): qr.make(fit=False) - def test_fit(self): - qr = qrcode.QRCode() - qr.add_data("a") - qr.make() - assert qr.version == 1 - qr.add_data("bcdefghijklmno") - qr.make() - assert qr.version == 2 - - def test_mode_number(self): - qr = qrcode.QRCode() - qr.add_data("1234567890123456789012345678901234", optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_NUMBER - - def test_mode_alpha(self): - qr = qrcode.QRCode() - qr.add_data("ABCDEFGHIJ1234567890", optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_ALPHA_NUM - - def test_regression_mode_comma(self): - qr = qrcode.QRCode() - qr.add_data(",", optimize=0) - qr.make() - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - def test_mode_8bit(self): - qr = qrcode.QRCode() - qr.add_data("abcABC" + UNICODE_TEXT, optimize=0) - qr.make() - assert qr.version == 1 - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - def test_mode_8bit_newline(self): - qr = qrcode.QRCode() - qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0) - qr.make() - assert qr.data_list[0].mode == MODE_8BIT_BYTE - - def test_make_image_with_wrong_pattern(self): - with pytest.raises(TypeError): - qrcode.QRCode(mask_pattern="string pattern") - - with pytest.raises(ValueError): - qrcode.QRCode(mask_pattern=-1) - - with pytest.raises(ValueError): - qrcode.QRCode(mask_pattern=42) - - def test_mask_pattern_setter(self): - qr = qrcode.QRCode() - - with pytest.raises(TypeError): - qr.mask_pattern = "string pattern" - - with pytest.raises(ValueError): - qr.mask_pattern = -1 - - with pytest.raises(ValueError): - qr.mask_pattern = 8 - - def test_qrcode_bad_factory(self): - with pytest.raises(TypeError): - qrcode.QRCode(image_factory="not_BaseImage") # type: ignore - - with pytest.raises(AssertionError): - qrcode.QRCode(image_factory=dict) # type: ignore - - def test_qrcode_factory(self): - class MockFactory(BaseImage): - drawrect = mock.Mock() - new_image = mock.Mock() - - qr = qrcode.QRCode(image_factory=MockFactory) - qr.add_data(UNICODE_TEXT) + +def test_add_qrdata(): + qr = qrcode.QRCode(version=1) + data = QRData("a") + qr.add_data(data) + qr.make(fit=False) + + +def test_fit(): + qr = qrcode.QRCode() + qr.add_data("a") + qr.make() + assert qr.version == 1 + qr.add_data("bcdefghijklmno") + qr.make() + assert qr.version == 2 + + +def test_mode_number(): + qr = qrcode.QRCode() + qr.add_data("1234567890123456789012345678901234", optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_NUMBER + + +def test_mode_alpha(): + qr = qrcode.QRCode() + qr.add_data("ABCDEFGHIJ1234567890", optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM + + +def test_regression_mode_comma(): + qr = qrcode.QRCode() + qr.add_data(",", optimize=0) + qr.make() + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_mode_8bit(): + qr = qrcode.QRCode() + qr.add_data("abcABC" + UNICODE_TEXT, optimize=0) + qr.make() + assert qr.version == 1 + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_mode_8bit_newline(): + qr = qrcode.QRCode() + qr.add_data("ABCDEFGHIJ1234567890\n", optimize=0) + qr.make() + assert qr.data_list[0].mode == MODE_8BIT_BYTE + + +def test_make_image_with_wrong_pattern(): + with pytest.raises(TypeError): + qrcode.QRCode(mask_pattern="string pattern") + + with pytest.raises(ValueError): + qrcode.QRCode(mask_pattern=-1) + + with pytest.raises(ValueError): + qrcode.QRCode(mask_pattern=42) + + +def test_mask_pattern_setter(): + qr = qrcode.QRCode() + + with pytest.raises(TypeError): + qr.mask_pattern = "string pattern" + + with pytest.raises(ValueError): + qr.mask_pattern = -1 + + with pytest.raises(ValueError): + qr.mask_pattern = 8 + + +def test_qrcode_bad_factory(): + with pytest.raises(TypeError): + qrcode.QRCode(image_factory="not_BaseImage") # type: ignore + + with pytest.raises(AssertionError): + qrcode.QRCode(image_factory=dict) # type: ignore + + +def test_qrcode_factory(): + class MockFactory(BaseImage): + drawrect = mock.Mock() + new_image = mock.Mock() + + qr = qrcode.QRCode(image_factory=MockFactory) + qr.add_data(UNICODE_TEXT) + qr.make_image() + assert MockFactory.new_image.called + assert MockFactory.drawrect.called + + +def test_optimize(): + qr = qrcode.QRCode() + text = "A1abc12345def1HELLOa" + qr.add_data(text, optimize=4) + qr.make() + assert [d.mode for d in qr.data_list] == [ + MODE_8BIT_BYTE, + MODE_NUMBER, + MODE_8BIT_BYTE, + MODE_ALPHA_NUM, + MODE_8BIT_BYTE, + ] + assert qr.version == 2 + + +def test_optimize_short(): + qr = qrcode.QRCode() + text = "A1abc1234567def1HELLOa" + qr.add_data(text, optimize=7) + qr.make() + assert len(qr.data_list) == 3 + assert [d.mode for d in qr.data_list] == [ + MODE_8BIT_BYTE, + MODE_NUMBER, + MODE_8BIT_BYTE, + ] + assert qr.version == 2 + + +def test_optimize_longer_than_data(): + qr = qrcode.QRCode() + text = "ABCDEFGHIJK" + qr.add_data(text, optimize=12) + assert len(qr.data_list) == 1 + assert qr.data_list[0].mode == MODE_ALPHA_NUM + + +def test_optimize_size(): + text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5 + + qr = qrcode.QRCode() + qr.add_data(text) + qr.make() + assert qr.version == 10 + + qr = qrcode.QRCode() + qr.add_data(text, optimize=0) + qr.make() + assert qr.version == 11 + + +def test_qrdata_repr(): + data = b"hello" + data_obj = qrcode.util.QRData(data) + assert repr(data_obj) == repr(data) + + +def test_print_ascii_stdout(): + qr = qrcode.QRCode() + with mock.patch("sys.stdout") as fake_stdout: + fake_stdout.isatty.return_value = None + with pytest.raises(OSError): + qr.print_ascii(tty=True) + assert fake_stdout.isatty.called + + +def test_print_ascii(): + qr = qrcode.QRCode(border=0) + f = io.StringIO() + qr.print_ascii(out=f) + printed = f.getvalue() + f.close() + expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588" + assert printed[:len(expected)] == expected + + f = io.StringIO() + f.isatty = lambda: True + qr.print_ascii(out=f, tty=True) + printed = f.getvalue() + f.close() + expected = ( + "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0" + ) + assert printed[:len(expected)] == expected + + +def test_print_tty_stdout(): + qr = qrcode.QRCode() + with mock.patch("sys.stdout") as fake_stdout: + fake_stdout.isatty.return_value = None + pytest.raises(OSError, qr.print_tty) + assert fake_stdout.isatty.called + + +def test_print_tty(): + qr = qrcode.QRCode() + f = io.StringIO() + f.isatty = lambda: True + qr.print_tty(out=f) + printed = f.getvalue() + f.close() + BOLD_WHITE_BG = "\x1b[1;47m" + BLACK_BG = "\x1b[40m" + WHITE_BLOCK = BOLD_WHITE_BG + " " + BLACK_BG + EOL = "\x1b[0m\n" + expected = ( + BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK + ) + assert printed[:len(expected)] == expected + + +def test_get_matrix(): + qr = qrcode.QRCode(border=0) + qr.add_data("1") + assert qr.get_matrix() == qr.modules + + +def test_get_matrix_border(): + qr = qrcode.QRCode(border=1) + qr.add_data("1") + matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]] + assert matrix == qr.modules + + +def test_negative_size_at_construction(): + with pytest.raises(ValueError): + qrcode.QRCode(box_size=-1) + + +def test_negative_size_at_usage(): + qr = qrcode.QRCode() + qr.box_size = -1 + with pytest.raises(ValueError): qr.make_image() - assert MockFactory.new_image.called - assert MockFactory.drawrect.called - - def test_optimize(self): - qr = qrcode.QRCode() - text = "A1abc12345def1HELLOa" - qr.add_data(text, optimize=4) - qr.make() - assert [d.mode for d in qr.data_list] == [MODE_8BIT_BYTE, MODE_NUMBER, MODE_8BIT_BYTE, MODE_ALPHA_NUM, MODE_8BIT_BYTE] - assert qr.version == 2 - - def test_optimize_short(self): - qr = qrcode.QRCode() - text = "A1abc1234567def1HELLOa" - qr.add_data(text, optimize=7) - qr.make() - assert len(qr.data_list) == 3 - assert [d.mode for d in qr.data_list] == [MODE_8BIT_BYTE, MODE_NUMBER, MODE_8BIT_BYTE] - assert qr.version == 2 - - def test_optimize_longer_than_data(self): - qr = qrcode.QRCode() - text = "ABCDEFGHIJK" - qr.add_data(text, optimize=12) - assert len(qr.data_list) == 1 - assert qr.data_list[0].mode == MODE_ALPHA_NUM - - def test_optimize_size(self): - text = "A1abc12345123451234512345def1HELLOHELLOHELLOHELLOa" * 5 - - qr = qrcode.QRCode() - qr.add_data(text) - qr.make() - assert qr.version == 10 - - qr = qrcode.QRCode() - qr.add_data(text, optimize=0) - qr.make() - assert qr.version == 11 - - def test_qrdata_repr(self): - data = b"hello" - data_obj = qrcode.util.QRData(data) - assert repr(data_obj) == repr(data) - - def test_print_ascii_stdout(self): - qr = qrcode.QRCode() - with mock.patch("sys.stdout") as fake_stdout: - fake_stdout.isatty.return_value = None - with pytest.raises(OSError): - qr.print_ascii(tty=True) - assert fake_stdout.isatty.called - - def test_print_ascii(self): - qr = qrcode.QRCode(border=0) - f = io.StringIO() - qr.print_ascii(out=f) - printed = f.getvalue() - f.close() - expected = "\u2588\u2580\u2580\u2580\u2580\u2580\u2588" - assert printed[:len(expected)] == expected - - f = io.StringIO() - f.isatty = lambda: True - qr.print_ascii(out=f, tty=True) - printed = f.getvalue() - f.close() - expected = ( - "\x1b[48;5;232m\x1b[38;5;255m" + "\xa0\u2584\u2584\u2584\u2584\u2584\xa0" - ) - assert printed[:len(expected)] == expected - - def test_print_tty_stdout(self): - qr = qrcode.QRCode() - with mock.patch("sys.stdout") as fake_stdout: - fake_stdout.isatty.return_value = None - pytest.raises(OSError, qr.print_tty) - assert fake_stdout.isatty.called - - def test_print_tty(self): - qr = qrcode.QRCode() - f = io.StringIO() - f.isatty = lambda: True - qr.print_tty(out=f) - printed = f.getvalue() - f.close() - BOLD_WHITE_BG = "\x1b[1;47m" - BLACK_BG = "\x1b[40m" - WHITE_BLOCK = BOLD_WHITE_BG + " " + BLACK_BG - EOL = "\x1b[0m\n" - expected = ( - BOLD_WHITE_BG + " " * 23 + EOL + WHITE_BLOCK + " " * 7 + WHITE_BLOCK - ) - assert printed[:len(expected)] == expected - - def test_get_matrix(self): - qr = qrcode.QRCode(border=0) - qr.add_data("1") - assert qr.get_matrix() == qr.modules - - def test_get_matrix_border(self): - qr = qrcode.QRCode(border=1) - qr.add_data("1") - matrix = [row[1:-1] for row in qr.get_matrix()[1:-1]] - assert matrix == qr.modules - - def test_negative_size_at_construction(self): - with pytest.raises(ValueError): - qrcode.QRCode(box_size=-1) - - def test_negative_size_at_usage(self): - qr = qrcode.QRCode() - qr.box_size = -1 - with pytest.raises(ValueError): - qr.make_image() diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index 497896c1..9afa451c 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -1,6 +1,4 @@ import io -import os -import unittest import pytest @@ -13,185 +11,202 @@ Image = pytest.importorskip("PIL.Image", reason="PIL is not installed") -class PILQRCodeTests(unittest.TestCase): - - def test_render_pil(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - assert isinstance(img.get_image(), Image.Image) - - def test_render_pil_with_transparent_background(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="TransParent") - img.save(io.BytesIO()) - - def test_render_pil_with_red_background(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="red") - img.save(io.BytesIO()) - - def test_render_pil_with_rgb_color_tuples(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) - img.save(io.BytesIO()) - - def test_render_with_pattern(self): - qr = qrcode.QRCode(mask_pattern=3) - qr.add_data(UNICODE_TEXT) - img = qr.make_image() - img.save(io.BytesIO()) - - def test_render_styled_Image(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage) - img.save(io.BytesIO()) - - def test_render_styled_with_embeded_image(self): - embeded_img = Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) - img.save(io.BytesIO()) - - def test_render_styled_with_embeded_image_path(self, tmp_path): - tmpfile = os.path.join(str(tmp_path), "test.png") - embeded_img = Image.new("RGB", (10, 10), color="red") - embeded_img.save(tmpfile) - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embeded_image_path=tmpfile) - img.save(io.BytesIO()) - - def test_render_styled_with_square_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.SquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_gapped_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.GappedSquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_circle_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.CircleModuleDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_rounded_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.RoundedModuleDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_vertical_bars_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.VerticalBarsDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_horizontal_bars_module_drawer(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.HorizontalBarsDrawer(), - ) - img.save(io.BytesIO()) - - def test_render_styled_with_default_solid_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask() - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_solid_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_color_mask_with_transparency(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask( - back_color=(255, 0, 255, 255), front_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - assert img.mode == "RGBA" - - def test_render_styled_with_radial_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.RadialGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_square_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SquareGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_horizontal_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.HorizontalGradiantColorMask( - back_color=WHITE, left_color=RED, right_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_vertical_gradient_color_mask(self): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.VerticalGradiantColorMask( - back_color=WHITE, top_color=RED, bottom_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - def test_render_styled_with_image_color_mask(self): - img_mask = Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.ImageColorMask(back_color=WHITE, color_mask_image=img_mask) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -class ShortcutTest(unittest.TestCase): - - def runTest(self): - qrcode.make("image") +def test_render_pil(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + assert isinstance(img.get_image(), Image.Image) + + +def test_render_pil_with_transparent_background(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color="TransParent") + img.save(io.BytesIO()) + + +def test_render_pil_with_red_background(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color="red") + img.save(io.BytesIO()) + + +def test_render_pil_with_rgb_color_tuples(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35)) + img.save(io.BytesIO()) + + +def test_render_with_pattern(): + qr = qrcode.QRCode(mask_pattern=3) + qr.add_data(UNICODE_TEXT) + img = qr.make_image() + img.save(io.BytesIO()) + + +def test_render_styled_Image(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage) + img.save(io.BytesIO()) + + +def test_render_styled_with_embeded_image(): + embeded_img = Image.new("RGB", (10, 10), color="red") + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) + img.save(io.BytesIO()) + + +def test_render_styled_with_embeded_image_path(tmp_path): + tmpfile = str(tmp_path / "test.png") + embeded_img = Image.new("RGB", (10, 10), color="red") + embeded_img.save(tmpfile) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=StyledPilImage, embeded_image_path=tmpfile) + img.save(io.BytesIO()) + + +def test_render_styled_with_square_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.SquareModuleDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_gapped_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.GappedSquareModuleDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_circle_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.CircleModuleDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_rounded_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.RoundedModuleDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_vertical_bars_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.VerticalBarsDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_horizontal_bars_module_drawer(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + img = qr.make_image( + image_factory=StyledPilImage, + module_drawer=moduledrawers.HorizontalBarsDrawer(), + ) + img.save(io.BytesIO()) + + +def test_render_styled_with_default_solid_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask() + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_solid_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_color_mask_with_transparency(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SolidFillColorMask( + back_color=(255, 0, 255, 255), front_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + assert img.mode == "RGBA" + + +def test_render_styled_with_radial_gradient_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.RadialGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_square_gradient_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.SquareGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_horizontal_gradient_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.HorizontalGradiantColorMask( + back_color=WHITE, left_color=RED, right_color=BLACK + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_vertical_gradient_color_mask(): + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.VerticalGradiantColorMask( + back_color=WHITE, top_color=RED, bottom_color=BLACK + ) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_render_styled_with_image_color_mask(): + img_mask = Image.new("RGB", (10, 10), color="red") + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + mask = colormasks.ImageColorMask(back_color=WHITE, color_mask_image=img_mask) + img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) + img.save(io.BytesIO()) + + +def test_shortcut(): + qrcode.make("image") diff --git a/qrcode/tests/test_qrcode_svg.py b/qrcode/tests/test_qrcode_svg.py index 26ba0aeb..4774b245 100644 --- a/qrcode/tests/test_qrcode_svg.py +++ b/qrcode/tests/test_qrcode_svg.py @@ -1,7 +1,4 @@ import io -import os -import unittest -from tempfile import mkdtemp import qrcode from qrcode.image import svg @@ -12,48 +9,46 @@ class SvgImageWhite(svg.SvgImage): background = "white" -class QRCodeSvgTests(unittest.TestCase): - def setUp(self): - self.tmpdir = mkdtemp() - - def tearDown(self): - os.rmdir(self.tmpdir) - - def test_render_svg(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgImage) - img.save(io.BytesIO()) - - def test_render_svg_path(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgPathImage) - img.save(io.BytesIO()) - - def test_render_svg_fragment(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgFragmentImage) - img.save(io.BytesIO()) - - def test_svg_string(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgFragmentImage) - file_like = io.BytesIO() - img.save(file_like) - file_like.seek(0) - assert file_like.read() in img.to_string() - - def test_render_svg_with_background(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=SvgImageWhite) - img.save(io.BytesIO()) - - def test_svg_circle_drawer(self): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle") - img.save(io.BytesIO()) +def test_render_svg(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgImage) + img.save(io.BytesIO()) + + +def test_render_svg_path(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgPathImage) + img.save(io.BytesIO()) + + +def test_render_svg_fragment(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgFragmentImage) + img.save(io.BytesIO()) + + +def test_svg_string(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgFragmentImage) + file_like = io.BytesIO() + img.save(file_like) + file_like.seek(0) + assert file_like.read() in img.to_string() + + +def test_render_svg_with_background(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=SvgImageWhite) + img.save(io.BytesIO()) + + +def test_svg_circle_drawer(): + qr = qrcode.QRCode() + qr.add_data(UNICODE_TEXT) + img = qr.make_image(image_factory=svg.SvgPathImage, module_drawer="circle") + img.save(io.BytesIO()) diff --git a/qrcode/tests/test_release.py b/qrcode/tests/test_release.py index 39a267c8..de33a298 100644 --- a/qrcode/tests/test_release.py +++ b/qrcode/tests/test_release.py @@ -1,7 +1,6 @@ -import re import builtins import datetime -import unittest +import re from unittest import mock from qrcode.release import update_manpage @@ -10,31 +9,33 @@ DATA = 'test\n.TH "date" "version" "description"\nthis' -class UpdateManpageTests(unittest.TestCase): - @mock.patch(OPEN, new_callable=mock.mock_open, read_data=".TH invalid") - def test_invalid_data(self, mock_file): - update_manpage({"name": "qrcode", "new_version": "1.23"}) - mock_file.assert_called() - mock_file().write.assert_not_called() - - @mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) - def test_not_qrcode(self, mock_file): - update_manpage({"name": "not-qrcode"}) - mock_file.assert_not_called() - - @mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) - def test_no_change(self, mock_file): - update_manpage({"name": "qrcode", "new_version": "version"}) - mock_file.assert_called() - mock_file().write.assert_not_called() - - @mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) - def test_change(self, mock_file): - update_manpage({"name": "qrcode", "new_version": "3.11"}) - expected = re.split(r"([^\n]*(?:\n|$))", DATA)[1::2] - expected[1] = ( - expected[1] - .replace("version", "3.11") - .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) - ) - mock_file().write.has_calls([mock.call(line) for line in expected]) +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=".TH invalid") +def test_invalid_data(mock_file): + update_manpage({"name": "qrcode", "new_version": "1.23"}) + mock_file.assert_called() + mock_file().write.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_not_qrcode(mock_file): + update_manpage({"name": "not-qrcode"}) + mock_file.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_no_change(mock_file): + update_manpage({"name": "qrcode", "new_version": "version"}) + mock_file.assert_called() + mock_file().write.assert_not_called() + + +@mock.patch(OPEN, new_callable=mock.mock_open, read_data=DATA) +def test_change(mock_file): + update_manpage({"name": "qrcode", "new_version": "3.11"}) + expected = re.split(r"([^\n]*(?:\n|$))", DATA)[1::2] + expected[1] = ( + expected[1] + .replace("version", "3.11") + .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) + ) + mock_file().write.has_calls([mock.call(line) for line in expected]) diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index c19de76b..ca536897 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -1,12 +1,8 @@ -import pytest -import io -import os import sys -import unittest -from tempfile import mkdtemp from unittest import mock -from qrcode.compat.pil import Image +import pytest + from qrcode.console_scripts import commas, main @@ -14,92 +10,87 @@ def bad_read(): raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte") -class ScriptTest(unittest.TestCase): - def setUp(self): - self.tmpdir = mkdtemp() - - def tearDown(self): - os.rmdir(self.tmpdir) - - @mock.patch("os.isatty", lambda *args: True) - @mock.patch("qrcode.main.QRCode.print_ascii") - def test_isatty(self, mock_print_ascii): - main(["testtext"]) - mock_print_ascii.assert_called_with(tty=True) - - @mock.patch("os.isatty", lambda *args: False) - @mock.patch("sys.stdout") - @unittest.skipIf(not Image, "Requires PIL") - def test_piped(self, mock_stdout): - main(["testtext"]) - - @mock.patch("os.isatty", lambda *args: True) - @mock.patch("qrcode.main.QRCode.print_ascii") - @mock.patch("sys.stdin") - def test_stdin(self, mock_stdin, mock_print_ascii): - mock_stdin.buffer.read.return_value = "testtext" - main([]) - assert mock_stdin.buffer.read.called - mock_print_ascii.assert_called_with(tty=True) - - @mock.patch("os.isatty", lambda *args: True) - @mock.patch("qrcode.main.QRCode.print_ascii") - def test_stdin_py3_unicodedecodeerror(self, mock_print_ascii): - mock_stdin = mock.Mock(sys.stdin) - mock_stdin.buffer.read.return_value = "testtext" - mock_stdin.read.side_effect = bad_read - with mock.patch("sys.stdin", mock_stdin): +@mock.patch("os.isatty", lambda *args: True) +@mock.patch("qrcode.main.QRCode.print_ascii") +def test_isatty(mock_print_ascii): + main(["testtext"]) + mock_print_ascii.assert_called_with(tty=True) + + +@mock.patch("os.isatty", lambda *args: False) +def test_piped(): + pytest.importorskip("PIL", reason="Requires PIL") + main(["testtext"]) + + +@mock.patch("os.isatty", lambda *args: True) +def test_stdin(): + with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: + with mock.patch("sys.stdin") as mock_stdin: + mock_stdin.buffer.read.return_value = "testtext" + main([]) + assert mock_stdin.buffer.read.called + mock_print_ascii.assert_called_with(tty=True) + + +@mock.patch("os.isatty", lambda *args: True) +def test_stdin_py3_unicodedecodeerror(): + with mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii: + with mock.patch("sys.stdin") as mock_stdin: + mock_stdin.buffer.read.return_value = "testtext" + mock_stdin.read.side_effect = bad_read # sys.stdin.read() will raise an error... with pytest.raises(UnicodeDecodeError): sys.stdin.read() # ... but it won't be used now. main([]) - mock_print_ascii.assert_called_with(tty=True) - - @mock.patch("os.isatty", lambda *args: True) - @mock.patch("qrcode.main.QRCode.print_ascii") - def test_optimize(self, mock_print_ascii): - main("testtext --optimize 0".split()) - - @mock.patch("sys.stdout") - def test_factory(self, mock_stdout): - main("testtext --factory svg".split()) - - @mock.patch("sys.stderr") - def test_bad_factory(self, mock_stderr): - self.assertRaises(SystemExit, main, "testtext --factory fish".split()) - - @mock.patch.object(sys, "argv", "qr testtext output".split()) - @unittest.skipIf(not Image, "Requires PIL") - def test_sys_argv(self): - main() - - @unittest.skipIf(not Image, "Requires PIL") - def test_output(self): - tmpfile = os.path.join(self.tmpdir, "test.png") - main(["testtext", "--output", tmpfile]) - os.remove(tmpfile) - - @mock.patch("sys.stderr", new_callable=io.StringIO) - @unittest.skipIf(not Image, "Requires PIL") - def test_factory_drawer_none(self, mock_stderr): - with pytest.raises(SystemExit): - main("testtext --factory pil --factory-drawer nope".split()) - assert 'The selected factory has no drawer aliases' in mock_stderr.getvalue() - - @mock.patch("sys.stderr", new_callable=io.StringIO) - def test_factory_drawer_bad(self, mock_stderr): - with pytest.raises(SystemExit): - main("testtext --factory svg --factory-drawer sobad".split()) - assert 'sobad factory drawer not found' in mock_stderr.getvalue() - - @mock.patch("sys.stderr", new_callable=io.StringIO) - def test_factory_drawer(self, mock_stderr): - main("testtext --factory svg --factory-drawer circle".split()) - - def test_commas(self): - assert commas([]) == '' - assert commas(['A']) == 'A' - assert commas('AB') == 'A or B' - assert commas('ABC') == 'A, B or C' - assert commas('ABC', joiner='and') == 'A, B and C' + mock_print_ascii.assert_called_with(tty=True) + + +def test_optimize(): + main("testtext --optimize 0".split()) + + +def test_factory(): + main(["testtext", "--factory", "svg"]) + + +def test_bad_factory(): + with pytest.raises(SystemExit): + main(["testtext", "--factory", "nope"]) + + +@mock.patch.object(sys, "argv", "qr testtext output".split()) +def test_sys_argv(): + pytest.importorskip("PIL", reason="Requires PIL") + main() + + +def test_output(tmp_path): + pytest.importorskip("PIL", reason="Requires PIL") + main(["testtext", "--output", str(tmp_path / "test.png")]) + + +def test_factory_drawer_none(capsys): + pytest.importorskip("PIL", reason="Requires PIL") + with pytest.raises(SystemExit): + main("testtext --factory pil --factory-drawer nope".split()) + assert 'The selected factory has no drawer aliases' in capsys.readouterr()[1] + + +def test_factory_drawer_bad(capsys): + with pytest.raises(SystemExit): + main("testtext --factory svg --factory-drawer sobad".split()) + assert 'sobad factory drawer not found' in capsys.readouterr()[1] + + +def test_factory_drawer(capsys): + main("testtext --factory svg --factory-drawer circle".split()) + + +def test_commas(): + assert commas([]) == '' + assert commas(['A']) == 'A' + assert commas('AB') == 'A or B' + assert commas('ABC') == 'A, B or C' + assert commas('ABC', joiner='and') == 'A, B and C' diff --git a/qrcode/tests/test_util.py b/qrcode/tests/test_util.py index de19a103..e57badbc 100644 --- a/qrcode/tests/test_util.py +++ b/qrcode/tests/test_util.py @@ -1,13 +1,11 @@ import pytest -import unittest from qrcode import util -class UtilTests(unittest.TestCase): - def test_check_wrong_version(self): - with pytest.raises(ValueError): - util.check_version(0) +def test_check_wrong_version(): + with pytest.raises(ValueError): + util.check_version(0) - with pytest.raises(ValueError): - util.check_version(41) + with pytest.raises(ValueError): + util.check_version(41) From 423579b7347f82c09993d2d1963a99f54de68101 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 15 Feb 2023 17:29:46 +0200 Subject: [PATCH 07/34] Parametrize PIL tests for DRYness --- qrcode/tests/test_qrcode_pil.py | 155 +++++++------------------------- 1 file changed, 30 insertions(+), 125 deletions(-) diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index 9afa451c..7f931402 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -19,20 +19,14 @@ def test_render_pil(): assert isinstance(img.get_image(), Image.Image) -def test_render_pil_with_transparent_background(): +@pytest.mark.parametrize("back_color", ["TransParent", "red", (255, 195, 235)]) +def test_render_pil_background(back_color): qr = qrcode.QRCode() qr.add_data(UNICODE_TEXT) img = qr.make_image(back_color="TransParent") img.save(io.BytesIO()) -def test_render_pil_with_red_background(): - qr = qrcode.QRCode() - qr.add_data(UNICODE_TEXT) - img = qr.make_image(back_color="red") - img.save(io.BytesIO()) - - def test_render_pil_with_rgb_color_tuples(): qr = qrcode.QRCode() qr.add_data(UNICODE_TEXT) @@ -72,138 +66,49 @@ def test_render_styled_with_embeded_image_path(tmp_path): img.save(io.BytesIO()) -def test_render_styled_with_square_module_drawer(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.SquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - -def test_render_styled_with_gapped_module_drawer(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.GappedSquareModuleDrawer(), - ) - img.save(io.BytesIO()) - - -def test_render_styled_with_circle_module_drawer(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.CircleModuleDrawer(), - ) - img.save(io.BytesIO()) - - -def test_render_styled_with_rounded_module_drawer(): +@pytest.mark.parametrize("drawer", [ + moduledrawers.CircleModuleDrawer, + moduledrawers.GappedSquareModuleDrawer, + moduledrawers.HorizontalBarsDrawer, + moduledrawers.RoundedModuleDrawer, + moduledrawers.SquareModuleDrawer, + moduledrawers.VerticalBarsDrawer, +]) +def test_render_styled_with_drawer(drawer): qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) qr.add_data(UNICODE_TEXT) img = qr.make_image( image_factory=StyledPilImage, - module_drawer=moduledrawers.RoundedModuleDrawer(), + module_drawer=drawer(), ) img.save(io.BytesIO()) -def test_render_styled_with_vertical_bars_module_drawer(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.VerticalBarsDrawer(), - ) - img.save(io.BytesIO()) - - -def test_render_styled_with_horizontal_bars_module_drawer(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - img = qr.make_image( - image_factory=StyledPilImage, - module_drawer=moduledrawers.HorizontalBarsDrawer(), - ) - img.save(io.BytesIO()) - - -def test_render_styled_with_default_solid_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask() - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_solid_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_color_mask_with_transparency(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SolidFillColorMask( +@pytest.mark.parametrize("mask", [ + colormasks.SolidFillColorMask(), + colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED), + colormasks.SolidFillColorMask( back_color=(255, 0, 255, 255), front_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - assert img.mode == "RGBA" - - -def test_render_styled_with_radial_gradient_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.RadialGradiantColorMask( + ), + colormasks.RadialGradiantColorMask( back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_square_gradient_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.SquareGradiantColorMask( + ), + colormasks.SquareGradiantColorMask( back_color=WHITE, center_color=BLACK, edge_color=RED - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_horizontal_gradient_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.HorizontalGradiantColorMask( + ), + colormasks.HorizontalGradiantColorMask( back_color=WHITE, left_color=RED, right_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_vertical_gradient_color_mask(): - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) - qr.add_data(UNICODE_TEXT) - mask = colormasks.VerticalGradiantColorMask( + ), + colormasks.VerticalGradiantColorMask( back_color=WHITE, top_color=RED, bottom_color=BLACK - ) - img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) - img.save(io.BytesIO()) - - -def test_render_styled_with_image_color_mask(): - img_mask = Image.new("RGB", (10, 10), color="red") + ), + colormasks.ImageColorMask( + back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red") + ), +]) +def test_render_styled_with_mask(mask): qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) qr.add_data(UNICODE_TEXT) - mask = colormasks.ImageColorMask(back_color=WHITE, color_mask_image=img_mask) img = qr.make_image(image_factory=StyledPilImage, color_mask=mask) img.save(io.BytesIO()) From 876bec5bca04c14a767b7e96dc6ace48bc9c5cb1 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Sun, 11 Aug 2024 17:25:58 +0200 Subject: [PATCH 08/34] Poetry base files --- .gitignore | 13 +++++++------ MANIFEST.in | 7 ------- setup.cfg => __setup.cfg | 2 ++ setup.py => __setup.py | 0 pyproject.toml | 29 +++++++++++++++++++++++++++-- qrcode/console_scripts.py | 4 ++-- qrcode/tests/test_release.py | 2 +- tox.ini | 10 ++++++---- 8 files changed, 45 insertions(+), 22 deletions(-) delete mode 100644 MANIFEST.in rename setup.cfg => __setup.cfg (95%) rename setup.py => __setup.py (100%) diff --git a/.gitignore b/.gitignore index e7af1f2f..b8775750 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ *.pyc -.tox -dist/ .coverage .coverage.* -htmlcov/ -build/ -qrcode.egg-info/ -.pytest_cache/ .idea/ +.pytest_cache/ +.tox +build/ cov.xml +dist/ +htmlcov/ +poetry.lock +qrcode.egg-info/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index bc485772..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -recursive-include doc *.png -include *.rst -include LICENSE -include signing-key.asc -include tox.ini -include doc/qr.1 -ignore .pypirc \ No newline at end of file diff --git a/setup.cfg b/__setup.cfg similarity index 95% rename from setup.cfg rename to __setup.cfg index 3aff842a..fd028fc7 100644 --- a/setup.cfg +++ b/__setup.cfg @@ -19,6 +19,8 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: 3 :: Only Topic :: Multimedia :: Graphics Topic :: Software Development :: Libraries :: Python Modules diff --git a/setup.py b/__setup.py similarity index 100% rename from setup.py rename to __setup.py diff --git a/pyproject.toml b/pyproject.toml index e9a14aa7..2dff492e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,28 @@ [build-system] -requires = ["setuptools >= 48"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "qrcode" +version = "7.5.dev0" +packages = [{include = "qrcode"}] +description = "QR Code image generator" +authors = ["Lincoln Loop "] +license = "BSD" +readme = "README.rst" + +[tool.poetry.dependencies] +python = "^3.7" +colorama = {version = "*", platform = "win32"} +importlib = {version = "*", python = "<3.10"} +pypng = "*" +typing-extensions = "*" + +[tool.poetry.group.dev.dependencies] +coverage = "*" +pytest = "*" +pytest-cov = "*" +tox = "*" + +[tool.poetry.group.pil.dependencies] +pillow = ">=9.1.0" diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 424fe6fd..230a5b93 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -9,6 +9,7 @@ import os import sys from typing import Dict, Iterable, NoReturn, Optional, Set, Type +from importlib import metadata import qrcode from qrcode.image.base import BaseImage, DrawerAliases @@ -40,9 +41,8 @@ def main(args=None): if args is None: args = sys.argv[1:] - from pkg_resources import get_distribution - version = get_distribution("qrcode").version + version = metadata.version("qrcode") parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version) # Wrap parser.error in a typed NoReturn method for better typing. diff --git a/qrcode/tests/test_release.py b/qrcode/tests/test_release.py index 39a267c8..7eec9891 100644 --- a/qrcode/tests/test_release.py +++ b/qrcode/tests/test_release.py @@ -37,4 +37,4 @@ def test_change(self, mock_file): .replace("version", "3.11") .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) ) - mock_file().write.has_calls([mock.call(line) for line in expected]) + mock_file().write.assert_has_calls([mock.call(line) for line in expected if line != ''], any_order=True) diff --git a/tox.ini b/tox.ini index 9967a1f5..b9a73228 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ distribute = False envlist = coverage_setup nopil - py{36,37,38,39,310} + py{38,39,310,311,312} readme coverage_report skip_missing_interpreters = True @@ -12,8 +12,10 @@ skip_missing_interpreters = True python = 3.7: py37 3.8: py38 - 3.9: py39, readme, nopil + 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312, readme, nopil [testenv] depends = coverage_setup @@ -21,7 +23,7 @@ usedevelop = True extras = test pil -commands = coverage run -m pytest +commands = coverage run -m pytest -vvv {envsitepackagesdir}/qrcode [testenv:nopil] extras = @@ -44,7 +46,7 @@ commands = coverage erase [testenv:coverage_report] depends = - py{36,37,38,39,310} + py{36,37,38,39,310,311,312} nopil skip_install = True deps = coverage From 28db8fb78816b264504ddc2b11c48009d3cfa7eb Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 12:07:20 +0200 Subject: [PATCH 09/34] Fix tox tests --- pyproject.toml | 15 ++++++------- qrcode/image/svg.py | 4 +--- qrcode/main.py | 4 ++-- tox.ini | 51 +++++++-------------------------------------- 4 files changed, 16 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2dff492e..9b177aa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,15 +14,12 @@ readme = "README.rst" [tool.poetry.dependencies] python = "^3.7" colorama = {version = "*", platform = "win32"} -importlib = {version = "*", python = "<3.10"} pypng = "*" -typing-extensions = "*" +pillow = {version = ">=9.1.0", optional = true} -[tool.poetry.group.dev.dependencies] -coverage = "*" -pytest = "*" -pytest-cov = "*" -tox = "*" +[tool.poetry.extras] +pil = ["pillow"] -[tool.poetry.group.pil.dependencies] -pillow = ">=9.1.0" +[tool.poetry.group.dev.dependencies] +pytest = {version = "*"} +pytest-cov = {version = "*"} \ No newline at end of file diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index bf0ec870..5d70fc83 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,8 +1,6 @@ import decimal from decimal import Decimal -from typing import List, Optional, Type, Union, overload - -from typing_extensions import Literal +from typing import List, Optional, Type, Union, overload, Literal import qrcode.image.base from qrcode.compat.etree import ET diff --git a/qrcode/main.py b/qrcode/main.py index 0ac91bbb..6165bc55 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -10,10 +10,9 @@ TypeVar, cast, overload, + Literal ) -from typing_extensions import Literal - from qrcode import constants, exceptions, util from qrcode.image.base import BaseImage from qrcode.image.pure import PyPNGImage @@ -23,6 +22,7 @@ precomputed_qr_blanks: Dict[int, ModulesType] = {} + def make(data=None, **kwargs): qr = QRCode(**kwargs) qr.add_data(data) diff --git a/tox.ini b/tox.ini index b9a73228..14563864 100644 --- a/tox.ini +++ b/tox.ini @@ -1,57 +1,20 @@ [tox] distribute = False -envlist = - coverage_setup - nopil - py{38,39,310,311,312} - readme - coverage_report +envlist = py{38,39,310,311,312}-{pil,nopil} skip_missing_interpreters = True [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 - 3.12: py312, readme, nopil + 3.12: py312 [testenv] -depends = coverage_setup -usedevelop = True -extras = - test - pil -commands = coverage run -m pytest -vvv {envsitepackagesdir}/qrcode - -[testenv:nopil] -extras = - test - -[testenv:readme] -skip_install = True -deps = - docutils - Pygments commands = - {envbindir}/rst2html.py --report=info --halt=warning README.rst /dev/null - {envbindir}/rst2html.py --report=info --halt=warning CHANGES.rst /dev/null - -[testenv:coverage_setup] -depends = -skip_install = True -deps = coverage -commands = coverage erase - -[testenv:coverage_report] -depends = - py{36,37,38,39,310,311,312} - nopil -skip_install = True -deps = coverage -commands = - coverage combine - coverage html - coverage report --omit="qrcode/tests/*" --fail-under=98 -m - coverage report --include="qrcode/tests/*" --no-skip-covered --fail-under=100 -m + pytest {envsitepackagesdir}/qrcode +deps = + pil: pillow>=9.1.0 + pytest + pytest-cov From b37797a73aabb586f988d70e627ec46fa4986a20 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 12:11:13 +0200 Subject: [PATCH 10/34] Better Github workflows --- .github/workflows/linting.yml | 23 +++++++++++++++++++++++ .github/workflows/push.yml | 25 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/workflows/linting.yml create mode 100644 .github/workflows/push.yml diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000..1a81f7b7 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,23 @@ +name: Code Quality Checks + +on: + push: + +jobs: + linting: + name: Code Quality Checks + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Dependencies + run: pip install ruff + + - name: Code Linting + if: always() + run: ruff check qrcode + + - name: Code Formatting + if: always() + run: ruff format --check qrcode diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 00000000..f03e8ede --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,25 @@ +name: Testsuite Run + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install --disable-pip-version-check tox tox-gh-actions + - name: Test with tox + run: tox From 5d60da17455607e9c8b7e7fef9940d20027894f2 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 12:48:37 +0200 Subject: [PATCH 11/34] Basic Ruff formatting --- qrcode/console_scripts.py | 1 + qrcode/image/styles/moduledrawers/base.py | 3 +-- qrcode/image/styles/moduledrawers/pil.py | 4 +++- qrcode/image/styles/moduledrawers/svg.py | 6 ++---- qrcode/image/svg.py | 6 ++---- qrcode/main.py | 23 +++++------------------ qrcode/release.py | 1 + qrcode/tests/test_qrcode.py | 6 +++++- qrcode/tests/test_release.py | 4 +++- qrcode/util.py | 1 - 10 files changed, 23 insertions(+), 32 deletions(-) diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index 230a5b93..124265a0 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -5,6 +5,7 @@ When stdout is a tty the QR Code is printed to the terminal and when stdout is a pipe to a file an image is written. The default image format is PNG. """ + import optparse import os import sys diff --git a/qrcode/image/styles/moduledrawers/base.py b/qrcode/image/styles/moduledrawers/base.py index 8de33059..e2d0dd7d 100644 --- a/qrcode/image/styles/moduledrawers/base.py +++ b/qrcode/image/styles/moduledrawers/base.py @@ -32,5 +32,4 @@ def initialize(self, img: "BaseImage") -> None: self.img = img @abc.abstractmethod - def drawrect(self, box, is_active) -> None: - ... + def drawrect(self, box, is_active) -> None: ... diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index 398010c6..b37b3fce 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -249,7 +249,9 @@ def setup_edges(self): base_draw = ImageDraw.Draw(base) base_draw.ellipse((0, 0, fake_width * 2, fake_height), fill=front_color) - self.ROUND_LEFT = base.resize((width, shrunken_height), Image.Resampling.LANCZOS) + self.ROUND_LEFT = base.resize( + (width, shrunken_height), Image.Resampling.LANCZOS + ) self.ROUND_RIGHT = self.ROUND_LEFT.transpose(Image.Transpose.FLIP_LEFT_RIGHT) def drawrect(self, box, is_active: "ActiveWithNeighbors"): diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index 6e629759..cf5b9e7d 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -60,8 +60,7 @@ def drawrect(self, box, is_active: bool): self.img._img.append(self.el(box)) @abc.abstractmethod - def el(self, box): - ... + def el(self, box): ... class SvgSquareDrawer(SvgQRModuleDrawer): @@ -106,8 +105,7 @@ def drawrect(self, box, is_active: bool): self.img._subpaths.append(self.subpath(box)) @abc.abstractmethod - def subpath(self, box) -> str: - ... + def subpath(self, box) -> str: ... class SvgPathSquareDrawer(SvgPathQRModuleDrawer): diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 5d70fc83..4ad371bb 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -27,12 +27,10 @@ def __init__(self, *args, **kwargs): self.unit_size = self.units(self.box_size) @overload - def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: - ... + def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ... @overload - def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: - ... + def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ... def units(self, pixels, text=True): """ diff --git a/qrcode/main.py b/qrcode/main.py index 6165bc55..a2be2e31 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -10,7 +10,7 @@ TypeVar, cast, overload, - Literal + Literal, ) from qrcode import constants, exceptions, util @@ -22,7 +22,6 @@ precomputed_qr_blanks: Dict[int, ModulesType] = {} - def make(data=None, **kwargs): qr = QRCode(**kwargs) qr.add_data(data) @@ -193,12 +192,10 @@ def makeImpl(self, test, mask_pattern): def setup_position_probe_pattern(self, row, col): for r in range(-1, 8): - if row + r <= -1 or self.modules_count <= row + r: continue for c in range(-1, 8): - if col + c <= -1 or self.modules_count <= col + c: continue @@ -333,14 +330,14 @@ def get_module(x, y) -> int: out.flush() @overload - def make_image(self, image_factory: Literal[None] = None, **kwargs) -> GenericImage: - ... + def make_image( + self, image_factory: Literal[None] = None, **kwargs + ) -> GenericImage: ... @overload def make_image( self, image_factory: Type[GenericImageLocal] = None, **kwargs - ) -> GenericImageLocal: - ... + ) -> GenericImageLocal: ... def make_image(self, image_factory=None, **kwargs): """ @@ -406,20 +403,16 @@ def setup_position_adjust_pattern(self): pos = util.pattern_position(self.version) for i in range(len(pos)): - row = pos[i] for j in range(len(pos)): - col = pos[j] if self.modules[row][col] is not None: continue for r in range(-2, 3): - for c in range(-2, 3): - if ( r == -2 or r == 2 @@ -448,7 +441,6 @@ def setup_type_info(self, test, mask_pattern): # vertical for i in range(15): - mod = not test and ((bits >> i) & 1) == 1 if i < 6: @@ -460,7 +452,6 @@ def setup_type_info(self, test, mask_pattern): # horizontal for i in range(15): - mod = not test and ((bits >> i) & 1) == 1 if i < 8: @@ -484,18 +475,14 @@ def map_data(self, data, mask_pattern): data_len = len(data) for col in range(self.modules_count - 1, 0, -2): - if col <= 6: col -= 1 col_range = (col, col - 1) while True: - for c in col_range: - if self.modules[row][c] is None: - dark = False if byteIndex < data_len: diff --git a/qrcode/release.py b/qrcode/release.py index b93de39c..208ac1ee 100644 --- a/qrcode/release.py +++ b/qrcode/release.py @@ -2,6 +2,7 @@ This file provides zest.releaser entrypoints using when releasing new qrcode versions. """ + import os import re import datetime diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index a4e636e8..0600e2a1 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -216,7 +216,11 @@ def test_render_styled_with_embeded_image_and_ratio(self): embeded_img = pil_Image.new("RGB", (10, 10), color="red") qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) qr.add_data(UNICODE_TEXT) - img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img, embeded_image_ratio=0.3) + img = qr.make_image( + image_factory=StyledPilImage, + embeded_image=embeded_img, + embeded_image_ratio=0.3, + ) img.save(io.BytesIO()) @unittest.skipIf(not pil_Image, "Requires PIL") diff --git a/qrcode/tests/test_release.py b/qrcode/tests/test_release.py index 7eec9891..f700eb4b 100644 --- a/qrcode/tests/test_release.py +++ b/qrcode/tests/test_release.py @@ -37,4 +37,6 @@ def test_change(self, mock_file): .replace("version", "3.11") .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) ) - mock_file().write.assert_has_calls([mock.call(line) for line in expected if line != ''], any_order=True) + mock_file().write.assert_has_calls( + [mock.call(line) for line in expected if line != ""], any_order=True + ) diff --git a/qrcode/util.py b/qrcode/util.py index 8284003b..02fe11d1 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -549,7 +549,6 @@ def create_bytes(buffer: BitBuffer, rs_blocks: List[RSBlock]): def create_data(version, error_correction, data_list): - buffer = BitBuffer() for data in data_list: buffer.put(data.mode, 4) From dd7bdf93793473c89246d0f729f6b1df2db51068 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 12:48:54 +0200 Subject: [PATCH 12/34] Remove unused imports --- qrcode/tests/test_qrcode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 0600e2a1..8d768bd9 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -1,7 +1,6 @@ import io import os import unittest -import warnings from tempfile import mkdtemp from unittest import mock From 2e395ed118393e7b29240fa27c6819202a769c8f Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 12:49:36 +0200 Subject: [PATCH 13/34] Add ruff format to git ignore rev file. --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..879790f1 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Ruff format the entire project +5d60da17455607e9c8b7e7fef9940d20027894f2 From 0e59e56f7c2102e71af56722c6266f07f4c75854 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 13:03:45 +0200 Subject: [PATCH 14/34] Fix piped tox test. --- qrcode/tests/test_script.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index 4ae4ccbc..475c8ab8 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -30,6 +30,8 @@ def test_isatty(self, mock_print_ascii): @mock.patch("sys.stdout") @unittest.skipIf(not Image, "Requires PIL") def test_piped(self, mock_stdout): + mock_stdout.buffer = io.BytesIO() + mock_stdout.fileno = lambda: 1 main(["testtext"]) @mock.patch("os.isatty", lambda *args: True) From 49b885a69530b17a49a2119c8125ac226b3594a2 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:01:36 +0200 Subject: [PATCH 15/34] Remove deprecated setup files. --- __setup.cfg | 101 ---------------------------------------------------- __setup.py | 7 ---- 2 files changed, 108 deletions(-) delete mode 100644 __setup.cfg delete mode 100644 __setup.py diff --git a/__setup.cfg b/__setup.cfg deleted file mode 100644 index fd028fc7..00000000 --- a/__setup.cfg +++ /dev/null @@ -1,101 +0,0 @@ -[metadata] -name = qrcode -version = 7.5.dev0 -description = QR Code image generator -long_description = file: README.rst, CHANGES.rst -author = Lincoln Loop -author_email = info@lincolnloop.com -url = https://github.com/lincolnloop/python-qrcode -keywords = qr denso-wave IEC18004 -license = BSD -classifiers = - Development Status :: 5 - Production/Stable - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Intended Audience :: Developers - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3 :: Only - Topic :: Multimedia :: Graphics - Topic :: Software Development :: Libraries :: Python Modules - -[options] -zip_safe = False -include_package_data = True -packages = find: -install_requires = - colorama;platform_system=="Windows" - typing_extensions - pypng -python_requires = >= 3.7 - -[options.extras_require] -maintainer = - zest.releaser[recommended] -dev = - tox - pytest - pytest-cov -test = - coverage - pytest -pil = - pillow>=9.1.0 -all = - zest.releaser[recommended] - tox - pytest - pytest - pytest-cov - pillow>=9.1.0 - -[options.entry_points] -console_scripts = - qr = qrcode.console_scripts:main - -[bdist_wheel] -# Having this section in here will trigger zest to build a wheel. -universal = 0 - -[flake8] -exclude = - .tox - .git - __pycache__ - build - dist -max-line-length = 88 -ignore = E203,W503 - -[isort] -profile = black - -[coverage:run] -source = qrcode -parallel = True - -[coverage:report] -exclude_lines = - pragma: no cover - @overload - if (typing\.)?TYPE_CHECKING: -skip_covered = True - -[zest.releaser] -less-zeros = yes -version-levels = 2 -tag-format = v{version} -tag-message = Version {version} -tag-signing = yes -date-format = %%-d %%B %%Y -prereleaser.middle = - qrcode.release.update_manpage - -[tool:pytest] -filterwarnings = module diff --git a/__setup.py b/__setup.py deleted file mode 100644 index d002ffb8..00000000 --- a/__setup.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup - -# See setup.cfg for configuration. -setup( - data_files=[('share/man/man1', ['doc/qr.1'])], -) From a59b038015bfb5f31db48ec075b2a6a3f8fd3faa Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:01:43 +0200 Subject: [PATCH 16/34] Generic editorconfig --- .editorconfig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..ef842181 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_size = 4 +max_line_length = 88 + +[*.{rst,md}] +indent_size = 4 From 6436aded198c165f6be5fdef28a1e1fbed12c0aa Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:06:23 +0200 Subject: [PATCH 17/34] Documentation updates and bring Poetry config to the same state as setuptools. --- PACKAGING.rst | 4 ++-- TESTING.rst | 6 +++++- pyproject.toml | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/PACKAGING.rst b/PACKAGING.rst index 5d00fdb3..db252ca7 100644 --- a/PACKAGING.rst +++ b/PACKAGING.rst @@ -3,8 +3,8 @@ Packaging quick reminder Make sure maintainer dependencies are installed:: - pip install -e .[maintainer,dev] + poetry install Run release command and follow prompt instructions:: - fullrelease + poetry run fullrelease diff --git a/TESTING.rst b/TESTING.rst index 5686832e..948baffa 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -15,5 +15,9 @@ the libraries to build PIL, too. Here's the Ubuntu commands:: sudo apt-get install build-essential python-dev python3-dev sudo apt-get install libjpeg8-dev zlib1g-dev +Here's the OSX Homebrew command: + + brew install libjpeg libtiff little-cms2 openjpeg webp + Finally, just run ``tox``! -If you want, you can test against a specific version like this: ``tox -e py36`` +If you want, you can test against a specific version like this: ``tox -e py312-pil`` diff --git a/pyproject.toml b/pyproject.toml index 9b177aa8..e840cdfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,38 @@ packages = [{include = "qrcode"}] description = "QR Code image generator" authors = ["Lincoln Loop "] license = "BSD" -readme = "README.rst" +readme = ["README.rst", "CHANGES.rst"] +homepage = "https://github.com/lincolnloop/python-qrcode" +keywords = ["qr", "denso-wave", "IEC18004"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +# There is no support for data files yet. +# https://github.com/python-poetry/poetry/issues/9519 +# +# data_files = [ +# { destination = "share/man/man1", from = [ "doc/qr.1" ] }, +# ] + +[tool.poetry.scripts] +qr = 'qrcode.console_scripts:main' + [tool.poetry.dependencies] -python = "^3.7" +python = "^3.9" colorama = {version = "*", platform = "win32"} pypng = "*" pillow = {version = ">=9.1.0", optional = true} @@ -22,4 +50,17 @@ pil = ["pillow"] [tool.poetry.group.dev.dependencies] pytest = {version = "*"} -pytest-cov = {version = "*"} \ No newline at end of file +pytest-cov = {version = "*"} +docutils = "^0.21.2" +zest-releaser = {extras = ["recommended"], version = "^9.2.0"} + +[tool.zest-releaser] +less-zeros = "yes" +version-levels = 2 +tag-format = "v{version}" +tag-message = "Version {version}" +tag-signing = "yes" +date-format =" %%-d %%B %%Y" +prereleaser.middle = [ + "qrcode.release.update_manpage" +] From 983b0c16a06eeb79fd40ab2b081de8ead90f35df Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:07:28 +0200 Subject: [PATCH 18/34] Drop support for Python <=3.8. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index fc5ec62e..8539b9f5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Change log 7.5 (unreleased) ================ -- Nothing changed yet. +* Drop support for Python <=3.8. 7.4.2 (6 February 2023) From 35fcfafcde3b8e01d42bcf60d06069346063babb Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:08:14 +0200 Subject: [PATCH 19/34] Remove Python 3.8 support from tox --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 14563864..c89f4787 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] distribute = False -envlist = py{38,39,310,311,312}-{pil,nopil} +envlist = py{39,310,311,312}-{pil,nopil} skip_missing_interpreters = True [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 From af3a5748081e3e6829e02fa31d23cd759d969215 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:10:10 +0200 Subject: [PATCH 20/34] Remove Python 3.8 support from Github Actions --- .github/workflows/push.yml | 2 +- CHANGES.rst | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f03e8ede..22cd7d4c 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: [3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/CHANGES.rst b/CHANGES.rst index 8539b9f5..9183a9c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,14 +5,21 @@ Change log 7.5 (unreleased) ================ -* Drop support for Python <=3.8. +- Added support for Python 3.11 and 3.12. + +- Drop support for Python <=3.8. + +- Change local development setup to use Poetry_. + + +.. _Poetry: https://python-poetry.org 7.4.2 (6 February 2023) ======================= - Allow ``pypng`` factory to allow for saving to a string (like - ``qr.save("some_file.png")``) in addition to file-like objects. + ``qr.save("some_file.png")``) in addition to file-like objects. 7.4.1 (3 February 2023) From dee7e1df78c66d1e37589473fd014bb2fe99f0bb Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 14:32:56 +0200 Subject: [PATCH 21/34] Fix quote issue --- .github/workflows/push.yml | 2 +- CHANGES.rst | 7 +++++++ pyproject.toml | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 22cd7d4c..ac181bcd 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/CHANGES.rst b/CHANGES.rst index 9183a9c6..da2a8616 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,8 +11,15 @@ Change log - Change local development setup to use Poetry_. +- Testsuite and code quality checks are done through Github Actions. + +- Code quality and formatting utilises ruff_. + +- Removed ``typing_extensions`` as a dependency, as it's no longer required with + having Python 3.9+ as a requirement. .. _Poetry: https://python-poetry.org +.. _ruff: https://astral.sh/ruff 7.4.2 (6 February 2023) diff --git a/pyproject.toml b/pyproject.toml index e840cdfe..9f8d2ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,11 +42,12 @@ qr = 'qrcode.console_scripts:main' [tool.poetry.dependencies] python = "^3.9" colorama = {version = "*", platform = "win32"} -pypng = "*" +pypng = {version = "*", optional = true} pillow = {version = ">=9.1.0", optional = true} [tool.poetry.extras] pil = ["pillow"] +png = ["pypng"] [tool.poetry.group.dev.dependencies] pytest = {version = "*"} From dee4a3536eb26028321478be533f6963ad9ce0b4 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 17:27:45 +0200 Subject: [PATCH 22/34] Make PNG library optional. Refs #338 --- qrcode/compat/png.py | 7 +++++++ qrcode/image/pure.py | 9 ++++----- qrcode/tests/test_qrcode.py | 8 +++++--- tox.ini | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 qrcode/compat/png.py diff --git a/qrcode/compat/png.py b/qrcode/compat/png.py new file mode 100644 index 00000000..8d7b9056 --- /dev/null +++ b/qrcode/compat/png.py @@ -0,0 +1,7 @@ +# Try to import png library. +PngWriter = None + +try: + from png import Writer as PngWriter # type: ignore # noqa: F401 +except ImportError: # pragma: no cover + pass diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index 690ebe0c..2b995376 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -1,11 +1,10 @@ from itertools import chain -import png +from qrcode.compat.png import PngWriter +from qrcode.image.base import BaseImage -import qrcode.image.base - -class PyPNGImage(qrcode.image.base.BaseImage): +class PyPNGImage(BaseImage): """ pyPNG image builder. """ @@ -15,7 +14,7 @@ class PyPNGImage(qrcode.image.base.BaseImage): needs_drawrect = False def new_image(self, **kwargs): - return png.Writer(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) + return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) def drawrect(self, row, col): """ diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 8d768bd9..63eaca9f 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -4,11 +4,11 @@ from tempfile import mkdtemp from unittest import mock -import png import qrcode import qrcode.util from qrcode.compat.pil import Image as pil_Image +from qrcode.compat.png import PngWriter from qrcode.exceptions import DataOverflowError from qrcode.image.base import BaseImage from qrcode.image.pure import PyPNGImage @@ -174,20 +174,22 @@ class MockFactory(BaseImage): self.assertTrue(MockFactory.new_image.called) self.assertTrue(MockFactory.drawrect.called) + @unittest.skipIf(not PngWriter, "Requires PNG") def test_render_pypng(self): qr = qrcode.QRCode() qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=PyPNGImage) - self.assertIsInstance(img.get_image(), png.Writer) + self.assertIsInstance(img.get_image(), PngWriter) print(img.width, img.box_size, img.border) img.save(io.BytesIO()) + @unittest.skipIf(not PngWriter, "Requires PNG") def test_render_pypng_to_str(self): qr = qrcode.QRCode() qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=PyPNGImage) - self.assertIsInstance(img.get_image(), png.Writer) + self.assertIsInstance(img.get_image(), PngWriter) mock_open = mock.mock_open() with mock.patch("qrcode.image.pure.open", mock_open, create=True): diff --git a/tox.ini b/tox.ini index c89f4787..0b46e48f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distribute = False -envlist = py{39,310,311,312}-{pil,nopil} +envlist = py{39,310,311,312}-{pil,png,none} skip_missing_interpreters = True [gh-actions] @@ -15,5 +15,6 @@ commands = pytest {envsitepackagesdir}/qrcode deps = pil: pillow>=9.1.0 + png: pypng pytest pytest-cov From 75622766daf33d0fa16283d42aeffcaa999b95bb Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 17:30:09 +0200 Subject: [PATCH 23/34] Increase major version number since changes are backwards incompatible. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9f8d2ba7..bb74cd6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "qrcode" -version = "7.5.dev0" +version = "8.0.dev0" packages = [{include = "qrcode"}] description = "QR Code image generator" authors = ["Lincoln Loop "] From d0c13e0de464ec90646f7d81a2f22fa37299fb3f Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 13 Aug 2024 18:54:51 +0200 Subject: [PATCH 24/34] Notify if dependencies are not met. --- qrcode/image/pil.py | 3 +++ qrcode/image/pure.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 5767148d..814e4840 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -10,6 +10,9 @@ class PilImage(qrcode.image.base.BaseImage): kind = "PNG" def new_image(self, **kwargs): + if not Image: + raise ImportError("PIL library not found.") + back_color = kwargs.get("back_color", "white") fill_color = kwargs.get("fill_color", "black") diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index 2b995376..5a8b2c5e 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -14,6 +14,9 @@ class PyPNGImage(BaseImage): needs_drawrect = False def new_image(self, **kwargs): + if not PngWriter: + raise ImportError("PyPNG library not installed.") + return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) def drawrect(self, row, col): From 453c1182d54efc4ab6d65e5e971ec933cd4d2049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Justen=20=28=40turicas=29?= Date: Thu, 26 Sep 2024 15:54:13 -0300 Subject: [PATCH 25/34] Raise error if image is provided & correction != H --- qrcode/main.py | 2 ++ qrcode/tests/test_qrcode.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/qrcode/main.py b/qrcode/main.py index 0ac91bbb..336f56f2 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -348,6 +348,8 @@ def make_image(self, image_factory=None, **kwargs): If the data has not been compiled yet, make it first. """ + if kwargs.get("embeded_image_path") and self.error_correction != constants.ERROR_CORRECT_H: + raise ValueError("Error correction level must be ERROR_CORRECT_H if an embedded image is provided") _check_box_size(self.box_size) if self.data_cache is None: self.make() diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index a4e636e8..482a4300 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -230,6 +230,35 @@ def test_render_styled_with_embeded_image_path(self): img.save(io.BytesIO()) os.remove(tmpfile) + @unittest.skipIf(not pil_Image, "Requires PIL") + def test_embedded_image_and_error_correction(self): + "If an embedded image is specified, error correction must be the highest so the QR code is readable" + tmpfile = os.path.join(self.tmpdir, "test.png") + embedded_img = pil_Image.new("RGB", (10, 10), color="red") + embedded_img.save(tmpfile) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr.add_data(UNICODE_TEXT) + with self.assertRaises(ValueError): + qr.make_image(embeded_image_path=tmpfile) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_M) + qr.add_data(UNICODE_TEXT) + with self.assertRaises(ValueError): + qr.make_image(embeded_image_path=tmpfile) + + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_Q) + qr.add_data(UNICODE_TEXT) + with self.assertRaises(ValueError): + qr.make_image(embeded_image_path=tmpfile) + + # The only accepted correction level when an embedded image is provided + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) + qr.add_data(UNICODE_TEXT) + qr.make_image(embeded_image_path=tmpfile) + + os.remove(tmpfile) + @unittest.skipIf(not pil_Image, "Requires PIL") def test_render_styled_with_square_module_drawer(self): qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) From f997fce315bc48143460a8cfc1468f3a45f6a245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Justen=20=28=40turicas=29?= Date: Thu, 26 Sep 2024 15:56:03 -0300 Subject: [PATCH 26/34] Fix embedded image tests to use H error correction --- qrcode/tests/test_qrcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index 482a4300..e120b570 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -206,7 +206,7 @@ def test_render_styled_Image(self): @unittest.skipIf(not pil_Image, "Requires PIL") def test_render_styled_with_embeded_image(self): embeded_img = pil_Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) img.save(io.BytesIO()) @@ -214,7 +214,7 @@ def test_render_styled_with_embeded_image(self): @unittest.skipIf(not pil_Image, "Requires PIL") def test_render_styled_with_embeded_image_and_ratio(self): embeded_img = pil_Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img, embeded_image_ratio=0.3) img.save(io.BytesIO()) @@ -224,7 +224,7 @@ def test_render_styled_with_embeded_image_path(self): tmpfile = os.path.join(self.tmpdir, "test.png") embeded_img = pil_Image.new("RGB", (10, 10), color="red") embeded_img.save(tmpfile) - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=StyledPilImage, embeded_image_path=tmpfile) img.save(io.BytesIO()) From befab77532995afbe76cb87a2235b8354927e943 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 26 Sep 2024 16:44:33 -0300 Subject: [PATCH 27/34] Add tox and ruff to dev dependencies and update testing instructions --- TESTING.rst | 12 ++++++++++-- pyproject.toml | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/TESTING.rst b/TESTING.rst index 948baffa..2a25d2ed 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -1,9 +1,9 @@ Testing ======= -First, install tox into your virtualenv:: +First, install dev dependencies:: - pip install --upgrade tox + poetry install --with dev To run all tests, you'll need to install multiple Python interpreters. On a modern Ubuntu distribution you can use ``add-apt-repository @@ -21,3 +21,11 @@ Here's the OSX Homebrew command: Finally, just run ``tox``! If you want, you can test against a specific version like this: ``tox -e py312-pil`` + + +Linting +------- + +Run `ruff` to check linting:: + + ruff check diff --git a/pyproject.toml b/pyproject.toml index bb74cd6d..c33b2809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,8 @@ png = ["pypng"] [tool.poetry.group.dev.dependencies] pytest = {version = "*"} pytest-cov = {version = "*"} +tox = {version = "*"} +ruff = {version = "*"} docutils = "^0.21.2" zest-releaser = {extras = ["recommended"], version = "^9.2.0"} From 12b73936ffd1580e06bef9df12617f8ae0eb9210 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 26 Sep 2024 17:10:15 -0300 Subject: [PATCH 28/34] Run pytest coverage on tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0b46e48f..96d72d0d 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ python = [testenv] commands = - pytest {envsitepackagesdir}/qrcode + pytest --cov deps = pil: pillow>=9.1.0 png: pypng From 137946a71519e0cc4dfd65c0101b0c0b75050e78 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 26 Sep 2024 17:41:21 -0300 Subject: [PATCH 29/34] Fix import errors in tests --- qrcode/tests/test_qrcode_pypng.py | 5 ++++- qrcode/tests/test_script.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qrcode/tests/test_qrcode_pypng.py b/qrcode/tests/test_qrcode_pypng.py index 0245b207..c502a3b3 100644 --- a/qrcode/tests/test_qrcode_pypng.py +++ b/qrcode/tests/test_qrcode_pypng.py @@ -1,13 +1,16 @@ import io from unittest import mock -import png +import pytest + import qrcode import qrcode.util from qrcode.image.pure import PyPNGImage from qrcode.tests.consts import UNICODE_TEXT +png = pytest.importorskip("png", reason="png is not installed") + def test_render_pypng(): qr = qrcode.QRCode() diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index fbb8b285..5135b1fb 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -48,6 +48,7 @@ def test_stdin_py3_unicodedecodeerror(): def test_optimize(): + pytest.importorskip("PyPNG", reason="Requires PyPNG") main("testtext --optimize 0".split()) From 5760eec999ead5729afc8fb13229aebe2593ca54 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Thu, 26 Sep 2024 17:45:05 -0300 Subject: [PATCH 30/34] Run ruf and fix formatting --- TESTING.rst | 4 +-- qrcode/tests/test_qrcode_pil.py | 64 +++++++++++++++++---------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/TESTING.rst b/TESTING.rst index 2a25d2ed..cd99cbbb 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -26,6 +26,6 @@ If you want, you can test against a specific version like this: ``tox -e py312-p Linting ------- -Run `ruff` to check linting:: +Run `ruff` to check formatting:: - ruff check + ruff format qrcode diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index 7f931402..a3b8e5c9 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -66,14 +66,17 @@ def test_render_styled_with_embeded_image_path(tmp_path): img.save(io.BytesIO()) -@pytest.mark.parametrize("drawer", [ - moduledrawers.CircleModuleDrawer, - moduledrawers.GappedSquareModuleDrawer, - moduledrawers.HorizontalBarsDrawer, - moduledrawers.RoundedModuleDrawer, - moduledrawers.SquareModuleDrawer, - moduledrawers.VerticalBarsDrawer, -]) +@pytest.mark.parametrize( + "drawer", + [ + moduledrawers.CircleModuleDrawer, + moduledrawers.GappedSquareModuleDrawer, + moduledrawers.HorizontalBarsDrawer, + moduledrawers.RoundedModuleDrawer, + moduledrawers.SquareModuleDrawer, + moduledrawers.VerticalBarsDrawer, + ], +) def test_render_styled_with_drawer(drawer): qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) qr.add_data(UNICODE_TEXT) @@ -84,28 +87,29 @@ def test_render_styled_with_drawer(drawer): img.save(io.BytesIO()) -@pytest.mark.parametrize("mask", [ - colormasks.SolidFillColorMask(), - colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED), - colormasks.SolidFillColorMask( - back_color=(255, 0, 255, 255), front_color=RED - ), - colormasks.RadialGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ), - colormasks.SquareGradiantColorMask( - back_color=WHITE, center_color=BLACK, edge_color=RED - ), - colormasks.HorizontalGradiantColorMask( - back_color=WHITE, left_color=RED, right_color=BLACK - ), - colormasks.VerticalGradiantColorMask( - back_color=WHITE, top_color=RED, bottom_color=BLACK - ), - colormasks.ImageColorMask( - back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red") - ), -]) +@pytest.mark.parametrize( + "mask", + [ + colormasks.SolidFillColorMask(), + colormasks.SolidFillColorMask(back_color=WHITE, front_color=RED), + colormasks.SolidFillColorMask(back_color=(255, 0, 255, 255), front_color=RED), + colormasks.RadialGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ), + colormasks.SquareGradiantColorMask( + back_color=WHITE, center_color=BLACK, edge_color=RED + ), + colormasks.HorizontalGradiantColorMask( + back_color=WHITE, left_color=RED, right_color=BLACK + ), + colormasks.VerticalGradiantColorMask( + back_color=WHITE, top_color=RED, bottom_color=BLACK + ), + colormasks.ImageColorMask( + back_color=WHITE, color_mask_image=Image.new("RGB", (10, 10), color="red") + ), + ], +) def test_render_styled_with_mask(mask): qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) qr.add_data(UNICODE_TEXT) From d991b67b086fe031b38c0b91177a25bd49e637ec Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 27 Sep 2024 13:05:48 -0300 Subject: [PATCH 31/34] Update poetry dependency groups to allow having pillow and png together --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c33b2809..fdc40d64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,12 +48,15 @@ pillow = {version = ">=9.1.0", optional = true} [tool.poetry.extras] pil = ["pillow"] png = ["pypng"] +all = ["pypng","pillow"] [tool.poetry.group.dev.dependencies] pytest = {version = "*"} pytest-cov = {version = "*"} tox = {version = "*"} ruff = {version = "*"} +pypng = {version = "*"} +pillow = {version = ">=9.1.0"} docutils = "^0.21.2" zest-releaser = {extras = ["recommended"], version = "^9.2.0"} From 39cf5023d01d3f47cf86dbe5e7f630cdf9dc7718 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 27 Sep 2024 13:11:00 -0300 Subject: [PATCH 32/34] Fix test dependency skip --- TESTING.rst | 8 +++++++- qrcode/tests/test_script.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/TESTING.rst b/TESTING.rst index cd99cbbb..2ac28c7e 100644 --- a/TESTING.rst +++ b/TESTING.rst @@ -19,7 +19,13 @@ Here's the OSX Homebrew command: brew install libjpeg libtiff little-cms2 openjpeg webp -Finally, just run ``tox``! +Finally, just run ``tox``:: + + poetry run tox + # or + poetry shell + tox + If you want, you can test against a specific version like this: ``tox -e py312-pil`` diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index 5135b1fb..d6338ded 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -48,7 +48,7 @@ def test_stdin_py3_unicodedecodeerror(): def test_optimize(): - pytest.importorskip("PyPNG", reason="Requires PyPNG") + pytest.importorskip("PIL", reason="Requires PIL") main("testtext --optimize 0".split()) From 68f3b292945cf03c8ffce5979763a2ae5ad1f4a0 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 27 Sep 2024 13:31:38 -0300 Subject: [PATCH 33/34] Raise error if embedded image is provided and error correction is lower than H --- qrcode/main.py | 8 ++++++-- qrcode/tests/test_qrcode_pil.py | 9 ++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/qrcode/main.py b/qrcode/main.py index 2afe2c98..46116b5c 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -345,8 +345,12 @@ def make_image(self, image_factory=None, **kwargs): If the data has not been compiled yet, make it first. """ - if kwargs.get("embeded_image_path") and self.error_correction != constants.ERROR_CORRECT_H: - raise ValueError("Error correction level must be ERROR_CORRECT_H if an embedded image is provided") + if ( + kwargs.get("embeded_image_path") or kwargs.get("embeded_image") + ) and self.error_correction != constants.ERROR_CORRECT_H: + raise ValueError( + "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" + ) _check_box_size(self.box_size) if self.data_cache is None: self.make() diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index ad364196..d4a59e15 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -52,7 +52,7 @@ def test_render_styled_Image(): def test_render_styled_with_embeded_image(): embeded_img = Image.new("RGB", (10, 10), color="red") - qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_L) + qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) qr.add_data(UNICODE_TEXT) img = qr.make_image(image_factory=StyledPilImage, embeded_image=embeded_img) img.save(io.BytesIO()) @@ -129,21 +129,28 @@ def test_embedded_image_and_error_correction(tmp_path): qr.add_data(UNICODE_TEXT) with pytest.raises(ValueError): qr.make_image(embeded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embeded_image=embedded_img) qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_M) qr.add_data(UNICODE_TEXT) with pytest.raises(ValueError): qr.make_image(embeded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embeded_image=embedded_img) qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_Q) qr.add_data(UNICODE_TEXT) with pytest.raises(ValueError): qr.make_image(embeded_image_path=tmpfile) + with pytest.raises(ValueError): + qr.make_image(embeded_image=embedded_img) # The only accepted correction level when an embedded image is provided qr = qrcode.QRCode(error_correction=qrcode.ERROR_CORRECT_H) qr.add_data(UNICODE_TEXT) qr.make_image(embeded_image_path=tmpfile) + qr.make_image(embeded_image=embedded_img) def test_shortcut(): From dc783eddd9a92abb7b7feccdd284083316e2e153 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 27 Sep 2024 13:46:22 -0300 Subject: [PATCH 34/34] Update CHANGELOG and README --- CHANGES.rst | 3 +++ README.rst | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index da2a8616..55c4df39 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,9 @@ Change log - Removed ``typing_extensions`` as a dependency, as it's no longer required with having Python 3.9+ as a requirement. +- Only allow high error correction rate (`qrcode.ERROR_CORRECT_H`) when generating + QR codes with embedded images to ensure content is readable + .. _Poetry: https://python-poetry.org .. _ruff: https://astral.sh/ruff diff --git a/README.rst b/README.rst index 529fa7f6..ec383462 100644 --- a/README.rst +++ b/README.rst @@ -227,7 +227,7 @@ and an embedded image: from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer from qrcode.image.styles.colormasks import RadialGradiantColorMask - qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_L) + qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H) qr.add_data('Some data') img_1 = qr.make_image(image_factory=StyledPilImage, module_drawer=RoundedModuleDrawer())