Skip to content

Commit

Permalink
Merge pull request #32 from labelle-org/tshalev-add-tests
Browse files Browse the repository at this point in the history
Add unit tests for render engines
  • Loading branch information
maresb authored May 4, 2024
2 parents cbeb91f + 80bc57f commit f442494
Show file tree
Hide file tree
Showing 53 changed files with 424 additions and 61 deletions.
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ classifiers = [
dynamic = ["version"]
requires-python = ">=3.8,<4"

[project.optional-dependencies]
test = [
"pytest-cov",
"pytest-image-diff"
]

[project.urls]
Homepage = "https://github.com/labelle-org/labelle"
source = "https://github.com/labelle-org/labelle"
Expand Down Expand Up @@ -77,9 +83,12 @@ python =
3.12: py312
[testenv]
deps =
.[test]
commands =
pip check
pip freeze
pytest --cov=src/labelle --cov-report html:{work_dir}/{env_name}/htmlcov --cov-fail-under=45
labelle --version
labelle --help
python -c "import labelle.gui.gui; print('GUI import succeeded')"
Expand Down
33 changes: 21 additions & 12 deletions src/labelle/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
QrRenderEngine,
RenderContext,
RenderEngine,
TestPatternRenderEngine,
SamplePatternRenderEngine,
TextRenderEngine,
)

