1
0
mirror of https://github.com/python-escpos/python-escpos synced 2025-09-13 09:09:58 +00:00

17 Commits
v3.0 ... v3.1

Author SHA1 Message Date
Patrick Kanzler
0d22896689 Prepare release 3.1 (#614)
* update authors
* update changelog
* update spelling
* update black
2023-12-17 22:53:19 +01:00
Patrick Kanzler
b66dafc90e improve type annotations in tests (#613) 2023-12-17 01:15:59 +01:00
Patrick Kanzler
0c824cf295 More mypy (#612)
* remove type comment where type is annotated
* move function tests
* remove six from tests
* add none annotations
* add more types
* change mock (so that mypy understands it)
2023-12-16 23:09:20 +01:00
Alexandre Detiste
66a2e78e16 start removal of six and improve type annotation (#607)
* fix unfinished Python2 -> 3 translation
* remove some six usage
* annotate
* fix regression in Six removal
* mypy: self.enf is always defined
* fix return type of cups.py
* Usb idVendor/idProduct are integers
* self.default_args is always defined
* tweak usb_args, PEP589 is better
* lp.py: reassure mypy
* correctly cast call to CashDrawerError()
* update CUPS test
* add missing close() method in metaclass
* document a bug in typeshed
* query_status() returns bytes as seen in constants.py
* remove more SIX usage
* test examples too
* remove type comment where type is annotated
* adapt behavior of cups printer to match other implementations

---------

Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2023-12-16 22:02:24 +01:00
dependabot[bot]
06bdb56937 Bump github/codeql-action from 2 to 3 (#610)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 11:53:52 +01:00
Patrick Kanzler
91cbc264fc Refactor to using f-strings (#608)
* update authors

* refactor to using f-strings
2023-12-11 00:34:29 +01:00
tuxmaster
dcc71ce47d argparse is an part of the python core (#606) 2023-12-09 21:26:19 +01:00
dependabot[bot]
d2b213dcdc Bump actions/setup-python from 4.8.0 to 5.0.0 (#605)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.8.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 23:52:37 +01:00
dependabot[bot]
c91eec544e Bump actions/setup-python from 4.7.1 to 4.8.0 (#604)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.7.1 to 4.8.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.7.1...v4.8.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 00:45:25 +01:00
Patrick Kanzler
a9e49c909d Update CHANGELOG.rst (#603) 2023-12-06 00:31:12 +01:00
Patrick Kanzler
815f247df1 Improve test coverage and fix issue in path loading (#602)
* reactivate skipped tests

* remove unused internal interfaces

* enable pytest in VS Code

* simplify and fix path logic in loader

* increase test coverage

* test missing config

* check for wrong printer names

* Skipped appdir test
2023-12-06 00:25:36 +01:00
Patrick Kanzler
5914c7c560 allow qr to set all arguments to image (#600)
* allow qr to set all arguments to image

* increase coverage
2023-12-04 01:15:19 +01:00
Patrick Kanzler
86a715cf02 update capabilities data (#601) 2023-12-04 01:11:05 +01:00
Patrick Kanzler
ac23c083b6 update to newest python-barcode (#595)
* update to newest python-barcode
2023-12-03 23:57:34 +01:00
Patrick Kanzler
9a1699ab94 add type annotations for escpos image handler (#599) 2023-12-03 23:36:35 +01:00
dependabot[bot]
8274833255 Bump sphinx-rtd-theme from 1.3.0 to 2.0.0 (#596)
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.3.0 to 2.0.0.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.3.0...2.0.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-30 08:38:40 +01:00
Patrick Kanzler
33d17615a0 prepare next release development cycle (#594) 2023-11-17 01:12:31 +01:00
60 changed files with 643 additions and 448 deletions

View File

@@ -9,5 +9,5 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: psf/black@stable - uses: psf/black@stable
with: with:
version: "23.3.0" version: "23.12.0"

View File

@@ -38,7 +38,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -49,7 +49,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -63,4 +63,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@@ -19,7 +19,7 @@ jobs:
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@@ -22,7 +22,7 @@ jobs:
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.7.1 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@@ -14,4 +14,6 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": true
}, },
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
} }

View File

@@ -2,6 +2,7 @@ Ahmed Tahri
akeonly akeonly
Alejandro Hernández Alejandro Hernández
Alexander Bougakov Alexander Bougakov
Alexandre Detiste
Alex Debiasio Alex Debiasio
Alfredo Orozco Alfredo Orozco
Asuki Kono Asuki Kono
@@ -46,6 +47,7 @@ Sergio Pulgarin
Stephan Sokolow Stephan Sokolow
Thijs Triemstra Thijs Triemstra
Thomas van den Berg Thomas van den Berg
tuxmaster
vendryan vendryan
Yaisel Hurtado Yaisel Hurtado
ysuolmai ysuolmai

View File

@@ -1,6 +1,30 @@
Changelog Changelog
========= =========
2023-12-17 - Version 3.1 - "Rubric Of Ruin"
-------------------------------------------
This is the minor release of the new version 3.1.
It adds a modification of the API of the qr-method,
hence the minor release.
changes
^^^^^^^
- extend API of the qr-method to allow passing image
parameters in non-native mode
- use version 0.15 and upwards of python-barcode
- fix an issue in the config provider that prevented
config files to be found when only a path was supplied
- Improve type annotations, usage of six and other
packaging relevant parts.
contributors
^^^^^^^^^^^^
- Patrick Kanzler
- Alexandre Detiste
- tuxmaster
- belono
2023-11-17 - Version 3.0 - "Quietly Confident" 2023-11-17 - Version 3.0 - "Quietly Confident"
---------------------------------------------- ----------------------------------------------
This is the major release of the new version 3.0. This is the major release of the new version 3.0.

View File

@@ -2,12 +2,12 @@ pyusb
Pillow>=2.0 Pillow>=2.0
qrcode>=4.0 qrcode>=4.0
pyserial pyserial
sphinx-rtd-theme==1.3.0 sphinx-rtd-theme==2.0.0
setuptools setuptools
setuptools-scm setuptools-scm
docutils>=0.12 docutils>=0.12
sphinxcontrib-spelling>=7.2.0 sphinxcontrib-spelling>=7.2.0
python-barcode>=0.11.0,<1 python-barcode>=0.15.0,<1
importlib-metadata importlib-metadata
importlib_resources importlib_resources
sphinxcontrib.datatemplates sphinxcontrib.datatemplates

View File

@@ -68,6 +68,8 @@ Qian
Lehtonen Lehtonen
Kanzler Kanzler
Rotondo Rotondo
Alexandre
Detiste
barcode barcode
barcodes barcodes

View File

@@ -3,8 +3,6 @@
import sys import sys
import six
from escpos import printer from escpos import printer
from escpos.constants import ( from escpos.constants import (
CODEPAGE_CHANGE, CODEPAGE_CHANGE,
@@ -38,7 +36,7 @@ def print_codepage(printer, codepage):
"""Print a codepage.""" """Print a codepage."""
if codepage.isdigit(): if codepage.isdigit():
codepage = int(codepage) codepage = int(codepage)
printer._raw(CODEPAGE_CHANGE + six.int2byte(codepage)) printer._raw(CODEPAGE_CHANGE + bytes((codepage,)))
printer._raw("after") printer._raw("after")
else: else:
printer.charcode(codepage) printer.charcode(codepage)
@@ -47,18 +45,20 @@ def print_codepage(printer, codepage):
# Table header # Table header
printer.set(font="b") printer.set(font="b")
printer._raw(" {}\n".format(sep.join(map(lambda s: hex(s)[2:], range(0, 16))))) printer._raw(f" {sep.join(map(lambda s: hex(s)[2:], range(0, 16)))}\n")
printer.set() printer.set()
# The table # The table
for x in range(0, 16): for x in range(0, 16):
# First column # First column
printer.set(font="b") printer.set(font="b")
printer._raw("{} ".format(hex(x)[2:])) printer._raw(f"{hex(x)[2:]} ")
printer.set() printer.set()
for y in range(0, 16): for y in range(0, 16):
byte = six.int2byte(x * 16 + y) byte = bytes(
(x * 16 + y),
)
if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT): if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT):
byte = " " byte = " "

View File

@@ -18,8 +18,8 @@ import calendar
import json import json
import os import os
import time import time
import urllib
from datetime import datetime from datetime import datetime
from urllib.request import urlopen
from escpos.printer import Usb from escpos.printer import Usb
@@ -93,7 +93,7 @@ url = (
+ LONG + LONG
+ "?exclude=[alerts,minutely,hourly,flags]&units=si" + "?exclude=[alerts,minutely,hourly,flags]&units=si"
) # change last bit to 'us' for Fahrenheit ) # change last bit to 'us' for Fahrenheit
response = urllib.urlopen(url) response = urlopen(url)
data = json.loads(response.read()) data = json.loads(response.read())
printer.print_and_feed(n=1) printer.print_and_feed(n=1)

View File

@@ -3,6 +3,7 @@ name = python-escpos
url = https://github.com/python-escpos/python-escpos url = https://github.com/python-escpos/python-escpos
description = Python library to manipulate ESC/POS Printers description = Python library to manipulate ESC/POS Printers
long_description = file: README.rst long_description = file: README.rst
long_description_content_type = text/x-rst
license = MIT license = MIT
license_file = LICENSE license_file = LICENSE
author = python-escpos developers author = python-escpos developers
@@ -38,12 +39,11 @@ include_package_data = true
install_requires = install_requires =
Pillow>=2.0 Pillow>=2.0
qrcode>=4.0 qrcode>=4.0
python-barcode>=0.9.1,<1 python-barcode>=0.15.0,<1
setuptools setuptools
six six
appdirs appdirs
PyYAML PyYAML
argparse
argcomplete argcomplete
importlib_resources importlib_resources
setup_requires = setuptools_scm setup_requires = setuptools_scm

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""python-escpos enables you to manipulate escpos-printers.""" """python-escpos enables you to manipulate escpos-printers."""
__all__ = ["constants", "escpos", "exceptions", "printer"] __all__ = ["constants", "escpos", "exceptions", "printer", "__version__"]
try: try:
from .version import version as __version__ # noqa from .version import version as __version__ # noqa

View File

@@ -11,16 +11,13 @@ from tempfile import mkdtemp
from typing import Any, Dict, Optional, Type from typing import Any, Dict, Optional, Type
import importlib_resources import importlib_resources
import six
import yaml import yaml
logging.basicConfig() logging.basicConfig()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", mkdtemp()) pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", mkdtemp())
pickle_path = path.join( pickle_path = path.join(pickle_dir, f"{platform.python_version()}.capabilities.pickle")
pickle_dir, "{v}.capabilities.pickle".format(v=platform.python_version())
)
# get a temporary file from importlib_resources if no file is specified in env # get a temporary file from importlib_resources if no file is specified in env
file_manager = ExitStack() file_manager = ExitStack()
atexit.register(file_manager.close) atexit.register(file_manager.close)
@@ -94,7 +91,7 @@ class NotSupported(Exception):
BARCODE_B = "barcodeB" BARCODE_B = "barcodeB"
class BaseProfile(object): class BaseProfile:
"""This represents a printer profile. """This represents a printer profile.
A printer profile knows about the number of columns, supported A printer profile knows about the number of columns, supported
@@ -113,22 +110,22 @@ class BaseProfile(object):
Makes sure that the requested `font` is valid. Makes sure that the requested `font` is valid.
""" """
font = {"a": 0, "b": 1}.get(font, font) font = {"a": 0, "b": 1}.get(font, font)
if not six.text_type(font) in self.fonts: if not str(font) in self.fonts:
raise NotSupported( raise NotSupported(f'"{font}" is not a valid font in the current profile')
'"{}" is not a valid font in the current profile'.format(font)
)
return font return font
def get_columns(self, font): def get_columns(self, font) -> int:
"""Return the number of columns for the given font.""" """Return the number of columns for the given font."""
font = self.get_font(font) font = self.get_font(font)
return self.fonts[six.text_type(font)]["columns"] columns = self.fonts[str(font)]["columns"]
assert type(columns) is int
return columns
def supports(self, feature): def supports(self, feature) -> bool:
"""Return true/false for the given feature.""" """Return true/false for the given feature."""
return self.features.get(feature) return self.features.get(feature)
def get_code_pages(self): def get_code_pages(self) -> Dict[str, int]:
"""Return the support code pages as a ``{name: index}`` dict.""" """Return the support code pages as a ``{name: index}`` dict."""
return {v: k for k, v in self.codePages.items()} return {v: k for k, v in self.codePages.items()}
@@ -158,14 +155,14 @@ def get_profile_class(name: str) -> Type[BaseProfile]:
profiles: Dict[str, Any] = CAPABILITIES["profiles"] profiles: Dict[str, Any] = CAPABILITIES["profiles"]
profile_data = profiles[name] profile_data = profiles[name]
profile_name = clean(name) profile_name = clean(name)
class_name = "{}{}Profile".format(profile_name[0].upper(), profile_name[1:]) class_name = f"{profile_name[0].upper()}{profile_name[1:]}Profile"
new_class = type(class_name, (BaseProfile,), {"profile_data": profile_data}) new_class = type(class_name, (BaseProfile,), {"profile_data": profile_data})
CLASS_CACHE[name] = new_class CLASS_CACHE[name] = new_class
return CLASS_CACHE[name] return CLASS_CACHE[name]
def clean(s): def clean(s: str) -> str:
"""Clean profile name.""" """Clean profile name."""
# Remove invalid characters # Remove invalid characters
s = re.sub("[^0-9a-zA-Z_]", "", s) s = re.sub("[^0-9a-zA-Z_]", "", s)
@@ -184,14 +181,14 @@ class Profile(ProfileBaseClass):
For users, who want to provide their own profile. For users, who want to provide their own profile.
""" """
def __init__(self, columns=None, features=None): def __init__(self, columns: Optional[int] = None, features=None) -> None:
"""Initialize profile.""" """Initialize profile."""
super(Profile, self).__init__() super(Profile, self).__init__()
self.columns = columns self.columns = columns
self.features = features or {} self.features = features or {}
def get_columns(self, font): def get_columns(self, font) -> int:
"""Get column count of printer.""" """Get column count of printer."""
if self.columns is not None: if self.columns is not None:
return self.columns return self.columns

View File

@@ -20,15 +20,13 @@ except ImportError:
pass # noqa pass # noqa
import sys import sys
import six from . import config, escpos
from . import config
from . import printer as escpos_printer_module from . import printer as escpos_printer_module
from . import version from . import version
# Must be defined before it's used in DEMO_FUNCTIONS # Must be defined before it's used in DEMO_FUNCTIONS
def str_to_bool(string): def str_to_bool(string: str) -> bool:
"""Convert string to bool. """Convert string to bool.
Used as a type in argparse so that we get back a proper Used as a type in argparse so that we get back a proper
@@ -563,7 +561,7 @@ def generate_parser() -> argparse.ArgumentParser:
return parser return parser
def main(): def main() -> None:
"""Handle main entry point of CLI script. """Handle main entry point of CLI script.
Handles loading of configuration and creating and processing of command Handles loading of configuration and creating and processing of command
@@ -580,9 +578,7 @@ def main():
if not args_dict: if not args_dict:
parser.print_help() parser.print_help()
sys.exit() sys.exit()
command_arguments = dict( command_arguments = dict([k, v] for k, v in args_dict.items() if v is not None)
[k, v] for k, v in six.iteritems(args_dict) if v is not None
)
# If version should be printed, do this, then exit # If version should be printed, do this, then exit
print_version = command_arguments.pop("version", None) print_version = command_arguments.pop("version", None)
@@ -621,7 +617,7 @@ def main():
globals()[target_command](**command_arguments) globals()[target_command](**command_arguments)
def demo(printer, **kwargs): def demo(printer: escpos.Escpos, **kwargs) -> None:
"""Print demos. """Print demos.
Called when CLI is passed `demo`. This function Called when CLI is passed `demo`. This function

View File

@@ -12,10 +12,6 @@ class CodePageManager:
"""Initialize codepage manager.""" """Initialize codepage manager."""
self.data = data self.data = data
def get_all(self):
"""Get list of all codepages."""
return self.data.values()
@staticmethod @staticmethod
def get_encoding_name(encoding): def get_encoding_name(encoding):
"""Get encoding name. """Get encoding name.

View File

@@ -3,6 +3,7 @@
This module contains the implementations of abstract base class :py:class:`Config`. This module contains the implementations of abstract base class :py:class:`Config`.
""" """
import os import os
import pathlib
import appdirs import appdirs
import yaml import yaml
@@ -10,7 +11,7 @@ import yaml
from . import exceptions, printer from . import exceptions, printer
class Config(object): class Config:
"""Configuration handler class. """Configuration handler class.
This class loads configuration from a default or specified directory. It This class loads configuration from a default or specified directory. It
@@ -20,7 +21,7 @@ class Config(object):
_app_name = "python-escpos" _app_name = "python-escpos"
_config_file = "config.yaml" _config_file = "config.yaml"
def __init__(self): def __init__(self) -> None:
"""Initialize configuration. """Initialize configuration.
Remember to add anything that needs to be reset between configurations Remember to add anything that needs to be reset between configurations
@@ -32,7 +33,7 @@ class Config(object):
self._printer_name = None self._printer_name = None
self._printer_config = None self._printer_config = None
def _reset_config(self): def _reset_config(self) -> None:
"""Clear the loaded configuration. """Clear the loaded configuration.
If we are loading a changed config, we don't want to have leftover If we are loading a changed config, we don't want to have leftover
@@ -56,23 +57,19 @@ class Config(object):
config_path = os.path.join( config_path = os.path.join(
appdirs.user_config_dir(self._app_name), self._config_file appdirs.user_config_dir(self._app_name), self._config_file
) )
if isinstance(config_path, pathlib.Path):
# store string if posixpath
config_path = config_path.as_posix()
if not os.path.isfile(config_path):
# supplied path is not a file --> assume default file
config_path = os.path.join(config_path, self._config_file)
try: try:
# First check if it's file like. If it is, pyyaml can load it. with open(config_path, "rb") as config_file:
# I'm checking type instead of catching exceptions to keep the config = yaml.safe_load(config_file)
# exception handling simple
if hasattr(config_path, "read"):
config = yaml.safe_load(config_path)
else:
# If it isn't, it's a path. We have to open it first, otherwise
# pyyaml will try to read it as yaml
with open(config_path, "rb") as config_file:
config = yaml.safe_load(config_file)
except EnvironmentError: except EnvironmentError:
raise exceptions.ConfigNotFoundError( raise exceptions.ConfigNotFoundError(
"Couldn't read config at {config_path}".format( f"Couldn't read config at {config_path}"
config_path=str(config_path),
)
) )
except yaml.YAMLError: except yaml.YAMLError:
raise exceptions.ConfigSyntaxError("Error parsing YAML") raise exceptions.ConfigSyntaxError("Error parsing YAML")
@@ -83,9 +80,7 @@ class Config(object):
if not self._printer_name or not hasattr(printer, self._printer_name): if not self._printer_name or not hasattr(printer, self._printer_name):
raise exceptions.ConfigSyntaxError( raise exceptions.ConfigSyntaxError(
'Printer type "{printer_name}" is invalid'.format( f'Printer type "{self._printer_name}" is invalid'
printer_name=self._printer_name,
)
) )
self._has_loaded = True self._has_loaded = True

View File

@@ -46,9 +46,7 @@ HW_RESET: bytes = ESC + b"\x3f\x0a\x00" # Reset printer hardware
# (TODO: Where is this specified?) # (TODO: Where is this specified?)
# Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>) # Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>)
_CASH_DRAWER = ( _CASH_DRAWER = lambda m, t1="", t2="": ESC + b"p" + m + bytes((t1, t2))
lambda m, t1="", t2="": ESC + b"p" + m + six.int2byte(t1) + six.int2byte(t2)
)
#: decimal cash drawer kick sequence #: decimal cash drawer kick sequence
CD_KICK_DEC_SEQUENCE = ( CD_KICK_DEC_SEQUENCE = (

View File

@@ -9,12 +9,14 @@ This module contains the abstract base class :py:class:`Escpos`.
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT :license: MIT
""" """
from __future__ import annotations
import textwrap import textwrap
import warnings
from abc import ABCMeta, abstractmethod # abstract base class support from abc import ABCMeta, abstractmethod # abstract base class support
from re import match as re_match from re import match as re_match
from typing import List, Literal, Optional, Union from types import TracebackType
from typing import Any, Literal, Optional, Union
import barcode import barcode
import qrcode import qrcode
@@ -106,8 +108,7 @@ SW_BARCODE_NAMES = {
} }
@six.add_metaclass(ABCMeta) class Escpos(object, metaclass=ABCMeta):
class Escpos(object):
"""ESC/POS Printer object. """ESC/POS Printer object.
This class is the abstract base class for an Esc/Pos-printer. The printer implementations are children of this This class is the abstract base class for an Esc/Pos-printer. The printer implementations are children of this
@@ -153,6 +154,10 @@ class Escpos(object):
"""Open a printer device/connection.""" """Open a printer device/connection."""
pass pass
def close(self):
"""Close a printer device/connection."""
pass
@abstractmethod @abstractmethod
def _raw(self, msg: bytes) -> None: def _raw(self, msg: bytes) -> None:
"""Send raw data to the printer. """Send raw data to the printer.
@@ -163,7 +168,7 @@ class Escpos(object):
""" """
pass pass
def _read(self): def _read(self) -> bytes:
"""Read from printer. """Read from printer.
Returns a NotImplementedError if the instance of the class doesn't override this method. Returns a NotImplementedError if the instance of the class doesn't override this method.
@@ -174,11 +179,11 @@ class Escpos(object):
def image( def image(
self, self,
img_source, img_source,
high_density_vertical=True, high_density_vertical: bool = True,
high_density_horizontal=True, high_density_horizontal: bool = True,
impl="bitImageRaster", impl: str = "bitImageRaster",
fragment_height=960, fragment_height: int = 960,
center=False, center: bool = False,
) -> None: ) -> None:
"""Print an image. """Print an image.
@@ -218,7 +223,7 @@ class Escpos(object):
max_width = int(self.profile.profile_data["media"]["width"]["pixels"]) max_width = int(self.profile.profile_data["media"]["width"]["pixels"])
if im.width > max_width: if im.width > max_width:
raise ImageWidthError("{} > {}".format(im.width, max_width)) raise ImageWidthError(f"{im.width} > {max_width}")
if center: if center:
im.center(max_width) im.center(max_width)
@@ -249,7 +254,7 @@ class Escpos(object):
header = ( header = (
GS GS
+ b"v0" + b"v0"
+ six.int2byte(density_byte) + bytes((density_byte,))
+ self._int_low_high(im.width_bytes, 2) + self._int_low_high(im.width_bytes, 2)
+ self._int_low_high(im.height, 2) + self._int_low_high(im.height, 2)
) )
@@ -262,8 +267,8 @@ class Escpos(object):
) )
tone = b"0" tone = b"0"
colors = b"1" colors = b"1"
ym = six.int2byte(1 if high_density_vertical else 2) ym = b"\x01" if high_density_vertical else b"\x02"
xm = six.int2byte(1 if high_density_horizontal else 2) xm = b"\x01" if high_density_horizontal else b"\x02"
header = tone + xm + ym + colors + img_header header = tone + xm + ym + colors + img_header
raster_data = im.to_raster_format() raster_data = im.to_raster_format()
self._image_send_graphics_data(b"0", b"p", header + raster_data) self._image_send_graphics_data(b"0", b"p", header + raster_data)
@@ -304,7 +309,8 @@ class Escpos(object):
model=QR_MODEL_2, model=QR_MODEL_2,
native=False, native=False,
center=False, center=False,
impl="bitImageRaster", impl=None,
image_arguments: Optional[dict] = None,
) -> None: ) -> None:
"""Print QR Code for the provided string. """Print QR Code for the provided string.
@@ -319,6 +325,8 @@ class Escpos(object):
printer (Default) printer (Default)
:param center: Centers the code *default:* False :param center: Centers the code *default:* False
:param impl: Image-printing-implementation, refer to :meth:`.image()` for details :param impl: Image-printing-implementation, refer to :meth:`.image()` for details
:param image_arguments: arguments passed to :meth:`.image()`.
Replaces `impl` and `center`. If `impl` or `center` are set, they will overwrite `image_arguments`.
""" """
# Basic validation # Basic validation
if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]: if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]:
@@ -333,6 +341,19 @@ class Escpos(object):
# Handle edge case by printing nothing. # Handle edge case by printing nothing.
return return
if not native: if not native:
# impl is deprecated in favor of image_arguments
if impl:
warnings.warn(
"Parameter impl is deprecated in favor of image_arguments and will be dropped in a future release.",
DeprecationWarning,
)
# assemble arguments for image
if not image_arguments:
image_arguments = {}
if impl:
image_arguments["impl"] = impl
if "center" not in image_arguments:
image_arguments["center"] = center
# Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image # Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image
if model != QR_MODEL_2: if model != QR_MODEL_2:
raise ValueError( raise ValueError(
@@ -354,7 +375,7 @@ class Escpos(object):
# Convert the RGB image in printable image # Convert the RGB image in printable image
self.text("\n") self.text("\n")
self.image(im, center=center, impl=impl) self.image(im, **image_arguments)
self.text("\n") self.text("\n")
self.text("\n") self.text("\n")
return return
@@ -403,9 +424,7 @@ class Escpos(object):
raise ValueError("Can only output 1-4 bytes") raise ValueError("Can only output 1-4 bytes")
if not 0 <= inp_number <= max_input: if not 0 <= inp_number <= max_input:
raise ValueError( raise ValueError(
"Number too large. Can only output up to {0} in {1} bytes".format( f"Number too large. Can only output up to {max_input} in {out_bytes} bytes"
max_input, out_bytes
)
) )
outp = b"" outp = b""
for _ in range(0, out_bytes): for _ in range(0, out_bytes):
@@ -694,21 +713,15 @@ class Escpos(object):
if not function_type or not BARCODE_TYPES.get(function_type.upper()): if not function_type or not BARCODE_TYPES.get(function_type.upper()):
raise BarcodeTypeError( raise BarcodeTypeError(
( (
"Barcode '{bc}' not valid for barcode function type " f"Barcode '{bc}' not valid for barcode function type "
"{function_type}" f"{function_type}"
).format(
bc=bc,
function_type=function_type,
) )
) )
bc_types = BARCODE_TYPES[function_type.upper()] bc_types = BARCODE_TYPES[function_type.upper()]
if check and not self.check_barcode(bc, code): if check and not self.check_barcode(bc, code):
raise BarcodeCodeError( raise BarcodeCodeError(
("Barcode '{code}' not in a valid format for type '{bc}'").format( f"Barcode '{code}' not in a valid format for type '{bc}'"
code=code,
bc=bc,
)
) )
# Align Bar Code() # Align Bar Code()
@@ -718,12 +731,12 @@ class Escpos(object):
if 1 <= height <= 255: if 1 <= height <= 255:
self._raw(BARCODE_HEIGHT + six.int2byte(height)) self._raw(BARCODE_HEIGHT + six.int2byte(height))
else: else:
raise BarcodeSizeError("height = {height}".format(height=height)) raise BarcodeSizeError(f"height = {height}")
# Width # Width
if 2 <= width <= 6: if 2 <= width <= 6:
self._raw(BARCODE_WIDTH + six.int2byte(width)) self._raw(BARCODE_WIDTH + six.int2byte(width))
else: else:
raise BarcodeSizeError("width = {width}".format(width=width)) raise BarcodeSizeError(f"width = {width}")
# Font # Font
if font.upper() == "B": if font.upper() == "B":
self._raw(BARCODE_FONT_B) self._raw(BARCODE_FONT_B)
@@ -816,9 +829,7 @@ class Escpos(object):
# Check if barcode type exists # Check if barcode type exists
if barcode_type not in barcode.PROVIDED_BARCODES: if barcode_type not in barcode.PROVIDED_BARCODES:
raise BarcodeTypeError( raise BarcodeTypeError(
"Barcode type {} not supported by software barcode renderer".format( f"Barcode type {barcode_type} not supported by software barcode renderer"
barcode_type
)
) )
# Render the barcode # Render the barcode
@@ -840,7 +851,7 @@ class Escpos(object):
image = my_code.writer._image image = my_code.writer._image
self.image(image, impl=impl, center=center) self.image(image, impl=impl, center=center)
def text(self, txt): def text(self, txt: str) -> None:
"""Print alpha-numeric text. """Print alpha-numeric text.
The text has to be encoded in the currently selected codepage. The text has to be encoded in the currently selected codepage.
@@ -849,10 +860,9 @@ class Escpos(object):
:param txt: text to be printed :param txt: text to be printed
:raises: :py:exc:`~escpos.exceptions.TextError` :raises: :py:exc:`~escpos.exceptions.TextError`
""" """
txt = six.text_type(txt) self.magic.write(str(txt))
self.magic.write(txt)
def textln(self, txt=""): def textln(self, txt: str = "") -> None:
"""Print alpha-numeric text with a newline. """Print alpha-numeric text with a newline.
The text has to be encoded in the currently selected codepage. The text has to be encoded in the currently selected codepage.
@@ -861,9 +871,9 @@ class Escpos(object):
:param txt: text to be printed with a newline :param txt: text to be printed with a newline
:raises: :py:exc:`~escpos.exceptions.TextError` :raises: :py:exc:`~escpos.exceptions.TextError`
""" """
self.text("{}\n".format(txt)) self.text(f"{txt}\n")
def ln(self, count=1): def ln(self, count: int = 1) -> None:
"""Print a newline or more. """Print a newline or more.
:param count: number of newlines to print :param count: number of newlines to print
@@ -874,7 +884,7 @@ class Escpos(object):
if count > 0: if count > 0:
self.text("\n" * count) self.text("\n" * count)
def block_text(self, txt, font="0", columns=None): def block_text(self, txt, font="0", columns=None) -> None:
"""Print text wrapped to specific columns. """Print text wrapped to specific columns.
Text has to be encoded in unicode. Text has to be encoded in unicode.
@@ -1125,7 +1135,7 @@ class Escpos(object):
try: try:
self._raw(CD_KICK_DEC_SEQUENCE(*pin)) self._raw(CD_KICK_DEC_SEQUENCE(*pin))
except TypeError as err: except TypeError as err:
raise CashDrawerError(err) raise CashDrawerError(str(err))
def linedisplay_select(self, select_display: bool = False) -> None: def linedisplay_select(self, select_display: bool = False) -> None:
"""Select the line display or the printer. """Select the line display or the printer.
@@ -1258,10 +1268,10 @@ class Escpos(object):
else: else:
self._raw(PANEL_BUTTON_OFF) self._raw(PANEL_BUTTON_OFF)
def query_status(self, mode: bytes) -> List[int]: def query_status(self, mode: bytes) -> bytes:
"""Query the printer for its status. """Query the printer for its status.
Returns an array of integers containing it. Returns byte array containing it.
:param mode: Integer that sets the status mode queried to the printer. :param mode: Integer that sets the status mode queried to the printer.
- RT_STATUS_ONLINE: Printer status. - RT_STATUS_ONLINE: Printer status.
@@ -1281,7 +1291,7 @@ class Escpos(object):
return False return False
return not (status[0] & RT_MASK_ONLINE) return not (status[0] & RT_MASK_ONLINE)
def paper_status(self): def paper_status(self) -> int: # could be IntEnum
"""Query the paper status of the printer. """Query the paper status of the printer.
Returns 2 if there is plenty of paper, 1 if the paper has arrived to Returns 2 if there is plenty of paper, 1 if the paper has arrived to
@@ -1298,6 +1308,8 @@ class Escpos(object):
return 1 return 1
if status[0] & RT_MASK_PAPER == RT_MASK_PAPER: if status[0] & RT_MASK_PAPER == RT_MASK_PAPER:
return 2 return 2
# not reached
return 0
def target(self, type: str = "ROLL") -> None: def target(self, type: str = "ROLL") -> None:
"""Select where to print to. """Select where to print to.
@@ -1352,7 +1364,7 @@ class Escpos(object):
self._raw(BUZZER + six.int2byte(times) + six.int2byte(duration)) self._raw(BUZZER + six.int2byte(times) + six.int2byte(duration))
class EscposIO(object): class EscposIO:
r"""ESC/POS Printer IO object. r"""ESC/POS Printer IO object.
Allows the class to be used together with the `with`-statement. You have to define a printer instance Allows the class to be used together with the `with`-statement. You have to define a printer instance
@@ -1398,41 +1410,40 @@ class EscposIO(object):
""" """
self.params.update(kwargs) self.params.update(kwargs)
def writelines(self, text, **kwargs): def writelines(self, text: str, **kwargs) -> None:
"""Print text.""" """Print text."""
params = dict(self.params) params = dict(self.params)
params.update(kwargs) params.update(kwargs)
if isinstance(text, six.text_type): if isinstance(text, str):
lines = text.split("\n") lines = text.split("\n")
elif isinstance(text, list) or isinstance(text, tuple): elif isinstance(text, list) or isinstance(text, tuple):
lines = text lines = text
else: else:
lines = [ lines = [
"{0}".format(text), f"{text}",
] ]
# TODO check unicode handling # TODO check unicode handling
# TODO flush? or on print? (this should prob rather be handled by the _raw-method) # TODO flush? or on print? (this should prob rather be handled by the _raw-method)
for line in lines: for line in lines:
self.printer.set(**params) self.printer.set(**params)
if isinstance(text, six.text_type): self.printer.text(f"{line}\n")
self.printer.text("{0}\n".format(line))
else:
self.printer.text("{0}\n".format(line))
def close(self): def close(self) -> None:
"""Close printer. """Close printer.
Called upon closing the `with`-statement. Called upon closing the `with`-statement.
""" """
self.printer.close() self.printer.close()
def __enter__(self, **kwargs): def __enter__(self, **kwargs: Any) -> "EscposIO":
"""Enter context.""" """Enter context."""
return self return self
def __exit__(self, type, value, traceback): def __exit__(
self, type: type[BaseException], value: BaseException, traceback: TracebackType
) -> None:
"""Cut and close if configured. """Cut and close if configured.
If :py:attr:`autocut <escpos.escpos.EscposIO.autocut>` is `True` (set by this class' constructor), If :py:attr:`autocut <escpos.escpos.EscposIO.autocut>` is `True` (set by this class' constructor),

View File

@@ -26,6 +26,8 @@ Result/Exit codes:
:license: MIT :license: MIT
""" """
from typing import Optional
class Error(Exception): class Error(Exception):
"""Base class for ESC/POS errors. """Base class for ESC/POS errors.
@@ -37,7 +39,7 @@ class Error(Exception):
""" """
def __init__(self, msg, status=None): def __init__(self, msg: str, status: Optional[int] = None) -> None:
"""Initialize Error object.""" """Initialize Error object."""
Exception.__init__(self) Exception.__init__(self)
self.msg = msg self.msg = msg
@@ -45,7 +47,7 @@ class Error(Exception):
if status is not None: if status is not None:
self.resultcode = status self.resultcode = status
def __str__(self): def __str__(self) -> str:
"""Return string representation of Error.""" """Return string representation of Error."""
return self.msg return self.msg
@@ -64,15 +66,15 @@ class BarcodeTypeError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeTypeError object.""" """Initialize BarcodeTypeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 10 self.resultcode = 10
def __str__(self): def __str__(self) -> str:
"""Return string representation of BarcodeTypeError.""" """Return string representation of BarcodeTypeError."""
return "No Barcode type is defined ({msg})".format(msg=self.msg) return f"No Barcode type is defined ({self.msg})"
class BarcodeSizeError(Error): class BarcodeSizeError(Error):
@@ -89,15 +91,15 @@ class BarcodeSizeError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeSizeError object.""" """Initialize BarcodeSizeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 20 self.resultcode = 20
def __str__(self): def __str__(self) -> str:
"""Return string representation of BarcodeSizeError.""" """Return string representation of BarcodeSizeError."""
return "Barcode size is out of range ({msg})".format(msg=self.msg) return f"Barcode size is out of range ({self.msg})"
class BarcodeCodeError(Error): class BarcodeCodeError(Error):
@@ -114,15 +116,15 @@ class BarcodeCodeError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeCodeError object.""" """Initialize BarcodeCodeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 30 self.resultcode = 30
def __str__(self): def __str__(self) -> str:
"""Return string representation of BarcodeCodeError.""" """Return string representation of BarcodeCodeError."""
return "No Barcode code was supplied ({msg})".format(msg=self.msg) return f"No Barcode code was supplied ({self.msg})"
class ImageSizeError(Error): class ImageSizeError(Error):
@@ -137,17 +139,15 @@ class ImageSizeError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ImageSizeError object.""" """Initialize ImageSizeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 40 self.resultcode = 40
def __str__(self): def __str__(self) -> str:
"""Return string representation of ImageSizeError.""" """Return string representation of ImageSizeError."""
return "Image height is longer than 255px and can't be printed ({msg})".format( return f"Image height is longer than 255px and can't be printed ({self.msg})"
msg=self.msg
)
class ImageWidthError(Error): class ImageWidthError(Error):
@@ -162,15 +162,15 @@ class ImageWidthError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ImageWidthError object.""" """Initialize ImageWidthError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 41 self.resultcode = 41
def __str__(self): def __str__(self) -> str:
"""Return string representation of ImageWidthError.""" """Return string representation of ImageWidthError."""
return "Image width is too large ({msg})".format(msg=self.msg) return f"Image width is too large ({self.msg})"
class TextError(Error): class TextError(Error):
@@ -186,17 +186,15 @@ class TextError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize TextError object.""" """Initialize TextError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 50 self.resultcode = 50
def __str__(self): def __str__(self) -> str:
"""Return string representation of TextError.""" """Return string representation of TextError."""
return "Text string must be supplied to the text() method ({msg})".format( return f"Text string must be supplied to the text() method ({self.msg})"
msg=self.msg
)
class CashDrawerError(Error): class CashDrawerError(Error):
@@ -212,15 +210,15 @@ class CashDrawerError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize CashDrawerError object.""" """Initialize CashDrawerError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 60 self.resultcode = 60
def __str__(self): def __str__(self) -> str:
"""Return string representation of CashDrawerError.""" """Return string representation of CashDrawerError."""
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg) return f"Valid pin must be set to send pulse ({self.msg})"
class TabPosError(Error): class TabPosError(Error):
@@ -239,17 +237,15 @@ class TabPosError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize TabPosError object.""" """Initialize TabPosError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 70 self.resultcode = 70
def __str__(self): def __str__(self) -> str:
"""Return string representation of TabPosError.""" """Return string representation of TabPosError."""
return "Valid tab positions must be in the range 0 to 16 ({msg})".format( return f"Valid tab positions must be in the range 0 to 16 ({self.msg})"
msg=self.msg
)
class CharCodeError(Error): class CharCodeError(Error):
@@ -265,15 +261,15 @@ class CharCodeError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize CharCodeError object.""" """Initialize CharCodeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 80 self.resultcode = 80
def __str__(self): def __str__(self) -> str:
"""Return string representation of CharCodeError.""" """Return string representation of CharCodeError."""
return "Valid char code must be set ({msg})".format(msg=self.msg) return f"Valid char code must be set ({self.msg})"
class DeviceNotFoundError(Error): class DeviceNotFoundError(Error):
@@ -289,13 +285,13 @@ class DeviceNotFoundError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize DeviceNotFoundError object.""" """Initialize DeviceNotFoundError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 90 self.resultcode = 90
def __str__(self): def __str__(self) -> str:
"""Return string representation of DeviceNotFoundError.""" """Return string representation of DeviceNotFoundError."""
return f"Device not found ({self.msg})" return f"Device not found ({self.msg})"
@@ -313,13 +309,13 @@ class USBNotFoundError(DeviceNotFoundError):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize USBNotFoundError object.""" """Initialize USBNotFoundError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 91 self.resultcode = 91
def __str__(self): def __str__(self) -> str:
"""Return string representation of USBNotFoundError.""" """Return string representation of USBNotFoundError."""
return f"USB device not found ({self.msg})" return f"USB device not found ({self.msg})"
@@ -337,15 +333,15 @@ class SetVariableError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize SetVariableError object.""" """Initialize SetVariableError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 100 self.resultcode = 100
def __str__(self): def __str__(self) -> str:
"""Return string representation of SetVariableError.""" """Return string representation of SetVariableError."""
return "Set variable out of range ({msg})".format(msg=self.msg) return f"Set variable out of range ({self.msg})"
# Configuration errors # Configuration errors
@@ -364,15 +360,15 @@ class ConfigNotFoundError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigNotFoundError object.""" """Initialize ConfigNotFoundError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 200 self.resultcode = 200
def __str__(self): def __str__(self) -> str:
"""Return string representation of ConfigNotFoundError.""" """Return string representation of ConfigNotFoundError."""
return "Configuration not found ({msg})".format(msg=self.msg) return f"Configuration not found ({self.msg})"
class ConfigSyntaxError(Error): class ConfigSyntaxError(Error):
@@ -388,15 +384,15 @@ class ConfigSyntaxError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigSyntaxError object.""" """Initialize ConfigSyntaxError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 210 self.resultcode = 210
def __str__(self): def __str__(self) -> str:
"""Return string representation of ConfigSyntaxError.""" """Return string representation of ConfigSyntaxError."""
return "Configuration syntax is invalid ({msg})".format(msg=self.msg) return f"Configuration syntax is invalid ({self.msg})"
class ConfigSectionMissingError(Error): class ConfigSectionMissingError(Error):
@@ -412,12 +408,12 @@ class ConfigSectionMissingError(Error):
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigSectionMissingError object.""" """Initialize ConfigSectionMissingError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 220 self.resultcode = 220
def __str__(self): def __str__(self) -> str:
"""Return string representation of ConfigSectionMissingError.""" """Return string representation of ConfigSectionMissingError."""
return "Configuration section is missing ({msg})".format(msg=self.msg) return f"Configuration section is missing ({self.msg})"

View File

@@ -10,11 +10,12 @@ This module contains the image format handler :py:class:`EscposImage`.
import math import math
from typing import Iterator, Union
from PIL import Image, ImageOps from PIL import Image, ImageOps
class EscposImage(object): class EscposImage:
""" """
Load images in, and output ESC/POS formats. Load images in, and output ESC/POS formats.
@@ -22,7 +23,7 @@ class EscposImage(object):
PIL, rather than spend CPU cycles looping over pixels. PIL, rather than spend CPU cycles looping over pixels.
""" """
def __init__(self, img_source): def __init__(self, img_source: Union[Image.Image, str]) -> None:
"""Load in an image. """Load in an image.
:param img_source: PIL.Image, or filename to load one from. :param img_source: PIL.Image, or filename to load one from.
@@ -48,23 +49,23 @@ class EscposImage(object):
self._im = im.convert("1") self._im = im.convert("1")
@property @property
def width(self): def width(self) -> int:
"""Return width of image in pixels.""" """Return width of image in pixels."""
width_pixels, _ = self._im.size width_pixels, _ = self._im.size
return width_pixels return width_pixels
@property @property
def width_bytes(self): def width_bytes(self) -> int:
"""Return width of image if you use 8 pixels per byte and 0-pad at the end.""" """Return width of image if you use 8 pixels per byte and 0-pad at the end."""
return (self.width + 7) >> 3 return (self.width + 7) >> 3
@property @property
def height(self): def height(self) -> int:
"""Height of image in pixels.""" """Height of image in pixels."""
_, height_pixels = self._im.size _, height_pixels = self._im.size
return height_pixels return height_pixels
def to_column_format(self, high_density_vertical=True): def to_column_format(self, high_density_vertical: bool = True) -> Iterator[bytes]:
"""Extract slices of an image as equal-sized blobs of column-format data. """Extract slices of an image as equal-sized blobs of column-format data.
:param high_density_vertical: Printed line height in dots :param high_density_vertical: Printed line height in dots
@@ -81,11 +82,11 @@ class EscposImage(object):
yield (im_bytes) yield (im_bytes)
left += line_height left += line_height
def to_raster_format(self): def to_raster_format(self) -> bytes:
"""Convert image to raster-format binary.""" """Convert image to raster-format binary."""
return self._im.tobytes() return self._im.tobytes()
def split(self, fragment_height): def split(self, fragment_height: int):
"""Split an image into multiple fragments after fragment_height pixels. """Split an image into multiple fragments after fragment_height pixels.
:param fragment_height: height of fragment :param fragment_height: height of fragment
@@ -102,7 +103,7 @@ class EscposImage(object):
fragments.append(self.img_original.crop(box)) fragments.append(self.img_original.crop(box))
return fragments return fragments
def center(self, max_width): def center(self, max_width: int) -> None:
"""Center image in place. """Center image in place.
:param: Maximum width in order to deduce x offset for centering :param: Maximum width in order to deduce x offset for centering

View File

@@ -11,7 +11,7 @@ except ImportError:
jaconv = None jaconv = None
def encode_katakana(text): def encode_katakana(text: str) -> bytes:
"""I don't think this quite works yet.""" """I don't think this quite works yet."""
encoded = [] encoded = []
for char in text: for char in text:

View File

@@ -23,7 +23,7 @@ from .constants import CODEPAGE_CHANGE
from .exceptions import Error from .exceptions import Error
class Encoder(object): class Encoder:
"""Take available code spaces and pick the right one for a given character. """Take available code spaces and pick the right one for a given character.
Note: To determine the code page, it needs to do the conversion, and Note: To determine the code page, it needs to do the conversion, and
@@ -57,9 +57,9 @@ class Encoder(object):
if encoding not in self.codepages: if encoding not in self.codepages:
raise ValueError( raise ValueError(
( (
'Encoding "{}" cannot be used for the current profile. ' f'Encoding "{encoding}" cannot be used for the current profile. '
"Valid encodings are: {}" f'Valid encodings are: {",".join(self.codepages.keys())}'
).format(encoding, ",".join(self.codepages.keys())) )
) )
return encoding return encoding
@@ -88,7 +88,7 @@ class Encoder(object):
# Non-encodable character, just skip it # Non-encodable character, just skip it
pass pass
return encodable_chars return encodable_chars
raise LookupError("Can't find a known encoding for {}".format(encoding)) raise LookupError(f"Can't find a known encoding for {encoding}")
def _get_codepage_char_map(self, encoding): def _get_codepage_char_map(self, encoding):
"""Get codepage character map. """Get codepage character map.
@@ -202,7 +202,7 @@ def split_writable_text(encoder, text, encoding):
return text, None return text, None
class MagicEncode(object): class MagicEncode:
"""Help switching to the right code page. """Help switching to the right code page.
A helper that helps us to automatically switch to the right A helper that helps us to automatically switch to the right
@@ -292,11 +292,9 @@ class MagicEncode(object):
def write_with_encoding(self, encoding, text): def write_with_encoding(self, encoding, text):
"""Write the text and inject necessary codepage switches.""" """Write the text and inject necessary codepage switches."""
if text is not None and type(text) is not six.text_type: if text is not None and type(text) is not str:
raise Error( raise Error(
"The supplied text has to be unicode, but is of type {type}.".format( f"The supplied text has to be unicode, but is of type {type(text)}."
type=type(text)
)
) )
# We always know the current code page; if the new codepage # We always know the current code page; if the new codepage

View File

@@ -84,7 +84,7 @@ class CupsPrinter(Escpos):
return is_usable() return is_usable()
@dependency_pycups @dependency_pycups
def __init__(self, printer_name: str = "", *args, **kwargs): def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
"""Class constructor for CupsPrinter. """Class constructor for CupsPrinter.
:param printer_name: CUPS printer name (Optional) :param printer_name: CUPS printer name (Optional)
@@ -163,11 +163,10 @@ class CupsPrinter(Escpos):
return return
logging.info("CupsPrinter printer enabled") logging.info("CupsPrinter printer enabled")
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Append any command sent in raw format to temporary file. """Append any command sent in raw format to temporary file.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
self.pending_job = True self.pending_job = True
try: try:
@@ -176,8 +175,9 @@ class CupsPrinter(Escpos):
self.pending_job = False self.pending_job = False
raise TypeError("Bytes required. Printer job not opened") raise TypeError("Bytes required. Printer job not opened")
def send(self): def send(self) -> None:
"""Send the print job to the printer.""" """Send the print job to the printer."""
assert self.device
if self.pending_job: if self.pending_job:
# Rewind tempfile # Rewind tempfile
self.tmpfile.seek(0) self.tmpfile.seek(0)
@@ -190,7 +190,7 @@ class CupsPrinter(Escpos):
) )
self._clear() self._clear()
def _clear(self): def _clear(self) -> None:
"""Finish the print job. """Finish the print job.
Remove temporary file. Remove temporary file.
@@ -198,18 +198,18 @@ class CupsPrinter(Escpos):
self.tmpfile.close() self.tmpfile.close()
self.pending_job = False self.pending_job = False
def _read(self): def _read(self) -> bytes:
"""Return a single-item array with the accepting state of the print queue. """Return a single-item array with the accepting state of the print queue.
states: idle = [3], printing a job = [4], stopped = [5] states: idle = [3], printing a job = [4], stopped = [5]
""" """
printer = self.printers.get(self.printer_name, {}) printer = self.printers.get(self.printer_name, {})
state = printer.get("printer-state") state = printer.get("printer-state")
if not state: if not state or state in [4, 5]:
return [] return b"8" # offline
return [state] return b"0" # online
def close(self): def close(self) -> None:
"""Close CUPS connection. """Close CUPS connection.
Send pending job to the printer if needed. Send pending job to the printer if needed.

View File

@@ -7,6 +7,7 @@
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos :copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT :license: MIT
""" """
from typing import List
from ..escpos import Escpos from ..escpos import Escpos
@@ -39,25 +40,24 @@ class Dummy(Escpos):
""" """
return is_usable() return is_usable()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
"""Init with empty output list.""" """Init with empty output list."""
Escpos.__init__(self, *args, **kwargs) Escpos.__init__(self, *args, **kwargs)
self._output_list = [] self._output_list: List[bytes] = []
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
self._output_list.append(msg) self._output_list.append(msg)
@property @property
def output(self): def output(self) -> bytes:
"""Get the data that was sent to this printer.""" """Get the data that was sent to this printer."""
return b"".join(self._output_list) return b"".join(self._output_list)
def clear(self): def clear(self) -> None:
"""Clear the buffer of the printer. """Clear the buffer of the printer.
This method can be called if you send the contents to a physical printer This method can be called if you send the contents to a physical printer
@@ -65,6 +65,6 @@ class Dummy(Escpos):
""" """
del self._output_list[:] del self._output_list[:]
def close(self): def close(self) -> None:
"""Close not implemented for Dummy printer.""" """Close not implemented for Dummy printer."""
pass pass

View File

@@ -88,12 +88,12 @@ class File(Escpos):
if self.device: if self.device:
self.device.flush() self.device.flush()
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
assert self.device
self.device.write(msg) self.device.write(msg)
if self.auto_flush: if self.auto_flush:
self.flush() self.flush()

View File

@@ -182,12 +182,13 @@ class LP(Escpos):
if not self._is_closing: if not self._is_closing:
self.open(_close_opened=False) self.open(_close_opened=False)
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Write raw command(s) to the printer. """Write raw command(s) to the printer.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
assert self.device is not None
assert self.device.stdin is not None
if self.device.stdin.writable(): if self.device.stdin.writable():
self.device.stdin.write(msg) self.device.stdin.write(msg)
else: else:

View File

@@ -110,16 +110,17 @@ class Network(Escpos):
return return
logging.info("Network printer enabled") logging.info("Network printer enabled")
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
assert self.device
self.device.sendall(msg) self.device.sendall(msg)
def _read(self): def _read(self) -> bytes:
"""Read data from the TCP socket.""" """Read data from the TCP socket."""
assert self.device
return self.device.recv(16) return self.device.recv(16)
def close(self) -> None: def close(self) -> None:

View File

@@ -155,16 +155,17 @@ class Serial(Escpos):
return return
logging.info("Serial printer enabled") logging.info("Serial printer enabled")
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
assert self.device
self.device.write(msg) self.device.write(msg)
def _read(self): def _read(self) -> bytes:
"""Read the data buffer and return it to the caller.""" """Read the data buffer and return it to the caller."""
assert self.device
return self.device.read(16) return self.device.read(16)
def close(self) -> None: def close(self) -> None:

View File

@@ -74,9 +74,9 @@ class Usb(Escpos):
def __init__( def __init__(
self, self,
idVendor: str = "", idVendor: Optional[int] = None,
idProduct: str = "", idProduct: Optional[int] = None,
usb_args: Dict[str, str] = {}, usb_args: Dict[str, Union[str, int]] = {},
timeout: Union[int, float] = 0, timeout: Union[int, float] = 0,
in_ep: int = 0x82, in_ep: int = 0x82,
out_ep: int = 0x01, out_ep: int = 0x01,
@@ -181,16 +181,17 @@ class Usb(Escpos):
except usb.core.USBError as e: except usb.core.USBError as e:
logging.error("Could not set configuration: %s", str(e)) logging.error("Could not set configuration: %s", str(e))
def _raw(self, msg): def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
assert self.device
self.device.write(self.out_ep, msg, self.timeout) self.device.write(self.out_ep, msg, self.timeout)
def _read(self): def _read(self) -> bytes:
"""Read a data buffer and return it to the caller.""" """Read a data buffer and return it to the caller."""
assert self.device
return self.device.read(self.in_ep, 16) return self.device.read(self.in_ep, 16)
@dependency_usb @dependency_usb

View File

@@ -76,7 +76,7 @@ class Win32Raw(Escpos):
return is_usable() return is_usable()
@dependency_win32print @dependency_win32print
def __init__(self, printer_name: str = "", *args, **kwargs): def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
"""Initialize default printer.""" """Initialize default printer."""
Escpos.__init__(self, *args, **kwargs) Escpos.__init__(self, *args, **kwargs)
self.printer_name = printer_name self.printer_name = printer_name
@@ -148,14 +148,17 @@ class Win32Raw(Escpos):
win32print.ClosePrinter(self._device) win32print.ClosePrinter(self._device)
self._device = False self._device = False
def _raw(self, msg) -> None: def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format. """Print any command sent in raw format.
:param msg: arbitrary code to be printed :param msg: arbitrary code to be printed
:type msg: bytes
""" """
if self.printer_name is None: if self.printer_name is None:
raise DeviceNotFoundError("Printer not found") raise DeviceNotFoundError("Printer not found")
if not self.device: if not self.device:
raise DeviceNotFoundError("Printer job not opened") raise DeviceNotFoundError("Printer job not opened")
win32print.WritePrinter(self.device, msg) win32print.WritePrinter(self.device, msg) # type: ignore
# there is a bug in the typeshed
# https://github.com/mhammond/pywin32/blob/main/win32/src/win32print/win32print.cpp#L976
# https://github.com/python/typeshed/blob/main/stubs/pywin32/win32/win32print.pyi#L27C4-L27C4

View File

@@ -5,32 +5,32 @@ from escpos.printer import LP, CupsPrinter, Dummy, File, Network, Serial, Usb, W
@pytest.fixture @pytest.fixture
def driver(): def driver() -> Dummy:
return Dummy() return Dummy()
@pytest.fixture @pytest.fixture
def usbprinter(): def usbprinter() -> Usb:
return Usb() return Usb()
@pytest.fixture @pytest.fixture
def serialprinter(): def serialprinter() -> Serial:
return Serial() return Serial()
@pytest.fixture @pytest.fixture
def networkprinter(): def networkprinter() -> Network:
return Network() return Network()
@pytest.fixture @pytest.fixture
def fileprinter(): def fileprinter() -> File:
return File() return File()
@pytest.fixture @pytest.fixture
def lpprinter(): def lpprinter() -> LP:
return LP() return LP()

View File

@@ -14,14 +14,14 @@ import pytest
import escpos.escpos as escpos import escpos.escpos as escpos
def test_abstract_base_class_raises(): def test_abstract_base_class_raises() -> None:
"""test whether the abstract base class raises an exception for ESC/POS""" """test whether the abstract base class raises an exception for ESC/POS"""
with pytest.raises(TypeError): with pytest.raises(TypeError):
# This call should raise TypeError because of abstractmethod _raw() # This call should raise TypeError because of abstractmethod _raw()
escpos.Escpos() escpos.Escpos() # type: ignore [abstract]
def test_abstract_base_class(): def test_abstract_base_class() -> None:
"""test whether Escpos has the metaclass ABCMeta""" """test whether Escpos has the metaclass ABCMeta"""
assert issubclass(escpos.Escpos, object) assert issubclass(escpos.Escpos, object)
assert type(escpos.Escpos) is ABCMeta assert type(escpos.Escpos) is ABCMeta

View File

@@ -7,7 +7,6 @@ import os
import shutil import shutil
import tempfile import tempfile
import pytest
from scripttest import TestFileEnvironment as TFE from scripttest import TestFileEnvironment as TFE
import escpos import escpos
@@ -31,22 +30,19 @@ class TestCLI:
"""Contains setups, teardowns, and tests for CLI""" """Contains setups, teardowns, and tests for CLI"""
@classmethod @classmethod
def setup_class(cls): def setup_class(cls) -> None:
"""Create a config file to read from""" """Create a config file to read from"""
with open(CONFIGFILE, "w") as config: with open(CONFIGFILE, "w") as config:
config.write(CONFIG_YAML) config.write(CONFIG_YAML)
@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls) -> None:
"""Remove config file""" """Remove config file"""
os.remove(CONFIGFILE) os.remove(CONFIGFILE)
shutil.rmtree(TEST_DIR) shutil.rmtree(TEST_DIR)
def setup_method(self): def setup_method(self) -> None:
"""Create a file to print to and set up env""" """Create a file to print to and set up env"""
self.env = None
self.default_args = None
self.env = TFE( self.env = TFE(
base_path=TEST_DIR, base_path=TEST_DIR,
cwd=os.getcwd(), cwd=os.getcwd(),
@@ -64,24 +60,24 @@ class TestCLI:
finally: finally:
fhandle.close() fhandle.close()
def teardown_method(self): def teardown_method(self) -> None:
"""Destroy printer file and env""" """Destroy printer file and env"""
os.remove(DEVFILE) os.remove(DEVFILE)
self.env.clear() self.env.clear()
def test_cli_help(self): def test_cli_help(self) -> None:
"""Test getting help from cli""" """Test getting help from cli"""
result = self.env.run("python-escpos", "-h") result = self.env.run("python-escpos", "-h")
assert not result.stderr assert not result.stderr
assert "usage" in result.stdout assert "usage" in result.stdout
def test_cli_version(self): def test_cli_version(self) -> None:
"""Test the version string""" """Test the version string"""
result = self.env.run("python-escpos", "version") result = self.env.run("python-escpos", "version")
assert not result.stderr assert not result.stderr
assert escpos.__version__ == result.stdout.strip() assert escpos.__version__ == result.stdout.strip()
def test_cli_version_extended(self): def test_cli_version_extended(self) -> None:
"""Test the extended version information""" """Test the extended version information"""
result = self.env.run("python-escpos", "version_extended") result = self.env.run("python-escpos", "version_extended")
assert not result.stderr assert not result.stderr
@@ -89,10 +85,7 @@ class TestCLI:
# test that additional information on e.g. Serial is printed # test that additional information on e.g. Serial is printed
assert "Serial" in result.stdout assert "Serial" in result.stdout
@pytest.mark.skip( def test_cli_text(self) -> None:
reason="disable this test as it is not that easy anymore to predict the outcome of this call"
)
def test_cli_text(self):
"""Make sure text returns what we sent it""" """Make sure text returns what we sent it"""
test_text = "this is some text" test_text = "this is some text"
result = self.env.run( result = self.env.run(
@@ -107,9 +100,11 @@ class TestCLI:
) )
assert not result.stderr assert not result.stderr
assert DEVFILE_NAME in result.files_updated.keys() assert DEVFILE_NAME in result.files_updated.keys()
assert result.files_updated[DEVFILE_NAME].bytes == test_text + "\n" assert (
result.files_updated[DEVFILE_NAME].bytes == "\x1bt\x00" + test_text + "\n"
)
def test_cli_text_invalid_args(self): def test_cli_text_invalid_args(self) -> None:
"""Test a failure to send valid arguments""" """Test a failure to send valid arguments"""
result = self.env.run( result = self.env.run(
*(self.default_args + ("text", "--invalid-param", "some data")), *(self.default_args + ("text", "--invalid-param", "some data")),

126
test/test_config.py Normal file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/python
"""tests for config module
:author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2023 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import pathlib
import appdirs
import pytest
import escpos.exceptions
def generate_dummy_config(path, content=None):
"""Generate a dummy config in path"""
dummy_config_content = content
if not content:
dummy_config_content = "printer:\n type: Dummy\n"
path.write_text(dummy_config_content)
assert path.read_text() == dummy_config_content
def simple_printer_test(config):
"""Simple test for the dummy printer."""
p = config.printer()
p._raw(b"1234")
assert p.output == b"1234"
def test_config_load_with_invalid_config_yaml(tmp_path):
"""Test the loading of a config with a invalid config file (yaml issue)."""
# generate a dummy config
config_file = tmp_path / "config.yaml"
generate_dummy_config(config_file, content="}invalid}yaml}")
# test the config loading
from escpos import config
c = config.Config()
with pytest.raises(escpos.exceptions.ConfigSyntaxError):
c.load(config_path=config_file)
def test_config_load_with_invalid_config_content(tmp_path):
"""Test the loading of a config with a invalid config file (content issue)."""
# generate a dummy config
config_file = tmp_path / "config.yaml"
generate_dummy_config(
config_file, content="printer:\n type: NoPrinterWithThatName\n"
)
# test the config loading
from escpos import config
c = config.Config()
with pytest.raises(escpos.exceptions.ConfigSyntaxError):
c.load(config_path=config_file)
def test_config_load_with_missing_config(tmp_path):
"""Test the loading of a config that does not exist."""
# test the config loading
from escpos import config
c = config.Config()
with pytest.raises(escpos.exceptions.ConfigNotFoundError):
c.load(config_path=tmp_path)
@pytest.mark.skip(
"This test creates in the actual appdir files and is therefore skipped."
)
def test_config_load_from_appdir() -> None:
"""Test the loading of a config in appdir."""
from escpos import config
# generate a dummy config
config_file = (
pathlib.Path(appdirs.user_config_dir(config.Config._app_name))
/ config.Config._config_file
)
generate_dummy_config(config_file)
# test the config loading
c = config.Config()
c.load()
# test the resulting printer object
simple_printer_test(c)
def test_config_load_with_file(tmp_path):
"""Test the loading of a config with a config file."""
# generate a dummy config
config_file = tmp_path / "config.yaml"
generate_dummy_config(config_file)
# test the config loading
from escpos import config
c = config.Config()
c.load(config_path=config_file)
# test the resulting printer object
simple_printer_test(c)
def test_config_load_with_path(tmp_path):
"""Test the loading of a config with a config path."""
# generate a dummy config
config_file = tmp_path / "config.yaml"
generate_dummy_config(config_file)
# test the config loading
from escpos import config
c = config.Config()
c.load(config_path=tmp_path)
# test the resulting printer object
simple_printer_test(c)

View File

@@ -1,38 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for the non-native part of qr()
:author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import mock
import pytest
from PIL import Image
from escpos.printer import Dummy
@mock.patch("escpos.printer.Dummy.image", spec=Dummy)
def test_type_of_object_passed_to_image_function(img_function):
"""
Test the type of object that is passed to the image function during non-native qr-printing.
The type should be PIL.Image
"""
d = Dummy()
d.qr("LoremIpsum")
args, kwargs = img_function.call_args
assert isinstance(args[0], Image.Image)
@pytest.fixture
def instance():
return Dummy()
def test_center(instance):
instance.qr("LoremIpsum", center=True)

View File

@@ -1,14 +1,13 @@
import pytest import pytest
import six
from escpos import printer from escpos import printer
from escpos.constants import BUZZER from escpos.constants import BUZZER
def test_buzzer_function_with_default_params(): def test_buzzer_function_with_default_params() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.buzzer() instance.buzzer()
expected = BUZZER + six.int2byte(2) + six.int2byte(4) expected = BUZZER + bytes((2, 4))
assert instance.output == expected assert instance.output == expected
@@ -26,10 +25,10 @@ def test_buzzer_function_with_default_params():
[9, 9], [9, 9],
], ],
) )
def test_buzzer_function(times, duration): def test_buzzer_function(times: int, duration: int) -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.buzzer(times, duration) instance.buzzer(times, duration)
expected = BUZZER + six.int2byte(times) + six.int2byte(duration) expected = BUZZER + bytes((times, duration))
assert instance.output == expected assert instance.output == expected
@@ -46,7 +45,9 @@ def test_buzzer_function(times, duration):
[3, 11, "duration must be between 1 and 9"], [3, 11, "duration must be between 1 and 9"],
], ],
) )
def test_buzzer_fuction_with_outrange_values(times, duration, expected_message): def test_buzzer_fuction_with_outrange_values(
times: int, duration: int, expected_message: str
) -> None:
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
instance.buzzer(times, duration) instance.buzzer(times, duration)

View File

@@ -6,7 +6,7 @@ import escpos.printer as printer
from escpos.exceptions import CashDrawerError from escpos.exceptions import CashDrawerError
def test_raise_CashDrawerError(): def test_raise_CashDrawerError() -> None:
"""should raise an error if the sequence is invalid.""" """should raise an error if the sequence is invalid."""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(CashDrawerError): with pytest.raises(CashDrawerError):

View File

@@ -1,12 +1,10 @@
import six
import escpos.printer as printer import escpos.printer as printer
from escpos.constants import GS from escpos.constants import GS
def test_cut_without_feed(): def test_cut_without_feed() -> None:
"""Test cut without feeding paper""" """Test cut without feeding paper"""
instance = printer.Dummy() instance = printer.Dummy()
instance.cut(feed=False) instance.cut(feed=False)
expected = GS + b"V" + six.int2byte(66) + b"\x00" expected = GS + b"V" + bytes((66,)) + b"\x00"
assert instance.output == expected assert instance.output == expected

View File

@@ -1,7 +1,7 @@
from escpos.printer import Dummy from escpos.printer import Dummy
def test_printer_dummy_clear(): def test_printer_dummy_clear() -> None:
printer = Dummy() printer = Dummy()
printer.text("Hello") printer.text("Hello")
printer.clear() printer.clear()

View File

@@ -16,7 +16,7 @@ from escpos.exceptions import ImageWidthError
# Raster format print # Raster format print
def test_bit_image_black(): def test_bit_image_black() -> None:
""" """
Test printing solid black bit image (raster) Test printing solid black bit image (raster)
""" """
@@ -30,7 +30,7 @@ def test_bit_image_black():
assert instance.output == b"\x1dv0\x00\x01\x00\x01\x00\x80" assert instance.output == b"\x1dv0\x00\x01\x00\x01\x00\x80"
def test_bit_image_white(): def test_bit_image_white() -> None:
""" """
Test printing solid white bit image (raster) Test printing solid white bit image (raster)
""" """
@@ -39,7 +39,7 @@ def test_bit_image_white():
assert instance.output == b"\x1dv0\x00\x01\x00\x01\x00\x00" assert instance.output == b"\x1dv0\x00\x01\x00\x01\x00\x00"
def test_bit_image_both(): def test_bit_image_both() -> None:
""" """
Test printing black/white bit image (raster) Test printing black/white bit image (raster)
""" """
@@ -48,7 +48,7 @@ def test_bit_image_both():
assert instance.output == b"\x1dv0\x00\x01\x00\x02\x00\xc0\x00" assert instance.output == b"\x1dv0\x00\x01\x00\x02\x00\xc0\x00"
def test_bit_image_transparent(): def test_bit_image_transparent() -> None:
""" """
Test printing black/transparent bit image (raster) Test printing black/transparent bit image (raster)
""" """
@@ -58,7 +58,7 @@ def test_bit_image_transparent():
# Column format print # Column format print
def test_bit_image_colfmt_black(): def test_bit_image_colfmt_black() -> None:
""" """
Test printing solid black bit image (column format) Test printing solid black bit image (column format)
""" """
@@ -67,7 +67,7 @@ def test_bit_image_colfmt_black():
assert instance.output == b"\x1b3\x10\x1b*!\x01\x00\x80\x00\x00\x0a\x1b2" assert instance.output == b"\x1b3\x10\x1b*!\x01\x00\x80\x00\x00\x0a\x1b2"
def test_bit_image_colfmt_white(): def test_bit_image_colfmt_white() -> None:
""" """
Test printing solid white bit image (column format) Test printing solid white bit image (column format)
""" """
@@ -76,7 +76,7 @@ def test_bit_image_colfmt_white():
assert instance.output == b"\x1b3\x10\x1b*!\x01\x00\x00\x00\x00\x0a\x1b2" assert instance.output == b"\x1b3\x10\x1b*!\x01\x00\x00\x00\x00\x0a\x1b2"
def test_bit_image_colfmt_both(): def test_bit_image_colfmt_both() -> None:
""" """
Test printing black/white bit image (column format) Test printing black/white bit image (column format)
""" """
@@ -87,7 +87,7 @@ def test_bit_image_colfmt_both():
) )
def test_bit_image_colfmt_transparent(): def test_bit_image_colfmt_transparent() -> None:
""" """
Test printing black/transparent bit image (column format) Test printing black/transparent bit image (column format)
""" """
@@ -99,7 +99,7 @@ def test_bit_image_colfmt_transparent():
# Graphics print # Graphics print
def test_graphics_black(): def test_graphics_black() -> None:
""" """
Test printing solid black graphics Test printing solid black graphics
""" """
@@ -111,7 +111,7 @@ def test_graphics_black():
) )
def test_graphics_white(): def test_graphics_white() -> None:
""" """
Test printing solid white graphics Test printing solid white graphics
""" """
@@ -123,7 +123,7 @@ def test_graphics_white():
) )
def test_graphics_both(): def test_graphics_both() -> None:
""" """
Test printing black/white graphics Test printing black/white graphics
""" """
@@ -135,7 +135,7 @@ def test_graphics_both():
) )
def test_graphics_transparent(): def test_graphics_transparent() -> None:
""" """
Test printing black/transparent graphics Test printing black/transparent graphics
""" """
@@ -147,7 +147,7 @@ def test_graphics_transparent():
) )
def test_large_graphics(): def test_large_graphics() -> None:
""" """
Test whether 'large' graphics that induce a fragmentation are handled correctly. Test whether 'large' graphics that induce a fragmentation are handled correctly.
""" """
@@ -162,13 +162,13 @@ def test_large_graphics():
@pytest.fixture @pytest.fixture
def dummy_with_width(): def dummy_with_width() -> printer.Dummy:
instance = printer.Dummy() instance = printer.Dummy()
instance.profile.profile_data = {"media": {"width": {"pixels": 384}}} instance.profile.profile_data = {"media": {"width": {"pixels": 384}}}
return instance return instance
def test_width_too_large(dummy_with_width): def test_width_too_large(dummy_with_width: printer.Dummy) -> None:
""" """
Test printing an image that is too large in width. Test printing an image that is too large in width.
""" """
@@ -180,7 +180,7 @@ def test_width_too_large(dummy_with_width):
instance.image(Image.new("RGB", (384, 200))) instance.image(Image.new("RGB", (384, 200)))
def test_center_image(dummy_with_width): def test_center_image(dummy_with_width: printer.Dummy) -> None:
instance = dummy_with_width instance = dummy_with_width
with pytest.raises(ImageWidthError): with pytest.raises(ImageWidthError):

View File

@@ -11,21 +11,21 @@
import escpos.printer as printer import escpos.printer as printer
def test_function_linedisplay_select_on(): def test_function_linedisplay_select_on() -> None:
"""test the linedisplay_select function (activate)""" """test the linedisplay_select function (activate)"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_select(select_display=True) instance.linedisplay_select(select_display=True)
assert instance.output == b"\x1B\x3D\x02" assert instance.output == b"\x1B\x3D\x02"
def test_function_linedisplay_select_off(): def test_function_linedisplay_select_off() -> None:
"""test the linedisplay_select function (deactivate)""" """test the linedisplay_select function (deactivate)"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_select(select_display=False) instance.linedisplay_select(select_display=False)
assert instance.output == b"\x1B\x3D\x01" assert instance.output == b"\x1B\x3D\x01"
def test_function_linedisplay_clear(): def test_function_linedisplay_clear() -> None:
"""test the linedisplay_clear function""" """test the linedisplay_clear function"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_clear() instance.linedisplay_clear()

View File

@@ -11,14 +11,14 @@
import escpos.printer as printer import escpos.printer as printer
def test_function_panel_button_on(): def test_function_panel_button_on() -> None:
"""test the panel button function (enabling) by comparing output""" """test the panel button function (enabling) by comparing output"""
instance = printer.Dummy() instance = printer.Dummy()
instance.panel_buttons() instance.panel_buttons()
assert instance.output == b"\x1B\x63\x35\x00" assert instance.output == b"\x1B\x63\x35\x00"
def test_function_panel_button_off(): def test_function_panel_button_off() -> None:
"""test the panel button function (disabling) by comparing output""" """test the panel button function (disabling) by comparing output"""
instance = printer.Dummy() instance = printer.Dummy()
instance.panel_buttons(False) instance.panel_buttons(False)

View File

@@ -3,7 +3,7 @@
:author: `Michael Billington <michael.billington@gmail.com>`_ :author: `Michael Billington <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_ :copyright: Copyright (c) 2023 `Michael Billington <michael.billington@gmail.com>`_
:license: MIT :license: MIT
""" """
@@ -14,7 +14,7 @@ import escpos.printer as printer
from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1 from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
def test_defaults(): def test_defaults() -> None:
"""Test QR code with defaults""" """Test QR code with defaults"""
instance = printer.Dummy() instance = printer.Dummy()
instance.qr("1234", native=True) instance.qr("1234", native=True)
@@ -25,14 +25,14 @@ def test_defaults():
assert instance.output == expected assert instance.output == expected
def test_empty(): def test_empty() -> None:
"""Test QR printing blank code""" """Test QR printing blank code"""
instance = printer.Dummy() instance = printer.Dummy()
instance.qr("", native=True) instance.qr("", native=True)
assert instance.output == b"" assert instance.output == b""
def test_ec(): def test_ec() -> None:
"""Test QR error correction setting""" """Test QR error correction setting"""
instance = printer.Dummy() instance = printer.Dummy()
instance.qr("1234", native=True, ec=QR_ECLEVEL_H) instance.qr("1234", native=True, ec=QR_ECLEVEL_H)
@@ -43,7 +43,7 @@ def test_ec():
assert instance.output == expected assert instance.output == expected
def test_size(): def test_size() -> None:
"""Test QR box size""" """Test QR box size"""
instance = printer.Dummy() instance = printer.Dummy()
instance.qr("1234", native=True, size=7) instance.qr("1234", native=True, size=7)
@@ -54,7 +54,7 @@ def test_size():
assert instance.output == expected assert instance.output == expected
def test_model(): def test_model() -> None:
"""Test QR model""" """Test QR model"""
instance = printer.Dummy() instance = printer.Dummy()
instance.qr("1234", native=True, model=QR_MODEL_1) instance.qr("1234", native=True, model=QR_MODEL_1)
@@ -65,44 +65,28 @@ def test_model():
assert instance.output == expected assert instance.output == expected
def test_invalid_ec(): def test_invalid_ec() -> None:
"""Test invalid QR error correction""" """Test invalid QR error correction"""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(ValueError): with pytest.raises(ValueError):
instance.qr("1234", native=True, ec=-1) instance.qr("1234", native=True, ec=-1)
def test_invalid_size(): def test_invalid_size() -> None:
"""Test invalid QR size""" """Test invalid QR size"""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(ValueError): with pytest.raises(ValueError):
instance.qr("1234", native=True, size=0) instance.qr("1234", native=True, size=0)
def test_invalid_model(): def test_invalid_model() -> None:
"""Test invalid QR model""" """Test invalid QR model"""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(ValueError): with pytest.raises(ValueError):
instance.qr("1234", native=True, model="Hello") instance.qr("1234", native=True, model="Hello")
@pytest.mark.skip("this test has to be debugged") def test_image_invalid_model() -> None:
def test_image():
"""Test QR as image"""
instance = printer.Dummy()
instance.qr("1", native=False, size=1)
print(instance.output)
expected = (
b"\x1bt\x00\n"
b"\x1dv0\x00\x03\x00\x17\x00\x00\x00\x00\x7f]\xfcA\x19\x04]it]et"
b"]ItA=\x04\x7fU\xfc\x00\x0c\x00y~t4\x7f =\xa84j\xd9\xf0\x05\xd4\x90\x00"
b"i(\x7f<\xa8A \xd8]'\xc4]y\xf8]E\x80Ar\x94\x7fR@\x00\x00\x00"
b"\n\n"
)
assert instance.output == expected
def test_image_invalid_model():
"""Test unsupported QR model as image""" """Test unsupported QR model as image"""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(ValueError): with pytest.raises(ValueError):
@@ -114,6 +98,6 @@ def instance():
return printer.Dummy() return printer.Dummy()
def test_center_not_implementer(instance): def test_center_not_implementer(instance: printer.Dummy) -> None:
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
instance.qr("test", center=True, native=True) instance.qr("test", center=True, native=True)

View File

@@ -0,0 +1,99 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for the non-native part of qr()
:author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2023 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import warnings
import mock
import pytest
from PIL import Image
from escpos.printer import Dummy
def test_image() -> None:
"""Test QR as image"""
instance = Dummy()
image_arguments = {
"high_density_vertical": True,
"high_density_horizontal": True,
"impl": "bitImageRaster",
"fragment_height": 960,
"center": False,
}
instance.qr("1", native=False, image_arguments=image_arguments, size=1)
print(instance.output)
expected = (
b"\x1bt\x00\n"
b"\x1dv0\x00\x03\x00\x17\x00\x00\x00\x00\x7f\x1d\xfcAu\x04]\x1dt]et"
b"]%tAI\x04\x7fU\xfc\x00 \x00}\xca\xa8h\xdf u\x95\x80x/ \x0b\xf4\x98\x00"
b"T\x90\x7fzxA\x00\xd0]zp]o ]u\x80Ao(\x7fd\x90\x00\x00\x00"
b"\n\n"
)
assert instance.output == expected
@mock.patch("escpos.printer.Dummy.image", spec=Dummy)
def test_type_of_object_passed_to_image_function(img_function):
"""
Test the type of object that is passed to the image function during non-native qr-printing.
The type should be PIL.Image
"""
d = Dummy()
d.qr("LoremIpsum")
args, kwargs = img_function.call_args
assert isinstance(args[0], Image.Image)
@mock.patch("escpos.printer.Dummy.image", spec=Dummy)
def test_parameter_image_arguments_passed_to_image_function(img_function):
"""Test the parameter passed to non-native qr printing."""
d = Dummy()
d.qr(
"LoremIpsum",
native=False,
image_arguments={
"impl": "bitImageColumn",
"high_density_vertical": False,
"center": True,
},
)
args, kwargs = img_function.call_args
assert "impl" in kwargs
assert kwargs["impl"] == "bitImageColumn"
assert "high_density_vertical" in kwargs
assert kwargs["high_density_vertical"] is False
assert "center" in kwargs
assert kwargs["center"]
@pytest.fixture
def instance():
return Dummy()
def test_center(instance):
"""Test printing qr codes."""
instance.qr("LoremIpsum", center=True)
def test_deprecated_arguments(instance):
"""Test deprecation warning."""
with warnings.catch_warnings(record=True) as w:
# cause all warnings to always be triggered
warnings.simplefilter("always")
instance.qr(
"LoremIpsum",
impl="bitImageRaster",
image_arguments={"impl": "bitImageColumn"},
)
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated" in str(w[-1].message)

View File

@@ -1,12 +1,13 @@
from typing import Optional
import pytest import pytest
import six
import escpos.printer as printer import escpos.printer as printer
from escpos.constants import SET_FONT, TXT_NORMAL, TXT_SIZE, TXT_STYLE from escpos.constants import SET_FONT, TXT_NORMAL, TXT_SIZE, TXT_STYLE
from escpos.exceptions import SetVariableError from escpos.exceptions import SetVariableError
def test_default_values_with_default(): def test_default_values_with_default() -> None:
"""Default test, please copy and paste this block to test set method calls""" """Default test, please copy and paste this block to test set method calls"""
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default() instance.set_with_default()
@@ -26,7 +27,7 @@ def test_default_values_with_default():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_default_values(): def test_default_values() -> None:
"""Default test""" """Default test"""
instance = printer.Dummy() instance = printer.Dummy()
instance.set() instance.set()
@@ -37,7 +38,7 @@ def test_default_values():
# Size tests # Size tests
def test_set_size_2h(): def test_set_size_2h() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(double_height=True) instance.set_with_default(double_height=True)
@@ -56,7 +57,7 @@ def test_set_size_2h():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2h_no_default(): def test_set_size_2h_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_height=True) instance.set(double_height=True)
@@ -68,7 +69,7 @@ def test_set_size_2h_no_default():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2w(): def test_set_size_2w() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(double_width=True) instance.set_with_default(double_width=True)
@@ -87,7 +88,7 @@ def test_set_size_2w():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2w_no_default(): def test_set_size_2w_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_width=True) instance.set(double_width=True)
@@ -99,7 +100,7 @@ def test_set_size_2w_no_default():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2x(): def test_set_size_2x() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(double_height=True, double_width=True) instance.set_with_default(double_height=True, double_width=True)
@@ -118,7 +119,7 @@ def test_set_size_2x():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2x_no_default(): def test_set_size_2x_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_width=True, double_height=True) instance.set(double_width=True, double_height=True)
@@ -130,13 +131,13 @@ def test_set_size_2x_no_default():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_custom(): def test_set_size_custom() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(custom_size=True, width=8, height=7) instance.set_with_default(custom_size=True, width=8, height=7)
expected_sequence = ( expected_sequence = (
TXT_SIZE, # Custom text size, no normal reset TXT_SIZE, # Custom text size, no normal reset
six.int2byte(TXT_STYLE["width"][8] + TXT_STYLE["height"][7]), bytes((TXT_STYLE["width"][8] + TXT_STYLE["height"][7],)),
TXT_STYLE["flip"][False], # Flip OFF TXT_STYLE["flip"][False], # Flip OFF
TXT_STYLE["smooth"][False], # Smooth OFF TXT_STYLE["smooth"][False], # Smooth OFF
TXT_STYLE["bold"][False], # Bold OFF TXT_STYLE["bold"][False], # Bold OFF
@@ -151,13 +152,13 @@ def test_set_size_custom():
@pytest.mark.parametrize("width", [1, 8]) @pytest.mark.parametrize("width", [1, 8])
@pytest.mark.parametrize("height", [1, 8]) @pytest.mark.parametrize("height", [1, 8])
def test_set_size_custom_no_default(width, height): def test_set_size_custom_no_default(width: int, height: int) -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(custom_size=True, width=width, height=height) instance.set(custom_size=True, width=width, height=height)
expected_sequence = ( expected_sequence = (
TXT_SIZE, # Custom text size, no normal reset TXT_SIZE, # Custom text size, no normal reset
six.int2byte(TXT_STYLE["width"][width] + TXT_STYLE["height"][height]), bytes((TXT_STYLE["width"][width] + TXT_STYLE["height"][height],)),
) )
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
@@ -165,7 +166,9 @@ def test_set_size_custom_no_default(width, height):
@pytest.mark.parametrize("width", [None, 0, 9, 10, 4444]) @pytest.mark.parametrize("width", [None, 0, 9, 10, 4444])
@pytest.mark.parametrize("height", [None, 0, 9, 10, 4444]) @pytest.mark.parametrize("height", [None, 0, 9, 10, 4444])
def test_set_size_custom_invalid_input(width, height): def test_set_size_custom_invalid_input(
width: Optional[int], height: Optional[int]
) -> None:
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(SetVariableError): with pytest.raises(SetVariableError):
instance.set(custom_size=True, width=width, height=height) instance.set(custom_size=True, width=width, height=height)
@@ -174,7 +177,7 @@ def test_set_size_custom_invalid_input(width, height):
# Flip # Flip
def test_set_flip(): def test_set_flip() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(flip=True) instance.set_with_default(flip=True)
@@ -193,7 +196,7 @@ def test_set_flip():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_flip_no_default(): def test_set_flip_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(flip=True) instance.set(flip=True)
@@ -205,7 +208,7 @@ def test_set_flip_no_default():
# Smooth # Smooth
def test_smooth(): def test_smooth() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(smooth=True) instance.set_with_default(smooth=True)
@@ -227,7 +230,7 @@ def test_smooth():
# Type # Type
def test_set_bold(): def test_set_bold() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(bold=True) instance.set_with_default(bold=True)
@@ -246,7 +249,7 @@ def test_set_bold():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_underline(): def test_set_underline() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(underline=1) instance.set_with_default(underline=1)
@@ -265,7 +268,7 @@ def test_set_underline():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_underline2(): def test_set_underline2() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(underline=2) instance.set_with_default(underline=2)
@@ -287,7 +290,7 @@ def test_set_underline2():
# Align # Align
def test_align_center(): def test_align_center() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(align="center") instance.set_with_default(align="center")
@@ -306,7 +309,7 @@ def test_align_center():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_align_right(): def test_align_right() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(align="right") instance.set_with_default(align="right")
@@ -328,7 +331,7 @@ def test_align_right():
# Densities # Densities
def test_densities(): def test_densities() -> None:
for density in range(8): for density in range(8):
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(density=density) instance.set_with_default(density=density)
@@ -352,7 +355,7 @@ def test_densities():
# Invert # Invert
def test_invert(): def test_invert() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set_with_default(invert=True) instance.set_with_default(invert=True)

View File

@@ -11,16 +11,16 @@ def instance():
return printer.Dummy() return printer.Dummy()
def test_soft_barcode_ean8_invalid(instance): def test_soft_barcode_ean8_invalid(instance: printer.Dummy) -> None:
"""test with an invalid barcode""" """test with an invalid barcode"""
with pytest.raises(barcode.errors.BarcodeError): with pytest.raises(barcode.errors.BarcodeError):
instance.barcode("1234", "ean8", force_software=True) instance.barcode("1234", "ean8", force_software=True)
def test_soft_barcode_ean8(instance): def test_soft_barcode_ean8(instance: printer.Dummy) -> None:
"""test with a valid ean8 barcode""" """test with a valid ean8 barcode"""
instance.barcode("1234567", "ean8", force_software=True) instance.barcode("1234567", "ean8", force_software=True)
def test_soft_barcode_ean8_nocenter(instance): def test_soft_barcode_ean8_nocenter(instance: printer.Dummy) -> None:
instance.barcode("1234567", "ean8", align_ct=False, force_software=True) instance.barcode("1234567", "ean8", align_ct=False, force_software=True)

View File

@@ -10,26 +10,25 @@
import hypothesis.strategies as st import hypothesis.strategies as st
import mock import mock
import pytest from hypothesis import given
from hypothesis import assume, given
from escpos.printer import Dummy from escpos.printer import Dummy
def get_printer(): def get_printer() -> Dummy:
return Dummy(magic_encode_args={"disabled": True, "encoding": "CP437"}) return Dummy(magic_encode_args={"disabled": True, "encoding": "CP437"})
@given(text=st.text()) @given(text=st.text())
def test_text(text): def test_text(text: str):
"""Test that text() calls the MagicEncode object.""" """Test that text() calls the MagicEncode object."""
instance = get_printer() instance = get_printer()
instance.magic.write = mock.Mock() with mock.patch.object(instance.magic, "write") as write:
instance.text(text) instance.text(text)
instance.magic.write.assert_called_with(text) write.assert_called_with(text)
def test_block_text(): def test_block_text() -> None:
printer = get_printer() printer = get_printer()
printer.block_text( printer.block_text(
"All the presidents men were eating falafel for breakfast.", font="a" "All the presidents men were eating falafel for breakfast.", font="a"
@@ -39,25 +38,25 @@ def test_block_text():
) )
def test_textln(): def test_textln() -> None:
printer = get_printer() printer = get_printer()
printer.textln("hello, world") printer.textln("hello, world")
assert printer.output == b"hello, world\n" assert printer.output == b"hello, world\n"
def test_textln_empty(): def test_textln_empty() -> None:
printer = get_printer() printer = get_printer()
printer.textln() printer.textln()
assert printer.output == b"\n" assert printer.output == b"\n"
def test_ln(): def test_ln() -> None:
printer = get_printer() printer = get_printer()
printer.ln() printer.ln()
assert printer.output == b"\n" assert printer.output == b"\n"
def test_multiple_ln(): def test_multiple_ln() -> None:
printer = get_printer() printer = get_printer()
printer.ln(3) printer.ln(3)
assert printer.output == b"\n\n\n" assert printer.output == b"\n\n\n"

View File

@@ -3,19 +3,19 @@ import pytest
from escpos.printer import Dummy from escpos.printer import Dummy
def test_line_spacing_code_gen(): def test_line_spacing_code_gen() -> None:
printer = Dummy() printer = Dummy()
printer.line_spacing(10) printer.line_spacing(10)
assert printer.output == b"\x1b3\n" assert printer.output == b"\x1b3\n"
def test_line_spacing_rest(): def test_line_spacing_rest() -> None:
printer = Dummy() printer = Dummy()
printer.line_spacing() printer.line_spacing()
assert printer.output == b"\x1b2" assert printer.output == b"\x1b2"
def test_line_spacing_error_handling(): def test_line_spacing_error_handling() -> None:
printer = Dummy() printer = Dummy()
with pytest.raises(ValueError): with pytest.raises(ValueError):
printer.line_spacing(99, divisor=44) printer.line_spacing(99, divisor=44)

View File

@@ -7,11 +7,12 @@ converted to ESC/POS column & raster formats.
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_ :copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
:license: MIT :license: MIT
""" """
from typing import List
from escpos.image import EscposImage from escpos.image import EscposImage
def test_image_black(): def test_image_black() -> None:
""" """
Test rendering solid black image Test rendering solid black image
""" """
@@ -19,7 +20,7 @@ def test_image_black():
_load_and_check_img("canvas_black." + img_format, 1, 1, b"\x80", [b"\x80"]) _load_and_check_img("canvas_black." + img_format, 1, 1, b"\x80", [b"\x80"])
def test_image_black_transparent(): def test_image_black_transparent() -> None:
""" """
Test rendering black/transparent image Test rendering black/transparent image
""" """
@@ -29,7 +30,7 @@ def test_image_black_transparent():
) )
def test_image_black_white(): def test_image_black_white() -> None:
""" """
Test rendering black/white image Test rendering black/white image
""" """
@@ -39,7 +40,7 @@ def test_image_black_white():
) )
def test_image_white(): def test_image_white() -> None:
""" """
Test rendering solid white image Test rendering solid white image
""" """
@@ -47,7 +48,7 @@ def test_image_white():
_load_and_check_img("canvas_white." + img_format, 1, 1, b"\x00", [b"\x00"]) _load_and_check_img("canvas_white." + img_format, 1, 1, b"\x00", [b"\x00"])
def test_split(): def test_split() -> None:
""" """
test whether the split-function works as expected test whether the split-function works as expected
""" """
@@ -62,12 +63,12 @@ def test_split():
def _load_and_check_img( def _load_and_check_img(
filename, filename: str,
width_expected, width_expected: int,
height_expected, height_expected: int,
raster_format_expected, raster_format_expected: bytes,
column_format_expected, column_format_expected: List[bytes],
): ) -> None:
""" """
Load an image, and test whether raster & column formatted output, sizes, etc match expectations. Load an image, and test whether raster & column formatted output, sizes, etc match expectations.
""" """

View File

@@ -11,7 +11,7 @@
import escpos.printer as printer import escpos.printer as printer
def test_instantiation(): def test_instantiation() -> None:
"""test the instantiation of a escpos-printer class and basic printing""" """test the instantiation of a escpos-printer class and basic printing"""
instance = printer.Dummy() instance = printer.Dummy()
instance.text("This is a test\n") instance.text("This is a test\n")

View File

@@ -13,7 +13,7 @@ import tempfile
import pytest import pytest
def test_instantiation(): def test_instantiation() -> None:
"""test the instantiation of a escpos-printer class""" """test the instantiation of a escpos-printer class"""
# inject an environment variable that points to an empty capabilities file # inject an environment variable that points to an empty capabilities file
os.environ["ESCPOS_CAPABILITIES_FILE"] = tempfile.NamedTemporaryFile().name os.environ["ESCPOS_CAPABILITIES_FILE"] = tempfile.NamedTemporaryFile().name

View File

@@ -13,7 +13,8 @@ import hypothesis.strategies as st
import pytest import pytest
from hypothesis import example, given from hypothesis import example, given
from escpos.exceptions import CharCodeError, Error from escpos import printer
from escpos.exceptions import Error
from escpos.katakana import encode_katakana from escpos.katakana import encode_katakana
from escpos.magicencode import Encoder, MagicEncode from escpos.magicencode import Encoder, MagicEncode
@@ -23,23 +24,23 @@ class TestEncoder:
Tests the single encoders. Tests the single encoders.
""" """
def test_can_encode(self): def test_can_encode(self) -> None:
assert not Encoder({"CP437": 1}).can_encode("CP437", "") assert not Encoder({"CP437": 1}).can_encode("CP437", "")
assert Encoder({"CP437": 1}).can_encode("CP437", "á") assert Encoder({"CP437": 1}).can_encode("CP437", "á")
assert not Encoder({"foobar": 1}).can_encode("foobar", "a") assert not Encoder({"foobar": 1}).can_encode("foobar", "a")
def test_find_suitable_encoding(self): def test_find_suitable_encoding(self) -> None:
assert not Encoder({"CP437": 1}).find_suitable_encoding("") assert not Encoder({"CP437": 1}).find_suitable_encoding("")
assert Encoder({"CP858": 1}).find_suitable_encoding("") == "CP858" assert Encoder({"CP858": 1}).find_suitable_encoding("") == "CP858"
def test_find_suitable_encoding_unnecessary_codepage_swap(self): def test_find_suitable_encoding_unnecessary_codepage_swap(self) -> None:
enc = Encoder({"CP857": 1, "CP437": 2, "CP1252": 3, "CP852": 4, "CP858": 5}) enc = Encoder({"CP857": 1, "CP437": 2, "CP1252": 3, "CP852": 4, "CP858": 5})
# desired behavior would be that the encoder always stays in the lower # desired behavior would be that the encoder always stays in the lower
# available codepages if possible # available codepages if possible
for character in ("Á", "É", "Í", "Ó", "Ú"): for character in ("Á", "É", "Í", "Ó", "Ú"):
assert enc.find_suitable_encoding(character) == "CP857" assert enc.find_suitable_encoding(character) == "CP857"
def test_get_encoding(self): def test_get_encoding(self) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Encoder({}).get_encoding_name("latin1") Encoder({}).get_encoding_name("latin1")
@@ -54,7 +55,7 @@ class TestMagicEncode:
Test initialization. Test initialization.
""" """
def test_disabled_requires_encoding(self, driver): def test_disabled_requires_encoding(self, driver: printer.Dummy) -> None:
""" """
Test that disabled without encoder raises an error. Test that disabled without encoder raises an error.
@@ -64,33 +65,33 @@ class TestMagicEncode:
MagicEncode(driver, disabled=True) MagicEncode(driver, disabled=True)
class TestWriteWithEncoding: class TestWriteWithEncoding:
def test_init_from_none(self, driver): def test_init_from_none(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding=None) encode = MagicEncode(driver, encoding=None)
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\x1bt\x13\xd5 ist teuro." assert driver.output == b"\x1bt\x13\xd5 ist teuro."
def test_change_from_another(self, driver): def test_change_from_another(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP437") encode = MagicEncode(driver, encoding="CP437")
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\x1bt\x13\xd5 ist teuro." assert driver.output == b"\x1bt\x13\xd5 ist teuro."
def test_no_change(self, driver): def test_no_change(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP858") encode = MagicEncode(driver, encoding="CP858")
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\xd5 ist teuro." assert driver.output == b"\xd5 ist teuro."
class TestWrite: class TestWrite:
def test_write(self, driver): def test_write(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.write("€ ist teuro.") encode.write("€ ist teuro.")
assert driver.output == b"\x1bt\x0f\xa4 ist teuro." assert driver.output == b"\x1bt\x0f\xa4 ist teuro."
def test_write_disabled(self, driver): def test_write_disabled(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP437", disabled=True) encode = MagicEncode(driver, encoding="CP437", disabled=True)
encode.write("€ ist teuro.") encode.write("€ ist teuro.")
assert driver.output == b"? ist teuro." assert driver.output == b"? ist teuro."
def test_write_no_codepage(self, driver): def test_write_no_codepage(self, driver: printer.Dummy) -> None:
encode = MagicEncode( encode = MagicEncode(
driver, driver,
defaultsymbol="_", defaultsymbol="_",
@@ -101,7 +102,7 @@ class TestMagicEncode:
assert driver.output == b"_ ist teuro." assert driver.output == b"_ ist teuro."
class TestForceEncoding: class TestForceEncoding:
def test(self, driver): def test(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.force_encoding("CP437") encode.force_encoding("CP437")
assert driver.output == b"\x1bt\x00" assert driver.output == b"\x1bt\x00"
@@ -122,9 +123,9 @@ class TestKatakana:
@example("カタカナ") @example("カタカナ")
@example("あいうえお") @example("あいうえお")
@example("ハンカクカタカナ") @example("ハンカクカタカナ")
def test_accept(self, text): def test_accept(self, text: str) -> None:
encode_katakana(text) encode_katakana(text)
def test_result(self): def test_result(self) -> None:
assert encode_katakana("カタカナ") == b"\xb6\xc0\xb6\xc5" assert encode_katakana("カタカナ") == b"\xb6\xc0\xb6\xc5"
assert encode_katakana("あいうえお") == b"\xb1\xb2\xb3\xb4\xb5" assert encode_katakana("あいうえお") == b"\xb1\xb2\xb3\xb4\xb5"

View File

@@ -19,7 +19,7 @@ pytestmark = pytest.mark.skipif(
) )
def test_device_not_initialized(cupsprinter): def test_device_not_initialized(cupsprinter) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN it is not initialized WHEN it is not initialized
@@ -28,7 +28,7 @@ def test_device_not_initialized(cupsprinter):
assert cupsprinter._device is False assert cupsprinter._device is False
def test_open_raise_exception(cupsprinter, devicenotfounderror): def test_open_raise_exception(cupsprinter, devicenotfounderror) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN open() is set to raise a DeviceNotFoundError on error WHEN open() is set to raise a DeviceNotFoundError on error
@@ -40,7 +40,7 @@ def test_open_raise_exception(cupsprinter, devicenotfounderror):
cupsprinter.open(raise_not_found=True) cupsprinter.open(raise_not_found=True)
def test_open_not_raise_exception(cupsprinter, caplog): def test_open_not_raise_exception(cupsprinter, caplog) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN open() is set to not raise on error but simply cancel WHEN open() is set to not raise on error but simply cancel
@@ -55,7 +55,7 @@ def test_open_not_raise_exception(cupsprinter, caplog):
assert cupsprinter.device is None assert cupsprinter.device is None
def test_open(cupsprinter, caplog, mocker): def test_open(cupsprinter, caplog, mocker) -> None:
""" """
GIVEN a cups printer object and a mocked pycups device GIVEN a cups printer object and a mocked pycups device
WHEN a valid connection to a device is opened WHEN a valid connection to a device is opened
@@ -74,7 +74,7 @@ def test_open(cupsprinter, caplog, mocker):
assert cupsprinter.device assert cupsprinter.device
def test_close_on_reopen(cupsprinter, mocker): def test_close_on_reopen(cupsprinter, mocker) -> None:
""" """
GIVEN a cups printer object and a mocked connection GIVEN a cups printer object and a mocked connection
WHEN a valid connection to a device is reopened before close WHEN a valid connection to a device is reopened before close
@@ -112,7 +112,7 @@ def test_close(cupsprinter, caplog, mocker):
assert cupsprinter._device is False assert cupsprinter._device is False
def test_send_on_close(cupsprinter, mocker): def test_send_on_close(cupsprinter, mocker) -> None:
""" """
GIVEN a cups printer object and a mocked pycups device GIVEN a cups printer object and a mocked pycups device
WHEN closing connection before send the buffer WHEN closing connection before send the buffer
@@ -133,7 +133,7 @@ def test_send_on_close(cupsprinter, mocker):
assert cupsprinter.pending_job is False assert cupsprinter.pending_job is False
def test_raw_raise_exception(cupsprinter): def test_raw_raise_exception(cupsprinter) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN passing a non byte string to _raw() WHEN passing a non byte string to _raw()
@@ -145,7 +145,7 @@ def test_raw_raise_exception(cupsprinter):
assert cupsprinter.pending_job is False assert cupsprinter.pending_job is False
def test_raw(cupsprinter): def test_raw(cupsprinter) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN passing a byte string to _raw() WHEN passing a byte string to _raw()
@@ -156,7 +156,7 @@ def test_raw(cupsprinter):
assert cupsprinter.tmpfile.read() == b"Test" assert cupsprinter.tmpfile.read() == b"Test"
def test_printers_no_device(cupsprinter): def test_printers_no_device(cupsprinter) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN device is None WHEN device is None
@@ -166,11 +166,11 @@ def test_printers_no_device(cupsprinter):
assert cupsprinter.printers == {} assert cupsprinter.printers == {}
def test_read_no_device(cupsprinter): def test_read_no_device(cupsprinter) -> None:
""" """
GIVEN a cups printer object GIVEN a cups printer object
WHEN device is None WHEN device is None
THEN check the return value is [] THEN check the return value is b'8'
""" """
cupsprinter.device = None cupsprinter.device = None
assert cupsprinter._read() == [] assert cupsprinter._read() == b"8"

View File

@@ -14,16 +14,16 @@ import escpos
import escpos.exceptions import escpos.exceptions
def test_raise_error_wrongly(): def test_raise_error_wrongly() -> None:
"""raise error the wrong way """raise error the wrong way
should reproduce https://github.com/python-escpos/python-escpos/issues/257 should reproduce https://github.com/python-escpos/python-escpos/issues/257
""" """
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
raise escpos.Error("This should raise an AttributeError.") raise escpos.Error("This should raise an AttributeError.") # type: ignore [attr-defined]
def tests_raise_error(): def tests_raise_error() -> None:
"""raise error the right way""" """raise error the right way"""
with pytest.raises(escpos.exceptions.Error): with pytest.raises(escpos.exceptions.Error):
raise escpos.exceptions.Error("This should raise an error.") raise escpos.exceptions.Error("This should raise an error.")

View File

@@ -12,7 +12,7 @@ import escpos.escpos as escpos
import escpos.printer as printer import escpos.printer as printer
def test_with_statement(): def test_with_statement() -> None:
"""Use with statement """Use with statement
.. todo:: Extend these tests as they don't really do anything at the moment""" .. todo:: Extend these tests as they don't really do anything at the moment"""

View File

@@ -21,7 +21,7 @@ deps = jaconv
pytest-cov pytest-cov
pytest-mock pytest-mock
hypothesis>=6.83 hypothesis>=6.83
python-barcode python-barcode>=0.15.0,<1
extras = all extras = all
commands = pytest commands = pytest
passenv = ESCPOS_CAPABILITIES_PICKLE_DIR, ESCPOS_CAPABILITIES_FILE, CI, TRAVIS, TRAVIS_*, APPVEYOR, APPVEYOR_*, CODECOV_* passenv = ESCPOS_CAPABILITIES_PICKLE_DIR, ESCPOS_CAPABILITIES_FILE, CI, TRAVIS, TRAVIS_*, APPVEYOR, APPVEYOR_*, CODECOV_*
@@ -32,7 +32,7 @@ basepython = python
changedir = doc changedir = doc
deps = sphinx>=7.2.3 deps = sphinx>=7.2.3
setuptools_scm setuptools_scm
python-barcode python-barcode>=0.15.0,<1
sphinx-argparse sphinx-argparse
sphinxcontrib-spelling>=8.0.0 sphinxcontrib-spelling>=8.0.0
sphinxcontrib.datatemplates sphinxcontrib.datatemplates
@@ -58,6 +58,7 @@ deps = mypy
types-Pillow types-Pillow
types-pyserial types-pyserial
types-pywin32>=306.0.0.6 types-pywin32>=306.0.0.6
types-flask
hypothesis>=6.83 hypothesis>=6.83
jaconv jaconv
commands = mypy src test commands = mypy src test examples