From 3177c8d4113c32103c59ed9db84de2fda51c0181 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler <4189642+patkan@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:37:50 +0200 Subject: [PATCH] split off dependencies for optional installation (#546) * add inheritance diagrams to all printers and exceptions * split off printer implementations into separate files * add wrapper that thros RuntimeError if not importable * add dependency check for lp * add dependency check for pyserial * added check for usability * import Win32Raw * include WIn32Raw in documentation * enable all extras on tox * update github workflow --- .github/workflows/pythonpackage.yml | 1 + INSTALL | 2 +- doc/user/installation.rst | 10 +- doc/user/printers.rst | 8 +- setup.cfg | 17 +- src/escpos/constants.py | 2 +- src/escpos/escpos.py | 2 +- src/escpos/exceptions.py | 95 ++++- src/escpos/printer.py | 614 ---------------------------- src/escpos/printer/__init__.py | 22 + src/escpos/printer/cups.py | 180 ++++++++ src/escpos/printer/dummy.py | 70 ++++ src/escpos/printer/file.py | 78 ++++ src/escpos/printer/lp.py | 110 +++++ src/escpos/printer/network.py | 94 +++++ src/escpos/printer/serial.py | 153 +++++++ src/escpos/printer/usb.py | 162 ++++++++ src/escpos/printer/win32raw.py | 115 ++++++ test/test_function_check_barcode.py | 6 +- tox.ini | 1 + 20 files changed, 1115 insertions(+), 627 deletions(-) delete mode 100644 src/escpos/printer.py create mode 100644 src/escpos/printer/__init__.py create mode 100644 src/escpos/printer/cups.py create mode 100644 src/escpos/printer/dummy.py create mode 100644 src/escpos/printer/file.py create mode 100644 src/escpos/printer/lp.py create mode 100644 src/escpos/printer/network.py create mode 100644 src/escpos/printer/serial.py create mode 100644 src/escpos/printer/usb.py create mode 100644 src/escpos/printer/win32raw.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b269661..9929408 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -27,6 +27,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + sudo apt-get install -y graphviz libenchant-2-2 gcc libcups2-dev python3-dev xindy python -m pip install --upgrade pip pip install flake8 pytest tox tox-gh-actions if [ -f requirements.txt ]; then pip install -r requirements.txt; fi diff --git a/INSTALL b/INSTALL index b6d6644..53bee6f 100644 --- a/INSTALL +++ b/INSTALL @@ -4,7 +4,7 @@ python-escpos This library is available over pypi. So for most of the use-cases it should be sufficient to run ``` -pip install python-escpos --user # add --pre if you want to install pre-releases +pip install python-escpos[all] --user # add --pre if you want to install pre-releases ``` For more information please read the documentation at https://python-escpos.readthedocs.io/en/latest/user/installation.html diff --git a/doc/user/installation.rst b/doc/user/installation.rst index afb8cc8..fd4d6f7 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -12,7 +12,7 @@ so you can simply enter: :: - pip install python-escpos + pip install python-escpos[all] This should install all necessary dependencies. Apart from that python-escpos is for some versions also available as a Debian package. @@ -21,6 +21,14 @@ always install from PyPi. If you use the ``--pre`` parameter for ``pip``, you will get the latest pre-release. +The following installation options exist: + + * `all`: install all packages available for this platform + * `usb`: install packages required for USB printers + * `serial`: install packages required for serial printers + * `win32`: install packages required for win32 printing (only Windows) + * `cups`: install packages required for CUPS printing + Setup udev for USB-Printers --------------------------- 1. Get the *Product ID* and *Vendor ID* from the lsusb command diff --git a/doc/user/printers.rst b/doc/user/printers.rst index 36ef3a8..342d5d3 100644 --- a/doc/user/printers.rst +++ b/doc/user/printers.rst @@ -1,7 +1,7 @@ Printers ======== -:Last Reviewed: 2023-08-10 +:Last Reviewed: 2023-08-23 As of now there are 8 different types of printer implementations. @@ -116,4 +116,8 @@ This driver uses a native WIN32 interface of Windows in order to print. Please refer to the code for documentation as this driver is currently not included in the documentation build. -.. todo:: Include Win32Raw in documentation build +.. autoclass:: escpos.printer.Win32Raw + :members: + :special-members: + :member-order: bysource + :noindex: \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index ddc4195..f5f47b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,10 +35,8 @@ python_requires = >=3.8 zip_safe = false include_package_data = true install_requires = - pyusb>=1.0.0 Pillow>=2.0 qrcode>=4.0 - pyserial python-barcode>=0.9.1,<1 setuptools six @@ -60,6 +58,21 @@ tests_require = flake8 sphinxcontrib-spelling>=7.2.0 +[options.extras_require] +usb = + pyusb>=1.0.0 +serial = + pyserial +cups = + pycups +win32 = + pywin32; platform_system=='Windows' +all = + pyusb>=1.0.0 + pyserial + pycups + pywin32; platform_system=='Windows' + [flake8] exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py max-line-length = 120 diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 803d9b4..24e3b11 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -5,7 +5,7 @@ This module contains constants that are described in the esc/pos-documentation. Since there is no definitive and unified specification for all esc/pos-like printers the constants could later be moved to `capabilities` as in `escpos-php by @mike42 `_. -:author: `Manuel F Martinez `_ and others +:author: python-escpos developers :organization: Bashlinux and `python-escpos `_ :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :license: MIT diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 50af002..237d960 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -4,7 +4,7 @@ This module contains the abstract base class :py:class:`Escpos`. -:author: `Manuel F Martinez `_ and others +:author: python-escpos developers :organization: Bashlinux and `python-escpos `_ :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :license: MIT diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 8a357d5..b578bf1 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -19,7 +19,7 @@ Result/Exit codes: - `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError` - `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError` -:author: `Manuel F Martinez `_ and others +:author: python-escpos developers :organization: Bashlinux and `python-escpos `_ :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :license: MIT @@ -27,7 +27,14 @@ Result/Exit codes: class Error(Exception): - """Base class for ESC/POS errors.""" + """Base class for ESC/POS errors. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.Error + :parts: 1 + + """ def __init__(self, msg, status=None): """Initialize Error object.""" @@ -48,6 +55,12 @@ class BarcodeTypeError(Error): This exception indicates that no known barcode-type has been entered. The barcode-type has to be one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`. The returned error code is `10`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.BarcodeTypeError + :parts: 1 + """ def __init__(self, msg=""): @@ -67,6 +80,12 @@ class BarcodeSizeError(Error): This exception indicates that the values for the barcode size are out of range. The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`. The resulting returncode is `20`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.BarcodeSizeError + :parts: 1 + """ def __init__(self, msg=""): @@ -86,6 +105,12 @@ class BarcodeCodeError(Error): No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter was True and the check failed. The returncode for this exception is `30`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.BarcodeCodeError + :parts: 1 + """ def __init__(self, msg=""): @@ -103,6 +128,12 @@ class ImageSizeError(Error): """Image height is longer than 255px and can't be printed. The returncode for this exception is `40`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.ImageSizeError + :parts: 1 + """ def __init__(self, msg=""): @@ -122,6 +153,12 @@ class ImageWidthError(Error): """Image width is too large. The return code for this exception is `41`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.ImageWidthError + :parts: 1 + """ def __init__(self, msg=""): @@ -140,6 +177,12 @@ class TextError(Error): This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`. The returncode for this exception is `50`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.TextError + :parts: 1 + """ def __init__(self, msg=""): @@ -160,6 +203,12 @@ class CashDrawerError(Error): A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`. The returncode for this exception is `60`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.CashDrawerError + :parts: 1 + """ def __init__(self, msg=""): @@ -181,6 +230,12 @@ class TabPosError(Error): This exception is raised by :py:meth:`escpos.escpos.Escpos.control`. The returncode for this exception is `70`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.TabPosError + :parts: 1 + """ def __init__(self, msg=""): @@ -201,6 +256,12 @@ class CharCodeError(Error): The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown. Ths returncode for this exception is `80`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.CharCodeError + :parts: 1 + """ def __init__(self, msg=""): @@ -219,6 +280,12 @@ class USBNotFoundError(Error): The USB device seems to be not plugged in. Ths returncode for this exception is `90`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.USBNotFoundError + :parts: 1 + """ def __init__(self, msg=""): @@ -237,6 +304,12 @@ class SetVariableError(Error): Check set variables against minimum and maximum values Ths returncode for this exception is `100`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.SetVariableError + :parts: 1 + """ def __init__(self, msg=""): @@ -258,6 +331,12 @@ class ConfigNotFoundError(Error): The default or passed configuration file could not be read Ths returncode for this exception is `200`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.ConfigNotFoundError + :parts: 1 + """ def __init__(self, msg=""): @@ -276,6 +355,12 @@ class ConfigSyntaxError(Error): The syntax is incorrect Ths returncode for this exception is `210`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.ConfigSyntaxError + :parts: 1 + """ def __init__(self, msg=""): @@ -294,6 +379,12 @@ class ConfigSectionMissingError(Error): The part of the config asked for doesn't exist in the loaded configuration Ths returncode for this exception is `220`. + + inheritance: + + .. inheritance-diagram:: escpos.exceptions.ConfigSectionMissingError + :parts: 1 + """ def __init__(self, msg=""): diff --git a/src/escpos/printer.py b/src/escpos/printer.py deleted file mode 100644 index 93eb9aa..0000000 --- a/src/escpos/printer.py +++ /dev/null @@ -1,614 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -"""This module contains the implementations of abstract base class :py:class:`Escpos`. - -:author: `Manuel F Martinez `_ and others -:organization: Bashlinux and `python-escpos `_ -:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos -:license: MIT -""" - - -import os -import socket -import subprocess -import sys - -import serial -import usb.core -import usb.util - -from .escpos import Escpos -from .exceptions import USBNotFoundError - -_WIN32PRINT = False -try: - import win32print - - _WIN32PRINT = True -except ImportError: - pass - -_CUPSPRINT = False -try: - import tempfile - - import cups - - _CUPSPRINT = True -except ImportError: - pass - - -class Usb(Escpos): - """USB printer. - - This class describes a printer that natively speaks USB. - - inheritance: - - .. inheritance-diagram:: escpos.printer.Usb - :parts: 1 - - """ - - def __init__( - self, - idVendor, - idProduct, - usb_args=None, - timeout=0, - in_ep=0x82, - out_ep=0x01, - *args, - **kwargs - ): # noqa: N803 - """Initialize USB printer. - - :param idVendor: Vendor ID - :param idProduct: Product ID - :param usb_args: Optional USB arguments (e.g. custom_match) - :param timeout: Is the time limit of the USB operation. Default without timeout. - :param in_ep: Input end point - :param out_ep: Output end point - """ - Escpos.__init__(self, *args, **kwargs) - self.timeout = timeout - self.in_ep = in_ep - self.out_ep = out_ep - - usb_args = usb_args or {} - if idVendor: - usb_args["idVendor"] = idVendor - if idProduct: - usb_args["idProduct"] = idProduct - self.open(usb_args) - - def open(self, usb_args): - """Search device on USB tree and set it as escpos device. - - :param usb_args: USB arguments - """ - self.device = usb.core.find(**usb_args) - if self.device is None: - raise USBNotFoundError("Device not found or cable not plugged in.") - - self.idVendor = self.device.idVendor - self.idProduct = self.device.idProduct - - # pyusb has three backends: libusb0, libusb1 and openusb but - # only libusb1 backend implements the methods is_kernel_driver_active() - # and detach_kernel_driver(). - # This helps enable this library to work on Windows. - if self.device.backend.__module__.endswith("libusb1"): - check_driver = None - - try: - check_driver = self.device.is_kernel_driver_active(0) - except NotImplementedError: - pass - - if check_driver is None or check_driver: - try: - self.device.detach_kernel_driver(0) - except NotImplementedError: - pass - except usb.core.USBError as e: - if check_driver is not None: - print("Could not detatch kernel driver: {0}".format(str(e))) - - try: - self.device.set_configuration() - self.device.reset() - except usb.core.USBError as e: - print("Could not set configuration: {0}".format(str(e))) - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self.device.write(self.out_ep, msg, self.timeout) - - def _read(self): - """Read a data buffer and return it to the caller.""" - return self.device.read(self.in_ep, 16) - - def close(self): - """Release USB interface.""" - if self.device: - usb.util.dispose_resources(self.device) - self.device = None - - -class Serial(Escpos): - """Serial printer. - - This class describes a printer that is connected by serial interface. - - inheritance: - - .. inheritance-diagram:: escpos.printer.Serial - :parts: 1 - - """ - - def __init__( - self, - devfile="/dev/ttyS0", - baudrate=9600, - bytesize=8, - timeout=1, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - xonxoff=False, - dsrdtr=True, - *args, - **kwargs - ): - """Initialize serial printer. - - :param devfile: Device file under dev filesystem - :param baudrate: Baud rate for serial transmission - :param bytesize: Serial buffer size - :param timeout: Read/Write timeout - :param parity: Parity checking - :param stopbits: Number of stop bits - :param xonxoff: Software flow control - :param dsrdtr: Hardware flow control (False to enable RTS/CTS) - """ - Escpos.__init__(self, *args, **kwargs) - self.devfile = devfile - self.baudrate = baudrate - self.bytesize = bytesize - self.timeout = timeout - self.parity = parity - self.stopbits = stopbits - self.xonxoff = xonxoff - self.dsrdtr = dsrdtr - - self.open() - - def open(self): - """Set up serial port and set is as escpos device.""" - if self.device is not None and self.device.is_open: - self.close() - self.device = serial.Serial( - port=self.devfile, - baudrate=self.baudrate, - bytesize=self.bytesize, - parity=self.parity, - stopbits=self.stopbits, - timeout=self.timeout, - xonxoff=self.xonxoff, - dsrdtr=self.dsrdtr, - ) - - if self.device is not None: - print("Serial printer enabled") - else: - print("Unable to open serial printer on: {0}".format(str(self.devfile))) - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self.device.write(msg) - - def _read(self): - """Read the data buffer and return it to the caller.""" - return self.device.read(16) - - def close(self): - """Close Serial interface.""" - if self.device is not None and self.device.is_open: - self.device.flush() - self.device.close() - - -class Network(Escpos): - """Network printer. - - This class is used to attach to a networked printer. You can also use this in order to attach to a printer that - is forwarded with ``socat``. - - If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with: - - .. code-block:: none - - socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0 - - Then you should be able to attach to port ``4242`` with this class. - Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should - work the same with this class. For the address of the printer check its manuals. - - inheritance: - - .. inheritance-diagram:: escpos.printer.Network - :parts: 1 - - """ - - def __init__(self, host, port=9100, timeout=60, *args, **kwargs): - """Initialize network printer. - - :param host: Printer's hostname or IP address - :param port: Port to write to - :param timeout: timeout in seconds for the socket-library - """ - Escpos.__init__(self, *args, **kwargs) - self.host = host - self.port = port - self.timeout = timeout - self.open() - - def open(self): - """Open TCP socket with ``socket``-library and set it as escpos device.""" - self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.device.settimeout(self.timeout) - self.device.connect((self.host, self.port)) - - if self.device is None: - print("Could not open socket for {0}".format(self.host)) - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self.device.sendall(msg) - - def _read(self): - """Read data from the TCP socket.""" - return self.device.recv(16) - - def close(self): - """Close TCP connection.""" - if self.device is not None: - try: - self.device.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - self.device.close() - - -class File(Escpos): - """Generic file printer. - - This class is used for parallel port printer or other printers that are directly attached to the filesystem. - Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable - and produce arbitrary errors. - - inheritance: - - .. inheritance-diagram:: escpos.printer.File - :parts: 1 - - """ - - def __init__(self, devfile="/dev/usb/lp0", auto_flush=True, *args, **kwargs): - """Initialize file printer with device file. - - :param devfile: Device file under dev filesystem - :param auto_flush: automatically call flush after every call of _raw() - """ - Escpos.__init__(self, *args, **kwargs) - self.devfile = devfile - self.auto_flush = auto_flush - self.open() - - def open(self): - """Open system file.""" - self.device = open(self.devfile, "wb") - - if self.device is None: - print("Could not open the specified file {0}".format(self.devfile)) - - def flush(self): - """Flush printing content.""" - self.device.flush() - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self.device.write(msg) - if self.auto_flush: - self.flush() - - def close(self): - """Close system file.""" - if self.device is not None: - self.device.flush() - self.device.close() - - -class Dummy(Escpos): - """Dummy printer. - - This class is used for saving commands to a variable, for use in situations where - there is no need to send commands to an actual printer. This includes - generating print jobs for later use, or testing output. - - inheritance: - - .. inheritance-diagram:: escpos.printer.Dummy - :parts: 1 - - """ - - def __init__(self, *args, **kwargs): - """Init with empty output list.""" - Escpos.__init__(self, *args, **kwargs) - self._output_list = [] - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self._output_list.append(msg) - - @property - def output(self): - """Get the data that was sent to this printer.""" - return b"".join(self._output_list) - - def clear(self): - """Clear the buffer of the printer. - - This method can be called if you send the contents to a physical printer - and want to use the Dummy printer for new output. - """ - del self._output_list[:] - - def close(self): - """Close not implemented for Dummy printer.""" - pass - - -if _WIN32PRINT: - - class Win32Raw(Escpos): - """Printer binding for win32 API. - - Uses the module pywin32 for printing. - """ - - def __init__(self, printer_name=None, *args, **kwargs): - """Initialize default printer.""" - Escpos.__init__(self, *args, **kwargs) - if printer_name is not None: - self.printer_name = printer_name - else: - self.printer_name = win32print.GetDefaultPrinter() - self.hPrinter = None - self.open() - - def open(self, job_name="python-escpos"): - """Open connection to default printer.""" - if self.printer_name is None: - raise Exception("Printer not found") - self.hPrinter = win32print.OpenPrinter(self.printer_name) - self.current_job = win32print.StartDocPrinter( - self.hPrinter, 1, (job_name, None, "RAW") - ) - win32print.StartPagePrinter(self.hPrinter) - - def close(self): - """Close connection to default printer.""" - if not self.hPrinter: - return - win32print.EndPagePrinter(self.hPrinter) - win32print.EndDocPrinter(self.hPrinter) - win32print.ClosePrinter(self.hPrinter) - self.hPrinter = None - - def _raw(self, msg): - """Print any command sent in raw format. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - if self.printer_name is None: - raise Exception("Printer not found") - if self.hPrinter is None: - raise Exception("Printer job not opened") - win32print.WritePrinter(self.hPrinter, msg) - - -if _CUPSPRINT: - - class CupsPrinter(Escpos): - """Simple CUPS printer connector. - - .. note:: - - Requires ``pycups`` which in turn needs the cups development library package: - - Ubuntu/Debian: ``libcups2-dev`` - - OpenSuse/Fedora: ``cups-devel`` - - """ - - def __init__(self, printer_name=None, *args, **kwargs): - """Class constructor for CupsPrinter. - - :param printer_name: CUPS printer name (Optional) - :type printer_name: str - :param host: CUPS server host/ip (Optional) - :type host: str - :param port: CUPS server port (Optional) - :type port: int - """ - Escpos.__init__(self, *args, **kwargs) - host, port = args or ( - kwargs.get("host", cups.getServer()), - kwargs.get("port", cups.getPort()), - ) - cups.setServer(host) - cups.setPort(port) - self.conn = cups.Connection() - self.tmpfile = None - self.printer_name = printer_name - self.job_name = "" - self.pending_job = False - self.open() - - @property - def printers(self): - """Available CUPS printers.""" - return self.conn.getPrinters() - - def open(self, job_name="python-escpos"): - """Set up a new print job and target the printer. - - A call to this method is required to send new jobs to - the same CUPS connection. - - Defaults to default CUPS printer. - Creates a new temporary file buffer. - """ - self.job_name = job_name - if self.printer_name not in self.printers: - self.printer_name = self.conn.getDefault() - self.tmpfile = tempfile.NamedTemporaryFile(delete=True) - - def _raw(self, msg): - """Append any command sent in raw format to temporary file. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - self.pending_job = True - try: - self.tmpfile.write(msg) - except ValueError: - self.pending_job = False - raise ValueError("Printer job not opened") - - def send(self): - """Send the print job to the printer.""" - if self.pending_job: - # Rewind tempfile - self.tmpfile.seek(0) - # Print temporary file via CUPS printer. - self.conn.printFile( - self.printer_name, - self.tmpfile.name, - self.job_name, - {"document-format": cups.CUPS_FORMAT_RAW}, - ) - self._clear() - - def _clear(self): - """Finish the print job. - - Remove temporary file. - """ - self.tmpfile.close() - self.pending_job = False - - def _read(self): - """Return a single-item array with the accepting state of the print queue. - - states: idle = [3], printing a job = [4], stopped = [5] - """ - printer = self.printers.get(self.printer_name, {}) - state = printer.get("printer-state") - if not state: - return [] - return [state] - - def close(self): - """Close CUPS connection. - - Send pending job to the printer if needed. - """ - if self.pending_job: - self.send() - if self.conn: - print("Closing CUPS connection to printer {}".format(self.printer_name)) - self.conn = None - - -if not sys.platform.startswith("win"): - - class LP(Escpos): - """Simple UNIX lp command raw printing. - - Thanks to `Oyami-Srk comment `_. - """ - - def __init__(self, printer_name: str, *args, **kwargs): - """LP class constructor. - - :param printer_name: CUPS printer name (Optional) - :type printer_name: str - :param auto_flush: Automatic flush after every _raw() (Optional) - :type auto_flush: bool - """ - Escpos.__init__(self, *args, **kwargs) - self.printer_name = printer_name - self.auto_flush = kwargs.get("auto_flush", True) - self.open() - - def open(self): - """Invoke _lp_ in a new subprocess and wait for commands.""" - self.lp = subprocess.Popen( - ["lp", "-d", self.printer_name, "-o", "raw"], - stdin=subprocess.PIPE, - stdout=open(os.devnull, "w"), - ) - - def close(self): - """Stop the subprocess.""" - self.lp.terminate() - - def flush(self): - """End line and wait for new commands.""" - if self.lp.stdin.writable(): - self.lp.stdin.write(b"\n") - if self.lp.stdin.closed is False: - self.lp.stdin.close() - self.lp.wait() - self.open() - - def _raw(self, msg): - """Write raw command(s) to the printer. - - :param msg: arbitrary code to be printed - :type msg: bytes - """ - if self.lp.stdin.writable(): - self.lp.stdin.write(msg) - else: - raise Exception("Not a valid pipe for lp process") - if self.auto_flush: - self.flush() diff --git a/src/escpos/printer/__init__.py b/src/escpos/printer/__init__.py new file mode 100644 index 0000000..fa48e4c --- /dev/null +++ b/src/escpos/printer/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""printer implementations.""" + +from .cups import CupsPrinter +from .dummy import Dummy +from .file import File +from .lp import LP +from .network import Network +from .serial import Serial +from .usb import Usb +from .win32raw import Win32Raw + +__all__ = [ + "Usb", + "File", + "Network", + "Serial", + "LP", + "Dummy", + "CupsPrinter", + "Win32Raw", +] diff --git a/src/escpos/printer/cups.py b/src/escpos/printer/cups.py new file mode 100644 index 0000000..60e8468 --- /dev/null +++ b/src/escpos/printer/cups.py @@ -0,0 +1,180 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +import functools +import tempfile + +from ..escpos import Escpos + +#: keeps track if the pycups dependency could be loaded (:py:class:`escpos.printer.CupsPrinter`) +_DEP_PYCUPS = False + +try: + import cups + + _DEP_PYCUPS = True +except ImportError: + pass + + +# TODO: dev build mode that let's the wrapper bypass? + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + usable = False + if _DEP_PYCUPS: + usable = True + return usable + + +def dependency_pycups(func): + """Indicate dependency on pycups.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + """Throw a RuntimeError if pycups is not imported.""" + if not is_usable(): + raise RuntimeError( + "Printing with PyCups requires the pycups library to" + "be installed. Please refer to the documentation on" + "what to install and install the dependencies for pycups." + ) + return func(*args, **kwargs) + + return wrapper + + +class CupsPrinter(Escpos): + """Simple CUPS printer connector. + + .. note:: + + Requires ``pycups`` which in turn needs the cups development library package: + - Ubuntu/Debian: ``libcups2-dev`` + - OpenSuse/Fedora: ``cups-devel`` + + inheritance: + + .. inheritance-diagram:: escpos.printer.CupsPrinter + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + @dependency_pycups + def __init__(self, printer_name=None, *args, **kwargs): + """Class constructor for CupsPrinter. + + :param printer_name: CUPS printer name (Optional) + :type printer_name: str + :param host: CUPS server host/ip (Optional) + :type host: str + :param port: CUPS server port (Optional) + :type port: int + """ + Escpos.__init__(self, *args, **kwargs) + host, port = args or ( + kwargs.get("host", cups.getServer()), + kwargs.get("port", cups.getPort()), + ) + cups.setServer(host) + cups.setPort(port) + self.conn = cups.Connection() + self.tmpfile = None + self.printer_name = printer_name + self.job_name = "" + self.pending_job = False + self.open() + + @property + def printers(self): + """Available CUPS printers.""" + return self.conn.getPrinters() + + def open(self, job_name="python-escpos"): + """Set up a new print job and target the printer. + + A call to this method is required to send new jobs to + the same CUPS connection. + + Defaults to default CUPS printer. + Creates a new temporary file buffer. + """ + self.job_name = job_name + if self.printer_name not in self.printers: + self.printer_name = self.conn.getDefault() + self.tmpfile = tempfile.NamedTemporaryFile(delete=True) + + def _raw(self, msg): + """Append any command sent in raw format to temporary file. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self.pending_job = True + try: + self.tmpfile.write(msg) + except ValueError: + self.pending_job = False + raise ValueError("Printer job not opened") + + @dependency_pycups + def send(self): + """Send the print job to the printer.""" + if self.pending_job: + # Rewind tempfile + self.tmpfile.seek(0) + # Print temporary file via CUPS printer. + self.conn.printFile( + self.printer_name, + self.tmpfile.name, + self.job_name, + {"document-format": cups.CUPS_FORMAT_RAW}, + ) + self._clear() + + def _clear(self): + """Finish the print job. + + Remove temporary file. + """ + self.tmpfile.close() + self.pending_job = False + + def _read(self): + """Return a single-item array with the accepting state of the print queue. + + states: idle = [3], printing a job = [4], stopped = [5] + """ + printer = self.printers.get(self.printer_name, {}) + state = printer.get("printer-state") + if not state: + return [] + return [state] + + def close(self): + """Close CUPS connection. + + Send pending job to the printer if needed. + """ + if self.pending_job: + self.send() + if self.conn: + print("Closing CUPS connection to printer {}".format(self.printer_name)) + self.conn = None diff --git a/src/escpos/printer/dummy.py b/src/escpos/printer/dummy.py new file mode 100644 index 0000000..36601ba --- /dev/null +++ b/src/escpos/printer/dummy.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +from ..escpos import Escpos + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + return True + + +class Dummy(Escpos): + """Dummy printer. + + This class is used for saving commands to a variable, for use in situations where + there is no need to send commands to an actual printer. This includes + generating print jobs for later use, or testing output. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Dummy + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + def __init__(self, *args, **kwargs): + """Init with empty output list.""" + Escpos.__init__(self, *args, **kwargs) + self._output_list = [] + + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self._output_list.append(msg) + + @property + def output(self): + """Get the data that was sent to this printer.""" + return b"".join(self._output_list) + + def clear(self): + """Clear the buffer of the printer. + + This method can be called if you send the contents to a physical printer + and want to use the Dummy printer for new output. + """ + del self._output_list[:] + + def close(self): + """Close not implemented for Dummy printer.""" + pass diff --git a/src/escpos/printer/file.py b/src/escpos/printer/file.py new file mode 100644 index 0000000..acf4802 --- /dev/null +++ b/src/escpos/printer/file.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +from ..escpos import Escpos + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + return True + + +class File(Escpos): + """Generic file printer. + + This class is used for parallel port printer or other printers that are directly attached to the filesystem. + Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable + and produce arbitrary errors. + + inheritance: + + .. inheritance-diagram:: escpos.printer.File + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + def __init__(self, devfile="/dev/usb/lp0", auto_flush=True, *args, **kwargs): + """Initialize file printer with device file. + + :param devfile: Device file under dev filesystem + :param auto_flush: automatically call flush after every call of _raw() + """ + Escpos.__init__(self, *args, **kwargs) + self.devfile = devfile + self.auto_flush = auto_flush + self.open() + + def open(self): + """Open system file.""" + self.device = open(self.devfile, "wb") + + if self.device is None: + print("Could not open the specified file {0}".format(self.devfile)) + + def flush(self): + """Flush printing content.""" + self.device.flush() + + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self.device.write(msg) + if self.auto_flush: + self.flush() + + def close(self): + """Close system file.""" + if self.device is not None: + self.device.flush() + self.device.close() diff --git a/src/escpos/printer/lp.py b/src/escpos/printer/lp.py new file mode 100644 index 0000000..980c35f --- /dev/null +++ b/src/escpos/printer/lp.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +import functools +import os +import subprocess +import sys + +from ..escpos import Escpos + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + usable = False + if sys.platform.startswith("win"): + usable = True + return usable + + +def dependency_linux_lp(func): + """Indicate dependency on non Windows.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + """Throw a RuntimeError if not on a non-Windows system.""" + if not is_usable(): + raise RuntimeError( + "This printer driver depends on LP which is not" + "available on Windows systems." + ) + return func(*args, **kwargs) + + return wrapper + + +class LP(Escpos): + """Simple UNIX lp command raw printing. + + Thanks to `Oyami-Srk comment `_. + + inheritance: + + .. inheritance-diagram:: escpos.printer.LP + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + def __init__(self, printer_name: str, *args, **kwargs): + """LP class constructor. + + :param printer_name: CUPS printer name (Optional) + :type printer_name: str + :param auto_flush: Automatic flush after every _raw() (Optional) + :type auto_flush: bool + """ + Escpos.__init__(self, *args, **kwargs) + self.printer_name = printer_name + self.auto_flush = kwargs.get("auto_flush", True) + self.open() + + @dependency_linux_lp + def open(self): + """Invoke _lp_ in a new subprocess and wait for commands.""" + self.lp = subprocess.Popen( + ["lp", "-d", self.printer_name, "-o", "raw"], + stdin=subprocess.PIPE, + stdout=open(os.devnull, "w"), + ) + + def close(self): + """Stop the subprocess.""" + self.lp.terminate() + + def flush(self): + """End line and wait for new commands.""" + if self.lp.stdin.writable(): + self.lp.stdin.write(b"\n") + if self.lp.stdin.closed is False: + self.lp.stdin.close() + self.lp.wait() + self.open() + + def _raw(self, msg): + """Write raw command(s) to the printer. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + if self.lp.stdin.writable(): + self.lp.stdin.write(msg) + else: + raise Exception("Not a valid pipe for lp process") + if self.auto_flush: + self.flush() diff --git a/src/escpos/printer/network.py b/src/escpos/printer/network.py new file mode 100644 index 0000000..9d1f9d8 --- /dev/null +++ b/src/escpos/printer/network.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +import socket + +from ..escpos import Escpos + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + return True + + +class Network(Escpos): + """Network printer. + + This class is used to attach to a networked printer. You can also use this in order to attach to a printer that + is forwarded with ``socat``. + + If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with: + + .. code-block:: none + + socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0 + + Then you should be able to attach to port ``4242`` with this class. + Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should + work the same with this class. For the address of the printer check its manuals. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Network + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + def __init__(self, host, port=9100, timeout=60, *args, **kwargs): + """Initialize network printer. + + :param host: Printer's hostname or IP address + :param port: Port to write to + :param timeout: timeout in seconds for the socket-library + """ + Escpos.__init__(self, *args, **kwargs) + self.host = host + self.port = port + self.timeout = timeout + self.open() + + def open(self): + """Open TCP socket with ``socket``-library and set it as escpos device.""" + self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.device.settimeout(self.timeout) + self.device.connect((self.host, self.port)) + + if self.device is None: + print("Could not open socket for {0}".format(self.host)) + + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self.device.sendall(msg) + + def _read(self): + """Read data from the TCP socket.""" + return self.device.recv(16) + + def close(self): + """Close TCP connection.""" + if self.device is not None: + try: + self.device.shutdown(socket.SHUT_RDWR) + except socket.error: + pass + self.device.close() diff --git a/src/escpos/printer/serial.py b/src/escpos/printer/serial.py new file mode 100644 index 0000000..0b80e12 --- /dev/null +++ b/src/escpos/printer/serial.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + + +import functools + +from ..escpos import Escpos + +#: keeps track if the pyserial dependency could be loaded (:py:class:`escpos.printer.Serial`) +_DEP_PYSERIAL = False + +try: + import serial + + _DEP_PYSERIAL = True +except ImportError: + pass + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + usable = False + if _DEP_PYSERIAL: + usable = True + return usable + + +def dependency_pyserial(func): + """Indicate dependency on pyserial.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + """Throw a RuntimeError if pyserial not installed.""" + if not is_usable(): + raise RuntimeError( + "Printing with Serial requires the pyserial library to" + "be installed. Please refer to the documentation on" + "what to install and install the dependencies for pyserial." + ) + return func(*args, **kwargs) + + return wrapper + + +class Serial(Escpos): + """Serial printer. + + This class describes a printer that is connected by serial interface. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Serial + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + @dependency_pyserial + def __init__( + self, + devfile="/dev/ttyS0", + baudrate=9600, + bytesize=8, + timeout=1, + parity=None, + stopbits=None, + xonxoff=False, + dsrdtr=True, + *args, + **kwargs + ): + """Initialize serial printer. + + :param devfile: Device file under dev filesystem + :param baudrate: Baud rate for serial transmission + :param bytesize: Serial buffer size + :param timeout: Read/Write timeout + :param parity: Parity checking + :param stopbits: Number of stop bits + :param xonxoff: Software flow control + :param dsrdtr: Hardware flow control (False to enable RTS/CTS) + """ + Escpos.__init__(self, *args, **kwargs) + self.devfile = devfile + self.baudrate = baudrate + self.bytesize = bytesize + self.timeout = timeout + if parity: + self.parity = parity + else: + self.parity = serial.PARITY_NONE + if stopbits: + self.stopbits = stopbits + else: + self.stopbits = serial.STOPBITS_ONE + self.xonxoff = xonxoff + self.dsrdtr = dsrdtr + + self.open() + + @dependency_pyserial + def open(self): + """Set up serial port and set is as escpos device.""" + if self.device is not None and self.device.is_open: + self.close() + self.device = serial.Serial( + port=self.devfile, + baudrate=self.baudrate, + bytesize=self.bytesize, + parity=self.parity, + stopbits=self.stopbits, + timeout=self.timeout, + xonxoff=self.xonxoff, + dsrdtr=self.dsrdtr, + ) + + if self.device is not None: + print("Serial printer enabled") + else: + print("Unable to open serial printer on: {0}".format(str(self.devfile))) + + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self.device.write(msg) + + def _read(self): + """Read the data buffer and return it to the caller.""" + return self.device.read(16) + + def close(self): + """Close Serial interface.""" + if self.device is not None and self.device.is_open: + self.device.flush() + self.device.close() diff --git a/src/escpos/printer/usb.py b/src/escpos/printer/usb.py new file mode 100644 index 0000000..03f911f --- /dev/null +++ b/src/escpos/printer/usb.py @@ -0,0 +1,162 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the USB printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" +import functools + +from ..escpos import Escpos +from ..exceptions import USBNotFoundError + +#: keeps track if the usb dependency could be loaded (:py:class:`escpos.printer.Usb`) +_DEP_USB = False + +try: + import usb.core + import usb.util + + _DEP_USB = True +except ImportError: + pass + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + usable = False + if _DEP_USB: + usable = True + return usable + + +def dependency_usb(func): + """Indicate dependency on usb.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + """Throw a RuntimeError if usb not installed.""" + if not is_usable(): + raise RuntimeError( + "Printing with USB connection requires a usb library to" + "be installed. Please refer to the documentation on" + "what to install and install the dependencies for USB." + ) + return func(*args, **kwargs) + + return wrapper + + +class Usb(Escpos): + """USB printer. + + This class describes a printer that natively speaks USB. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Usb + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + def __init__( + self, + idVendor, + idProduct, + usb_args=None, + timeout=0, + in_ep=0x82, + out_ep=0x01, + *args, + **kwargs + ): + """Initialize USB printer. + + :param idVendor: Vendor ID + :param idProduct: Product ID + :param usb_args: Optional USB arguments (e.g. custom_match) + :param timeout: Is the time limit of the USB operation. Default without timeout. + :param in_ep: Input end point + :param out_ep: Output end point + """ + Escpos.__init__(self, *args, **kwargs) + self.timeout = timeout + self.in_ep = in_ep + self.out_ep = out_ep + + usb_args = usb_args or {} + if idVendor: + usb_args["idVendor"] = idVendor + if idProduct: + usb_args["idProduct"] = idProduct + self.open(usb_args) + + @dependency_usb + def open(self, usb_args): + """Search device on USB tree and set it as escpos device. + + :param usb_args: USB arguments + """ + self.device = usb.core.find(**usb_args) + if self.device is None: + raise USBNotFoundError("Device not found or cable not plugged in.") + + self.idVendor = self.device.idVendor + self.idProduct = self.device.idProduct + + # pyusb has three backends: libusb0, libusb1 and openusb but + # only libusb1 backend implements the methods is_kernel_driver_active() + # and detach_kernel_driver(). + # This helps enable this library to work on Windows. + if self.device.backend.__module__.endswith("libusb1"): + check_driver = None + + try: + check_driver = self.device.is_kernel_driver_active(0) + except NotImplementedError: + pass + + if check_driver is None or check_driver: + try: + self.device.detach_kernel_driver(0) + except NotImplementedError: + pass + except usb.core.USBError as e: + if check_driver is not None: + print("Could not detatch kernel driver: {0}".format(str(e))) + + try: + self.device.set_configuration() + self.device.reset() + except usb.core.USBError as e: + print("Could not set configuration: {0}".format(str(e))) + + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + self.device.write(self.out_ep, msg, self.timeout) + + def _read(self): + """Read a data buffer and return it to the caller.""" + return self.device.read(self.in_ep, 16) + + @dependency_usb + def close(self): + """Release USB interface.""" + if self.device: + usb.util.dispose_resources(self.device) + self.device = None diff --git a/src/escpos/printer/win32raw.py b/src/escpos/printer/win32raw.py new file mode 100644 index 0000000..d28b8c2 --- /dev/null +++ b/src/escpos/printer/win32raw.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""This module contains the implementation of the CupsPrinter printer driver. + +:author: python-escpos developers +:organization: `python-escpos `_ +:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos +:license: MIT +""" + +import functools + +from ..escpos import Escpos + +#: keeps track if the win32print dependency could be loaded (:py:class:`escpos.printer.Win32Raw`) +_DEP_WIN32PRINT = False + +try: + import win32print + + _DEP_WIN32PRINT = True +except ImportError: + pass + + +def is_usable() -> bool: + """Indicate whether this component can be used due to dependencies.""" + usable = False + if _DEP_WIN32PRINT: + usable = True + return usable + + +def dependency_win32print(func): + """Indicate dependency on win32print.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + """Throw a RuntimeError if win32print not installed.""" + if not is_usable(): + raise RuntimeError( + "Printing with Win32Raw requires a win32print library to" + "be installed. Please refer to the documentation on" + "what to install and install the dependencies for win32print." + ) + return func(*args, **kwargs) + + return wrapper + + +class Win32Raw(Escpos): + """Printer binding for win32 API. + + Uses the module pywin32 for printing. + + inheritance: + + .. inheritance-diagram:: escpos.printer.Win32Raw + :parts: 1 + + """ + + @staticmethod + def is_usable() -> bool: + """Indicate whether this printer class is usable. + + Will return True if dependencies are available. + Will return False if not. + """ + return is_usable() + + @dependency_win32print + def __init__(self, printer_name=None, *args, **kwargs): + """Initialize default printer.""" + Escpos.__init__(self, *args, **kwargs) + if printer_name is not None: + self.printer_name = printer_name + else: + self.printer_name = win32print.GetDefaultPrinter() + self.hPrinter = None + self.open() + + @dependency_win32print + def open(self, job_name="python-escpos"): + """Open connection to default printer.""" + if self.printer_name is None: + raise Exception("Printer not found") + self.hPrinter = win32print.OpenPrinter(self.printer_name) + self.current_job = win32print.StartDocPrinter( + self.hPrinter, 1, (job_name, None, "RAW") + ) + win32print.StartPagePrinter(self.hPrinter) + + @dependency_win32print + def close(self): + """Close connection to default printer.""" + if not self.hPrinter: + return + win32print.EndPagePrinter(self.hPrinter) + win32print.EndDocPrinter(self.hPrinter) + win32print.ClosePrinter(self.hPrinter) + self.hPrinter = None + + @dependency_win32print + def _raw(self, msg): + """Print any command sent in raw format. + + :param msg: arbitrary code to be printed + :type msg: bytes + """ + if self.printer_name is None: + raise Exception("Printer not found") + if self.hPrinter is None: + raise Exception("Printer job not opened") + win32print.WritePrinter(self.hPrinter, msg) diff --git a/test/test_function_check_barcode.py b/test/test_function_check_barcode.py index 80e3a96..f0c25b3 100644 --- a/test/test_function_check_barcode.py +++ b/test/test_function_check_barcode.py @@ -4,7 +4,7 @@ import pytest -import escpos.printer as printer +import escpos.escpos @pytest.mark.parametrize( @@ -48,7 +48,7 @@ import escpos.printer as printer ], ) def test_check_valid_barcode(bctype, data): - assert printer.Escpos.check_barcode(bctype, data) + assert escpos.escpos.Escpos.check_barcode(bctype, data) @pytest.mark.parametrize( @@ -104,4 +104,4 @@ def test_check_valid_barcode(bctype, data): ], ) def test_check_invalid_barcode(bctype, data): - assert not printer.Escpos.check_barcode(bctype, data) + assert not escpos.escpos.Escpos.check_barcode(bctype, data) diff --git a/tox.ini b/tox.ini index 9106c37..c990d90 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ deps = jaconv pytest-mock hypothesis>4 python-barcode +extras = all commands = pytest passenv = ESCPOS_CAPABILITIES_PICKLE_DIR, ESCPOS_CAPABILITIES_FILE, CI, TRAVIS, TRAVIS_*, APPVEYOR, APPVEYOR_*, CODECOV_* setenv = PY_IGNORE_IMPORTMISMATCH=1