Skip to content

Commit 980f3ff

Browse files
authored
Type hints and cleanup of Barcode.build() and surrounding code (#230)
* 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
1 parent 6e14fe7 commit 980f3ff

File tree

8 files changed

+199
-133
lines changed

8 files changed

+199
-133
lines changed

barcode/__init__.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
"""
66
from __future__ import annotations
77

8+
import os
89
from typing import TYPE_CHECKING
910
from typing import BinaryIO
11+
from typing import overload
1012

1113
from barcode.codabar import CODABAR
1214
from barcode.codex import PZN
@@ -28,11 +30,10 @@
2830
from barcode.version import version # noqa: F401
2931

3032
if TYPE_CHECKING:
31-
import os
32-
33+
from barcode.base import Barcode
3334
from barcode.writer import BaseWriter
3435

35-
__BARCODE_MAP = {
36+
__BARCODE_MAP: dict[str, type[Barcode]] = {
3637
"codabar": CODABAR,
3738
"code128": Code128,
3839
"code39": Code39,
@@ -61,12 +62,29 @@
6162
PROVIDED_BARCODES.sort()
6263

6364

65+
@overload
66+
def get(
67+
name: str, code: str, writer: BaseWriter | None = None, options: dict | None = None
68+
) -> Barcode:
69+
...
70+
71+
72+
@overload
73+
def get(
74+
name: str,
75+
code: None = None,
76+
writer: BaseWriter | None = None,
77+
options: dict | None = None,
78+
) -> type[Barcode]:
79+
...
80+
81+
6482
def get(
6583
name: str,
6684
code: str | None = None,
6785
writer: BaseWriter | None = None,
6886
options: dict | None = None,
69-
):
87+
) -> Barcode | type[Barcode]:
7088
"""Helper method for getting a generator or even a generated code.
7189
7290
:param name: The name of the type of barcode desired.
@@ -79,6 +97,7 @@ def get(
7997
generating.
8098
"""
8199
options = options or {}
100+
barcode: type[Barcode]
82101
try:
83102
barcode = __BARCODE_MAP[name.lower()]
84103
except KeyError as e:
@@ -89,15 +108,15 @@ def get(
89108
return barcode
90109

91110

92-
def get_class(name: str):
111+
def get_class(name: str) -> type[Barcode]:
93112
return get_barcode(name)
94113

95114

96115
def generate(
97116
name: str,
98117
code: str,
99118
writer: BaseWriter | None = None,
100-
output: str | (os.PathLike | (BinaryIO | None)) = None,
119+
output: str | os.PathLike | BinaryIO | None = None,
101120
writer_options: dict | None = None,
102121
text: str | None = None,
103122
) -> str | None:
@@ -113,18 +132,22 @@ def generate(
113132
"""
114133
from barcode.base import Barcode
115134

135+
if output is None:
136+
raise TypeError("'output' cannot be None")
137+
116138
writer = writer or Barcode.default_writer()
117139
writer.set_options(writer_options or {})
118140

119141
barcode = get(name, code, writer)
120142

121143
if isinstance(output, str):
122144
return barcode.save(output, writer_options, text)
123-
if output:
124-
barcode.write(output, writer_options, text)
145+
if isinstance(output, os.PathLike):
146+
with open(output, "wb") as fp:
147+
barcode.write(fp, writer_options, text)
125148
return None
126-
127-
raise TypeError("'output' cannot be None")
149+
barcode.write(output, writer_options, text)
150+
return None
128151

129152

130153
get_barcode = get

barcode/base.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,24 @@ class Barcode:
3434

3535
writer: BaseWriter
3636

37+
def __init__(self, code: str, writer: BaseWriter | None = None, **options) -> None:
38+
raise NotImplementedError
39+
3740
def to_ascii(self) -> str:
38-
code = self.build()
39-
for i, line in enumerate(code):
40-
code[i] = line.replace("1", "X").replace("0", " ")
41-
return "\n".join(code)
41+
code_list = self.build()
42+
if not len(code_list) == 1:
43+
raise RuntimeError("Code list must contain a single element.")
44+
code = code_list[0]
45+
return code.replace("1", "X").replace("0", " ")
4246

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

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

4957
def get_fullcode(self):
@@ -101,5 +109,8 @@ def render(self, writer_options: dict | None = None, text: str | None = None):
101109
else:
102110
options["text"] = self.get_fullcode()
103111
self.writer.set_options(options)
104-
code = self.build()
105-
return self.writer.render(code)
112+
code_list = self.build()
113+
if not len(code_list) == 1:
114+
raise RuntimeError("Code list must contain a single element.")
115+
code = code_list[0]
116+
return self.writer.render([code])

barcode/codabar.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __str__(self) -> str:
4141
def get_fullcode(self):
4242
return self.code
4343

44-
def build(self):
44+
def build(self) -> list[str]:
4545
try:
4646
data = (
4747
codabar.STARTSTOP[self.code[0]] + "n"

barcode/codex.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"""
55
from __future__ import annotations
66

7+
from typing import TYPE_CHECKING
78
from typing import Collection
9+
from typing import Literal
810

911
from barcode.base import Barcode
1012
from barcode.charsets import code39
@@ -13,6 +15,9 @@
1315
from barcode.errors import IllegalCharacterError
1416
from barcode.errors import NumberOfDigitsError
1517

18+
if TYPE_CHECKING:
19+
from barcode.writer import BaseWriter
20+
1621
__docformat__ = "restructuredtext en"
1722

1823
# Sizes
@@ -66,12 +71,13 @@ def calculate_checksum(self):
6671
return k
6772
return None
6873

69-
def build(self):
74+
def build(self) -> list[str]:
7075
chars = [code39.EDGE]
7176
for char in self.code:
7277
chars.append(code39.MAP[char][1])
7378
chars.append(code39.EDGE)
74-
return [code39.MIDDLE.join(chars)]
79+
result = code39.MIDDLE.join(chars)
80+
return [result]
7581

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

137143
name = "Code 128"
144+
_charset: Literal["A", "B", "C"]
145+
code: str
146+
writer: BaseWriter
147+
buffer: str
138148

139-
def __init__(self, code, writer=None) -> None:
149+
def __init__(self, code: str, writer=None) -> None:
140150
self.code = code
141151
self.writer = writer or self.default_writer()
142152
self._charset = "B"
@@ -147,13 +157,15 @@ def __str__(self) -> str:
147157
return self.code
148158

149159
@property
150-
def encoded(self):
160+
def encoded(self) -> list[int]:
151161
return self._build()
152162

153-
def get_fullcode(self):
163+
def get_fullcode(self) -> str:
154164
return self.code
155165

156-
def _new_charset(self, which):
166+
def _new_charset(self, which: Literal["A", "B", "C"]) -> list[int]:
167+
if which == self._charset:
168+
raise ValueError(f"Already in charset {which}")
157169
if which == "A":
158170
code = self._convert("TO_A")
159171
elif which == "B":
@@ -163,11 +175,11 @@ def _new_charset(self, which):
163175
self._charset = which
164176
return [code]
165177

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

170-
def look_next():
182+
def look_next() -> bool:
171183
digits = 0
172184
for c in next_:
173185
if c.isdigit():
@@ -176,7 +188,7 @@ def look_next():
176188
break
177189
return digits > 3
178190

179-
codes = []
191+
codes: list[int] = []
180192
if self._charset == "C" and not char.isdigit():
181193
if char in code128.B:
182194
codes = self._new_charset("B")
@@ -197,7 +209,7 @@ def look_next():
197209
codes = self._new_charset("B")
198210
return codes
199211

200-
def _convert(self, char):
212+
def _convert(self, char: str):
201213
if self._charset == "A":
202214
return code128.A[char]
203215
if self._charset == "B":
@@ -212,22 +224,23 @@ def _convert(self, char):
212224
self._buffer = ""
213225
return value
214226
return None
215-
return None
216-
return None
227+
raise RuntimeError(
228+
f"Character {char} could not be converted in charset {self._charset}."
229+
)
217230

218-
def _try_to_optimize(self, encoded):
231+
def _try_to_optimize(self, encoded: list[int]) -> list[int]:
219232
if encoded[1] in code128.TO:
220233
encoded[:2] = [code128.TO[encoded[1]]]
221234
return encoded
222235

223-
def _calculate_checksum(self, encoded):
236+
def _calculate_checksum(self, encoded: list[int]) -> int:
224237
cs = [encoded[0]]
225238
for i, code_num in enumerate(encoded[1:], start=1):
226239
cs.append(i * code_num)
227240
return sum(cs) % 103
228241

229-
def _build(self):
230-
encoded = [code128.START_CODES[self._charset]]
242+
def _build(self) -> list[int]:
243+
encoded: list[int] = [code128.START_CODES[self._charset]]
231244
for i, char in enumerate(self.code):
232245
encoded.extend(self._maybe_switch_charset(i))
233246
code_num = self._convert(char)
@@ -240,7 +253,7 @@ def _build(self):
240253
self._buffer = ""
241254
return self._try_to_optimize(encoded)
242255

243-
def build(self):
256+
def build(self) -> list[str]:
244257
encoded = self._build()
245258
encoded.append(self._calculate_checksum(encoded))
246259
code = ""

barcode/ean.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,11 @@ def calculate_checksum(self, value: str | None = None) -> int:
9494
oddsum = sum(int(x) for x in ean_without_checksum[-1::-2])
9595
return (10 - ((evensum + oddsum * 3) % 10)) % 10
9696

97-
def build(self):
97+
def build(self) -> list[str]:
9898
"""Builds the barcode pattern from `self.ean`.
9999
100100
:returns: The pattern as string
101-
:rtype: String
101+
:rtype: List containing the string as a single element
102102
"""
103103
code = self.EDGE[:]
104104
pattern = _ean.LEFT_PATTERN[int(self.ean[0])]
@@ -110,15 +110,16 @@ def build(self):
110110
code += self.EDGE
111111
return [code]
112112

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

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

172173
digits = 7
173174

174-
def build(self):
175+
def build(self) -> list[str]:
175176
"""Builds the barcode pattern from `self.ean`.
176177
177-
:returns: The pattern as string
178-
:rtype: String
178+
:returns: A list containing the string as a single element
179179
"""
180180
code = self.EDGE[:]
181181
for number in self.ean[:4]:

barcode/itf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def __str__(self) -> str:
4848
def get_fullcode(self):
4949
return self.code
5050

51-
def build(self):
51+
def build(self) -> list[str]:
5252
data = itf.START
5353
for i in range(0, len(self.code), 2):
5454
bars_digit = int(self.code[i])

barcode/upc.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ def sum_(x, y):
7777

7878
return 10 - check
7979

80-
def build(self):
80+
def build(self) -> list[str]:
8181
"""Builds the barcode pattern from 'self.upc'
8282
8383
:return: The pattern as string
84-
:rtype: str
84+
:rtype: List containing the string as a single element
8585
"""
8686
code = _upc.EDGE[:]
8787

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

9898
return [code]
9999

100-
def to_ascii(self):
100+
def to_ascii(self) -> str:
101101
"""Returns an ascii representation of the barcode.
102102
103103
:rtype: str
104104
"""
105105

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

111112
def render(self, writer_options=None, text=None):
112113
options = {"module_width": 0.33}

0 commit comments

Comments
 (0)