Skip to content

Commit f442494

Browse files
authored
Merge pull request #32 from labelle-org/tshalev-add-tests
Add unit tests for render engines
2 parents cbeb91f + 80bc57f commit f442494

File tree

53 files changed

+424
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+424
-61
lines changed

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ classifiers = [
3838
dynamic = ["version"]
3939
requires-python = ">=3.8,<4"
4040

41+
[project.optional-dependencies]
42+
test = [
43+
"pytest-cov",
44+
"pytest-image-diff"
45+
]
46+
4147
[project.urls]
4248
Homepage = "https://github.com/labelle-org/labelle"
4349
source = "https://github.com/labelle-org/labelle"
@@ -77,9 +83,12 @@ python =
7783
3.12: py312
7884
7985
[testenv]
86+
deps =
87+
.[test]
8088
commands =
8189
pip check
8290
pip freeze
91+
pytest --cov=src/labelle --cov-report html:{work_dir}/{env_name}/htmlcov --cov-fail-under=45
8392
labelle --version
8493
labelle --help
8594
python -c "import labelle.gui.gui; print('GUI import succeeded')"

src/labelle/cli/cli.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
QrRenderEngine,
4848
RenderContext,
4949
RenderEngine,
50-
TestPatternRenderEngine,
50+
SamplePatternRenderEngine,
5151
TextRenderEngine,
5252
)
5353

