From 94fd322628e7a3fc279f664dd21126698ad30b9a Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Wed, 23 Jul 2025 16:15:44 +0200 Subject: [PATCH 01/14] Implement Ruff rules and perform comprehensive code cleanup. --- CHANGES.rst | 11 +- pyproject.toml | 109 ++++++++++++------ qrcode/LUT.py | 2 +- qrcode/__init__.py | 21 +++- qrcode/base.py | 13 ++- qrcode/compat/etree.py | 4 +- qrcode/compat/png.py | 8 +- qrcode/console_scripts.py | 26 +++-- qrcode/image/base.py | 37 +++--- qrcode/image/pil.py | 16 +-- qrcode/image/pure.py | 6 +- qrcode/image/styledpil.py | 8 +- qrcode/image/styles/colormasks.py | 8 +- qrcode/image/styles/moduledrawers/__init__.py | 22 ++-- qrcode/image/styles/moduledrawers/base.py | 2 +- qrcode/image/styles/moduledrawers/pil.py | 1 + qrcode/image/styles/moduledrawers/svg.py | 7 +- qrcode/image/svg.py | 32 ++--- qrcode/main.py | 59 +++++----- qrcode/release.py | 17 +-- qrcode/tests/test_module.py | 1 - qrcode/tests/test_qrcode.py | 4 +- qrcode/tests/test_qrcode_pypng.py | 6 +- qrcode/tests/test_release.py | 8 +- qrcode/tests/test_script.py | 70 ++++++----- qrcode/util.py | 83 +++++++------ 26 files changed, 331 insertions(+), 250 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e1f20e8..4e6adf74 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,16 +5,13 @@ Change log WIP === -- Added ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_) -- Removed the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_) +- **Added** ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_) +- **Added** ability to execute as a Python module: ``python -m qrcode --output qrcode.png "hello world"`` (stefansjs in `#400`_) +- **Removed** the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_) - Improved test coveraged (akx in `#315`_) - Fixed typos in code that used ``embeded`` instead of ``embedded``. For backwards compatibility, the misspelled parameter names are still accepted but now emit deprecation warnings. These deprecated parameter names will be removed in v9.0. (benjnicholls in `#349`_) - Migrate pyproject.toml to PEP 621-compliant [project] metadata format. (hroncok in `#399`_) -- Allow execution as a Python module. (stefansjs in `#400`_) - - :: - - python -m qrcode --output qrcode.png "hello world" +- Implement Ruff rules and perform comprehensive code cleanup. .. _#315: https://github.com/lincolnloop/python-qrcode/pull/315 .. _#349: https://github.com/lincolnloop/python-qrcode/pull/349 diff --git a/pyproject.toml b/pyproject.toml index 2694bcc4..04bea4c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,31 +7,31 @@ name = "qrcode" version = "8.2" description = "QR Code image generator" authors = [ - { name = "Lincoln Loop", email = "info@lincolnloop.com" }, + { name = "Lincoln Loop", email = "info@lincolnloop.com" }, ] license = { text = "BSD-3-Clause" } -dynamic = [ "readme" ] +dynamic = ["readme"] 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.13", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Multimedia :: Graphics", - "Topic :: Software Development :: Libraries :: Python Modules", + "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.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries :: Python Modules", ] requires-python = "~=3.9" dependencies = [ - "colorama; sys_platform == 'win32'", - "deprecation", + "colorama; sys_platform == 'win32'", + "deprecation", ] @@ -51,7 +51,7 @@ changelog = "https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst" qr = "qrcode.console_scripts:main" [tool.poetry] -packages = [{include = "qrcode"}] +packages = [{ include = "qrcode" }] readme = ["README.rst", "CHANGES.rst"] # There is no support for data files yet. @@ -62,14 +62,14 @@ readme = ["README.rst", "CHANGES.rst"] # ] [tool.poetry.group.dev.dependencies] -pytest = {version = "*"} -pytest-cov = {version = "*"} -tox = {version = "*"} -ruff = {version = "*"} -pypng = {version = "*"} -pillow = {version = ">=9.1.0"} +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"} +zest-releaser = { extras = ["recommended"], version = "^9.2.0" } [tool.zest-releaser] less-zeros = "yes" @@ -77,9 +77,9 @@ version-levels = 2 tag-format = "v{version}" tag-message = "Version {version}" tag-signing = "yes" -date-format =" %%-d %%B %%Y" +date-format = " %%-d %%B %%Y" prereleaser.middle = [ - "qrcode.release.update_manpage" + "qrcode.release.update_manpage" ] [tool.coverage.run] @@ -88,10 +88,53 @@ parallel = true [tool.coverage.report] exclude_lines = [ - "@abc.abstractmethod", - "@overload", - "if (typing\\.)?TYPE_CHECKING:", - "pragma: no cover", - "raise NotImplementedError" + "@abc.abstractmethod", + "@overload", + "if (typing\\.)?TYPE_CHECKING:", + "pragma: no cover", + "raise NotImplementedError" ] skip_covered = true + +[tool.ruff] +target-version = "py39" +exclude = ["migrations"] +lint.select = ["ALL"] +lint.ignore = [ + # Safe to ignore + "A001", # Variable is shadowing a Python builtin + "A002", # Function argument is shadowing a Python builtin + "ANN", # Missing Type Annotation + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs`" + "ARG001", # Unused function argument (request, ...) + "ARG002", # Unused method argument (*args, **kwargs) + "ARG005", # Unused lambda argument + "D", # Missing or badly formatted docstrings + "E501", # Line too long (>88) + "ERA001", # Found commented-out code + "FBT", # Flake Boolean Trap (don't use arg=True in functions) + "N999", # Invalid module name + "PLR091", # Too many statements/branches/arguments + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable + "RUF012", # Mutable class attributes https://github.com/astral-sh/ruff/issues/5243 + "SLF001", # private-member-access + + # Should be fixed later + "C901", # Too complex + "N802", # Function name should be lowercase + "N803", # Argument name should be lowercase + "N806", # Variable should be lowercase + "PERF401", # Use `list.extend` to create a transformed list + "S101", # Use of 'assert' detected + + # Required for `ruff format` to work correctly + "COM812", # Checks for the absence of trailing commas + "ISC001", # Checks for implicitly concatenated strings on a single line +] + +[tool.ruff.lint.extend-per-file-ignores] +"qrcode/tests/*.py" = [ + "S101", # Use of 'assert' detected + "S603", # `subprocess` call: check for execution of untrusted input + "PT011", # pytest.raises is too broad +] diff --git a/qrcode/LUT.py b/qrcode/LUT.py index 115892f1..88ee87b7 100644 --- a/qrcode/LUT.py +++ b/qrcode/LUT.py @@ -24,7 +24,7 @@ # Result. Usage: input: ecCount, output: Polynomial.num # e.g. rsPoly = base.Polynomial(LUT.rsPoly_LUT[ecCount], 0) -rsPoly_LUT = { +rsPoly_LUT = { # noqa: N816 7: [1, 127, 122, 154, 164, 11, 68, 117], 10: [1, 216, 194, 159, 111, 199, 94, 95, 113, 157, 193], 13: [1, 137, 73, 227, 17, 177, 17, 52, 13, 46, 43, 83, 132, 120], diff --git a/qrcode/__init__.py b/qrcode/__init__.py index 6b238d33..4cbda4d0 100644 --- a/qrcode/__init__.py +++ b/qrcode/__init__.py @@ -1,13 +1,22 @@ -from qrcode.main import QRCode -from qrcode.main import make # noqa -from qrcode.constants import ( # noqa +from qrcode import image +from qrcode.constants import ( + ERROR_CORRECT_H, ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, - ERROR_CORRECT_H, ) - -from qrcode import image # noqa +from qrcode.main import QRCode, make + +__all__ = [ + "ERROR_CORRECT_H", + "ERROR_CORRECT_L", + "ERROR_CORRECT_M", + "ERROR_CORRECT_Q", + "QRCode", + "image", + "make", + "run_example", +] def run_example(data="http://www.lincolnloop.com", *args, **kwargs): diff --git a/qrcode/base.py b/qrcode/base.py index 20f81f6f..25d6325f 100644 --- a/qrcode/base.py +++ b/qrcode/base.py @@ -1,4 +1,5 @@ from typing import NamedTuple + from qrcode import constants EXP_TABLE = list(range(256)) @@ -233,7 +234,8 @@ def glog(n): if n < 1: # pragma: no cover - raise ValueError(f"glog({n})") + msg = f"glog({n})" + raise ValueError(msg) return LOG_TABLE[n] @@ -244,7 +246,8 @@ def gexp(n): class Polynomial: def __init__(self, num, shift): if not num: # pragma: no cover - raise Exception(f"{len(num)}/{shift}") + msg = f"{len(num)}/{shift}" + raise ValueError(msg) offset = 0 for offset in range(len(num)): @@ -296,10 +299,10 @@ class RSBlock(NamedTuple): def rs_blocks(version, error_correction): if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover - raise Exception( - "bad rs block @ version: %s / error_correction: %s" - % (version, error_correction) + msg = ( + f"bad rs block @ version: {version} / error_correction: {error_correction}" ) + raise ValueError(msg) offset = RS_BLOCK_OFFSET[error_correction] rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset] diff --git a/qrcode/compat/etree.py b/qrcode/compat/etree.py index 6739d227..300a757a 100644 --- a/qrcode/compat/etree.py +++ b/qrcode/compat/etree.py @@ -1,4 +1,4 @@ try: - import lxml.etree as ET # type: ignore # noqa: F401 + import lxml.etree as ET # noqa: N812 except ImportError: - import xml.etree.ElementTree as ET # type: ignore # noqa: F401 + import xml.etree.ElementTree as ET # noqa: F401 diff --git a/qrcode/compat/png.py b/qrcode/compat/png.py index 8d7b9056..04abc9eb 100644 --- a/qrcode/compat/png.py +++ b/qrcode/compat/png.py @@ -1,7 +1,7 @@ +import contextlib + # Try to import png library. PngWriter = None -try: - from png import Writer as PngWriter # type: ignore # noqa: F401 -except ImportError: # pragma: no cover - pass +with contextlib.suppress(ImportError): + from png import Writer as PngWriter # noqa: F401 diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index ebe8810f..cee25222 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -6,19 +6,25 @@ a pipe to a file an image is written. The default image format is PNG. """ +from __future__ import annotations + import optparse import os import sys -from typing import NoReturn, Optional -from collections.abc import Iterable from importlib import metadata +from pathlib import Path +from typing import TYPE_CHECKING, NoReturn import qrcode -from qrcode.image.base import BaseImage, DrawerAliases + +if TYPE_CHECKING: + from collections.abc import Iterable + + from qrcode.image.base import BaseImage, DrawerAliases # The next block is added to get the terminal to display properly on MS platforms if sys.platform.startswith(("win", "cygwin")): # pragma: no cover - import colorama # type: ignore + import colorama colorama.init() @@ -50,7 +56,7 @@ def main(args=None): # Wrap parser.error in a typed NoReturn method for better typing. def raise_error(msg: str) -> NoReturn: parser.error(msg) - raise # pragma: no cover + raise # pragma: no cover # noqa: PLE0704 parser.add_option( "--factory", @@ -114,7 +120,7 @@ def raise_error(msg: str) -> NoReturn: if opts.output: img = qr.make_image() - with open(opts.output, "wb") as out: + with Path(opts.output).open("wb") as out: img.save(out) else: if image_factory is None and (os.isatty(sys.stdout.fileno()) or opts.ascii): @@ -122,7 +128,7 @@ def raise_error(msg: str) -> NoReturn: return kwargs = {} - aliases: Optional[DrawerAliases] = getattr( + aliases: DrawerAliases | None = getattr( qr.image_factory, "drawer_aliases", None ) if opts.factory_drawer: @@ -143,7 +149,8 @@ def raise_error(msg: str) -> NoReturn: def get_factory(module: str) -> type[BaseImage]: if "." not in module: - raise ValueError("The image factory is not a full python path") + msg = "The image factory is not a full python path" + raise ValueError(msg) module, name = module.rsplit(".", 1) imp = __import__(module, {}, {}, [name]) return getattr(imp, name) @@ -151,12 +158,13 @@ def get_factory(module: str) -> type[BaseImage]: def get_drawer_help() -> str: help: dict[str, set] = {} + for alias, module in default_factories.items(): try: image = get_factory(module) except ImportError: # pragma: no cover continue - aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None) + aliases: DrawerAliases | None = getattr(image, "drawer_aliases", None) if not aliases: continue factories = help.setdefault(commas(aliases), set()) diff --git a/qrcode/image/base.py b/qrcode/image/base.py index 3767b836..c8ef7e95 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import abc -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any from qrcode.image.styles.moduledrawers.base import QRModuleDrawer @@ -15,8 +17,8 @@ class BaseImage(abc.ABC): Base QRCode image output class. """ - kind: Optional[str] = None - allowed_kinds: Optional[tuple[str]] = None + kind: str | None = None + allowed_kinds: tuple[str] | None = None needs_context = False needs_processing = False needs_drawrect = True @@ -36,17 +38,19 @@ def drawrect(self, row, col): Draw a single rectangle of the QR code. """ - def drawrect_context(self, row: int, col: int, qr: "QRCode"): + def drawrect_context(self, row: int, col: int, qr: QRCode): """ Draw a single rectangle of the QR code given the surrounding context """ - raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover + msg = "BaseImage.drawrect_context" + raise NotImplementedError(msg) # pragma: no cover def process(self): """ Processes QR code after completion """ - raise NotImplementedError("BaseImage.drawimage") # pragma: no cover + msg = "BaseImage.drawimage" + raise NotImplementedError(msg) # pragma: no cover @abc.abstractmethod def save(self, stream, kind=None): @@ -72,7 +76,7 @@ def new_image(self, **kwargs) -> Any: Build the image class. Subclasses should return the class created. """ - def init_new_image(self): + def init_new_image(self): # noqa: B027 pass def get_image(self, **kwargs): @@ -93,7 +97,8 @@ def check_kind(self, kind, transform=None): if not allowed: allowed = kind in self.allowed_kinds if not allowed: - raise ValueError(f"Cannot set {type(self).__name__} type to {kind}") + msg = f"Cannot set {type(self).__name__} type to {kind}" + raise ValueError(msg) return kind def is_eye(self, row: int, col: int): @@ -119,14 +124,14 @@ def get_default_eye_drawer(self) -> QRModuleDrawer: needs_context = True - module_drawer: "QRModuleDrawer" - eye_drawer: "QRModuleDrawer" + module_drawer: QRModuleDrawer + eye_drawer: QRModuleDrawer def __init__( self, *args, - module_drawer: Union[QRModuleDrawer, str, None] = None, - eye_drawer: Union[QRModuleDrawer, str, None] = None, + module_drawer: QRModuleDrawer | str | None = None, + eye_drawer: QRModuleDrawer | str | None = None, **kwargs, ): self.module_drawer = ( @@ -138,9 +143,7 @@ def __init__( self.eye_drawer = self.get_drawer(eye_drawer) or self.get_default_eye_drawer() super().__init__(*args, **kwargs) - def get_drawer( - self, drawer: Union[QRModuleDrawer, str, None] - ) -> Optional[QRModuleDrawer]: + def get_drawer(self, drawer: QRModuleDrawer | str | None) -> QRModuleDrawer | None: if not isinstance(drawer, str): return drawer drawer_cls, kwargs = self.drawer_aliases[drawer] @@ -152,10 +155,10 @@ def init_new_image(self): return super().init_new_image() - def drawrect_context(self, row: int, col: int, qr: "QRCode"): + def drawrect_context(self, row: int, col: int, qr: QRCode): box = self.pixel_box(row, col) drawer = self.eye_drawer if self.is_eye(row, col) else self.module_drawer - is_active: Union[bool, ActiveWithNeighbors] = ( + is_active: bool | ActiveWithNeighbors = ( qr.active_with_neighbors(row, col) if drawer.needs_neighbors else bool(qr.modules[row][col]) diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 57ee13a8..490ab991 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,6 +1,9 @@ -import qrcode.image.base +import contextlib + from PIL import Image, ImageDraw +import qrcode.image.base + class PilImage(qrcode.image.base.BaseImage): """ @@ -11,20 +14,17 @@ class PilImage(qrcode.image.base.BaseImage): def new_image(self, **kwargs): if not Image: - raise ImportError("PIL library not found.") + msg = "PIL library not found." + raise ImportError(msg) back_color = kwargs.get("back_color", "white") fill_color = kwargs.get("fill_color", "black") - try: + with contextlib.suppress(AttributeError): fill_color = fill_color.lower() - except AttributeError: - pass - try: + with contextlib.suppress(AttributeError): back_color = back_color.lower() - except AttributeError: - pass # L mode (1 mode) color = (r*299 + g*587 + b*114)//1000 if fill_color == "black" and back_color == "white": diff --git a/qrcode/image/pure.py b/qrcode/image/pure.py index 5a8b2c5e..ee668146 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -1,4 +1,5 @@ from itertools import chain +from pathlib import Path from qrcode.compat.png import PngWriter from qrcode.image.base import BaseImage @@ -15,7 +16,8 @@ class PyPNGImage(BaseImage): def new_image(self, **kwargs): if not PngWriter: - raise ImportError("PyPNG library not installed.") + msg = "PyPNG library not installed." + raise ImportError(msg) return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) @@ -26,7 +28,7 @@ def drawrect(self, row, col): def save(self, stream, kind=None): if isinstance(stream, str): - stream = open(stream, "wb") + stream = Path(stream).open("wb") # noqa: SIM115 self._img.write(stream, self.rows_iter()) def rows_iter(self): diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index 70f791e1..a269e185 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -60,11 +60,9 @@ def __init__(self, *args, **kwargs): # allow embeded_ parameters with typos for backwards compatibility embedded_image_path = kwargs.get( - "embedded_image_path", kwargs.get("embeded_image_path", None) - ) - self.embedded_image = kwargs.get( - "embedded_image", kwargs.get("embeded_image", None) + "embedded_image_path", kwargs.get("embeded_image_path") ) + self.embedded_image = kwargs.get("embedded_image", kwargs.get("embeded_image")) self.embedded_image_ratio = kwargs.get( "embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25) ) @@ -80,7 +78,7 @@ def __init__(self, *args, **kwargs): # are replaced by a newly-calculated color self.paint_color = tuple(0 for i in self.color_mask.back_color) if self.color_mask.has_transparency: - self.paint_color = tuple([*self.color_mask.back_color[:3], 255]) + self.paint_color = (*self.color_mask.back_color[:3], 255) super().__init__(*args, **kwargs) diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 9599f7fb..9a0d42ff 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -56,7 +56,8 @@ def apply_mask(self, image, use_cache=False): pixels[x, y] = self.get_bg_pixel(image, x, y) def get_fg_pixel(self, image, x, y): - raise NotImplementedError("QRModuleDrawer.paint_fg_pixel") + msg = "QRModuleDrawer.paint_fg_pixel" + raise NotImplementedError(msg) def get_bg_pixel(self, image, x, y): return self.back_color @@ -75,8 +76,7 @@ def interp_color(self, col1, col2, norm): def extrap_num(self, n1, n2, interped_num): if n2 == n1: return None - else: - return (interped_num - n1) / (n2 - n1) + return (interped_num - n1) / (n2 - n1) # find the interpolation coefficient between two numbers def extrap_color(self, col1, col2, interped_color): @@ -108,7 +108,7 @@ def apply_mask(self, image): # mask. pass else: - # TODO there's probably a way to use PIL.ImageMath instead of doing + # There's probably a way to use PIL.ImageMath instead of doing # the individual pixel comparisons that the base class uses, which # would be a lot faster. (In fact doing this would probably remove # the need for the B&W optimization above.) diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index ae8b22af..14c25f0c 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -1,11 +1,13 @@ # For backwards compatibility, importing the PIL drawers here. -try: - from .pil import CircleModuleDrawer # noqa: F401 - from .pil import GappedCircleModuleDrawer # noqa: F401 - from .pil import GappedSquareModuleDrawer # noqa: F401 - from .pil import HorizontalBarsDrawer # noqa: F401 - from .pil import RoundedModuleDrawer # noqa: F401 - from .pil import SquareModuleDrawer # noqa: F401 - from .pil import VerticalBarsDrawer # noqa: F401 -except ImportError: # pragma: no cover - pass +import contextlib + +with contextlib.suppress(ImportError): + from .pil import ( + CircleModuleDrawer, # noqa: F401 + GappedCircleModuleDrawer, # noqa: F401 + GappedSquareModuleDrawer, # noqa: F401 + HorizontalBarsDrawer, # noqa: F401 + RoundedModuleDrawer, # noqa: F401 + SquareModuleDrawer, # noqa: F401 + VerticalBarsDrawer, # noqa: F401 + ) diff --git a/qrcode/image/styles/moduledrawers/base.py b/qrcode/image/styles/moduledrawers/base.py index 154d2cfa..e96c8d5d 100644 --- a/qrcode/image/styles/moduledrawers/base.py +++ b/qrcode/image/styles/moduledrawers/base.py @@ -23,7 +23,7 @@ class QRModuleDrawer(abc.ABC): needs_neighbors = False - def __init__(self, **kwargs): + def __init__(self, **kwargs): # noqa: B027 pass def initialize(self, img: "BaseImage") -> None: diff --git a/qrcode/image/styles/moduledrawers/pil.py b/qrcode/image/styles/moduledrawers/pil.py index cd3ef83c..b393a593 100644 --- a/qrcode/image/styles/moduledrawers/pil.py +++ b/qrcode/image/styles/moduledrawers/pil.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from PIL import Image, ImageDraw + from qrcode.image.styles.moduledrawers.base import QRModuleDrawer if TYPE_CHECKING: diff --git a/qrcode/image/styles/moduledrawers/svg.py b/qrcode/image/styles/moduledrawers/svg.py index cf5b9e7d..d09f1991 100644 --- a/qrcode/image/styles/moduledrawers/svg.py +++ b/qrcode/image/styles/moduledrawers/svg.py @@ -2,8 +2,8 @@ from decimal import Decimal from typing import TYPE_CHECKING, NamedTuple -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer from qrcode.compat.etree import ET +from qrcode.image.styles.moduledrawers.base import QRModuleDrawer if TYPE_CHECKING: from qrcode.image.svg import SvgFragmentImage, SvgPathImage @@ -24,6 +24,7 @@ class BaseSvgQRModuleDrawer(QRModuleDrawer): img: "SvgFragmentImage" def __init__(self, *, size_ratio: Decimal = Decimal(1), **kwargs): + super().__init__(**kwargs) self.size_ratio = size_ratio def initialize(self, *args, **kwargs) -> None: @@ -71,7 +72,7 @@ def initialize(self, *args, **kwargs) -> None: def el(self, box): coords = self.coords(box) return ET.Element( - self.tag_qname, # type: ignore + self.tag_qname, x=self.img.units(coords.x0), y=self.img.units(coords.y0), width=self.unit_size, @@ -89,7 +90,7 @@ def initialize(self, *args, **kwargs) -> None: def el(self, box): coords = self.coords(box) return ET.Element( - self.tag_qname, # type: ignore + self.tag_qname, cx=self.img.units(coords.xh), cy=self.img.units(coords.yh), r=self.radius, diff --git a/qrcode/image/svg.py b/qrcode/image/svg.py index 3bfbeaf2..356f4f43 100644 --- a/qrcode/image/svg.py +++ b/qrcode/image/svg.py @@ -1,11 +1,15 @@ +from __future__ import annotations + import decimal from decimal import Decimal -from typing import Optional, Union, overload, Literal +from typing import TYPE_CHECKING, Literal, overload import qrcode.image.base from qrcode.compat.etree import ET from qrcode.image.styles.moduledrawers import svg as svg_drawers -from qrcode.image.styles.moduledrawers.base import QRModuleDrawer + +if TYPE_CHECKING: + from qrcode.image.styles.moduledrawers.base import QRModuleDrawer class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer): @@ -33,10 +37,10 @@ def drawrect(self, row, col): """ @overload - def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ... + def units(self, pixels: int | Decimal, text: Literal[False]) -> Decimal: ... @overload - def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ... + def units(self, pixels: int | Decimal, text: Literal[True] = True) -> str: ... def units(self, pixels, text=True): """ @@ -48,7 +52,7 @@ def units(self, pixels, text=True): units = units.quantize(Decimal("0.001")) context = decimal.Context(traps=[decimal.Inexact]) try: - for d in (Decimal("0.01"), Decimal("0.1"), Decimal("0")): + for d in (Decimal("0.01"), Decimal("0.1"), Decimal(0)): units = units.quantize(d, context=context) except decimal.Inexact: pass @@ -69,7 +73,7 @@ def _svg(self, tag=None, version="1.1", **kwargs): tag = ET.QName(self._SVG_namespace, "svg") dimension = self.units(self.pixel_size) return ET.Element( - tag, # type: ignore + tag, width=dimension, height=dimension, version=version, @@ -87,11 +91,11 @@ class SvgImage(SvgFragmentImage): Creates a QR-code image as a standalone SVG document. """ - background: Optional[str] = None + background: str | None = None drawer_aliases: qrcode.image.base.DrawerAliases = { "circle": (svg_drawers.SvgCircleDrawer, {}), - "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}), - "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}), + "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal("0.8")}), + "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal("0.8")}), } def _svg(self, tag="svg", **kwargs): @@ -128,17 +132,17 @@ class SvgPathImage(SvgImage): } needs_processing = True - path: Optional[ET.Element] = None + path: ET.Element | None = None default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer drawer_aliases = { "circle": (svg_drawers.SvgPathCircleDrawer, {}), "gapped-circle": ( svg_drawers.SvgPathCircleDrawer, - {"size_ratio": Decimal(0.8)}, + {"size_ratio": Decimal("0.8")}, ), "gapped-square": ( svg_drawers.SvgPathSquareDrawer, - {"size_ratio": Decimal(0.8)}, + {"size_ratio": Decimal("0.8")}, ), } @@ -149,14 +153,14 @@ def __init__(self, *args, **kwargs): def _svg(self, viewBox=None, **kwargs): if viewBox is None: dimension = self.units(self.pixel_size, text=False) - viewBox = "0 0 {d} {d}".format(d=dimension) + viewBox = f"0 0 {dimension} {dimension}" return super()._svg(viewBox=viewBox, **kwargs) def process(self): # Store the path just in case someone wants to use it again or in some # unique way. self.path = ET.Element( - ET.QName("path"), # type: ignore + ET.QName("path"), d="".join(self._subpaths), **self.QR_PATH_STYLE, ) diff --git a/qrcode/main.py b/qrcode/main.py index 9c23ed60..8bbbd2ad 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -1,14 +1,16 @@ +from __future__ import annotations + import sys import warnings from bisect import bisect_left from typing import ( Generic, + Literal, NamedTuple, Optional, TypeVar, cast, overload, - Literal, ) from qrcode import constants, exceptions, util @@ -28,25 +30,25 @@ def make(data=None, **kwargs): def _check_box_size(size): if int(size) <= 0: - raise ValueError(f"Invalid box size (was {size}, expected larger than 0)") + msg = f"Invalid box size (was {size}, expected larger than 0)" + raise ValueError(msg) def _check_border(size): if int(size) < 0: - raise ValueError( - "Invalid border value (was %s, expected 0 or larger than that)" % size - ) + msg = f"Invalid border value (was {size}, expected 0 or larger than that)" + raise ValueError(msg) def _check_mask_pattern(mask_pattern): if mask_pattern is None: return if not isinstance(mask_pattern, int): - raise TypeError( - f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" - ) + msg = f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" + raise TypeError(msg) if mask_pattern < 0 or mask_pattern > 7: - raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})") + msg = f"Mask pattern should be in range(8) (got {mask_pattern})" + raise ValueError(msg) def copy_2d_array(x): @@ -74,7 +76,7 @@ def __bool__(self) -> bool: class QRCode(Generic[GenericImage]): modules: ModulesType - _version: Optional[int] = None + _version: int | None = None def __init__( self, @@ -82,7 +84,7 @@ def __init__( error_correction=constants.ERROR_CORRECT_M, box_size=10, border=4, - image_factory: Optional[type[GenericImage]] = None, + image_factory: type[GenericImage] | None = None, mask_pattern=None, ): _check_box_size(box_size) @@ -103,7 +105,7 @@ def __init__( def version(self) -> int: if self._version is None: self.best_fit() - return cast(int, self._version) + return cast("int", self._version) @version.setter def version(self, value) -> None: @@ -228,7 +230,7 @@ def best_fit(self, start=None): util.BIT_LIMIT_TABLE[self.error_correction], needed_bits, start ) if self.version == 41: - raise exceptions.DataOverflowError() + raise exceptions.DataOverflowError # Now check whether we need more bits for the mode sizes, recursing if # our guess was too low @@ -261,12 +263,11 @@ def print_tty(self, out=None): If the data has not been compiled yet, make it first. """ if out is None: - import sys - out = sys.stdout if not out.isatty(): - raise OSError("Not a tty") + msg = "Not a tty" + raise OSError(msg) if self.data_cache is None: self.make() @@ -295,7 +296,8 @@ def print_ascii(self, out=None, tty=False, invert=False): out = sys.stdout if tty and not out.isatty(): - raise OSError("Not a tty") + msg = "Not a tty" + raise OSError(msg) if self.data_cache is None: self.make() @@ -312,7 +314,7 @@ def get_module(x, y) -> int: return 1 if min(x, y) < 0 or max(x, y) >= modcount: return 0 - return cast(int, self.modules[x][y]) + return cast("int", self.modules[x][y]) for r in range(-self.border, modcount + self.border, 2): if tty: @@ -334,7 +336,7 @@ def make_image( @overload def make_image( - self, image_factory: type[GenericImageLocal] = None, **kwargs + self, image_factory: type[GenericImageLocal] | None = None, **kwargs ) -> GenericImageLocal: ... def make_image(self, image_factory=None, **kwargs): @@ -360,9 +362,8 @@ def make_image(self, image_factory=None, **kwargs): or 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" - ) + msg = "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" + raise ValueError(msg) _check_box_size(self.box_size) if self.data_cache is None: @@ -373,7 +374,7 @@ def make_image(self, image_factory=None, **kwargs): else: image_factory = self.image_factory if image_factory is None: - from qrcode.image.pil import Image, PilImage + from qrcode.image.pil import Image, PilImage # noqa: PLC0415 # Use PIL by default if available, otherwise use PyPNG. image_factory = PilImage if Image else PyPNGImage @@ -432,13 +433,7 @@ def setup_position_adjust_pattern(self): for r in range(-2, 3): for c in range(-2, 3): - if ( - r == -2 - or r == 2 - or c == -2 - or c == 2 - or (r == 0 and c == 0) - ): + if r in (-2, 2) or c in (-2, 2) or (r == 0 and c == 0): self.modules[row + r][col + c] = True else: self.modules[row + r][col + c] = False @@ -495,7 +490,7 @@ def map_data(self, data, mask_pattern): for col in range(self.modules_count - 1, 0, -2): if col <= 6: - col -= 1 + col -= 1 # noqa: PLW2901 col_range = (col, col - 1) @@ -540,7 +535,7 @@ def get_matrix(self): code = [[False] * width] * self.border x_border = [False] * self.border for module in self.modules: - code.append(x_border + cast(list[bool], module) + x_border) + code.append(x_border + cast("list[bool]", module) + x_border) code += [[False] * width] * self.border return code diff --git a/qrcode/release.py b/qrcode/release.py index 208ac1ee..c466f1d2 100644 --- a/qrcode/release.py +++ b/qrcode/release.py @@ -3,9 +3,9 @@ qrcode versions. """ -import os -import re import datetime +import re +from pathlib import Path def update_manpage(data): @@ -15,9 +15,10 @@ def update_manpage(data): if data["name"] != "qrcode": return - base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - filename = os.path.join(base_dir, "doc", "qr.1") - with open(filename) as f: + base_dir = Path(__file__).parent.parent.resolve() + filename = base_dir / "doc" / "qr.1" + + with filename.open("r") as f: lines = f.readlines() changed = False @@ -32,11 +33,13 @@ def update_manpage(data): # Update version parts[3] = data["new_version"] # Update date - parts[1] = datetime.datetime.now().strftime("%-d %b %Y") + parts[1] = datetime.datetime.now(tz=datetime.timezone.utc).strftime( + "%-d %b %Y" + ) lines[i] = '"'.join(parts) break if changed: - with open(filename, "w") as f: + with filename.open("w") as f: for line in lines: f.write(line) diff --git a/qrcode/tests/test_module.py b/qrcode/tests/test_module.py index be5de478..70c00fa3 100644 --- a/qrcode/tests/test_module.py +++ b/qrcode/tests/test_module.py @@ -6,7 +6,6 @@ import pytest - PIL_NOT_AVAILABLE = find_spec("PIL") is None diff --git a/qrcode/tests/test_qrcode.py b/qrcode/tests/test_qrcode.py index efdacdf7..c8c90654 100644 --- a/qrcode/tests/test_qrcode.py +++ b/qrcode/tests/test_qrcode.py @@ -121,10 +121,10 @@ def test_mask_pattern_setter(): def test_qrcode_bad_factory(): with pytest.raises(TypeError): - qrcode.QRCode(image_factory="not_BaseImage") # type: ignore + qrcode.QRCode(image_factory="not_BaseImage") with pytest.raises(AssertionError): - qrcode.QRCode(image_factory=dict) # type: ignore + qrcode.QRCode(image_factory=dict) def test_qrcode_factory(): diff --git a/qrcode/tests/test_qrcode_pypng.py b/qrcode/tests/test_qrcode_pypng.py index c502a3b3..4e74cef4 100644 --- a/qrcode/tests/test_qrcode_pypng.py +++ b/qrcode/tests/test_qrcode_pypng.py @@ -3,7 +3,6 @@ import pytest - import qrcode import qrcode.util from qrcode.image.pure import PyPNGImage @@ -18,7 +17,6 @@ def test_render_pypng(): 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()) @@ -29,7 +27,7 @@ def test_render_pypng_to_str(): assert isinstance(img.get_image(), png.Writer) mock_open = mock.mock_open() - with mock.patch("qrcode.image.pure.open", mock_open, create=True): + with mock.patch("pathlib.Path.open", mock_open, create=True): img.save("test_file.png") - mock_open.assert_called_once_with("test_file.png", "wb") + mock_open.assert_called_once_with("wb") mock_open("test_file.png", "wb").write.assert_called() diff --git a/qrcode/tests/test_release.py b/qrcode/tests/test_release.py index d61454b6..8ac0ec10 100644 --- a/qrcode/tests/test_release.py +++ b/qrcode/tests/test_release.py @@ -1,11 +1,10 @@ -import builtins import datetime import re from unittest import mock from qrcode.release import update_manpage -OPEN = f"{builtins.__name__}.open" +OPEN = "pathlib.Path.open" DATA = 'test\n.TH "date" "version" "description"\nthis' @@ -36,7 +35,10 @@ def test_change(mock_file): expected[1] = ( expected[1] .replace("version", "3.11") - .replace("date", datetime.datetime.now().strftime("%-d %b %Y")) + .replace( + "date", + datetime.datetime.now(tz=datetime.timezone.utc).strftime("%-d %b %Y"), + ) ) mock_file().write.assert_has_calls( [mock.call(line) for line in expected if line != ""], any_order=True diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index d6338ded..79cd5e10 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -7,49 +7,55 @@ def bad_read(): - raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte") + msg = "utf-8" + raise UnicodeDecodeError(msg, b"0x80", 0, 1, "invalid start byte") -@mock.patch("os.isatty", lambda *args: True) +@mock.patch("os.isatty", return_value=True) @mock.patch("qrcode.main.QRCode.print_ascii") -def test_isatty(mock_print_ascii): +def test_isatty(mock_print_ascii, mock_isatty): main(["testtext"]) mock_print_ascii.assert_called_with(tty=True) -@mock.patch("os.isatty", lambda *args: False) -def test_piped(): +@mock.patch("os.isatty", return_value=True) +@mock.patch("sys.stdout.isatty", return_value=True) +def test_piped(mock_stdout_isatty, mock_isatty): 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", return_value=True) +def test_stdin(mock_isatty): + with ( + mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii, + 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", return_value=True) +def test_stdin_py3_unicodedecodeerror(mock_isatty): + with ( + mock.patch("qrcode.main.QRCode.print_ascii") as mock_print_ascii, + 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) def test_optimize(): pytest.importorskip("PIL", reason="Requires PIL") - main("testtext --optimize 0".split()) + main(["testtext", "--optimize", "0"]) def test_factory(): @@ -61,7 +67,7 @@ def test_bad_factory(): main(["testtext", "--factory", "nope"]) -@mock.patch.object(sys, "argv", "qr testtext output".split()) +@mock.patch.object(sys, "argv", ["qr", "testtext", "output"]) def test_sys_argv(): pytest.importorskip("PIL", reason="Requires PIL") main() @@ -75,18 +81,18 @@ def test_output(tmp_path): def test_factory_drawer_none(capsys): pytest.importorskip("PIL", reason="Requires PIL") with pytest.raises(SystemExit): - main("testtext --factory pil --factory-drawer nope".split()) + main(["testtext", "--factory", "pil", "--factory-drawer", "nope"]) 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()) + main(["testtext", "--factory", "svg", "--factory-drawer", "sobad"]) assert "sobad factory drawer not found" in capsys.readouterr()[1] def test_factory_drawer(capsys): - main("testtext --factory svg --factory-drawer circle".split()) + main(["testtext", "--factory", "svg", "--factory-drawer", "circle"]) def test_commas(): diff --git a/qrcode/util.py b/qrcode/util.py index fe25548f..31638212 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -164,15 +164,15 @@ def mask_func(pattern): def mode_sizes_for_version(version): if version < 10: return MODE_SIZE_SMALL - elif version < 27: + if version < 27: return MODE_SIZE_MEDIUM - else: - return MODE_SIZE_LARGE + return MODE_SIZE_LARGE def length_in_bits(mode, version): if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI): - raise TypeError(f"Invalid mode ({mode})") # pragma: no cover + msg = f"Invalid mode ({mode})" + raise TypeError(msg) # pragma: no cover check_version(version) @@ -181,7 +181,8 @@ def length_in_bits(mode, version): def check_version(version): if version < 1 or version > 40: - raise ValueError(f"Invalid version (was {version}, expected 1 to 40)") + msg = f"Invalid version (was {version}, expected 1 to 40)" + raise ValueError(msg) def lost_point(modules): @@ -257,9 +258,7 @@ def _lost_point_level2(modules, modules_count): # reduce 33.3% of runtime via next(). # None: raise nothing if there is no next item. next(modules_range_iter, None) - elif top_right != this_row[col]: - continue - elif top_right != next_row[col]: + elif top_right != this_row[col] or top_right != next_row[col]: continue else: lost_point += 3 @@ -288,18 +287,22 @@ def _lost_point_level3(modules, modules_count): and this_row[col + 6] and not this_row[col + 9] and ( - this_row[col + 0] - and this_row[col + 2] - and this_row[col + 3] - and not this_row[col + 7] - and not this_row[col + 8] - and not this_row[col + 10] - or not this_row[col + 0] - and not this_row[col + 2] - and not this_row[col + 3] - and this_row[col + 7] - and this_row[col + 8] - and this_row[col + 10] + ( + this_row[col + 0] + and this_row[col + 2] + and this_row[col + 3] + and not this_row[col + 7] + and not this_row[col + 8] + and not this_row[col + 10] + ) + or ( + not this_row[col + 0] + and not this_row[col + 2] + and not this_row[col + 3] + and this_row[col + 7] + and this_row[col + 8] + and this_row[col + 10] + ) ) ): lost_point += 40 @@ -322,18 +325,22 @@ def _lost_point_level3(modules, modules_count): and modules[row + 6][col] and not modules[row + 9][col] and ( - modules[row + 0][col] - and modules[row + 2][col] - and modules[row + 3][col] - and not modules[row + 7][col] - and not modules[row + 8][col] - and not modules[row + 10][col] - or not modules[row + 0][col] - and not modules[row + 2][col] - and not modules[row + 3][col] - and modules[row + 7][col] - and modules[row + 8][col] - and modules[row + 10][col] + ( + modules[row + 0][col] + and modules[row + 2][col] + and modules[row + 3][col] + and not modules[row + 7][col] + and not modules[row + 8][col] + and not modules[row + 10][col] + ) + or ( + not modules[row + 0][col] + and not modules[row + 2][col] + and not modules[row + 3][col] + and modules[row + 7][col] + and modules[row + 8][col] + and modules[row + 10][col] + ) ) ): lost_point += 40 @@ -432,9 +439,11 @@ def __init__(self, data, mode=None, check_data=True): else: self.mode = mode if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE): - raise TypeError(f"Invalid mode ({mode})") # pragma: no cover + msg = f"Invalid mode ({mode})" + raise TypeError(msg) # pragma: no cover if check_data and mode < optimal_mode(data): # pragma: no cover - raise ValueError(f"Provided data can not be represented in mode {mode}") + msg = f"Provided data can not be represented in mode {mode}" + raise ValueError(msg) self.data = data @@ -558,10 +567,8 @@ def create_data(version, error_correction, data_list): rs_blocks = base.rs_blocks(version, error_correction) bit_limit = sum(block.data_count * 8 for block in rs_blocks) if len(buffer) > bit_limit: - raise exceptions.DataOverflowError( - "Code length overflow. Data size (%s) > size available (%s)" - % (len(buffer), bit_limit) - ) + msg = f"Code length overflow. Data size ({len(buffer)}) > size available ({bit_limit})" + raise exceptions.DataOverflowError(msg) # Terminate the bits (add up to four 0s). for _ in range(min(bit_limit - len(buffer), 4)): From 82729c2529e0d57e0bdc77e29167bfcd95420cd4 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Wed, 23 Jul 2025 16:18:41 +0200 Subject: [PATCH 02/14] Reformat Imports --- qrcode/main.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/qrcode/main.py b/qrcode/main.py index 8bbbd2ad..ed7316f7 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -3,15 +3,7 @@ import sys import warnings from bisect import bisect_left -from typing import ( - Generic, - Literal, - NamedTuple, - Optional, - TypeVar, - cast, - overload, -) +from typing import Generic, Literal, NamedTuple, Optional, TypeVar, cast, overload from qrcode import constants, exceptions, util from qrcode.image.base import BaseImage From b132ef924b5599f2b5c2f5f91461f2b95d749657 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Wed, 23 Jul 2025 19:15:43 +0200 Subject: [PATCH 03/14] Removed backwards-compatible imports for PIL drawers in qrcode.image.styles.moduledrawers. --- CHANGES.rst | 1 + qrcode/image/styledpil.py | 2 +- qrcode/image/styles/moduledrawers/__init__.py | 13 ------------- qrcode/tests/test_qrcode_pil.py | 14 +++++++------- 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4e6adf74..6c25d846 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ WIP - **Added** ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_) - **Added** ability to execute as a Python module: ``python -m qrcode --output qrcode.png "hello world"`` (stefansjs in `#400`_) - **Removed** the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_) +- **Removed** backwards-compatible imports for PIL drawers in ``qrcode.image.styles.moduledrawers``; users are required to import drawers directly from ``qrcode.image.styles.moduledrawers.pil``." - Improved test coveraged (akx in `#315`_) - Fixed typos in code that used ``embeded`` instead of ``embedded``. For backwards compatibility, the misspelled parameter names are still accepted but now emit deprecation warnings. These deprecated parameter names will be removed in v9.0. (benjnicholls in `#349`_) - Migrate pyproject.toml to PEP 621-compliant [project] metadata format. (hroncok in `#399`_) diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index a269e185..17f947f4 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -8,7 +8,7 @@ import qrcode.image.base from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask -from qrcode.image.styles.moduledrawers import SquareModuleDrawer +from qrcode.image.styles.moduledrawers.pil import SquareModuleDrawer class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index 14c25f0c..e69de29b 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -1,13 +0,0 @@ -# For backwards compatibility, importing the PIL drawers here. -import contextlib - -with contextlib.suppress(ImportError): - from .pil import ( - CircleModuleDrawer, # noqa: F401 - GappedCircleModuleDrawer, # noqa: F401 - GappedSquareModuleDrawer, # noqa: F401 - HorizontalBarsDrawer, # noqa: F401 - RoundedModuleDrawer, # noqa: F401 - SquareModuleDrawer, # noqa: F401 - VerticalBarsDrawer, # noqa: F401 - ) diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index b6ef5638..aa7c2cd7 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -71,13 +71,13 @@ def test_render_styled_with_embedded_image_path(tmp_path): @pytest.mark.parametrize( "drawer", [ - moduledrawers.CircleModuleDrawer, - moduledrawers.GappedCircleModuleDrawer, - moduledrawers.GappedSquareModuleDrawer, - moduledrawers.HorizontalBarsDrawer, - moduledrawers.RoundedModuleDrawer, - moduledrawers.SquareModuleDrawer, - moduledrawers.VerticalBarsDrawer, + moduledrawers.pil.CircleModuleDrawer, + moduledrawers.pil.GappedCircleModuleDrawer, + moduledrawers.pil.GappedSquareModuleDrawer, + moduledrawers.pil.HorizontalBarsDrawer, + moduledrawers.pil.RoundedModuleDrawer, + moduledrawers.pil.SquareModuleDrawer, + moduledrawers.pil.VerticalBarsDrawer, ], ) def test_render_styled_with_drawer(drawer): From 7528208025401db4414a50fc979e4bc5e3c93bd4 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 09:57:38 +0200 Subject: [PATCH 04/14] Revert "Removed backwards-compatible imports for PIL drawers in qrcode.image.styles.moduledrawers." This reverts commit b132ef924b5599f2b5c2f5f91461f2b95d749657. --- CHANGES.rst | 1 - qrcode/image/styledpil.py | 2 +- qrcode/image/styles/moduledrawers/__init__.py | 13 +++++++++++++ qrcode/tests/test_qrcode_pil.py | 14 +++++++------- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6c25d846..4e6adf74 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,6 @@ WIP - **Added** ``GappedCircleModuleDrawer`` (PIL) to render QR code modules as non-contiguous circles. (BenwestGate in `#373`_) - **Added** ability to execute as a Python module: ``python -m qrcode --output qrcode.png "hello world"`` (stefansjs in `#400`_) - **Removed** the hardcoded 'id' argument from SVG elements. The fixed element ID caused conflicts when embedding multiple QR codes in a single document. (m000 in `#385`_) -- **Removed** backwards-compatible imports for PIL drawers in ``qrcode.image.styles.moduledrawers``; users are required to import drawers directly from ``qrcode.image.styles.moduledrawers.pil``." - Improved test coveraged (akx in `#315`_) - Fixed typos in code that used ``embeded`` instead of ``embedded``. For backwards compatibility, the misspelled parameter names are still accepted but now emit deprecation warnings. These deprecated parameter names will be removed in v9.0. (benjnicholls in `#349`_) - Migrate pyproject.toml to PEP 621-compliant [project] metadata format. (hroncok in `#399`_) diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index 17f947f4..a269e185 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -8,7 +8,7 @@ import qrcode.image.base from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask -from qrcode.image.styles.moduledrawers.pil import SquareModuleDrawer +from qrcode.image.styles.moduledrawers import SquareModuleDrawer class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index e69de29b..14c25f0c 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -0,0 +1,13 @@ +# For backwards compatibility, importing the PIL drawers here. +import contextlib + +with contextlib.suppress(ImportError): + from .pil import ( + CircleModuleDrawer, # noqa: F401 + GappedCircleModuleDrawer, # noqa: F401 + GappedSquareModuleDrawer, # noqa: F401 + HorizontalBarsDrawer, # noqa: F401 + RoundedModuleDrawer, # noqa: F401 + SquareModuleDrawer, # noqa: F401 + VerticalBarsDrawer, # noqa: F401 + ) diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index aa7c2cd7..b6ef5638 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -71,13 +71,13 @@ def test_render_styled_with_embedded_image_path(tmp_path): @pytest.mark.parametrize( "drawer", [ - moduledrawers.pil.CircleModuleDrawer, - moduledrawers.pil.GappedCircleModuleDrawer, - moduledrawers.pil.GappedSquareModuleDrawer, - moduledrawers.pil.HorizontalBarsDrawer, - moduledrawers.pil.RoundedModuleDrawer, - moduledrawers.pil.SquareModuleDrawer, - moduledrawers.pil.VerticalBarsDrawer, + moduledrawers.CircleModuleDrawer, + moduledrawers.GappedCircleModuleDrawer, + moduledrawers.GappedSquareModuleDrawer, + moduledrawers.HorizontalBarsDrawer, + moduledrawers.RoundedModuleDrawer, + moduledrawers.SquareModuleDrawer, + moduledrawers.VerticalBarsDrawer, ], ) def test_render_styled_with_drawer(drawer): From eb73ec6d7338b0571ba540925b5ddde581255f03 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 12:50:00 +0200 Subject: [PATCH 05/14] Make an import from 'moduledrawers' directly raise a deprecation warning --- qrcode/image/styledpil.py | 2 +- qrcode/image/styles/moduledrawers/__init__.py | 60 +++++++++++++++---- qrcode/tests/test_qrcode_pil.py | 14 ++--- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/qrcode/image/styledpil.py b/qrcode/image/styledpil.py index a269e185..17f947f4 100644 --- a/qrcode/image/styledpil.py +++ b/qrcode/image/styledpil.py @@ -8,7 +8,7 @@ import qrcode.image.base from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask -from qrcode.image.styles.moduledrawers import SquareModuleDrawer +from qrcode.image.styles.moduledrawers.pil import SquareModuleDrawer class StyledPilImage(qrcode.image.base.BaseImageWithDrawer): diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index 14c25f0c..46d13838 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -1,13 +1,47 @@ -# For backwards compatibility, importing the PIL drawers here. -import contextlib - -with contextlib.suppress(ImportError): - from .pil import ( - CircleModuleDrawer, # noqa: F401 - GappedCircleModuleDrawer, # noqa: F401 - GappedSquareModuleDrawer, # noqa: F401 - HorizontalBarsDrawer, # noqa: F401 - RoundedModuleDrawer, # noqa: F401 - SquareModuleDrawer, # noqa: F401 - VerticalBarsDrawer, # noqa: F401 - ) +""" +Module for lazy importing of PIL drawers with a deprecation warning. + +Currently, importing a PIL drawer from this module is allowed for backwards +compatibility but will raise a DeprecationWarning. + +This will be removed in v9.0. +""" + +import warnings + +from qrcode.constants import PIL_AVAILABLE + + +def __getattr__(name): + """Lazy import with deprecation warning for PIL drawers.""" + # List of PIL drawer names that should trigger deprecation warnings + pil_drawers = { + "CircleModuleDrawer", + "GappedCircleModuleDrawer", + "GappedSquareModuleDrawer", + "HorizontalBarsDrawer", + "RoundedModuleDrawer", + "SquareModuleDrawer", + "VerticalBarsDrawer", + } + + if name in pil_drawers: + # Only render a warning if PIL is actually installed. Otherwise it would + # raise an ImportError directly, which is fine. + if PIL_AVAILABLE: + warnings.warn( + f"Importing '{name}' directly from this module is deprecated." + f"Please use 'from qrcode.image.styles.moduledrawers.pil import {name}' " + f"instead. This backwards compatibility import will be removed in v9.0.", + DeprecationWarning, + stacklevel=2, + ) + + # Import and return the drawer from the pil module + from . import pil # noqa: PLC0415 + + return getattr(pil, name) + + # For any other attribute, raise AttributeError + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) diff --git a/qrcode/tests/test_qrcode_pil.py b/qrcode/tests/test_qrcode_pil.py index b6ef5638..aa7c2cd7 100644 --- a/qrcode/tests/test_qrcode_pil.py +++ b/qrcode/tests/test_qrcode_pil.py @@ -71,13 +71,13 @@ def test_render_styled_with_embedded_image_path(tmp_path): @pytest.mark.parametrize( "drawer", [ - moduledrawers.CircleModuleDrawer, - moduledrawers.GappedCircleModuleDrawer, - moduledrawers.GappedSquareModuleDrawer, - moduledrawers.HorizontalBarsDrawer, - moduledrawers.RoundedModuleDrawer, - moduledrawers.SquareModuleDrawer, - moduledrawers.VerticalBarsDrawer, + moduledrawers.pil.CircleModuleDrawer, + moduledrawers.pil.GappedCircleModuleDrawer, + moduledrawers.pil.GappedSquareModuleDrawer, + moduledrawers.pil.HorizontalBarsDrawer, + moduledrawers.pil.RoundedModuleDrawer, + moduledrawers.pil.SquareModuleDrawer, + moduledrawers.pil.VerticalBarsDrawer, ], ) def test_render_styled_with_drawer(drawer): From c38fb6f5944b5d2d0c754b6553df5d4e142508fe Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 12:50:46 +0200 Subject: [PATCH 06/14] Add a global constant whether PIL is available or not. --- qrcode/constants.py | 5 +++++ qrcode/tests/test_module.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qrcode/constants.py b/qrcode/constants.py index 385dda08..cce629e7 100644 --- a/qrcode/constants.py +++ b/qrcode/constants.py @@ -1,5 +1,10 @@ +from importlib.util import find_spec + # QR error correct levels ERROR_CORRECT_L = 1 ERROR_CORRECT_M = 0 ERROR_CORRECT_Q = 3 ERROR_CORRECT_H = 2 + +# Constant whether the PIL library is installed. +PIL_AVAILABLE = find_spec("PIL") is not None diff --git a/qrcode/tests/test_module.py b/qrcode/tests/test_module.py index 70c00fa3..275edff6 100644 --- a/qrcode/tests/test_module.py +++ b/qrcode/tests/test_module.py @@ -1,12 +1,11 @@ import subprocess import sys import tempfile -from importlib.util import find_spec from pathlib import Path import pytest -PIL_NOT_AVAILABLE = find_spec("PIL") is None +from qrcode.constants import PIL_AVAILABLE def test_module_help(): @@ -27,7 +26,7 @@ def test_module_help(): assert "--factory" in result.stdout -@pytest.mark.skipif(PIL_NOT_AVAILABLE, reason="PIL is not installed") +@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") def test_module_generate_qrcode(): """Test that the module can generate a QR code image.""" with tempfile.TemporaryDirectory() as temp_dir: From c473ebb39a4c903b8e1813eeacc4349cfa668b79 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 12:51:14 +0200 Subject: [PATCH 07/14] Test and document deprecations. --- CHANGES.rst | 27 ++++++++ qrcode/tests/conftest.py | 25 +++++++ qrcode/tests/test_deprecation.py | 114 +++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 qrcode/tests/conftest.py create mode 100644 qrcode/tests/test_deprecation.py diff --git a/CHANGES.rst b/CHANGES.rst index 4e6adf74..851d2680 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,33 @@ Change log ========== +Deprecation Warnings +==================== + +Removed in v9.0: +---------------- + +- Importing a PIL drawer from ``qrcode.image.styles.moduledrawers`` has been deprecated. + Update your code to import directly from the ``pil`` module instead: + + .. code-block:: python + + from qrcode.image.styles.moduledrawers import SquareModuleDrawer # Old + from qrcode.image.styles.moduledrawers.pil import SquareModuleDrawer # New + +- Calling ``QRCode.make_image`` or ``StyledPilImage`` with the arguments ``embeded_image`` + or ``embeded_image_path`` have been deprecated due to typographical errors. Update + your code to use the correct arguments ``embedded_image`` and ``embededd_image_path``: + + .. code-block:: python + + qr = QRCode() + qr.make_image(embeded_image=..., embeded_image_path=...) # Old + qr.make_image(embedded_image=..., embedded_image_path=...) # New + + StyledPilImage(embeded_image=..., embeded_image_path=...) # Old + StyledPilImage(embedded_image=..., embedded_image_path=...) # New + WIP === diff --git a/qrcode/tests/conftest.py b/qrcode/tests/conftest.py new file mode 100644 index 00000000..ca852bd5 --- /dev/null +++ b/qrcode/tests/conftest.py @@ -0,0 +1,25 @@ +import tempfile +from importlib.util import find_spec + +import pytest + + +@pytest.fixture +def dummy_image() -> tempfile.NamedTemporaryFile: + """ + This function creates a red pixel image with full opacity, saves it as a PNG + file in a temporary location, and returns the temporary file. + + The file is not automatically deleted. The caller is responsible for deleting it. + """ + # Must import here as PIL might be not installed + from PIL import Image + + # A 1x1 Red Pixel + dummy_image = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + + # Save the image to a temporary file + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file: + dummy_image.save(temp_file.name) + + return temp_file diff --git a/qrcode/tests/test_deprecation.py b/qrcode/tests/test_deprecation.py new file mode 100644 index 00000000..5acc45a7 --- /dev/null +++ b/qrcode/tests/test_deprecation.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +from qrcode.constants import ERROR_CORRECT_H, PIL_AVAILABLE +from qrcode.main import QRCode + +if TYPE_CHECKING: + from tempfile import NamedTemporaryFile + + +@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") +def test_moduledrawer_import() -> None: + """ + Importing a drawer from qrcode.image.styles.moduledrawers is deprecated + and will raise a DeprecationWarning. + + Removed in v9.0. + """ + # These module imports are fine to import + from qrcode.image.styles.moduledrawers import base, pil, svg + + with pytest.warns( + DeprecationWarning, + match="Importing 'SquareModuleDrawer' directly from this module is deprecated.", + ): + from qrcode.image.styles.moduledrawers import SquareModuleDrawer + + +@pytest.mark.skipif(PIL_AVAILABLE, reason="PIL is installed") +def test_moduledrawer_import_pil_not_installed() -> None: + """ + Importing from qrcode.image.styles.moduledrawers is deprecated, however, + if PIL is not installed, there will be no (false) warning; it's a simple + ImportError. + + Removed in v9.0. + """ + # These module imports are fine to import + from qrcode.image.styles.moduledrawers import base, svg + + # Importing a backwards compatible module drawer does normally render a + # DeprecationWarning; however, since PIL is not installed, it will raise an + # ImportError. + with pytest.raises(ImportError): + from qrcode.image.styles.moduledrawers import SquareModuleDrawer + + +@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") +def test_make_image_embeded_parameters() -> None: + """ + Using 'embeded_image_path' or 'embeded_image' parameters with QRCode.make_image() + is deprecated and will raise a DeprecationWarning. + + Removed in v9.0. + """ + + # Create a QRCode required for embedded images + qr = QRCode(error_correction=ERROR_CORRECT_H) + qr.add_data("test") + + # Test with embeded_image_path parameter + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated" + ): + qr.make_image(embeded_image_path="dummy_path") + + # Test with embeded_image parameter + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." + ): + qr.make_image(embeded_image="dummy_image") + + +@pytest.mark.skipif(not PIL_AVAILABLE, reason="PIL is not installed") +def test_styledpilimage_embeded_parameters(dummy_image: NamedTemporaryFile) -> None: + """ + Using 'embeded_image_path' or 'embeded_image' parameters with StyledPilImage + is deprecated and will raise a DeprecationWarning. + + Removed in v9.0. + """ + from PIL import Image + + from qrcode.image.styledpil import StyledPilImage + + styled_kwargs = { + "border": 4, + "width": 21, + "box_size": 10, + "qrcode_modules": 1, + } + + try: + # Test with embeded_image_path parameter + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." + ): + StyledPilImage(embeded_image_path=dummy_image.name, **styled_kwargs) + + # Test with embeded_image parameter + embedded_img = Image.open(dummy_image.name) + + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." + ): + StyledPilImage(embeded_image=embedded_img, **styled_kwargs) + + # Make sure the temporary image is always deleted after the testrun. + finally: + Path(dummy_image.name).unlink(missing_ok=True) From fa212b773f57aa836453a3b162fc6e36dd64be90 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 12:51:27 +0200 Subject: [PATCH 08/14] Simplify Import handling within tests for Ruff --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 04bea4c4..defa60a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,9 @@ lint.ignore = [ [tool.ruff.lint.extend-per-file-ignores] "qrcode/tests/*.py" = [ + "F401", # Unused import + "PLC0415", # Import not at top of a file + "PT011", # pytest.raises is too broad "S101", # Use of 'assert' detected "S603", # `subprocess` call: check for execution of untrusted input - "PT011", # pytest.raises is too broad ] From b2a1f3f0e7537bd22d5386c51eeda3130b54021d Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 13:08:34 +0200 Subject: [PATCH 09/14] Revert contextlib change to improve performance. --- pyproject.toml | 1 + qrcode/compat/png.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index defa60a7..c2a28df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ lint.ignore = [ "PLR091", # Too many statements/branches/arguments "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "RUF012", # Mutable class attributes https://github.com/astral-sh/ruff/issues/5243 + "SIM105", # Use contextlib.suppress(ImportError) instead of try-except-pass "SLF001", # private-member-access # Should be fixed later diff --git a/qrcode/compat/png.py b/qrcode/compat/png.py index 04abc9eb..e7883dc4 100644 --- a/qrcode/compat/png.py +++ b/qrcode/compat/png.py @@ -1,7 +1,7 @@ -import contextlib - # Try to import png library. PngWriter = None -with contextlib.suppress(ImportError): +try: from png import Writer as PngWriter # noqa: F401 +except ImportError: + pass From f6e1715e2c0c1c4d74b15e808f927fc46c334618 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 13:12:59 +0200 Subject: [PATCH 10/14] Fix tuple annotation --- qrcode/image/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qrcode/image/base.py b/qrcode/image/base.py index c8ef7e95..133fd744 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -18,7 +18,7 @@ class BaseImage(abc.ABC): """ kind: str | None = None - allowed_kinds: tuple[str] | None = None + allowed_kinds: tuple[str, ...] | None = None needs_context = False needs_processing = False needs_drawrect = True From 20963195284a95ed19d1ce6fcf4025b090db3705 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 13:38:39 +0200 Subject: [PATCH 11/14] Revert EM101 102 --- pyproject.toml | 3 +++ qrcode/base.py | 9 +++---- qrcode/console_scripts.py | 3 +-- qrcode/image/base.py | 9 +++---- qrcode/image/pil.py | 3 +-- qrcode/image/pure.py | 3 +-- qrcode/image/styles/colormasks.py | 3 +-- qrcode/image/styles/moduledrawers/__init__.py | 3 +-- qrcode/main.py | 27 +++++++++---------- qrcode/tests/test_script.py | 3 +-- qrcode/util.py | 17 +++++------- 11 files changed, 35 insertions(+), 48 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c2a28df3..9d56b383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,8 @@ lint.ignore = [ "ARG005", # Unused lambda argument "D", # Missing or badly formatted docstrings "E501", # Line too long (>88) + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use an f-string literal, assign to variable first "ERA001", # Found commented-out code "ERA001", # Found commented-out code "FBT", # Flake Boolean Trap (don't use arg=True in functions) "N999", # Invalid module name @@ -119,6 +121,7 @@ lint.ignore = [ "RUF012", # Mutable class attributes https://github.com/astral-sh/ruff/issues/5243 "SIM105", # Use contextlib.suppress(ImportError) instead of try-except-pass "SLF001", # private-member-access + "TRY003", # Avoid specifying long messages outside the exception class # Should be fixed later "C901", # Too complex diff --git a/qrcode/base.py b/qrcode/base.py index 25d6325f..630542d5 100644 --- a/qrcode/base.py +++ b/qrcode/base.py @@ -234,8 +234,7 @@ def glog(n): if n < 1: # pragma: no cover - msg = f"glog({n})" - raise ValueError(msg) + raise ValueError(f"glog({n})") return LOG_TABLE[n] @@ -246,8 +245,7 @@ def gexp(n): class Polynomial: def __init__(self, num, shift): if not num: # pragma: no cover - msg = f"{len(num)}/{shift}" - raise ValueError(msg) + raise ValueError(f"{len(num)}/{shift}") offset = 0 for offset in range(len(num)): @@ -299,10 +297,9 @@ class RSBlock(NamedTuple): def rs_blocks(version, error_correction): if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover - msg = ( + raise ValueError( f"bad rs block @ version: {version} / error_correction: {error_correction}" ) - raise ValueError(msg) offset = RS_BLOCK_OFFSET[error_correction] rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset] diff --git a/qrcode/console_scripts.py b/qrcode/console_scripts.py index cee25222..431bcb20 100755 --- a/qrcode/console_scripts.py +++ b/qrcode/console_scripts.py @@ -149,8 +149,7 @@ def raise_error(msg: str) -> NoReturn: def get_factory(module: str) -> type[BaseImage]: if "." not in module: - msg = "The image factory is not a full python path" - raise ValueError(msg) + raise ValueError("The image factory is not a full python path") module, name = module.rsplit(".", 1) imp = __import__(module, {}, {}, [name]) return getattr(imp, name) diff --git a/qrcode/image/base.py b/qrcode/image/base.py index 133fd744..2453c5ce 100644 --- a/qrcode/image/base.py +++ b/qrcode/image/base.py @@ -42,15 +42,13 @@ def drawrect_context(self, row: int, col: int, qr: QRCode): """ Draw a single rectangle of the QR code given the surrounding context """ - msg = "BaseImage.drawrect_context" - raise NotImplementedError(msg) # pragma: no cover + raise NotImplementedError("BaseImage.drawrect_context") # pragma: no cover def process(self): """ Processes QR code after completion """ - msg = "BaseImage.drawimage" - raise NotImplementedError(msg) # pragma: no cover + raise NotImplementedError("BaseImage.drawimage") # pragma: no cover @abc.abstractmethod def save(self, stream, kind=None): @@ -97,8 +95,7 @@ def check_kind(self, kind, transform=None): if not allowed: allowed = kind in self.allowed_kinds if not allowed: - msg = f"Cannot set {type(self).__name__} type to {kind}" - raise ValueError(msg) + raise ValueError(f"Cannot set {type(self).__name__} type to {kind}") return kind def is_eye(self, row: int, col: int): diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index 490ab991..b5b469be 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -14,8 +14,7 @@ class PilImage(qrcode.image.base.BaseImage): def new_image(self, **kwargs): if not Image: - msg = "PIL library not found." - raise ImportError(msg) + 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 ee668146..9d68e5fb 100644 --- a/qrcode/image/pure.py +++ b/qrcode/image/pure.py @@ -16,8 +16,7 @@ class PyPNGImage(BaseImage): def new_image(self, **kwargs): if not PngWriter: - msg = "PyPNG library not installed." - raise ImportError(msg) + raise ImportError("PyPNG library not installed.") return PngWriter(self.pixel_size, self.pixel_size, greyscale=True, bitdepth=1) diff --git a/qrcode/image/styles/colormasks.py b/qrcode/image/styles/colormasks.py index 9a0d42ff..01dad916 100644 --- a/qrcode/image/styles/colormasks.py +++ b/qrcode/image/styles/colormasks.py @@ -56,8 +56,7 @@ def apply_mask(self, image, use_cache=False): pixels[x, y] = self.get_bg_pixel(image, x, y) def get_fg_pixel(self, image, x, y): - msg = "QRModuleDrawer.paint_fg_pixel" - raise NotImplementedError(msg) + raise NotImplementedError("QRModuleDrawer.paint_fg_pixel") def get_bg_pixel(self, image, x, y): return self.back_color diff --git a/qrcode/image/styles/moduledrawers/__init__.py b/qrcode/image/styles/moduledrawers/__init__.py index 46d13838..056da6df 100644 --- a/qrcode/image/styles/moduledrawers/__init__.py +++ b/qrcode/image/styles/moduledrawers/__init__.py @@ -43,5 +43,4 @@ def __getattr__(name): return getattr(pil, name) # For any other attribute, raise AttributeError - msg = f"module {__name__!r} has no attribute {name!r}" - raise AttributeError(msg) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/qrcode/main.py b/qrcode/main.py index ed7316f7..be62de81 100644 --- a/qrcode/main.py +++ b/qrcode/main.py @@ -22,25 +22,25 @@ def make(data=None, **kwargs): def _check_box_size(size): if int(size) <= 0: - msg = f"Invalid box size (was {size}, expected larger than 0)" - raise ValueError(msg) + raise ValueError(f"Invalid box size (was {size}, expected larger than 0)") def _check_border(size): if int(size) < 0: - msg = f"Invalid border value (was {size}, expected 0 or larger than that)" - raise ValueError(msg) + raise ValueError( + f"Invalid border value (was {size}, expected 0 or larger than that)" + ) def _check_mask_pattern(mask_pattern): if mask_pattern is None: return if not isinstance(mask_pattern, int): - msg = f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" - raise TypeError(msg) + raise TypeError( + f"Invalid mask pattern (was {type(mask_pattern)}, expected int)" + ) if mask_pattern < 0 or mask_pattern > 7: - msg = f"Mask pattern should be in range(8) (got {mask_pattern})" - raise ValueError(msg) + raise ValueError(f"Mask pattern should be in range(8) (got {mask_pattern})") def copy_2d_array(x): @@ -258,8 +258,7 @@ def print_tty(self, out=None): out = sys.stdout if not out.isatty(): - msg = "Not a tty" - raise OSError(msg) + raise OSError("Not a tty") if self.data_cache is None: self.make() @@ -288,8 +287,7 @@ def print_ascii(self, out=None, tty=False, invert=False): out = sys.stdout if tty and not out.isatty(): - msg = "Not a tty" - raise OSError(msg) + raise OSError("Not a tty") if self.data_cache is None: self.make() @@ -354,8 +352,9 @@ def make_image(self, image_factory=None, **kwargs): or kwargs.get("embeded_image_path") or kwargs.get("embeded_image") ) and self.error_correction != constants.ERROR_CORRECT_H: - msg = "Error correction level must be ERROR_CORRECT_H if an embedded image is provided" - raise ValueError(msg) + 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: diff --git a/qrcode/tests/test_script.py b/qrcode/tests/test_script.py index 79cd5e10..ab4d2903 100644 --- a/qrcode/tests/test_script.py +++ b/qrcode/tests/test_script.py @@ -7,8 +7,7 @@ def bad_read(): - msg = "utf-8" - raise UnicodeDecodeError(msg, b"0x80", 0, 1, "invalid start byte") + raise UnicodeDecodeError("utf-8", b"0x80", 0, 1, "invalid start byte") @mock.patch("os.isatty", return_value=True) diff --git a/qrcode/util.py b/qrcode/util.py index 31638212..4187aa5e 100644 --- a/qrcode/util.py +++ b/qrcode/util.py @@ -171,8 +171,7 @@ def mode_sizes_for_version(version): def length_in_bits(mode, version): if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI): - msg = f"Invalid mode ({mode})" - raise TypeError(msg) # pragma: no cover + raise TypeError(f"Invalid mode ({mode})") # pragma: no cover check_version(version) @@ -181,8 +180,7 @@ def length_in_bits(mode, version): def check_version(version): if version < 1 or version > 40: - msg = f"Invalid version (was {version}, expected 1 to 40)" - raise ValueError(msg) + raise ValueError(f"Invalid version (was {version}, expected 1 to 40)") def lost_point(modules): @@ -439,11 +437,9 @@ def __init__(self, data, mode=None, check_data=True): else: self.mode = mode if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE): - msg = f"Invalid mode ({mode})" - raise TypeError(msg) # pragma: no cover + raise TypeError(f"Invalid mode ({mode})") # pragma: no cover if check_data and mode < optimal_mode(data): # pragma: no cover - msg = f"Provided data can not be represented in mode {mode}" - raise ValueError(msg) + raise ValueError(f"Provided data can not be represented in mode {mode}") self.data = data @@ -567,8 +563,9 @@ def create_data(version, error_correction, data_list): rs_blocks = base.rs_blocks(version, error_correction) bit_limit = sum(block.data_count * 8 for block in rs_blocks) if len(buffer) > bit_limit: - msg = f"Code length overflow. Data size ({len(buffer)}) > size available ({bit_limit})" - raise exceptions.DataOverflowError(msg) + raise exceptions.DataOverflowError( + f"Code length overflow. Data size ({len(buffer)}) > size available ({bit_limit})" + ) # Terminate the bits (add up to four 0s). for _ in range(min(bit_limit - len(buffer), 4)): From 84b31ecad7707aa9f58958f46f911baa496b77b9 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Thu, 24 Jul 2025 13:43:46 +0200 Subject: [PATCH 12/14] Refactor temporary file handling in tests and simplify deprecation test structure --- qrcode/tests/conftest.py | 5 ++--- qrcode/tests/test_deprecation.py | 31 ++++++++++++++----------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/qrcode/tests/conftest.py b/qrcode/tests/conftest.py index ca852bd5..dc66687e 100644 --- a/qrcode/tests/conftest.py +++ b/qrcode/tests/conftest.py @@ -19,7 +19,6 @@ def dummy_image() -> tempfile.NamedTemporaryFile: dummy_image = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) # Save the image to a temporary file - with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file: + with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as temp_file: dummy_image.save(temp_file.name) - - return temp_file + yield temp_file diff --git a/qrcode/tests/test_deprecation.py b/qrcode/tests/test_deprecation.py index 5acc45a7..5abb45cc 100644 --- a/qrcode/tests/test_deprecation.py +++ b/qrcode/tests/test_deprecation.py @@ -27,7 +27,9 @@ def test_moduledrawer_import() -> None: DeprecationWarning, match="Importing 'SquareModuleDrawer' directly from this module is deprecated.", ): - from qrcode.image.styles.moduledrawers import SquareModuleDrawer + from qrcode.image.styles.moduledrawers import ( + SquareModuleDrawer, + ) @pytest.mark.skipif(PIL_AVAILABLE, reason="PIL is installed") @@ -94,21 +96,16 @@ def test_styledpilimage_embeded_parameters(dummy_image: NamedTemporaryFile) -> N "qrcode_modules": 1, } - try: - # Test with embeded_image_path parameter - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." - ): - StyledPilImage(embeded_image_path=dummy_image.name, **styled_kwargs) - - # Test with embeded_image parameter - embedded_img = Image.open(dummy_image.name) + # Test with embeded_image_path parameter + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." + ): + StyledPilImage(embeded_image_path=dummy_image.name, **styled_kwargs) - with pytest.warns( - DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." - ): - StyledPilImage(embeded_image=embedded_img, **styled_kwargs) + # Test with embeded_image parameter + embedded_img = Image.open(dummy_image.name) - # Make sure the temporary image is always deleted after the testrun. - finally: - Path(dummy_image.name).unlink(missing_ok=True) + with pytest.warns( + DeprecationWarning, match="The 'embeded_\\*' parameters are deprecated." + ): + StyledPilImage(embeded_image=embedded_img, **styled_kwargs) From d453a5ea97f9b5536e89c850c0ff5f1a2f214339 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Fri, 25 Jul 2025 11:00:21 +0200 Subject: [PATCH 13/14] File is automatically deleted --- qrcode/tests/conftest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qrcode/tests/conftest.py b/qrcode/tests/conftest.py index dc66687e..130da233 100644 --- a/qrcode/tests/conftest.py +++ b/qrcode/tests/conftest.py @@ -9,8 +9,6 @@ def dummy_image() -> tempfile.NamedTemporaryFile: """ This function creates a red pixel image with full opacity, saves it as a PNG file in a temporary location, and returns the temporary file. - - The file is not automatically deleted. The caller is responsible for deleting it. """ # Must import here as PIL might be not installed from PIL import Image From 60fdcc27bc4ca02ceb6ac18c6ee17ae5a4f182f0 Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Fri, 25 Jul 2025 11:05:57 +0200 Subject: [PATCH 14/14] Revert supress change --- qrcode/image/pil.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qrcode/image/pil.py b/qrcode/image/pil.py index b5b469be..7b542234 100644 --- a/qrcode/image/pil.py +++ b/qrcode/image/pil.py @@ -1,5 +1,3 @@ -import contextlib - from PIL import Image, ImageDraw import qrcode.image.base @@ -19,11 +17,11 @@ def new_image(self, **kwargs): back_color = kwargs.get("back_color", "white") fill_color = kwargs.get("fill_color", "black") - with contextlib.suppress(AttributeError): + try: fill_color = fill_color.lower() - - with contextlib.suppress(AttributeError): back_color = back_color.lower() + except AttributeError: + pass # L mode (1 mode) color = (r*299 + g*587 + b*114)//1000 if fill_color == "black" and back_color == "white":