Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Python 3.13 #386

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
"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",
Expand Down
7 changes: 4 additions & 3 deletions qrcode/console_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import optparse
import os
import sys
from typing import Dict, Iterable, NoReturn, Optional, Set, Type
from typing import NoReturn, Optional
from collections.abc import Iterable
from importlib import metadata

import qrcode
Expand Down Expand Up @@ -140,7 +141,7 @@ def raise_error(msg: str) -> NoReturn:
img.save(sys.stdout.buffer)


def get_factory(module: str) -> Type[BaseImage]:
def get_factory(module: str) -> type[BaseImage]:
if "." not in module:
raise ValueError("The image factory is not a full python path")
module, name = module.rsplit(".", 1)
Expand All @@ -149,7 +150,7 @@ def get_factory(module: str) -> Type[BaseImage]:


def get_drawer_help() -> str:
help: Dict[str, Set] = {}
help: dict[str, set] = {}
for alias, module in default_factories.items():
try:
image = get_factory(module)
Expand Down
8 changes: 4 additions & 4 deletions qrcode/image/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import abc
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Optional, Union

from qrcode.image.styles.moduledrawers.base import QRModuleDrawer

if TYPE_CHECKING:
from qrcode.main import ActiveWithNeighbors, QRCode


DrawerAliases = Dict[str, Tuple[Type[QRModuleDrawer], Dict[str, Any]]]
DrawerAliases = dict[str, tuple[type[QRModuleDrawer], dict[str, Any]]]


class BaseImage:
Expand All @@ -16,7 +16,7 @@ class BaseImage:
"""

kind: Optional[str] = None
allowed_kinds: Optional[Tuple[str]] = None
allowed_kinds: Optional[tuple[str]] = None
needs_context = False
needs_processing = False
needs_drawrect = True
Expand Down Expand Up @@ -108,7 +108,7 @@ def is_eye(self, row: int, col: int):


class BaseImageWithDrawer(BaseImage):
default_drawer_class: Type[QRModuleDrawer]
default_drawer_class: type[QRModuleDrawer]
drawer_aliases: DrawerAliases = {}

def get_default_module_drawer(self) -> QRModuleDrawer:
Expand Down
4 changes: 2 additions & 2 deletions qrcode/image/styles/moduledrawers/pil.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING

from PIL import Image, ImageDraw
from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
Expand Down Expand Up @@ -136,7 +136,7 @@ def setup_corners(self):
self.SE_ROUND = self.NW_ROUND.transpose(Image.Transpose.ROTATE_180)
self.NE_ROUND = self.NW_ROUND.transpose(Image.Transpose.FLIP_LEFT_RIGHT)

def drawrect(self, box: List[List[int]], is_active: "ActiveWithNeighbors"):
def drawrect(self, box: list[list[int]], is_active: "ActiveWithNeighbors"):
if not is_active:
return
# find rounded edges
Expand Down
8 changes: 4 additions & 4 deletions qrcode/image/svg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import decimal
from decimal import Decimal
from typing import List, Optional, Type, Union, overload, Literal
from typing import Optional, Union, overload, Literal

import qrcode.image.base
from qrcode.compat.etree import ET
Expand All @@ -18,7 +18,7 @@ class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer):
_SVG_namespace = "http://www.w3.org/2000/svg"
kind = "SVG"
allowed_kinds = ("SVG",)
default_drawer_class: Type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer
default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer

def __init__(self, *args, **kwargs):
ET.register_namespace("svg", self._SVG_namespace)
Expand Down Expand Up @@ -123,7 +123,7 @@ class SvgPathImage(SvgImage):

needs_processing = True
path: Optional[ET.Element] = None
default_drawer_class: Type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer
default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer
drawer_aliases = {
"circle": (svg_drawers.SvgPathCircleDrawer, {}),
"gapped-circle": (
Expand All @@ -137,7 +137,7 @@ class SvgPathImage(SvgImage):
}

def __init__(self, *args, **kwargs):
self._subpaths: List[str] = []
self._subpaths: list[str] = []
super().__init__(*args, **kwargs)

def _svg(self, viewBox=None, **kwargs):
Expand Down
15 changes: 6 additions & 9 deletions qrcode/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import sys
from bisect import bisect_left
from typing import (
Dict,
Generic,
List,
NamedTuple,
Optional,
Type,
TypeVar,
cast,
overload,
Expand All @@ -17,9 +14,9 @@
from qrcode.image.base import BaseImage
from qrcode.image.pure import PyPNGImage

ModulesType = List[List[Optional[bool]]]
ModulesType = list[list[Optional[bool]]]
# Cache modules generated just based on the QR Code version
precomputed_qr_blanks: Dict[int, ModulesType] = {}
precomputed_qr_blanks: dict[int, ModulesType] = {}


def make(data=None, **kwargs):
Expand Down Expand Up @@ -84,7 +81,7 @@ def __init__(
error_correction=constants.ERROR_CORRECT_M,
box_size=10,
border=4,
image_factory: Optional[Type[GenericImage]] = None,
image_factory: Optional[type[GenericImage]] = None,
mask_pattern=None,
):
_check_box_size(box_size)
Expand Down Expand Up @@ -336,7 +333,7 @@ def make_image(

@overload
def make_image(
self, image_factory: Type[GenericImageLocal] = None, **kwargs
self, image_factory: type[GenericImageLocal] = None, **kwargs
) -> GenericImageLocal: ...

def make_image(self, image_factory=None, **kwargs):
Expand Down Expand Up @@ -527,13 +524,13 @@ 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

def active_with_neighbors(self, row: int, col: int) -> ActiveWithNeighbors:
context: List[bool] = []
context: list[bool] = []
for r in range(row - 1, row + 2):
for c in range(col - 1, col + 2):
context.append(self.is_constrained(r, c) and bool(self.modules[r][c]))
Expand Down
9 changes: 4 additions & 5 deletions qrcode/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import math
import re
from typing import List

from qrcode import LUT, base, exceptions
from qrcode.base import RSBlock
Expand Down Expand Up @@ -470,7 +469,7 @@ def __repr__(self):

class BitBuffer:
def __init__(self):
self.buffer: List[int] = []
self.buffer: list[int] = []
self.length = 0

def __repr__(self):
Expand All @@ -496,14 +495,14 @@ def put_bit(self, bit):
self.length += 1


def create_bytes(buffer: BitBuffer, rs_blocks: List[RSBlock]):
def create_bytes(buffer: BitBuffer, rs_blocks: list[RSBlock]):
offset = 0

maxDcCount = 0
maxEcCount = 0

dcdata: List[List[int]] = []
ecdata: List[List[int]] = []
dcdata: list[list[int]] = []
ecdata: list[list[int]] = []

for rs_block in rs_blocks:
dcCount = rs_block.data_count
Expand Down
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
distribute = False
envlist = py{39,310,311,312}-{pil,png,none}
envlist = py{39,310,311,312,313}-{pil,png,none}
skip_missing_interpreters = True

[gh-actions]
Expand All @@ -9,6 +9,7 @@ python =
3.10: py310
3.11: py311
3.12: py312
3.13: py313

[testenv]
commands =
Expand Down