diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 222bfe3..13e8089 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -48,6 +48,11 @@ jobs: tox env: ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json + - name: Test mypy with tox + run: | + tox -e mypy + env: + ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/src/escpos/capabilities.py b/src/escpos/capabilities.py index a8057cd..d8a4b58 100644 --- a/src/escpos/capabilities.py +++ b/src/escpos/capabilities.py @@ -8,7 +8,7 @@ import time from contextlib import ExitStack from os import environ, path from tempfile import mkdtemp -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Type import importlib_resources import six @@ -148,7 +148,7 @@ def get_profile(name: Optional[str] = None, **kwargs): CLASS_CACHE = {} -def get_profile_class(name: str): +def get_profile_class(name: str) -> Type[BaseProfile]: """Load a profile class. For the given profile name, load the data from the external @@ -174,7 +174,11 @@ def clean(s): return str(s) -class Profile(get_profile_class("default")): +# mute the mypy type issue with this dynamic base class function for now (: Any) +ProfileBaseClass: Any = get_profile_class("default") + + +class Profile(ProfileBaseClass): """Profile class for user usage. For users, who want to provide their own profile. diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 8899efe..07fd887 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -574,14 +574,14 @@ class Escpos(object): def _hw_barcode( self, - code, - bc, + code: str, + bc: str, height: int = 64, width: int = 3, pos: str = "BELOW", font: str = "A", align_ct: bool = True, - function_type=None, + function_type: Optional[str] = None, check: bool = True, ) -> None: """Print Barcode. @@ -662,8 +662,8 @@ class Escpos(object): :py:exc:`~escpos.exceptions.BarcodeCodeError` """ # If function_type is specified, otherwise use guessing. - ft_guess = [ft for ft in ["A", "B"] if bc in BARCODE_TYPES.get(ft)] - ft_guess = ft_guess or [None] + ft_guess = [ft for ft in ["A", "B"] if bc in BARCODE_TYPES.get(ft, {"": b""})] + ft_guess = ft_guess or [""] function_type = function_type or ft_guess[0] if not function_type or not BARCODE_TYPES.get(function_type.upper()): @@ -864,22 +864,117 @@ class Escpos(object): def set( self, - align="left", - font="a", - bold=False, - underline=0, - width=1, - height=1, - density=9, - invert=False, - smooth=False, - flip=False, - double_width=False, - double_height=False, - custom_size=False, - ): + align: Optional[str] = None, + font: Optional[str] = None, + bold: Optional[bool] = None, + underline: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + density: Optional[int] = None, + invert: Optional[bool] = None, + smooth: Optional[bool] = None, + flip: Optional[bool] = None, + normal_textsize: Optional[bool] = None, + double_width: Optional[bool] = None, + double_height: Optional[bool] = None, + custom_size: Optional[bool] = None, + ) -> None: """Set text properties by sending them to the printer. + If a value for a parameter is not supplied, nothing is sent + for this type of format. + + :param align: horizontal position for text, possible values are: + + * 'center' + * 'left' + * 'right' + + :param font: font given as an index, a name, or one of the + special values 'a' or 'b', referring to fonts 0 and 1. + :param bold: text in bold + :param underline: underline mode for text, decimal range 0-2 + :param normal_textsize: switch to normal text size if True + :param double_height: doubles the height of the text + :param double_width: doubles the width of the text + :param custom_size: uses custom size specified by width and height + parameters. Cannot be used with double_width or double_height. + :param width: text width multiplier when custom_size is used, decimal range 1-8 + :param height: text height multiplier when custom_size is used, decimal range 1-8 + :param density: print density, value from 0-8, if something else is supplied the density remains unchanged + :param invert: True enables white on black printing + :param smooth: True enables text smoothing. Effective on 4x4 size text and larger + :param flip: True enables upside-down printing + """ + if custom_size: + if ( + isinstance(width, int) + and isinstance(height, int) + and 1 <= width <= 8 + and 1 <= height <= 8 + ): + size_byte = TXT_STYLE["width"][width] + TXT_STYLE["height"][height] + self._raw(TXT_SIZE + six.int2byte(size_byte)) + else: + raise SetVariableError() + elif normal_textsize or double_height or double_width: + self._raw(TXT_NORMAL) + if double_width and double_height: + self._raw(TXT_STYLE["size"]["2x"]) + elif double_width: + self._raw(TXT_STYLE["size"]["2w"]) + elif double_height: + self._raw(TXT_STYLE["size"]["2h"]) + else: + self._raw(TXT_STYLE["size"]["normal"]) + else: + # no text size handling requested + pass + + if flip is not None: + self._raw(TXT_STYLE["flip"][flip]) + if smooth is not None: + self._raw(TXT_STYLE["smooth"][smooth]) + if bold is not None: + self._raw(TXT_STYLE["bold"][bold]) + if underline is not None: + self._raw(TXT_STYLE["underline"][underline]) + if font is not None: + self._raw(SET_FONT(six.int2byte(self.profile.get_font(font)))) + if align is not None: + self._raw(TXT_STYLE["align"][align]) + + if density is not None and density != 9: + self._raw(TXT_STYLE["density"][density]) + + if invert is not None: + self._raw(TXT_STYLE["invert"][invert]) + + def set_with_default( + self, + align: Optional[str] = "left", + font: Optional[str] = "a", + bold: Optional[bool] = False, + underline: Optional[int] = 0, + width: Optional[int] = 1, + height: Optional[int] = 1, + density: Optional[int] = 9, + invert: Optional[bool] = False, + smooth: Optional[bool] = False, + flip: Optional[bool] = False, + double_width: Optional[bool] = False, + double_height: Optional[bool] = False, + custom_size: Optional[bool] = False, + ) -> None: + """Set default text properties by sending them to the printer. + + This function has the behavior of the `set()`-method from before + version 3. + If a parameter to this method is not supplied, a default value + will be sent. + Otherwise this method forwards the values to the + :py:meth:`escpos.Escpos.set()`. + :param align: horizontal position for text, possible values are: * 'center' @@ -902,54 +997,24 @@ class Escpos(object): :param invert: True enables white on black printing, *default*: False :param smooth: True enables text smoothing. Effective on 4x4 size text and larger, *default*: False :param flip: True enables upside-down printing, *default*: False - - :type font: str - :type invert: bool - :type bold: bool - :type underline: bool - :type smooth: bool - :type flip: bool - :type custom_size: bool - :type double_width: bool - :type double_height: bool - :type align: str - :type width: int - :type height: int - :type density: int """ - if custom_size: - if ( - isinstance(width, int) - and isinstance(height, int) - and 1 <= width <= 8 - and 1 <= height <= 8 - ): - size_byte = TXT_STYLE["width"][width] + TXT_STYLE["height"][height] - self._raw(TXT_SIZE + six.int2byte(size_byte)) - else: - raise SetVariableError() - else: - self._raw(TXT_NORMAL) - if double_width and double_height: - self._raw(TXT_STYLE["size"]["2x"]) - elif double_width: - self._raw(TXT_STYLE["size"]["2w"]) - elif double_height: - self._raw(TXT_STYLE["size"]["2h"]) - else: - self._raw(TXT_STYLE["size"]["normal"]) - - self._raw(TXT_STYLE["flip"][flip]) - self._raw(TXT_STYLE["smooth"][smooth]) - self._raw(TXT_STYLE["bold"][bold]) - self._raw(TXT_STYLE["underline"][underline]) - self._raw(SET_FONT(six.int2byte(self.profile.get_font(font)))) - self._raw(TXT_STYLE["align"][align]) - - if density != 9: - self._raw(TXT_STYLE["density"][density]) - - self._raw(TXT_STYLE["invert"][invert]) + normal_textsize = not custom_size and not double_width and not double_height + self.set( + align=align, + font=font, + bold=bold, + underline=underline, + width=width, + height=height, + density=density, + invert=invert, + smooth=smooth, + flip=flip, + normal_textsize=normal_textsize, + double_width=double_width, + double_height=double_height, + custom_size=custom_size, + ) def line_spacing(self, spacing: Optional[int] = None, divisor: int = 180) -> None: """Set line character spacing. diff --git a/test/test_function_set.py b/test/test_function_set.py index 3eff375..90458f3 100644 --- a/test/test_function_set.py +++ b/test/test_function_set.py @@ -1,14 +1,15 @@ +import pytest import six import escpos.printer as printer from escpos.constants import SET_FONT, TXT_NORMAL, TXT_SIZE, TXT_STYLE - -# Default test, please copy and paste this block to test set method calls +from escpos.exceptions import SetVariableError -def test_default_values(): +def test_default_values_with_default(): + """Default test, please copy and paste this block to test set method calls""" instance = printer.Dummy() - instance.set() + instance.set_with_default() expected_sequence = ( TXT_NORMAL, @@ -25,12 +26,20 @@ def test_default_values(): assert instance.output == b"".join(expected_sequence) +def test_default_values(): + """Default test""" + instance = printer.Dummy() + instance.set() + + assert instance.output == b"" + + # Size tests def test_set_size_2h(): instance = printer.Dummy() - instance.set(double_height=True) + instance.set_with_default(double_height=True) expected_sequence = ( TXT_NORMAL, @@ -47,9 +56,21 @@ def test_set_size_2h(): assert instance.output == b"".join(expected_sequence) +def test_set_size_2h_no_default(): + instance = printer.Dummy() + instance.set(double_height=True) + + expected_sequence = ( + TXT_NORMAL, + TXT_STYLE["size"]["2h"], # Double height text size + ) + + assert instance.output == b"".join(expected_sequence) + + def test_set_size_2w(): instance = printer.Dummy() - instance.set(double_width=True) + instance.set_with_default(double_width=True) expected_sequence = ( TXT_NORMAL, @@ -66,9 +87,21 @@ def test_set_size_2w(): assert instance.output == b"".join(expected_sequence) +def test_set_size_2w_no_default(): + instance = printer.Dummy() + instance.set(double_width=True) + + expected_sequence = ( + TXT_NORMAL, + TXT_STYLE["size"]["2w"], # Double width text size + ) + + assert instance.output == b"".join(expected_sequence) + + def test_set_size_2x(): instance = printer.Dummy() - instance.set(double_height=True, double_width=True) + instance.set_with_default(double_height=True, double_width=True) expected_sequence = ( TXT_NORMAL, @@ -85,9 +118,21 @@ def test_set_size_2x(): assert instance.output == b"".join(expected_sequence) +def test_set_size_2x_no_default(): + instance = printer.Dummy() + instance.set(double_width=True, double_height=True) + + expected_sequence = ( + TXT_NORMAL, + TXT_STYLE["size"]["2x"], # Quad area text size + ) + + assert instance.output == b"".join(expected_sequence) + + def test_set_size_custom(): instance = printer.Dummy() - instance.set(custom_size=True, width=8, height=7) + instance.set_with_default(custom_size=True, width=8, height=7) expected_sequence = ( TXT_SIZE, # Custom text size, no normal reset @@ -104,12 +149,34 @@ def test_set_size_custom(): assert instance.output == b"".join(expected_sequence) +@pytest.mark.parametrize("width", [1, 8]) +@pytest.mark.parametrize("height", [1, 8]) +def test_set_size_custom_no_default(width, height): + instance = printer.Dummy() + instance.set(custom_size=True, width=width, height=height) + + expected_sequence = ( + TXT_SIZE, # Custom text size, no normal reset + six.int2byte(TXT_STYLE["width"][width] + TXT_STYLE["height"][height]), + ) + + assert instance.output == b"".join(expected_sequence) + + +@pytest.mark.parametrize("width", [None, 0, 9, 10, 4444]) +@pytest.mark.parametrize("height", [None, 0, 9, 10, 4444]) +def test_set_size_custom_invalid_input(width, height): + instance = printer.Dummy() + with pytest.raises(SetVariableError): + instance.set(custom_size=True, width=width, height=height) + + # Flip def test_set_flip(): instance = printer.Dummy() - instance.set(flip=True) + instance.set_with_default(flip=True) expected_sequence = ( TXT_NORMAL, @@ -126,12 +193,21 @@ def test_set_flip(): assert instance.output == b"".join(expected_sequence) +def test_set_flip_no_default(): + instance = printer.Dummy() + instance.set(flip=True) + + expected_sequence = (TXT_STYLE["flip"][True],) # Flip ON + + assert instance.output == b"".join(expected_sequence) + + # Smooth def test_smooth(): instance = printer.Dummy() - instance.set(smooth=True) + instance.set_with_default(smooth=True) expected_sequence = ( TXT_NORMAL, @@ -153,7 +229,7 @@ def test_smooth(): def test_set_bold(): instance = printer.Dummy() - instance.set(bold=True) + instance.set_with_default(bold=True) expected_sequence = ( TXT_NORMAL, @@ -172,7 +248,7 @@ def test_set_bold(): def test_set_underline(): instance = printer.Dummy() - instance.set(underline=1) + instance.set_with_default(underline=1) expected_sequence = ( TXT_NORMAL, @@ -191,7 +267,7 @@ def test_set_underline(): def test_set_underline2(): instance = printer.Dummy() - instance.set(underline=2) + instance.set_with_default(underline=2) expected_sequence = ( TXT_NORMAL, @@ -213,7 +289,7 @@ def test_set_underline2(): def test_align_center(): instance = printer.Dummy() - instance.set(align="center") + instance.set_with_default(align="center") expected_sequence = ( TXT_NORMAL, @@ -232,7 +308,7 @@ def test_align_center(): def test_align_right(): instance = printer.Dummy() - instance.set(align="right") + instance.set_with_default(align="right") expected_sequence = ( TXT_NORMAL, @@ -255,7 +331,7 @@ def test_align_right(): def test_densities(): for density in range(8): instance = printer.Dummy() - instance.set(density=density) + instance.set_with_default(density=density) expected_sequence = ( TXT_NORMAL, @@ -278,7 +354,7 @@ def test_densities(): def test_invert(): instance = printer.Dummy() - instance.set(invert=True) + instance.set_with_default(invert=True) expected_sequence = ( TXT_NORMAL,