@@ -142,12 +142,12 @@ def default(
142142
FontStyle, typer.Option(help="Set fonts style", rich_help_panel="Design")
143143
] = DefaultFontStyle,
144144
frame_width_px: Annotated[
145-
Optional[int],
145+
int,
146146
typer.Option(
147147
help="Draw frame of given width [px] around text",
148148
rich_help_panel="Design",
149149
),
150-
] = None,
150+
] = 0,
151151
align: Annotated[
152152
Direction, typer.Option(help="Align multiline text", rich_help_panel="Design")
153153
] = Direction.LEFT,
@@ -159,7 +159,7 @@ def default(
159159
rich_help_panel="Design",
160160
),
161161
] = Direction.LEFT,
162-
test_pattern: Annotated[
162+
sample_pattern: Annotated[
163163
Optional[int],
164164
typer.Option(help="Prints test pattern of a desired dot width"),
165165
] = None,
@@ -205,13 +205,12 @@ def default(
205205
typer.Option("--barcode", help="Barcode", rich_help_panel="Elements"),
206206
] = None,
207207
barcode_type: Annotated[
208-
Optional[BarcodeType],
208+
BarcodeType,
209209
typer.Option(
210210
help="Barcode type",
211-
show_default=DEFAULT_BARCODE_TYPE.value,
212211
rich_help_panel="Elements",
213212
),
214-
] = None,
213+
] = DEFAULT_BARCODE_TYPE,
215214
barcode_with_text_content: Annotated[
216215
Optional[str],
217216
typer.Option(
@@ -364,6 +363,13 @@ def default(
364363
hidden=True,
365364
),
366365
] = None,
366+
test_pattern: Annotated[
367+
Optional[int],
368+
typer.Option(
369+
help="DEPRECATED",
370+
hidden=True,
371+
),
372+
] = None,
367373
) -> None:
368374
if ctx.invoked_subcommand is not None:
369375
return
@@ -420,6 +426,10 @@ def default(
420426
raise typer.BadParameter("The -l flag is deprecated. Use --min-length instead.")
421427
if old_justify is not None:
422428
raise typer.BadParameter("The -j flag is deprecated. Use --justify instead.")
429+
if test_pattern is not None:
430+
raise typer.BadParameter(
431+
"The --test-pattern flag is deprecated. Use --sample-pattern instead."
432+
)
423433

424434
# read config file
425435
try:
@@ -429,9 +439,6 @@ def default(
429439
msg = f"{e}. Valid fonts are: {', '.join(valid_font_names)}"
430440
raise typer.BadParameter(msg) from None
431441

432-
if barcode_type and not (barcode_content or barcode_with_text_content):
433-
raise typer.BadParameter("Cannot specify barcode type without a barcode value")
434-
435442
if barcode_with_text_content and barcode_content:
436443
raise typer.BadParameter(
437444
"Cannot specify both barcode with text and regular barcode"
@@ -454,8 +461,8 @@ def default(
454461

455462
render_engines: list[RenderEngine] = []
456463

457-
if test_pattern:
458-
render_engines.append(TestPatternRenderEngine(test_pattern))
464+
if sample_pattern:
465+
render_engines.append(SamplePatternRenderEngine(sample_pattern))
459466

460467
if qr_content:
461468
render_engines.append(QrRenderEngine(qr_content))
@@ -511,6 +518,8 @@ def default(
511518
device = None
512519

513520
dymo_labeler = DymoLabeler(tape_size_mm=tape_size_mm, device=device)
521+
if not render_engines:
522+
raise typer.BadParameter("No elements to print")
514523
render_engine = HorizontallyCombinedRenderEngine(render_engines)
515524
render_context = RenderContext(
516525
background_color="white",

src/labelle/gui/q_label_widgets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
BarcodeWithTextRenderEngine,
2626
EmptyRenderEngine,
2727
NoContentError,
28-
NoPictureFilePath,
28+
PicturePathDoesNotExist,
2929
PictureRenderEngine,
3030
QrRenderEngine,
3131
RenderContext,
@@ -445,5 +445,5 @@ def render_engine_impl(self):
445445
"""
446446
try:
447447
return PictureRenderEngine(picture_path=self.label.text())
448-
except NoPictureFilePath:
448+
except PicturePathDoesNotExist:
449449
return EmptyRenderEngine()

src/labelle/lib/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
USE_QR = True
2626
e_qrcode = None
27-
except ImportError as error:
27+
except ImportError as error: # pragma: no cover
2828
e_qrcode = error
2929
USE_QR = False
3030
QRCode = None
@@ -105,8 +105,9 @@ class Direction(str, Enum):
105105

106106

107107
class Output(str, Enum):
108-
PRINTER = "printer"
108+
BROWSER = "browser"
109109
CONSOLE = "console"
110110
CONSOLE_INVERTED = "console-inverted"
111-
BROWSER = "browser"
112111
IMAGEMAGICK = "imagemagick"
112+
PNG = "png"
113+
PRINTER = "printer"

src/labelle/lib/outputs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ def output_bitmap(bitmap: Image.Image, output: Output):
2020
inverted = ImageOps.invert(bitmap.convert("RGB"))
2121
ImageOps.invert(inverted).save(fp)
2222
webbrowser.open(f"file://{fp.name}")
23+
if output == Output.PNG:
24+
bitmap.save("output.png")
25+
typer.echo("Saved output.png")
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,41 @@
1-
from labelle.lib.render_engines.barcode import BarcodeRenderEngine
1+
from labelle.lib.render_engines.barcode import BarcodeRenderEngine, BarcodeRenderError
22
from labelle.lib.render_engines.barcode_with_text import BarcodeWithTextRenderEngine
33
from labelle.lib.render_engines.empty import EmptyRenderEngine
4+
from labelle.lib.render_engines.exceptions import NoContentError
45
from labelle.lib.render_engines.horizontally_combined import (
56
HorizontallyCombinedRenderEngine,
67
)
78
from labelle.lib.render_engines.margins import MarginsRenderEngine
8-
from labelle.lib.render_engines.picture import NoPictureFilePath, PictureRenderEngine
9+
from labelle.lib.render_engines.picture import (
10+
PicturePathDoesNotExist,
11+
PictureRenderEngine,
12+
UnidentifiedImageFileError,
13+
)
914
from labelle.lib.render_engines.print_payload import PrintPayloadRenderEngine
1015
from labelle.lib.render_engines.print_preview import PrintPreviewRenderEngine
11-
from labelle.lib.render_engines.qr import NoContentError, QrRenderEngine
16+
from labelle.lib.render_engines.qr import QrRenderEngine, QrTooBigError
1217
from labelle.lib.render_engines.render_context import RenderContext
1318
from labelle.lib.render_engines.render_engine import RenderEngine
14-
from labelle.lib.render_engines.test_pattern import TestPatternRenderEngine
19+
from labelle.lib.render_engines.sample_pattern import SamplePatternRenderEngine
1520
from labelle.lib.render_engines.text import TextRenderEngine
1621

1722
__all__ = [
1823
"BarcodeRenderEngine",
24+
"BarcodeRenderError",
1925
"BarcodeWithTextRenderEngine",
2026
"EmptyRenderEngine",
2127
"HorizontallyCombinedRenderEngine",
2228
"MarginsRenderEngine",
2329
"NoContentError",
24-
"NoPictureFilePath",
30+
"PicturePathDoesNotExist",
2531
"PictureRenderEngine",
2632
"PrintPayloadRenderEngine",
2733
"PrintPreviewRenderEngine",
2834
"QrRenderEngine",
35+
"QrTooBigError",
2936
"RenderContext",
3037
"RenderEngine",
31-
"TestPatternRenderEngine",
38+
"SamplePatternRenderEngine",
3239
"TextRenderEngine",
40+
"UnidentifiedImageFileError",
3341
]

src/labelle/lib/render_engines/barcode.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from labelle.lib.render_engines.render_context import RenderContext
1010
from labelle.lib.render_engines.render_engine import RenderEngine, RenderEngineException
1111

12-
if DEFAULT_BARCODE_TYPE != BarcodeType.CODE128:
12+
if DEFAULT_BARCODE_TYPE != BarcodeType.CODE128: # pragma: no cover
1313
# Ensure that we fail fast if the default barcode type is adjusted
1414
# and the code below hasn't been updated.
1515
raise RuntimeError(
@@ -21,16 +21,18 @@
2121

2222

2323
class BarcodeRenderError(RenderEngineException):
24-
def __init__(self) -> None:
25-
msg = "Barcode render error"
24+
def __init__(self, exception: BaseException) -> None:
25+
msg = f"Barcode render error: {exception!r}"
2626
super().__init__(msg)
2727

2828

2929
class BarcodeRenderEngine(RenderEngine):
30-
def __init__(self, content: str, barcode_type: str | None) -> None:
30+
def __init__(
31+
self, content: str, barcode_type: BarcodeType = DEFAULT_BARCODE_TYPE
32+
) -> None:
3133
super().__init__()
3234
self.content = content
33-
self.barcode_type = barcode_type or DEFAULT_BARCODE_TYPE
35+
self.barcode_type = barcode_type
3436

3537
def render(self, context: RenderContext) -> Image.Image:
3638
if (
@@ -47,7 +49,7 @@ def render(self, context: RenderContext) -> Image.Image:
4749
)
4850
result = code_obj.render()
4951
except BaseException as e:
50-
raise BarcodeRenderError from e
52+
raise BarcodeRenderError(e) from e
5153
bitmap = convert_binary_string_to_barcode_image(
5254
line=result.line,
5355
quiet_zone=result.quiet_zone,

src/labelle/lib/render_engines/barcode_with_text.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44

55
from PIL import Image
66

7-
from labelle.lib.constants import Direction
7+
from labelle.lib.constants import DEFAULT_BARCODE_TYPE, BarcodeType, Direction
88
from labelle.lib.render_engines.barcode import BarcodeRenderEngine
99
from labelle.lib.render_engines.render_context import RenderContext
10-
from labelle.lib.render_engines.render_engine import (
11-
RenderEngine,
12-
RenderEngineException,
13-
)
10+
from labelle.lib.render_engines.render_engine import RenderEngine
1411
from labelle.lib.render_engines.text import TextRenderEngine
1512

1613

@@ -20,9 +17,9 @@ class BarcodeWithTextRenderEngine(RenderEngine):
2017
def __init__(
2118
self,
2219
content: str,
23-
barcode_type: str | None,
2420
font_file_name: Path | str,
25-
frame_width_px: int | None,
21+
barcode_type: BarcodeType = DEFAULT_BARCODE_TYPE,
22+
frame_width_px: int = 0,
2623
font_size_ratio: float = 0.9,
2724
align: Direction = Direction.CENTER,
2825
):
@@ -45,14 +42,12 @@ def render(self, render_context: RenderContext) -> Image.Image:
4542
# Define the x and y of the upper-left corner of the text
4643
# to be pasted onto the barcode
4744
text_offset_x = bitmap.height - text_bitmap.height - 1
48-
if self.align == "left":
45+
if self.align == Direction.LEFT:
4946
text_offset_y = 0
50-
elif self.align == "center":
47+
elif self.align == Direction.CENTER:
5148
text_offset_y = bitmap.width // 2 - text_bitmap.width // 2
52-
elif self.align == "right":
49+
elif self.align == Direction.RIGHT:
5350
text_offset_y = bitmap.width - text_bitmap.width
54-
else:
55-
raise RenderEngineException(f"Invalid align value: {self.align}")
5651

5752
bitmap.paste(text_bitmap, (text_offset_y, text_offset_x))
5853
return bitmap
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from labelle.lib.render_engines.render_engine import RenderEngineException
2+
3+
4+
class NoContentError(RenderEngineException):
5+
pass

src/labelle/lib/render_engines/picture.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,45 @@
33
import math
44
from pathlib import Path
55

6-
from PIL import Image, ImageOps
6+
from PIL import Image, ImageOps, UnidentifiedImageError
77

8+
from labelle.lib.render_engines import NoContentError
89
from labelle.lib.render_engines.render_context import RenderContext
910
from labelle.lib.render_engines.render_engine import (
1011
RenderEngine,
1112
RenderEngineException,
1213
)
1314

1415

15-
class NoPictureFilePath(RenderEngineException):
16+
class PicturePathDoesNotExist(RenderEngineException):
1617
pass
1718

1819

20+
class UnidentifiedImageFileError(RenderEngineException):
21+
def __init__(self, exception) -> None:
22+
super().__init__(exception)
23+
24+
1925
class PictureRenderEngine(RenderEngine):
2026
def __init__(self, picture_path: Path | str) -> None:
2127
super().__init__()
22-
if not picture_path:
23-
raise NoPictureFilePath()
28+
if picture_path == "":
29+
raise NoContentError()
2430
self.picture_path = Path(picture_path)
2531
if not self.picture_path.is_file():
26-
raise RenderEngineException(f"Picture path does not exist: {picture_path}")
32+
raise PicturePathDoesNotExist(
33+
f"Picture path does not exist: {picture_path}"
34+
)
2735

2836
def render(self, context: RenderContext) -> Image.Image:
2937
height_px = context.height_px
30-
with Image.open(self.picture_path) as img:
31-
if img.height > height_px:
32-
ratio = height_px / img.height
33-
img = img.resize((int(math.ceil(img.width * ratio)), height_px))
34-
35-
img = img.convert("L", palette=Image.AFFINE)
36-
return ImageOps.invert(img).convert("1")
38+
try:
39+
with Image.open(self.picture_path) as img:
40+
if img.height > height_px:
41+
ratio = height_px / img.height
42+
img = img.resize((int(math.ceil(img.width * ratio)), height_px))
43+
44+
img = img.convert("L", palette=Image.AFFINE)
45+
return ImageOps.invert(img).convert("1")
46+
except UnidentifiedImageError as e:
47+
raise UnidentifiedImageFileError(e) from e

0 commit comments

Comments
 (0)