Expand Down Expand Up @@ -142,12 +142,12 @@ def default(
FontStyle, typer.Option(help="Set fonts style", rich_help_panel="Design")
] = DefaultFontStyle,
frame_width_px: Annotated[
Optional[int],
int,
typer.Option(
help="Draw frame of given width [px] around text",
rich_help_panel="Design",
),
] = None,
] = 0,
align: Annotated[
Direction, typer.Option(help="Align multiline text", rich_help_panel="Design")
] = Direction.LEFT,
Expand All @@ -159,7 +159,7 @@ def default(
rich_help_panel="Design",
),
] = Direction.LEFT,
test_pattern: Annotated[
sample_pattern: Annotated[
Optional[int],
typer.Option(help="Prints test pattern of a desired dot width"),
] = None,
Expand Down Expand Up @@ -205,13 +205,12 @@ def default(
typer.Option("--barcode", help="Barcode", rich_help_panel="Elements"),
] = None,
barcode_type: Annotated[
Optional[BarcodeType],
BarcodeType,
typer.Option(
help="Barcode type",
show_default=DEFAULT_BARCODE_TYPE.value,
rich_help_panel="Elements",
),
] = None,
] = DEFAULT_BARCODE_TYPE,
barcode_with_text_content: Annotated[
Optional[str],
typer.Option(
Expand Down Expand Up @@ -364,6 +363,13 @@ def default(
hidden=True,
),
] = None,
test_pattern: Annotated[
Optional[int],
typer.Option(
help="DEPRECATED",
hidden=True,
),
] = None,
) -> None:
if ctx.invoked_subcommand is not None:
return
Expand Down Expand Up @@ -420,6 +426,10 @@ def default(
raise typer.BadParameter("The -l flag is deprecated. Use --min-length instead.")
if old_justify is not None:
raise typer.BadParameter("The -j flag is deprecated. Use --justify instead.")
if test_pattern is not None:
raise typer.BadParameter(
"The --test-pattern flag is deprecated. Use --sample-pattern instead."
)

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

if barcode_type and not (barcode_content or barcode_with_text_content):
raise typer.BadParameter("Cannot specify barcode type without a barcode value")

if barcode_with_text_content and barcode_content:
raise typer.BadParameter(
"Cannot specify both barcode with text and regular barcode"
Expand All @@ -454,8 +461,8 @@ def default(

render_engines: list[RenderEngine] = []

if test_pattern:
render_engines.append(TestPatternRenderEngine(test_pattern))
if sample_pattern:
render_engines.append(SamplePatternRenderEngine(sample_pattern))

if qr_content:
render_engines.append(QrRenderEngine(qr_content))
Expand Down Expand Up @@ -511,6 +518,8 @@ def default(
device = None

dymo_labeler = DymoLabeler(tape_size_mm=tape_size_mm, device=device)
if not render_engines:
raise typer.BadParameter("No elements to print")
render_engine = HorizontallyCombinedRenderEngine(render_engines)
render_context = RenderContext(
background_color="white",
Expand Down
4 changes: 2 additions & 2 deletions src/labelle/gui/q_label_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
BarcodeWithTextRenderEngine,
EmptyRenderEngine,
NoContentError,
NoPictureFilePath,
PicturePathDoesNotExist,
PictureRenderEngine,
QrRenderEngine,
RenderContext,
Expand Down Expand Up @@ -445,5 +445,5 @@ def render_engine_impl(self):
"""
try:
return PictureRenderEngine(picture_path=self.label.text())
except NoPictureFilePath:
except PicturePathDoesNotExist:
return EmptyRenderEngine()
7 changes: 4 additions & 3 deletions src/labelle/lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

USE_QR = True
e_qrcode = None
except ImportError as error:
except ImportError as error: # pragma: no cover
e_qrcode = error
USE_QR = False
QRCode = None
Expand Down Expand Up @@ -105,8 +105,9 @@ class Direction(str, Enum):


class Output(str, Enum):
PRINTER = "printer"
BROWSER = "browser"
CONSOLE = "console"
CONSOLE_INVERTED = "console-inverted"
BROWSER = "browser"
IMAGEMAGICK = "imagemagick"
PNG = "png"
PRINTER = "printer"
3 changes: 3 additions & 0 deletions src/labelle/lib/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ def output_bitmap(bitmap: Image.Image, output: Output):
inverted = ImageOps.invert(bitmap.convert("RGB"))
ImageOps.invert(inverted).save(fp)
webbrowser.open(f"file://{fp.name}")
if output == Output.PNG:
bitmap.save("output.png")
typer.echo("Saved output.png")
20 changes: 14 additions & 6 deletions src/labelle/lib/render_engines/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
from labelle.lib.render_engines.barcode import BarcodeRenderEngine
from labelle.lib.render_engines.barcode import BarcodeRenderEngine, BarcodeRenderError
from labelle.lib.render_engines.barcode_with_text import BarcodeWithTextRenderEngine
from labelle.lib.render_engines.empty import EmptyRenderEngine
from labelle.lib.render_engines.exceptions import NoContentError
from labelle.lib.render_engines.horizontally_combined import (
HorizontallyCombinedRenderEngine,
)
from labelle.lib.render_engines.margins import MarginsRenderEngine
from labelle.lib.render_engines.picture import NoPictureFilePath, PictureRenderEngine
from labelle.lib.render_engines.picture import (
PicturePathDoesNotExist,
PictureRenderEngine,
UnidentifiedImageFileError,
)
from labelle.lib.render_engines.print_payload import PrintPayloadRenderEngine
from labelle.lib.render_engines.print_preview import PrintPreviewRenderEngine
from labelle.lib.render_engines.qr import NoContentError, QrRenderEngine
from labelle.lib.render_engines.qr import QrRenderEngine, QrTooBigError
from labelle.lib.render_engines.render_context import RenderContext
from labelle.lib.render_engines.render_engine import RenderEngine
from labelle.lib.render_engines.test_pattern import TestPatternRenderEngine
from labelle.lib.render_engines.sample_pattern import SamplePatternRenderEngine
from labelle.lib.render_engines.text import TextRenderEngine

__all__ = [
"BarcodeRenderEngine",
"BarcodeRenderError",
"BarcodeWithTextRenderEngine",
"EmptyRenderEngine",
"HorizontallyCombinedRenderEngine",
"MarginsRenderEngine",
"NoContentError",
"NoPictureFilePath",
"PicturePathDoesNotExist",
"PictureRenderEngine",
"PrintPayloadRenderEngine",
"PrintPreviewRenderEngine",
"QrRenderEngine",
"QrTooBigError",
"RenderContext",
"RenderEngine",
"TestPatternRenderEngine",
"SamplePatternRenderEngine",
"TextRenderEngine",
"UnidentifiedImageFileError",
]
14 changes: 8 additions & 6 deletions src/labelle/lib/render_engines/barcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from labelle.lib.render_engines.render_context import RenderContext
from labelle.lib.render_engines.render_engine import RenderEngine, RenderEngineException

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


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


class BarcodeRenderEngine(RenderEngine):
def __init__(self, content: str, barcode_type: str | None) -> None:
def __init__(
self, content: str, barcode_type: BarcodeType = DEFAULT_BARCODE_TYPE
) -> None:
super().__init__()
self.content = content
self.barcode_type = barcode_type or DEFAULT_BARCODE_TYPE
self.barcode_type = barcode_type

def render(self, context: RenderContext) -> Image.Image:
if (
Expand All @@ -47,7 +49,7 @@ def render(self, context: RenderContext) -> Image.Image:
)
result = code_obj.render()
except BaseException as e:
raise BarcodeRenderError from e
raise BarcodeRenderError(e) from e
bitmap = convert_binary_string_to_barcode_image(
line=result.line,
quiet_zone=result.quiet_zone,
Expand Down
19 changes: 7 additions & 12 deletions src/labelle/lib/render_engines/barcode_with_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@

from PIL import Image

from labelle.lib.constants import Direction
from labelle.lib.constants import DEFAULT_BARCODE_TYPE, BarcodeType, Direction
from labelle.lib.render_engines.barcode import BarcodeRenderEngine
from labelle.lib.render_engines.render_context import RenderContext
from labelle.lib.render_engines.render_engine import (
RenderEngine,
RenderEngineException,
)
from labelle.lib.render_engines.render_engine import RenderEngine
from labelle.lib.render_engines.text import TextRenderEngine


Expand All @@ -20,9 +17,9 @@ class BarcodeWithTextRenderEngine(RenderEngine):
def __init__(
self,
content: str,
barcode_type: str | None,
font_file_name: Path | str,
frame_width_px: int | None,
barcode_type: BarcodeType = DEFAULT_BARCODE_TYPE,
frame_width_px: int = 0,
font_size_ratio: float = 0.9,
align: Direction = Direction.CENTER,
):
Expand All @@ -45,14 +42,12 @@ def render(self, render_context: RenderContext) -> Image.Image:
# Define the x and y of the upper-left corner of the text
# to be pasted onto the barcode
text_offset_x = bitmap.height - text_bitmap.height - 1
if self.align == "left":
if self.align == Direction.LEFT:
text_offset_y = 0
elif self.align == "center":
elif self.align == Direction.CENTER:
text_offset_y = bitmap.width // 2 - text_bitmap.width // 2
elif self.align == "right":
elif self.align == Direction.RIGHT:
text_offset_y = bitmap.width - text_bitmap.width
else:
raise RenderEngineException(f"Invalid align value: {self.align}")

bitmap.paste(text_bitmap, (text_offset_y, text_offset_x))
return bitmap
5 changes: 5 additions & 0 deletions src/labelle/lib/render_engines/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from labelle.lib.render_engines.render_engine import RenderEngineException


class NoContentError(RenderEngineException):
pass
35 changes: 23 additions & 12 deletions src/labelle/lib/render_engines/picture.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,45 @@
import math
from pathlib import Path

from PIL import Image, ImageOps
from PIL import Image, ImageOps, UnidentifiedImageError

from labelle.lib.render_engines import NoContentError
from labelle.lib.render_engines.render_context import RenderContext
from labelle.lib.render_engines.render_engine import (
RenderEngine,
RenderEngineException,
)


class NoPictureFilePath(RenderEngineException):
class PicturePathDoesNotExist(RenderEngineException):
pass


class UnidentifiedImageFileError(RenderEngineException):
def __init__(self, exception) -> None:
super().__init__(exception)


class PictureRenderEngine(RenderEngine):
def __init__(self, picture_path: Path | str) -> None:
super().__init__()
if not picture_path:
raise NoPictureFilePath()
if picture_path == "":
raise NoContentError()
self.picture_path = Path(picture_path)
if not self.picture_path.is_file():
raise RenderEngineException(f"Picture path does not exist: {picture_path}")
raise PicturePathDoesNotExist(
f"Picture path does not exist: {picture_path}"
)

def render(self, context: RenderContext) -> Image.Image:
height_px = context.height_px
with Image.open(self.picture_path) as img:
if img.height > height_px:
ratio = height_px / img.height
img = img.resize((int(math.ceil(img.width * ratio)), height_px))

img = img.convert("L", palette=Image.AFFINE)
return ImageOps.invert(img).convert("1")
try:
with Image.open(self.picture_path) as img:
if img.height > height_px:
ratio = height_px / img.height
img = img.resize((int(math.ceil(img.width * ratio)), height_px))

img = img.convert("L", palette=Image.AFFINE)
return ImageOps.invert(img).convert("1")
except UnidentifiedImageError as e:
raise UnidentifiedImageFileError(e) from e
7 changes: 2 additions & 5 deletions src/labelle/lib/render_engines/qr.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from PIL import Image

from labelle.lib.constants import QRCode
from labelle.lib.render_engines import NoContentError
from labelle.lib.render_engines.render_context import RenderContext
from labelle.lib.render_engines.render_engine import (
RenderEngine,
Expand All @@ -15,14 +16,10 @@ def __init__(self) -> None:
super().__init__(msg)


class NoContentError(RenderEngineException):
pass


class QrRenderEngine(RenderEngine):
_content: str

def __init__(self, content):
def __init__(self, content: str):
super().__init__()
if not len(content):
raise NoContentError()
Expand Down
Loading

0 comments on commit f442494

Please sign in to comment.