Skip to content

Commit

Permalink
Type hints and cleanup of Barcode.build() and surrounding code (#230)
Browse files Browse the repository at this point in the history
* Raise a runtime error when converting missing character
* Enforce that Barcode.build() returns a single-item list
* Fix SVG DOM implementation to minidom
* Enforce that code is a singleton list for BaseWriter.render
* Assume Pillow
* Enforce singletons in ImageWriter and SVGWriter
* Fix bad info in docstring
* Make writer and output optional params of generate
* More type annotations
  • Loading branch information
maresb authored Jul 30, 2024
1 parent 6e14fe7 commit 980f3ff
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 133 deletions.
43 changes: 33 additions & 10 deletions barcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
"""
from __future__ import annotations

import os
from typing import TYPE_CHECKING
from typing import BinaryIO
from typing import overload

from barcode.codabar import CODABAR
from barcode.codex import PZN
Expand All @@ -28,11 +30,10 @@
from barcode.version import version # noqa: F401

if TYPE_CHECKING:
import os

from barcode.base import Barcode
from barcode.writer import BaseWriter

__BARCODE_MAP = {
__BARCODE_MAP: dict[str, type[Barcode]] = {
"codabar": CODABAR,
"code128": Code128,
"code39": Code39,
Expand Down Expand Up @@ -61,12 +62,29 @@
PROVIDED_BARCODES.sort()


@overload
def get(
name: str, code: str, writer: BaseWriter | None = None, options: dict | None = None
) -> Barcode:
...


@overload
def get(
name: str,
code: None = None,
writer: BaseWriter | None = None,
options: dict | None = None,
) -> type[Barcode]:
...


def get(
name: str,
code: str | None = None,
writer: BaseWriter | None = None,
options: dict | None = None,
):
) -> Barcode | type[Barcode]:
"""Helper method for getting a generator or even a generated code.
:param name: The name of the type of barcode desired.
Expand All @@ -79,6 +97,7 @@ def get(
generating.
"""
options = options or {}
barcode: type[Barcode]
try:
barcode = __BARCODE_MAP[name.lower()]
except KeyError as e:
Expand All @@ -89,15 +108,15 @@ def get(
return barcode


def get_class(name: str):
def get_class(name: str) -> type[Barcode]:
return get_barcode(name)


def generate(
name: str,
code: str,
writer: BaseWriter | None = None,
output: str | (os.PathLike | (BinaryIO | None)) = None,
output: str | os.PathLike | BinaryIO | None = None,
writer_options: dict | None = None,
text: str | None = None,
) -> str | None:
Expand All @@ -113,18 +132,22 @@ def generate(
"""
from barcode.base import Barcode

if output is None:
raise TypeError("'output' cannot be None")

writer = writer or Barcode.default_writer()
writer.set_options(writer_options or {})

barcode = get(name, code, writer)

if isinstance(output, str):
return barcode.save(output, writer_options, text)
if output:
barcode.write(output, writer_options, text)
if isinstance(output, os.PathLike):
with open(output, "wb") as fp:
barcode.write(fp, writer_options, text)
return None

raise TypeError("'output' cannot be None")
barcode.write(output, writer_options, text)
return None


get_barcode = get
Expand Down
23 changes: 17 additions & 6 deletions barcode/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,24 @@ class Barcode:

writer: BaseWriter

def __init__(self, code: str, writer: BaseWriter | None = None, **options) -> None:
raise NotImplementedError

def to_ascii(self) -> str:
code = self.build()
for i, line in enumerate(code):
code[i] = line.replace("1", "X").replace("0", " ")
return "\n".join(code)
code_list = self.build()
if not len(code_list) == 1:
raise RuntimeError("Code list must contain a single element.")
code = code_list[0]
return code.replace("1", "X").replace("0", " ")

def __repr__(self) -> str:
return f"<{self.__class__.__name__}({self.get_fullcode()!r})>"

def build(self) -> list[str]:
"""Return a single-element list with a string encoding the barcode.
Typically the string consists of 1s and 0s, although it can contain
other characters such as G for guard lines (e.g. in EAN13)."""
raise NotImplementedError

def get_fullcode(self):
Expand Down Expand Up @@ -101,5 +109,8 @@ def render(self, writer_options: dict | None = None, text: str | None = None):
else:
options["text"] = self.get_fullcode()
self.writer.set_options(options)
code = self.build()
return self.writer.render(code)
code_list = self.build()
if not len(code_list) == 1:
raise RuntimeError("Code list must contain a single element.")
code = code_list[0]
return self.writer.render([code])
2 changes: 1 addition & 1 deletion barcode/codabar.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __str__(self) -> str:
def get_fullcode(self):
return self.code

def build(self):
def build(self) -> list[str]:
try:
data = (
codabar.STARTSTOP[self.code[0]] + "n"
Expand Down
47 changes: 30 additions & 17 deletions barcode/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"""
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import Collection
from typing import Literal

from barcode.base import Barcode
from barcode.charsets import code39
Expand All @@ -13,6 +15,9 @@
from barcode.errors import IllegalCharacterError
from barcode.errors import NumberOfDigitsError

if TYPE_CHECKING:
from barcode.writer import BaseWriter

__docformat__ = "restructuredtext en"

# Sizes
Expand Down Expand Up @@ -66,12 +71,13 @@ def calculate_checksum(self):
return k
return None

def build(self):
def build(self) -> list[str]:
chars = [code39.EDGE]
for char in self.code:
chars.append(code39.MAP[char][1])
chars.append(code39.EDGE)
return [code39.MIDDLE.join(chars)]
result = code39.MIDDLE.join(chars)
return [result]

def render(self, writer_options=None, text=None):
options = {"module_width": MIN_SIZE, "quiet_zone": MIN_QUIET_ZONE}
Expand Down Expand Up @@ -135,8 +141,12 @@ class Code128(Barcode):
"""

name = "Code 128"
_charset: Literal["A", "B", "C"]
code: str
writer: BaseWriter
buffer: str

def __init__(self, code, writer=None) -> None:
def __init__(self, code: str, writer=None) -> None:
self.code = code
self.writer = writer or self.default_writer()
self._charset = "B"
Expand All @@ -147,13 +157,15 @@ def __str__(self) -> str:
return self.code

@property
def encoded(self):
def encoded(self) -> list[int]:
return self._build()

def get_fullcode(self):
def get_fullcode(self) -> str:
return self.code

def _new_charset(self, which):
def _new_charset(self, which: Literal["A", "B", "C"]) -> list[int]:
if which == self._charset:
raise ValueError(f"Already in charset {which}")
if which == "A":
code = self._convert("TO_A")
elif which == "B":
Expand All @@ -163,11 +175,11 @@ def _new_charset(self, which):
self._charset = which
return [code]

def _maybe_switch_charset(self, pos):
def _maybe_switch_charset(self, pos: int) -> list[int]:
char = self.code[pos]
next_ = self.code[pos : pos + 10]

def look_next():
def look_next() -> bool:
digits = 0
for c in next_:
if c.isdigit():
Expand All @@ -176,7 +188,7 @@ def look_next():
break
return digits > 3

codes = []
codes: list[int] = []
if self._charset == "C" and not char.isdigit():
if char in code128.B:
codes = self._new_charset("B")
Expand All @@ -197,7 +209,7 @@ def look_next():
codes = self._new_charset("B")
return codes

def _convert(self, char):
def _convert(self, char: str):
if self._charset == "A":
return code128.A[char]
if self._charset == "B":
Expand All @@ -212,22 +224,23 @@ def _convert(self, char):
self._buffer = ""
return value
return None
return None
return None
raise RuntimeError(
f"Character {char} could not be converted in charset {self._charset}."
)

def _try_to_optimize(self, encoded):
def _try_to_optimize(self, encoded: list[int]) -> list[int]:
if encoded[1] in code128.TO:
encoded[:2] = [code128.TO[encoded[1]]]
return encoded

def _calculate_checksum(self, encoded):
def _calculate_checksum(self, encoded: list[int]) -> int:
cs = [encoded[0]]
for i, code_num in enumerate(encoded[1:], start=1):
cs.append(i * code_num)
return sum(cs) % 103

def _build(self):
encoded = [code128.START_CODES[self._charset]]
def _build(self) -> list[int]:
encoded: list[int] = [code128.START_CODES[self._charset]]
for i, char in enumerate(self.code):
encoded.extend(self._maybe_switch_charset(i))
code_num = self._convert(char)
Expand All @@ -240,7 +253,7 @@ def _build(self):
self._buffer = ""
return self._try_to_optimize(encoded)

def build(self):
def build(self) -> list[str]:
encoded = self._build()
encoded.append(self._calculate_checksum(encoded))
code = ""
Expand Down
20 changes: 10 additions & 10 deletions barcode/ean.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ def calculate_checksum(self, value: str | None = None) -> int:
oddsum = sum(int(x) for x in ean_without_checksum[-1::-2])
return (10 - ((evensum + oddsum * 3) % 10)) % 10

def build(self):
def build(self) -> list[str]:
"""Builds the barcode pattern from `self.ean`.
:returns: The pattern as string
:rtype: String
:rtype: List containing the string as a single element
"""
code = self.EDGE[:]
pattern = _ean.LEFT_PATTERN[int(self.ean[0])]
Expand All @@ -110,15 +110,16 @@ def build(self):
code += self.EDGE
return [code]

def to_ascii(self):
def to_ascii(self) -> str:
"""Returns an ascii representation of the barcode.
:rtype: String
"""
code = self.build()
for i, line in enumerate(code):
code[i] = line.replace("G", "|").replace("1", "|").replace("0", " ")
return "\n".join(code)
code_list = self.build()
if not len(code_list) == 1:
raise RuntimeError("Code list must contain a single element.")
code = code_list[0]
return code.replace("G", "|").replace("1", "|").replace("0", " ")

def render(self, writer_options=None, text=None):
options = {"module_width": SIZES["SC2"]}
Expand Down Expand Up @@ -171,11 +172,10 @@ class EuropeanArticleNumber8(EuropeanArticleNumber13):

digits = 7

def build(self):
def build(self) -> list[str]:
"""Builds the barcode pattern from `self.ean`.
:returns: The pattern as string
:rtype: String
:returns: A list containing the string as a single element
"""
code = self.EDGE[:]
for number in self.ean[:4]:
Expand Down
2 changes: 1 addition & 1 deletion barcode/itf.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __str__(self) -> str:
def get_fullcode(self):
return self.code

def build(self):
def build(self) -> list[str]:
data = itf.START
for i in range(0, len(self.code), 2):
bars_digit = int(self.code[i])
Expand Down
15 changes: 8 additions & 7 deletions barcode/upc.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ def sum_(x, y):

return 10 - check

def build(self):
def build(self) -> list[str]:
"""Builds the barcode pattern from 'self.upc'
:return: The pattern as string
:rtype: str
:rtype: List containing the string as a single element
"""
code = _upc.EDGE[:]

Expand All @@ -97,16 +97,17 @@ def build(self):

return [code]

def to_ascii(self):
def to_ascii(self) -> str:
"""Returns an ascii representation of the barcode.
:rtype: str
"""

code = self.build()
for i, line in enumerate(code):
code[i] = line.replace("1", "|").replace("0", "_")
return "\n".join(code)
code_list = self.build()
if len(code_list) != 1:
raise RuntimeError("Code list must contain a single element.")
code = code_list[0]
return code.replace("1", "|").replace("0", "_")

def render(self, writer_options=None, text=None):
options = {"module_width": 0.33}
Expand Down
Loading

0 comments on commit 980f3ff

Please sign in to comment.