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

Type hints and cleanup of Barcode.build() and surrounding code #230

Merged
merged 11 commits into from
Jul 30, 2024
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
Loading