From 99291abd1004a32be039efaa466e4ef4ffb0f47c Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 12 Mar 2016 16:50:57 -0800 Subject: [PATCH 01/58] rewrite cli --- escpos/cli.py | 519 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 335 insertions(+), 184 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 4b52381..8be7566 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -1,213 +1,364 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- -"""A simple command-line interface for common python-escpos functionality - -Usage: python -m escpos.cli --help - -Dependencies: -- DavisGoglin/python-escpos or better -- A file named weather.png (for the 'test' subcommand) - -Reasons for using the DavisGoglin/python-escpos fork: -- image() accepts a PIL.Image object rather than requiring me to choose - between writing a temporary file to disk or calling a "private" method. -- fullimage() allows me to print images of arbitrary length using slicing. - -How to print unsupported barcodes: - barcode -b 'BARCODE' -e 'code39' -E | convert -density 200% eps:- code.png - python test_escpos.py --images code.png - -Copyright (C) 2014 Stephan Sokolow (deitarion/SSokolow) - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" +#!/usr/bin/env python from __future__ import absolute_import -__author__ = "Stephan Sokolow (deitarion/SSokolow)" -__license__ = "MIT" - -import re - +import argparse +import sys +import serial from escpos import printer +from escpos import constants -epson = printer.Usb(0x0416, 0x5011) -# TODO: Un-hardcode this +parser = argparse.ArgumentParser( + description='CLI for python-escpos', + epilog='To see help for escpos commands, run with a destination defined.', +) +dest_subparsers = parser.add_subparsers( + title='Destination', +) + +parser_dest_file = dest_subparsers.add_parser('file', help='Print to a file') +parser_dest_file.set_defaults(func=printer.File) +parser_dest_file.add_argument( + '--devfile', + help='Destination file', + required=True +) + +parser_dest_network = dest_subparsers.add_parser('network', help='Print to a network device') +parser_dest_network.set_defaults(func=printer.Network) +parser_dest_network.add_argument( + '--host', + help='Destination host', + required=True +) +parser_dest_network.add_argument( + '--port', + help='Destination port', + type=int +) +parser_dest_network.add_argument( + '--timeout', + help='Timeout in seconds', + type=int +) + +parser_dest_usb = dest_subparsers.add_parser('usb', help='Print to a usb device') +parser_dest_usb.set_defaults(func=printer.Usb) +parser_dest_usb.add_argument( + '--idVendor', + help='USB Vendor ID', + required=True +) +parser_dest_usb.add_argument( + '--idProduct', + help='USB Device ID', + required=True +) +parser_dest_usb.add_argument( + '--interface', + help='USB device interface', + type=int +) +parser_dest_usb.add_argument( + '--in_ep', + help='Input end point', + type=int +) +parser_dest_usb.add_argument( + '--out_ep', + help='Output end point', + type=int +) + +parser_dest_serial = dest_subparsers.add_parser( + 'serial', + help='Print to a serial device' +) +parser_dest_serial.set_defaults(func=printer.Serial) +parser_dest_serial.add_argument( + '--devfile', + help='Device file' +) +parser_dest_serial.add_argument( + '--baudrate', + help='Baudrate for serial transmission', + type=int +) +parser_dest_serial.add_argument( + '--bytesize', + help='Serial byte size', + type=int +) +parser_dest_serial.add_argument( + '--timeout', + help='Read/Write timeout in seconds', + type=int +) +parser_dest_serial.add_argument( + '--parity', + help='Parity checking', + choices=[serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD, serial.PARITY_MARK, serial.PARITY_SPACE], +) +parser_dest_serial.add_argument( + '--stopbits', + help='Number of stopbits', + choices=[serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE, serial.STOPBITS_TWO], +) +parser_dest_serial.add_argument( + '--xonxoff', + help='Software flow control', + type=bool +) +parser_dest_serial.add_argument( + '--dsrdtr', + help='Hardware flow control (False to enable RTS,CTS)', + type=bool +) -def _print_text_file(path): - """Print the given text file""" - epson.set(align='left') - with open(path, 'rU') as fobj: - for line in fobj: - epson.text(line) +cmd_parser = argparse.ArgumentParser( + description='Parser for escpos commands', + usage='{previous command parts} {espos command} ...', +) +command_subparsers = cmd_parser.add_subparsers( + title='ESCPOS Command', +) -def _print_image_file(path): - """Print the given image file.""" - epson.fullimage(path, histeq=False, width=384) +# From here on func needs to be a string, since we don't have a printer to work on yet +parser_command_qr = command_subparsers.add_parser('qr', help='Print a QR code') +parser_command_qr.set_defaults(func='qr') +parser_command_qr.add_argument( + '--text', + help='Text to print as a qr code', + required=True +) +parser_command_barcode = command_subparsers.add_parser('barcode', help='Print a barcode') +parser_command_barcode.set_defaults(func='barcode') +parser_command_barcode.add_argument( + '--code', + help='Barcode data to print', + required=True, +) +parser_command_barcode.add_argument( + '--bc', + help='Barcode format', + required=True, +) +parser_command_barcode.add_argument( + '--height', + help='Barcode height in px', + type=int +) +parser_command_barcode.add_argument( + '--width', + help='Barcode width', + type=int +) +parser_command_barcode.add_argument( + '--pos', + help='Label position', + choices=['BELOW', 'ABOVE', 'BOTH', 'OFF'] +) +parser_command_barcode.add_argument( + '--font', + help='Label font', + choices=['A', 'B'] +) +parser_command_barcode.add_argument( + '--align_ct', + help='Align barcode center', + type=bool, +) +parser_command_barcode.add_argument( + '--function_type', + help='ESCPOS function type', + choices=['A', 'B'] +) -def print_files(args): - """The 'print' subcommand""" - for path in args.paths: - if args.images: - _print_image_file(path) - else: - _print_text_file(path) - epson.cut() +parser_command_text = command_subparsers.add_parser('text', help='Print plain text') +parser_command_text.set_defaults(func='text') +parser_command_text.add_argument( + '--txt', + help='Text to print', + required=True +) -# {{{ 'echo' Subcommand +parser_command_block_text = command_subparsers.add_parser('block_text', help='Print wrapped text') +parser_command_block_text.set_defaults(func='block_text') +parser_command_block_text.add_argument( + '--txt', + help='block_text to print', + required=True +) +parser_command_block_text.add_argument( + '--columns', + help='Number of columns', + type=int, +) -KNOWN_BARCODE_TYPES = ['UPC-A', 'UPC-E', 'EAN13', 'ITF'] -re_barcode_escape = re.compile(r'^%(?P\S+)\s(?P[0-9X]+)$') +parser_command_cut = command_subparsers.add_parser('cut', help='Cut the paper') +parser_command_cut.set_defaults(func='cut') +parser_command_cut.add_argument( + '--mode', + help='Type of cut', + choices=['FULL', 'PART'] +) +parser_command_cashdraw = command_subparsers.add_parser('cashdraw', help='Kick the cash drawer') +parser_command_cashdraw.set_defaults(func='cashdraw') +parser_command_cashdraw.add_argument( + '--pin', + help='Which PIN to kick', + choices=[2, 5] +) -def echo(args): # pylint: disable=unused-argument - """TTY-like line-by-line keyboard-to-printer echo loop.""" - try: - while True: - line = raw_input() - match = re_barcode_escape.match(line) - if match and match.group('type') in KNOWN_BARCODE_TYPES: - bctype, data = match.groups() - epson.barcode(data, bctype, 48, 2, '', '') - epson.set(align='left') - else: - epson.text('{0}\n'.format(line)) - except KeyboardInterrupt: - epson.cut() +parser_command_image = command_subparsers.add_parser('image', help='Print an image') +parser_command_image.set_defaults(func='image') +parser_command_image.add_argument( + '--path_img', + help='Path to image', + required=True +) -# }}} -# {{{ 'test' Subcommand +parser_command_fullimage = command_subparsers.add_parser('fullimage', help='Print an fullimage') +parser_command_fullimage.set_defaults(func='fullimage') +parser_command_fullimage.add_argument( + '--img', + help='Path to img', + required=True +) +parser_command_fullimage.add_argument( + '--max_height', + help='Max height of image in px', + type=int +) +parser_command_fullimage.add_argument( + '--width', + help='Max width of image in px', + type=int +) +parser_command_fullimage.add_argument( + '--histeq', + help='Equalize the histrogram', + type=bool +) +parser_command_fullimage.add_argument( + '--bandsize', + help='Size of bands to divide into when printing', + type=int +) -from PIL import Image, ImageDraw +# Not supported +# parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', help='Print an direct_image') +# parser_command_direct_image.set_defaults(func='direct_image') +parser_command_charcode = command_subparsers.add_parser('charcode', help='Set character code table') +parser_command_charcode.set_defaults(func='charcode') +parser_command_charcode.add_argument( + '--code', + help='Character code', + required=True +) -def _stall_test(width, height): - """Generate a pattern to detect print glitches due to vertical stalling.""" - img = Image.new('1', (width, height)) - for pos in [(x, y) for y in range(0, height) for x in range(0, width)]: - img.putpixel(pos, not sum(pos) % 10) - return img +parser_command_set = command_subparsers.add_parser('set', help='Set text properties') +parser_command_set.set_defaults(func='set') +parser_command_set.add_argument( + '--align', + help='Horizontal alignment', + choices=['left', 'center', 'right'] +) +parser_command_set.add_argument( + '--font', + help='Font choice', + choices=['left', 'center', 'right'] +) +parser_command_set.add_argument( + '--text_type', + help='Text properties', + choices=['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'] +) +parser_command_set.add_argument( + '--width', + help='Width multiplier', + type=int +) +parser_command_set.add_argument( + '--height', + help='Height multiplier', + type=int +) +parser_command_set.add_argument( + '--density', + help='Print density', + type=int +) +parser_command_set.add_argument( + '--invert', + help='White on black printing', + type=bool +) +parser_command_set.add_argument( + '--smooth', + help='Text smoothing. Effective on >= 4x4 text', + type=bool +) +parser_command_set.add_argument( + '--flip', + help='Text smoothing. Effective on >= 4x4 text', + type=bool +) +parser_command_hw = command_subparsers.add_parser('hw', help='Hardware operations') +parser_command_hw.set_defaults(func='hw') +parser_command_hw.add_argument( + '--hw', + help='Operation', + choices=['INIT', 'SELECT', 'RESET'], + required=True +) -def _test_basic(): - """The original test code from python-escpos's Usage wiki page""" - epson.set(align='left') - # Print text - epson.text("TODO:\n") # pylint: disable=fixme - epson.text("[ ] Task 1\n") - epson.text("[ ] Task 2\n") - # Print image - # TODO: Bundle an image so this can be used - # epson.image("weather.png") - # Print QR Code (must have a white border to be scanned) - epson.set(align='center') - epson.text("Scan to recall TODO list") # pylint: disable=fixme - epson.qr("http://www.example.com/") - # Print barcode - epson.barcode('1234567890128', 'EAN13', 32, 2, '', '') - # Cut paper - epson.cut() +parser_command_control = command_subparsers.add_parser('control', help='Control sequences') +parser_command_control.set_defaults(func='control') +parser_command_control.add_argument( + '--ctl', + help='Control sequence', + choices=['LF', 'FF', 'CR', 'HT', 'VT'], + required=True +) +parser_command_control.add_argument( + '--pos', + help='Horizontal tab position (1-4)', + type=int +) +parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', help='Disables panel buttons') +parser_command_panel_buttons.set_defaults(func='panel_buttons') +parser_command_panel_buttons.add_argument( + '--enable', + help='Feed button enabled', + type=bool, + required=True +) -def _test_barcodes(): - """Print test barcodes for all ESCPOS-specified formats.""" - for name, data in ( - # pylint: disable=bad-continuation - ('UPC-A', '123456789012\x00'), - ('UPC-E', '02345036\x00'), - ('EAN13', '1234567890128\x00'), - ('EAN8', '12345670\x00'), - ('CODE39', 'BARCODE12345678\x00'), - ('ITF', '123456\x00'), - ('CODABAR', 'A40156B'), - # TODO: CODE93 and CODE128 - ): - # TODO: Fix the library to restore old alignment somehow - epson.set(align='center') - epson.text('\n{0}\n'.format(name)) - epson.barcode(data, name, 64, 2, '', '') +# Get arguments along with function to pass them to +args, rest = parser.parse_known_args() +# filter out function name and non passed arguments +func_args = dict((k, v) for k, v in vars(args).iteritems() if v and k != 'func') -def _test_patterns(width=384, height=255): - """Print a set of test patterns for raster image output.""" - # Test our guess of the paper width - img = Image.new('1', (width, height), color=1) - draw = ImageDraw.Draw(img) - draw.polygon(((0, 0), img.size, (0, img.size[1])), fill=0) - epson.image(img) - del draw, img +# define a printer +p = args.func(**func_args) - # Test the consistency of printing large data and whether stall rate is - # affected by data rate - epson.image(_stall_test(width, height)) - epson.image(_stall_test(width / 2, height)) +if not rest: + cmd_parser.print_help() + sys.exit(1) +cmd_args = cmd_parser.parse_args(rest) -def test(args): - """The 'test' subcommand""" - if args.barcodes: - _test_barcodes() - elif args.patterns: - _test_patterns() - else: - _test_basic() +# filter out function name and non passed arguments +func_args = dict((k, v) for k, v in vars(cmd_args).iteritems() if v and k != 'func') - -# }}} - -def main(): - """Wrapped in a function for import and entry point compatibility""" - # pylint: disable=bad-continuation - - import argparse - - parser = argparse.ArgumentParser( - description="Command-line interface to python-escpos") - subparsers = parser.add_subparsers(title='subcommands') - - echo_parser = subparsers.add_parser('echo', help='Echo the keyboard to ' - 'the printer line-by-line (Exit with Ctrl+C)') - echo_parser.set_defaults(func=echo) - - print_parser = subparsers.add_parser('print', help='Print the given files') - print_parser.add_argument('--images', action='store_true', - help="Provided files are images rather than text files.") - print_parser.add_argument('paths', metavar='path', nargs='+') - print_parser.set_defaults(func=print_files) - - test_parser = subparsers.add_parser('test', help='Print test patterns') - test_modes = test_parser.add_mutually_exclusive_group() - test_modes.add_argument('--barcodes', action='store_true', - help="Test supported barcode types (Warning: Some printers must be " - "reset after attempting an unsupported barcode type.)") - test_modes.add_argument('--patterns', action='store_true', - help="Print test patterns") - test_parser.set_defaults(func=test) - - args = parser.parse_args() - args.func(args) - - -if __name__ == '__main__': - main() - -# vim: set sw=4 sts=4 : +# print command with args +getattr(p, cmd_args.func)(**func_args) From d523b4d34260cacec9ea25ffe2562897e45872ef Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 12 Mar 2016 16:53:13 -0800 Subject: [PATCH 02/58] consistent syntax --- escpos/cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 8be7566..d2cb756 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -10,7 +10,7 @@ from escpos import constants parser = argparse.ArgumentParser( description='CLI for python-escpos', - epilog='To see help for escpos commands, run with a destination defined.', + epilog='To see help for escpos commands, run with a destination defined.' ) dest_subparsers = parser.add_subparsers( title='Destination', @@ -97,12 +97,12 @@ parser_dest_serial.add_argument( parser_dest_serial.add_argument( '--parity', help='Parity checking', - choices=[serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD, serial.PARITY_MARK, serial.PARITY_SPACE], + choices=[serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD, serial.PARITY_MARK, serial.PARITY_SPACE] ) parser_dest_serial.add_argument( '--stopbits', help='Number of stopbits', - choices=[serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE, serial.STOPBITS_TWO], + choices=[serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE, serial.STOPBITS_TWO] ) parser_dest_serial.add_argument( '--xonxoff', @@ -118,7 +118,7 @@ parser_dest_serial.add_argument( cmd_parser = argparse.ArgumentParser( description='Parser for escpos commands', - usage='{previous command parts} {espos command} ...', + usage='{previous command parts} {espos command} ...' ) command_subparsers = cmd_parser.add_subparsers( @@ -139,12 +139,12 @@ parser_command_barcode.set_defaults(func='barcode') parser_command_barcode.add_argument( '--code', help='Barcode data to print', - required=True, + required=True ) parser_command_barcode.add_argument( '--bc', help='Barcode format', - required=True, + required=True ) parser_command_barcode.add_argument( '--height', @@ -169,7 +169,7 @@ parser_command_barcode.add_argument( parser_command_barcode.add_argument( '--align_ct', help='Align barcode center', - type=bool, + type=bool ) parser_command_barcode.add_argument( '--function_type', @@ -195,7 +195,7 @@ parser_command_block_text.add_argument( parser_command_block_text.add_argument( '--columns', help='Number of columns', - type=int, + type=int ) parser_command_cut = command_subparsers.add_parser('cut', help='Cut the paper') From 28be6a204103645140205f42329925e18f327698 Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 12 Mar 2016 17:03:02 -0800 Subject: [PATCH 03/58] Add usage --- escpos/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/escpos/cli.py b/escpos/cli.py index d2cb756..fa50f0a 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -10,7 +10,8 @@ from escpos import constants parser = argparse.ArgumentParser( description='CLI for python-escpos', - epilog='To see help for escpos commands, run with a destination defined.' + epilog='To see help for escpos commands, run with a destination defined.', + usage='python -m escpos.cli destination_type [--args] command [--args]' ) dest_subparsers = parser.add_subparsers( title='Destination', From eea3e76eed4e57910c65613ba87b64a23cb35dc1 Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 12 Mar 2016 17:09:22 -0800 Subject: [PATCH 04/58] Remove unused import --- escpos/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/escpos/cli.py b/escpos/cli.py index fa50f0a..07d0de8 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -6,7 +6,6 @@ import argparse import sys import serial from escpos import printer -from escpos import constants parser = argparse.ArgumentParser( description='CLI for python-escpos', From 133241e7e94797b4ce3dfbf95a5508313f96e4ab Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 14 Mar 2016 14:03:46 -0700 Subject: [PATCH 05/58] Moved printer choice to groups and parse manually --- escpos/cli.py | 107 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 07d0de8..c42624e 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -5,31 +5,26 @@ from __future__ import absolute_import import argparse import sys import serial +import six from escpos import printer parser = argparse.ArgumentParser( description='CLI for python-escpos', epilog='To see help for escpos commands, run with a destination defined.', - usage='python -m escpos.cli destination_type [--args] command [--args]' -) -dest_subparsers = parser.add_subparsers( - title='Destination', ) -parser_dest_file = dest_subparsers.add_parser('file', help='Print to a file') +parser_dest_file = parser.add_argument_group('File Destination') parser_dest_file.set_defaults(func=printer.File) parser_dest_file.add_argument( - '--devfile', + '--file-devfile', help='Destination file', - required=True ) -parser_dest_network = dest_subparsers.add_parser('network', help='Print to a network device') +parser_dest_network = parser.add_argument_group('Network Destination') parser_dest_network.set_defaults(func=printer.Network) parser_dest_network.add_argument( '--host', help='Destination host', - required=True ) parser_dest_network.add_argument( '--port', @@ -37,22 +32,20 @@ parser_dest_network.add_argument( type=int ) parser_dest_network.add_argument( - '--timeout', + '--network-timeout', help='Timeout in seconds', type=int ) -parser_dest_usb = dest_subparsers.add_parser('usb', help='Print to a usb device') +parser_dest_usb = parser.add_argument_group('USB Destination') parser_dest_usb.set_defaults(func=printer.Usb) parser_dest_usb.add_argument( '--idVendor', help='USB Vendor ID', - required=True ) parser_dest_usb.add_argument( '--idProduct', help='USB Device ID', - required=True ) parser_dest_usb.add_argument( '--interface', @@ -61,23 +54,20 @@ parser_dest_usb.add_argument( ) parser_dest_usb.add_argument( '--in_ep', - help='Input end point', + help='In endpoint', type=int ) parser_dest_usb.add_argument( '--out_ep', - help='Output end point', + help='Out endpoint', type=int ) -parser_dest_serial = dest_subparsers.add_parser( - 'serial', - help='Print to a serial device' -) +parser_dest_serial = parser.add_argument_group('Serial Destination') parser_dest_serial.set_defaults(func=printer.Serial) parser_dest_serial.add_argument( - '--devfile', - help='Device file' + '--serial-devfile', + help='Destination device file', ) parser_dest_serial.add_argument( '--baudrate', @@ -90,7 +80,7 @@ parser_dest_serial.add_argument( type=int ) parser_dest_serial.add_argument( - '--timeout', + '--serial-timeout', help='Read/Write timeout in seconds', type=int ) @@ -115,13 +105,7 @@ parser_dest_serial.add_argument( type=bool ) - -cmd_parser = argparse.ArgumentParser( - description='Parser for escpos commands', - usage='{previous command parts} {espos command} ...' -) - -command_subparsers = cmd_parser.add_subparsers( +command_subparsers = parser.add_subparsers( title='ESCPOS Command', ) @@ -342,23 +326,62 @@ parser_command_panel_buttons.add_argument( required=True ) -# Get arguments along with function to pass them to -args, rest = parser.parse_known_args() +# Get only arguments actually passed +args = dict([k, v] for k, v in six.iteritems(vars(parser.parse_args())) if v) -# filter out function name and non passed arguments -func_args = dict((k, v) for k, v in vars(args).iteritems() if v and k != 'func') +# Argparse doesn't tell us what came in which group, so we need to check +# Is it better to dig into parser for this, or keep a hardcoded list? +group_args = { + 'File': ('file_devfile', ), + 'Network': ('host', 'port', 'network_timeout' ), + 'Usb': ('idVendor', 'idProduct', 'interface', 'in_ep', 'out_ep' ), + 'Serial': ('serial_devfile', 'baudrate', 'serial_timeout', 'parity', 'stopbits', 'xonxoff', 'dstdtr' ), +} -# define a printer -p = args.func(**func_args) +argument_translations = { + 'file_devfile': 'devfile', + 'serial_devfile': 'devfile', + 'network_timeout': 'timeout', + 'serial_timeout': 'timeout', +} -if not rest: - cmd_parser.print_help() - sys.exit(1) +# To be found +target_printer = None +printer_arguments = {} -cmd_args = cmd_parser.parse_args(rest) +target_command = args.pop('func') +command_arguments = {} -# filter out function name and non passed arguments -func_args = dict((k, v) for k, v in vars(cmd_args).iteritems() if v and k != 'func') +# Find the printer that matches the arguments sent +for printer_group, arguments in six.iteritems(group_args): + # See if there were any arguments passed that match this group + passed_args = dict([[k, v] for k, v in six.iteritems(args) if k in arguments]) + if not passed_args: + # Nope + continue + # We found a printer + target_printer = printer_group + break + +if not target_printer: + raise Exception('No printer matches passed arguments') + +# Sort the arguments into printer or escpos command +for arg, value in six.iteritems(args): + # This one belongs to the printer + if arg in group_args[target_printer]: + # Translate it if we need to + if arg in argument_translations.keys(): + target_argument = argument_translations[arg] + printer_arguments[target_argument] = value + else: + printer_arguments[arg] = value + else: + # This belongs to the escpos command + command_arguments[arg] = value + +# Create a printer +p = getattr(printer, target_printer)(**printer_arguments) # print command with args -getattr(p, cmd_args.func)(**func_args) +getattr(p, target_command)(**command_arguments) From 6096c15b8040d22ebad7ef3fc2e3b67c5ee03b80 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 14 Mar 2016 16:53:25 -0700 Subject: [PATCH 06/58] Add future requirement to be able to use configparser --- doc/requirements.txt | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index fd5f86f..8c938c3 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -3,4 +3,5 @@ Pillow>=2.0 qrcode>=4.0 pyserial sphinx-rtd-theme -setuptools-scm \ No newline at end of file +setuptools-scm +future diff --git a/setup.py b/setup.py index d8b9f58..f4e706f 100755 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( 'qrcode>=4.0', 'pyserial', 'six', + 'future', ], setup_requires=[ 'setuptools_scm', From 87a73beb31fa600e5713b16e8638d1eb1e7b73c6 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 14 Mar 2016 17:38:29 -0700 Subject: [PATCH 07/58] Moved printer destination configuration to config file --- escpos/cli.py | 196 +++++++++++++------------------------------------- 1 file changed, 50 insertions(+), 146 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index c42624e..fa6b7f0 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -6,103 +6,58 @@ import argparse import sys import serial import six +import os +import itertools +from ConfigParser import ConfigParser from escpos import printer +config_filenames = [ + '.python-escpos', + 'python-escpos.ini' +] +config_dirs = [ + os.path.join(os.environ['HOME'], '.config') +] + +for environ in ('HOME', 'XDG_CONFIG_HOME'): + try: + config_dirs.append(os.environ[environ]) + except (KeyError): + pass + +config_files = [os.path.join(x, y) for x, y in list(itertools.product(config_dirs, config_filenames))] + +config = ConfigParser() +files_read = config.read(config_files) +if not files_read: + raise Exception('Couldn\'t find config files at {config_files}'.format( + config_files=config_files, + )) + +if 'printer' not in config.sections(): + raise Exception('Couldn\'t find [printer] config section in config_file(s): {files}'.format( + files="\n".join(files_read), + )) + +printer_config = dict(config.items('printer')) +printer_name = printer_config.pop('type').title() + +if not hasattr(printer, printer_name): + raise Exception('Couldn\'t find printer type {printer_name}'.format( + printer_name=printer_name, + )) + +try: + target_printer = getattr(printer, printer_name)(**printer_config) +except TypeError as e: + raise Exception('Unable to create {printer_name} printer: {error}'.format( + printer_name=printer_name, + error=str(e), + )) + parser = argparse.ArgumentParser( description='CLI for python-escpos', - epilog='To see help for escpos commands, run with a destination defined.', -) - -parser_dest_file = parser.add_argument_group('File Destination') -parser_dest_file.set_defaults(func=printer.File) -parser_dest_file.add_argument( - '--file-devfile', - help='Destination file', -) - -parser_dest_network = parser.add_argument_group('Network Destination') -parser_dest_network.set_defaults(func=printer.Network) -parser_dest_network.add_argument( - '--host', - help='Destination host', -) -parser_dest_network.add_argument( - '--port', - help='Destination port', - type=int -) -parser_dest_network.add_argument( - '--network-timeout', - help='Timeout in seconds', - type=int -) - -parser_dest_usb = parser.add_argument_group('USB Destination') -parser_dest_usb.set_defaults(func=printer.Usb) -parser_dest_usb.add_argument( - '--idVendor', - help='USB Vendor ID', -) -parser_dest_usb.add_argument( - '--idProduct', - help='USB Device ID', -) -parser_dest_usb.add_argument( - '--interface', - help='USB device interface', - type=int -) -parser_dest_usb.add_argument( - '--in_ep', - help='In endpoint', - type=int -) -parser_dest_usb.add_argument( - '--out_ep', - help='Out endpoint', - type=int -) - -parser_dest_serial = parser.add_argument_group('Serial Destination') -parser_dest_serial.set_defaults(func=printer.Serial) -parser_dest_serial.add_argument( - '--serial-devfile', - help='Destination device file', -) -parser_dest_serial.add_argument( - '--baudrate', - help='Baudrate for serial transmission', - type=int -) -parser_dest_serial.add_argument( - '--bytesize', - help='Serial byte size', - type=int -) -parser_dest_serial.add_argument( - '--serial-timeout', - help='Read/Write timeout in seconds', - type=int -) -parser_dest_serial.add_argument( - '--parity', - help='Parity checking', - choices=[serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD, serial.PARITY_MARK, serial.PARITY_SPACE] -) -parser_dest_serial.add_argument( - '--stopbits', - help='Number of stopbits', - choices=[serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE, serial.STOPBITS_TWO] -) -parser_dest_serial.add_argument( - '--xonxoff', - help='Software flow control', - type=bool -) -parser_dest_serial.add_argument( - '--dsrdtr', - help='Hardware flow control (False to enable RTS,CTS)', - type=bool + epilog='Printer configuration is defined in the python-escpos config file.', ) command_subparsers = parser.add_subparsers( @@ -329,59 +284,8 @@ parser_command_panel_buttons.add_argument( # Get only arguments actually passed args = dict([k, v] for k, v in six.iteritems(vars(parser.parse_args())) if v) -# Argparse doesn't tell us what came in which group, so we need to check -# Is it better to dig into parser for this, or keep a hardcoded list? -group_args = { - 'File': ('file_devfile', ), - 'Network': ('host', 'port', 'network_timeout' ), - 'Usb': ('idVendor', 'idProduct', 'interface', 'in_ep', 'out_ep' ), - 'Serial': ('serial_devfile', 'baudrate', 'serial_timeout', 'parity', 'stopbits', 'xonxoff', 'dstdtr' ), -} - -argument_translations = { - 'file_devfile': 'devfile', - 'serial_devfile': 'devfile', - 'network_timeout': 'timeout', - 'serial_timeout': 'timeout', -} - -# To be found -target_printer = None -printer_arguments = {} - target_command = args.pop('func') -command_arguments = {} - -# Find the printer that matches the arguments sent -for printer_group, arguments in six.iteritems(group_args): - # See if there were any arguments passed that match this group - passed_args = dict([[k, v] for k, v in six.iteritems(args) if k in arguments]) - if not passed_args: - # Nope - continue - # We found a printer - target_printer = printer_group - break - -if not target_printer: - raise Exception('No printer matches passed arguments') - -# Sort the arguments into printer or escpos command -for arg, value in six.iteritems(args): - # This one belongs to the printer - if arg in group_args[target_printer]: - # Translate it if we need to - if arg in argument_translations.keys(): - target_argument = argument_translations[arg] - printer_arguments[target_argument] = value - else: - printer_arguments[arg] = value - else: - # This belongs to the escpos command - command_arguments[arg] = value - -# Create a printer -p = getattr(printer, target_printer)(**printer_arguments) +command_arguments = args # print command with args -getattr(p, target_command)(**command_arguments) +getattr(target_printer, target_command)(**command_arguments) From 27352b071cd6b6736ea79cb069f76b23c7b96d6e Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 14 Mar 2016 17:41:20 -0700 Subject: [PATCH 08/58] Refactor empty exception handler --- escpos/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index fa6b7f0..ebd2f9b 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -20,10 +20,8 @@ config_dirs = [ ] for environ in ('HOME', 'XDG_CONFIG_HOME'): - try: + if environ in os.environ: config_dirs.append(os.environ[environ]) - except (KeyError): - pass config_files = [os.path.join(x, y) for x, y in list(itertools.product(config_dirs, config_filenames))] From 73be1f2c489429820def6c8257984f9c92e4994e Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 11:04:12 -0700 Subject: [PATCH 09/58] Remove future from doc requirements. Doesn't need to be there --- doc/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 8c938c3..acd8cd8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,4 +4,3 @@ qrcode>=4.0 pyserial sphinx-rtd-theme setuptools-scm -future From 1adc66992db16d629973d98055d8ddcd2e8ab9d5 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:00:19 -0700 Subject: [PATCH 10/58] Replace future and ConfigParser with localconfig --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4e706f..ca1a774 100755 --- a/setup.py +++ b/setup.py @@ -72,7 +72,8 @@ setup( 'qrcode>=4.0', 'pyserial', 'six', - 'future', + 'appdirs', + 'localconfig', ], setup_requires=[ 'setuptools_scm', From 01328db808d3f5f1f9df55117ef70924fb615a6a Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:00:46 -0700 Subject: [PATCH 11/58] Create config reader --- escpos/config.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 escpos/config.py diff --git a/escpos/config.py b/escpos/config.py new file mode 100644 index 0000000..d305395 --- /dev/null +++ b/escpos/config.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import + +import os +import appdirs +from localconfig import config + +from . import printer +from .exceptions import * + +class Config(object): + + _app_name = 'python-escpos' + _config_file = 'config.ini' + + def __init__(self): + self._has_loaded = False + self._printer = None + + self._printer_name = None + self._printer_config = None + + def load(self, config_path=None): + # If they didn't pass one, load default + if not config_path: + config_path = os.path.join( + appdirs.user_config_dir(self._app_name), + self._config_file + ) + + # Deal with one config or a list of them + # Configparser does this, but I need it for the list in the error message + if isinstance(config_path, basestring): + config_path = [config_path] + + files_read = config.read(config_path) + if not files_read: + raise ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( + config_path="\n".join(config_path), + )) + + if 'printer' in config: + # For some reason, dict(config.printer) raises + # TypeError: attribute of type 'NoneType' is not callable + self._printer_config = dict(list(config.printer)) + self._printer_name = self._printer_config.pop('type').title() + + if not self._printer_name or not hasattr(printer, self._printer_name): + raise ConfigSyntaxError('Printer type "{printer_name}" is invalid'.format( + printer_name=self._printer_name, + )) + + self._has_loaded = True + + def printer(self): + if not self._has_loaded: + self.load() + + if not self._printer: + # We could catch init errors and make them a ConfigSyntaxError, + # but I'll just let them pass + self._printer = getattr(printer, self._printer_name)(**self._printer_config) + + return self._printer + From ef31e58d2625dc4a39addffadaba01d2f0a11071 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:00:56 -0700 Subject: [PATCH 12/58] Add base config documentation --- doc/user/usage.rst | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/doc/user/usage.rst b/doc/user/usage.rst index 074e8a0..26fdd68 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -120,6 +120,63 @@ on USB interface # Cut paper Epson.cut() +Configuration File +------------------ + +You can create a configuration file for python-escpos. This will +allow you to use the CLI, and skip some setup when using the library +programically. + +The default configuration file is named ``config.ini``. For windows it is +probably at:: + + %appdata%/python-escpos/config.ini + +And for linux:: + + $HOME/.config/python-escpos/config.ini + +If you aren't sure, run:: + + from escpos import config + c = config.Config() + c.load() + +If it can't find the configuration file in the default location, it will tell +you where it's looking. You can always pass a path or a list of paths to +search to the ``load()`` method. + + +To load the configured pritner, run:: + + from escpos import config + c = config.Config() + printer = c.printer() + + +The printer section +^^^^^^^^^^^^^^^^^^^ + +The ``[printer]`` configuration section defines a default printer to create. + +The only required paramter is ``type``. The value of this should be one of the +printers defined in :doc:`/user/printers`. + +The rest of the parameters are whatever you want to pass to the printer. + +An example file printer:: + + [printer] + type=File + devfile=/dev/someprinter + +And for a network printer:: + + [printer] + type=network + host=127.0.0.1 + port=9000 + How to update your code for USB printers ---------------------------------------- From 1a866f4d1f58e5c9f48bdd7492ed35e8d7dd6543 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:01:15 -0700 Subject: [PATCH 13/58] Add config parser exceptions --- escpos/exceptions.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/escpos/exceptions.py b/escpos/exceptions.py index ce2667a..3b1ac1a 100644 --- a/escpos/exceptions.py +++ b/escpos/exceptions.py @@ -13,6 +13,8 @@ Result/Exit codes: - `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError` - `90` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError` - `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError` + - `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError` + - `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError` :author: `Manuel F Martinez `_ and others :organization: Bashlinux and `python-escpos `_ @@ -188,3 +190,34 @@ class SetVariableError(Error): def __str__(self): return "Set variable out of range" + + +# Configuration errors + +class ConfigNotFoundError(Error): + """ The configuration file was not found + + The default or passed configuration file could not be read + Ths returncode for this exception is `200`. + """ + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 200 + + def __str__(self): + return "Configuration not found ({msg})".format(msg=self.msg) + +class ConfigSyntaxError(Error): + """ The configuration file is invalid + + The syntax is incorrect or there is a section missing + Ths returncode for this exception is `210`. + """ + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 210 + + def __str__(self): + return "Configuration syntax is invalid ({msg})".format(msg=self.msg) From d5073626aed59a02b9959934fa37e45b3271a91b Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:01:40 -0700 Subject: [PATCH 14/58] Update CLI to use config class --- escpos/cli.py | 63 +++++++++++---------------------------------------- 1 file changed, 13 insertions(+), 50 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index ebd2f9b..9034233 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -7,55 +7,14 @@ import sys import serial import six import os -import itertools -from ConfigParser import ConfigParser -from escpos import printer +from . import printer, config -config_filenames = [ - '.python-escpos', - 'python-escpos.ini' -] -config_dirs = [ - os.path.join(os.environ['HOME'], '.config') -] - -for environ in ('HOME', 'XDG_CONFIG_HOME'): - if environ in os.environ: - config_dirs.append(os.environ[environ]) - -config_files = [os.path.join(x, y) for x, y in list(itertools.product(config_dirs, config_filenames))] - -config = ConfigParser() -files_read = config.read(config_files) -if not files_read: - raise Exception('Couldn\'t find config files at {config_files}'.format( - config_files=config_files, - )) - -if 'printer' not in config.sections(): - raise Exception('Couldn\'t find [printer] config section in config_file(s): {files}'.format( - files="\n".join(files_read), - )) - -printer_config = dict(config.items('printer')) -printer_name = printer_config.pop('type').title() - -if not hasattr(printer, printer_name): - raise Exception('Couldn\'t find printer type {printer_name}'.format( - printer_name=printer_name, - )) - -try: - target_printer = getattr(printer, printer_name)(**printer_config) -except TypeError as e: - raise Exception('Unable to create {printer_name} printer: {error}'.format( - printer_name=printer_name, - error=str(e), - )) +c = config.Config() +printer = c.printer() parser = argparse.ArgumentParser( description='CLI for python-escpos', - epilog='Printer configuration is defined in the python-escpos config file.', + epilog='Printer configuration is defined in the python-escpos config file. See documentation for details.', ) command_subparsers = parser.add_subparsers( @@ -279,11 +238,15 @@ parser_command_panel_buttons.add_argument( required=True ) -# Get only arguments actually passed -args = dict([k, v] for k, v in six.iteritems(vars(parser.parse_args())) if v) -target_command = args.pop('func') -command_arguments = args +if not printer: + raise Exception('No printers loaded from config') + +# Get only arguments actually passed +args_dict = vars(parser.parse_args()) +command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) + +target_command = command_arguments.pop('func') # print command with args -getattr(target_printer, target_command)(**command_arguments) +getattr(printer, target_command)(**command_arguments) From 4548dd38305fb7775c51685ccb74e9a6572b8e24 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:11:06 -0700 Subject: [PATCH 15/58] Required for tests to pass --- doc/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index acd8cd8..6020fc7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,3 +4,5 @@ qrcode>=4.0 pyserial sphinx-rtd-theme setuptools-scm +appdirs +localconfig From c7b36916e7ada8e9992a22a4d779fd0f26ec7c64 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:14:12 -0700 Subject: [PATCH 16/58] Avoid importing * --- escpos/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index d305395..44bab6f 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -5,7 +5,7 @@ import appdirs from localconfig import config from . import printer -from .exceptions import * +from . import exceptions class Config(object): @@ -34,7 +34,7 @@ class Config(object): files_read = config.read(config_path) if not files_read: - raise ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( + raise exceptions.ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( config_path="\n".join(config_path), )) @@ -45,7 +45,7 @@ class Config(object): self._printer_name = self._printer_config.pop('type').title() if not self._printer_name or not hasattr(printer, self._printer_name): - raise ConfigSyntaxError('Printer type "{printer_name}" is invalid'.format( + raise exceptions.ConfigSyntaxError('Printer type "{printer_name}" is invalid'.format( printer_name=self._printer_name, )) From 39e912bef4f46873bb7fe6d9cc0276e8427a582f Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 12:14:56 -0700 Subject: [PATCH 17/58] I guess localconfig needs future --- doc/requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index 6020fc7..2c22fdd 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,4 +5,5 @@ pyserial sphinx-rtd-theme setuptools-scm appdirs +future localconfig diff --git a/setup.py b/setup.py index ca1a774..23d6df9 100755 --- a/setup.py +++ b/setup.py @@ -73,6 +73,7 @@ setup( 'pyserial', 'six', 'appdirs', + 'future', 'localconfig', ], setup_requires=[ From c26c875b61583da41511d8bb8ec57c1cd18cf1be Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 13:47:23 -0700 Subject: [PATCH 18/58] Convert ini format to yaml format. --- doc/requirements.txt | 3 +-- doc/user/usage.rst | 24 ++++++++++++------------ escpos/config.py | 22 +++++++++------------- setup.py | 3 +-- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 2c22fdd..cba8312 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -5,5 +5,4 @@ pyserial sphinx-rtd-theme setuptools-scm appdirs -future -localconfig +pyyaml diff --git a/doc/user/usage.rst b/doc/user/usage.rst index 26fdd68..23f38e7 100644 --- a/doc/user/usage.rst +++ b/doc/user/usage.rst @@ -127,14 +127,14 @@ You can create a configuration file for python-escpos. This will allow you to use the CLI, and skip some setup when using the library programically. -The default configuration file is named ``config.ini``. For windows it is -probably at:: +The default configuration file is named ``config.yaml``. It's in the YAML +format. For windows it is probably at:: - %appdata%/python-escpos/config.ini + %appdata%/python-escpos/config.yaml And for linux:: - $HOME/.config/python-escpos/config.ini + $HOME/.config/python-escpos/config.yaml If you aren't sure, run:: @@ -157,7 +157,7 @@ To load the configured pritner, run:: The printer section ^^^^^^^^^^^^^^^^^^^ -The ``[printer]`` configuration section defines a default printer to create. +The ``printer`` configuration section defines a default printer to create. The only required paramter is ``type``. The value of this should be one of the printers defined in :doc:`/user/printers`. @@ -166,16 +166,16 @@ The rest of the parameters are whatever you want to pass to the printer. An example file printer:: - [printer] - type=File - devfile=/dev/someprinter + printer: + type: File + devfile: /dev/someprinter And for a network printer:: - [printer] - type=network - host=127.0.0.1 - port=9000 + printer: + type: network + host: 127.0.0.1 + port: 9000 How to update your code for USB printers ---------------------------------------- diff --git a/escpos/config.py b/escpos/config.py index 44bab6f..cbeaea0 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -2,7 +2,7 @@ from __future__ import absolute_import import os import appdirs -from localconfig import config +import yaml from . import printer from . import exceptions @@ -10,7 +10,7 @@ from . import exceptions class Config(object): _app_name = 'python-escpos' - _config_file = 'config.ini' + _config_file = 'config.yaml' def __init__(self): self._has_loaded = False @@ -20,28 +20,24 @@ class Config(object): self._printer_config = None def load(self, config_path=None): - # If they didn't pass one, load default if not config_path: config_path = os.path.join( appdirs.user_config_dir(self._app_name), self._config_file ) - # Deal with one config or a list of them - # Configparser does this, but I need it for the list in the error message - if isinstance(config_path, basestring): - config_path = [config_path] - - files_read = config.read(config_path) - if not files_read: + try: + with open(config_path) as f: + config = yaml.load(f) + except EnvironmentError as e: raise exceptions.ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( config_path="\n".join(config_path), )) + except yaml.ParserError as e: + raise exceptions.ConfigSyntaxError('Error parsing YAML') if 'printer' in config: - # For some reason, dict(config.printer) raises - # TypeError: attribute of type 'NoneType' is not callable - self._printer_config = dict(list(config.printer)) + self._printer_config = config['printer'] self._printer_name = self._printer_config.pop('type').title() if not self._printer_name or not hasattr(printer, self._printer_name): diff --git a/setup.py b/setup.py index 23d6df9..d68d869 100755 --- a/setup.py +++ b/setup.py @@ -73,8 +73,7 @@ setup( 'pyserial', 'six', 'appdirs', - 'future', - 'localconfig', + 'pyyaml', ], setup_requires=[ 'setuptools_scm', From 5dc676bea721e29c449b30a67c3718c8efc08373 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 14:01:21 -0700 Subject: [PATCH 19/58] Convert to safe load. Also now allows loading everyting pyyaml supports --- escpos/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index cbeaea0..f8d0ea5 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -27,8 +27,7 @@ class Config(object): ) try: - with open(config_path) as f: - config = yaml.load(f) + config = yaml.safe_load(f) except EnvironmentError as e: raise exceptions.ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( config_path="\n".join(config_path), From 2bb9756d286fa3fe32a1635f70fb1e1dad015ba1 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 14:09:45 -0700 Subject: [PATCH 20/58] Add documentation for config --- doc/api/config.rst | 10 ++++++++++ doc/index.rst | 1 + 2 files changed, 11 insertions(+) create mode 100644 doc/api/config.rst diff --git a/doc/api/config.rst b/doc/api/config.rst new file mode 100644 index 0000000..c5e3691 --- /dev/null +++ b/doc/api/config.rst @@ -0,0 +1,10 @@ +Config +--------- +Module :py:mod:`escpos.config` + +.. automodule:: escpos.config + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :member-order: bysource diff --git a/doc/index.rst b/doc/index.rst index 4997e51..29d0439 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -35,6 +35,7 @@ Content api/printer api/constants api/exceptions + api/config Indices and tables ================== From d5e3d85c4bde3994146a0be5cd56c7def8c44c71 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 14:09:45 -0700 Subject: [PATCH 21/58] Add documentation for config --- escpos/config.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/escpos/config.py b/escpos/config.py index f8d0ea5..fe1918e 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -1,3 +1,9 @@ +""" ESC/POS configuration manager. + +This module contains the implentations of abstract base class :py:class:`Config`. + +""" + from __future__ import absolute_import import os @@ -8,7 +14,11 @@ from . import printer from . import exceptions class Config(object): + """ Configuration handler class. + This class loads configuration from a default or specificed directory. It + can create your defined printer and return it to you. + """ _app_name = 'python-escpos' _config_file = 'config.yaml' @@ -20,6 +30,12 @@ class Config(object): self._printer_config = None def load(self, config_path=None): + """ Load and parse the configuration file using pyyaml + + :param config_path: An optional file path, file handle, or byte string + for the configuration file. + + """ if not config_path: config_path = os.path.join( appdirs.user_config_dir(self._app_name), @@ -47,6 +63,11 @@ class Config(object): self._has_loaded = True def printer(self): + """ Returns a printer that was defined in the config, or None. + + This method loads the default config if one hasn't beeen already loaded. + + """ if not self._has_loaded: self.load() From cdf8f6be09d7d1bd9a629ee50cf2d42cf0a1b9fa Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 15:05:25 -0700 Subject: [PATCH 22/58] Fix config loading --- escpos/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index fe1918e..bf6e4cb 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -43,12 +43,16 @@ class Config(object): ) try: - config = yaml.safe_load(f) + if isinstance(config_path, file): + config = yaml.safe_load(config_path) + else: + with open(config_path, 'rb') as f: + config = yaml.safe_load(f) except EnvironmentError as e: raise exceptions.ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( config_path="\n".join(config_path), )) - except yaml.ParserError as e: + except yaml.YAMLError as e: raise exceptions.ConfigSyntaxError('Error parsing YAML') if 'printer' in config: From 7d74dcac00e7770a582e270ab0ae145614f94648 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Tue, 15 Mar 2016 16:01:07 -0700 Subject: [PATCH 23/58] Add demo functions --- escpos/cli.py | 520 +++++++++++++++++++++++++++++--------------------- 1 file changed, 300 insertions(+), 220 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 9034233..5be6086 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -9,244 +9,324 @@ import six import os from . import printer, config -c = config.Config() -printer = c.printer() +demo_functions = { + 'text': [ + {'txt': 'Hello, World!',} + ], + 'qr': [ + {'text': 'This tests a QR code'}, + {'text': 'https://en.wikipedia.org/'} + ], + 'barcodes_a': [ + {'bc': 'UPC-A', 'code': '13243546576'}, + {'bc': 'UPC-E', 'code': '132435'}, + {'bc': 'EAN13', 'code': '1324354657687'}, + {'bc': 'EAN8', 'code': '13243546'}, + {'bc': 'CODE39', 'code': '*TEST*'}, + {'bc': 'ITF', 'code': '55867492279103'}, + {'bc': 'NW7', 'code': 'A00000000A'}, + ], + 'barcodes_b': [ + {'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'}, + {'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'}, + {'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'}, + {'bc': 'EAN8', 'code': '13243546', 'function_type': 'B'}, + {'bc': 'CODE39', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'}, + {'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'}, + {'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'}, + {'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'}, + {'bc': 'CODE128A', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE128B', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE128C', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'}, + ] +} -parser = argparse.ArgumentParser( - description='CLI for python-escpos', - epilog='Printer configuration is defined in the python-escpos config file. See documentation for details.', -) +def main(): + c = config.Config() + printer = c.printer() -command_subparsers = parser.add_subparsers( - title='ESCPOS Command', -) + parser = argparse.ArgumentParser( + description='CLI for python-escpos', + epilog='Printer configuration is defined in the python-escpos config file. See documentation for details.', + ) -# From here on func needs to be a string, since we don't have a printer to work on yet -parser_command_qr = command_subparsers.add_parser('qr', help='Print a QR code') -parser_command_qr.set_defaults(func='qr') -parser_command_qr.add_argument( - '--text', - help='Text to print as a qr code', - required=True -) + command_subparsers = parser.add_subparsers( + title='ESCPOS Command', + ) -parser_command_barcode = command_subparsers.add_parser('barcode', help='Print a barcode') -parser_command_barcode.set_defaults(func='barcode') -parser_command_barcode.add_argument( - '--code', - help='Barcode data to print', - required=True -) -parser_command_barcode.add_argument( - '--bc', - help='Barcode format', - required=True -) -parser_command_barcode.add_argument( - '--height', - help='Barcode height in px', - type=int -) -parser_command_barcode.add_argument( - '--width', - help='Barcode width', - type=int -) -parser_command_barcode.add_argument( - '--pos', - help='Label position', - choices=['BELOW', 'ABOVE', 'BOTH', 'OFF'] -) -parser_command_barcode.add_argument( - '--font', - help='Label font', - choices=['A', 'B'] -) -parser_command_barcode.add_argument( - '--align_ct', - help='Align barcode center', - type=bool -) -parser_command_barcode.add_argument( - '--function_type', - help='ESCPOS function type', - choices=['A', 'B'] -) + # From here on func needs to be a string, since we don't have a printer to work on yet + parser_command_qr = command_subparsers.add_parser('qr', help='Print a QR code') + parser_command_qr.set_defaults(func='qr') + parser_command_qr.add_argument( + '--text', + help='Text to print as a qr code', + required=True + ) -parser_command_text = command_subparsers.add_parser('text', help='Print plain text') -parser_command_text.set_defaults(func='text') -parser_command_text.add_argument( - '--txt', - help='Text to print', - required=True -) + parser_command_barcode = command_subparsers.add_parser('barcode', help='Print a barcode') + parser_command_barcode.set_defaults(func='barcode') + parser_command_barcode.add_argument( + '--code', + help='Barcode data to print', + required=True + ) + parser_command_barcode.add_argument( + '--bc', + help='Barcode format', + required=True + ) + parser_command_barcode.add_argument( + '--height', + help='Barcode height in px', + type=int + ) + parser_command_barcode.add_argument( + '--width', + help='Barcode width', + type=int + ) + parser_command_barcode.add_argument( + '--pos', + help='Label position', + choices=['BELOW', 'ABOVE', 'BOTH', 'OFF'] + ) + parser_command_barcode.add_argument( + '--font', + help='Label font', + choices=['A', 'B'] + ) + parser_command_barcode.add_argument( + '--align_ct', + help='Align barcode center', + type=bool + ) + parser_command_barcode.add_argument( + '--function_type', + help='ESCPOS function type', + choices=['A', 'B'] + ) -parser_command_block_text = command_subparsers.add_parser('block_text', help='Print wrapped text') -parser_command_block_text.set_defaults(func='block_text') -parser_command_block_text.add_argument( - '--txt', - help='block_text to print', - required=True -) -parser_command_block_text.add_argument( - '--columns', - help='Number of columns', - type=int -) + parser_command_text = command_subparsers.add_parser('text', help='Print plain text') + parser_command_text.set_defaults(func='text') + parser_command_text.add_argument( + '--txt', + help='Text to print', + required=True + ) -parser_command_cut = command_subparsers.add_parser('cut', help='Cut the paper') -parser_command_cut.set_defaults(func='cut') -parser_command_cut.add_argument( - '--mode', - help='Type of cut', - choices=['FULL', 'PART'] -) + parser_command_block_text = command_subparsers.add_parser('block_text', help='Print wrapped text') + parser_command_block_text.set_defaults(func='block_text') + parser_command_block_text.add_argument( + '--txt', + help='block_text to print', + required=True + ) + parser_command_block_text.add_argument( + '--columns', + help='Number of columns', + type=int + ) -parser_command_cashdraw = command_subparsers.add_parser('cashdraw', help='Kick the cash drawer') -parser_command_cashdraw.set_defaults(func='cashdraw') -parser_command_cashdraw.add_argument( - '--pin', - help='Which PIN to kick', - choices=[2, 5] -) + parser_command_cut = command_subparsers.add_parser('cut', help='Cut the paper') + parser_command_cut.set_defaults(func='cut') + parser_command_cut.add_argument( + '--mode', + help='Type of cut', + choices=['FULL', 'PART'] + ) -parser_command_image = command_subparsers.add_parser('image', help='Print an image') -parser_command_image.set_defaults(func='image') -parser_command_image.add_argument( - '--path_img', - help='Path to image', - required=True -) + parser_command_cashdraw = command_subparsers.add_parser('cashdraw', help='Kick the cash drawer') + parser_command_cashdraw.set_defaults(func='cashdraw') + parser_command_cashdraw.add_argument( + '--pin', + help='Which PIN to kick', + choices=[2, 5] + ) -parser_command_fullimage = command_subparsers.add_parser('fullimage', help='Print an fullimage') -parser_command_fullimage.set_defaults(func='fullimage') -parser_command_fullimage.add_argument( - '--img', - help='Path to img', - required=True -) -parser_command_fullimage.add_argument( - '--max_height', - help='Max height of image in px', - type=int -) -parser_command_fullimage.add_argument( - '--width', - help='Max width of image in px', - type=int -) -parser_command_fullimage.add_argument( - '--histeq', - help='Equalize the histrogram', - type=bool -) -parser_command_fullimage.add_argument( - '--bandsize', - help='Size of bands to divide into when printing', - type=int -) + parser_command_image = command_subparsers.add_parser('image', help='Print an image') + parser_command_image.set_defaults(func='image') + parser_command_image.add_argument( + '--path_img', + help='Path to image', + required=True + ) -# Not supported -# parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', help='Print an direct_image') -# parser_command_direct_image.set_defaults(func='direct_image') + parser_command_fullimage = command_subparsers.add_parser('fullimage', help='Print an fullimage') + parser_command_fullimage.set_defaults(func='fullimage') + parser_command_fullimage.add_argument( + '--img', + help='Path to img', + required=True + ) + parser_command_fullimage.add_argument( + '--max_height', + help='Max height of image in px', + type=int + ) + parser_command_fullimage.add_argument( + '--width', + help='Max width of image in px', + type=int + ) + parser_command_fullimage.add_argument( + '--histeq', + help='Equalize the histrogram', + type=bool + ) + parser_command_fullimage.add_argument( + '--bandsize', + help='Size of bands to divide into when printing', + type=int + ) -parser_command_charcode = command_subparsers.add_parser('charcode', help='Set character code table') -parser_command_charcode.set_defaults(func='charcode') -parser_command_charcode.add_argument( - '--code', - help='Character code', - required=True -) + # Not supported + # parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', help='Print an direct_image') + # parser_command_direct_image.set_defaults(func='direct_image') -parser_command_set = command_subparsers.add_parser('set', help='Set text properties') -parser_command_set.set_defaults(func='set') -parser_command_set.add_argument( - '--align', - help='Horizontal alignment', - choices=['left', 'center', 'right'] -) -parser_command_set.add_argument( - '--font', - help='Font choice', - choices=['left', 'center', 'right'] -) -parser_command_set.add_argument( - '--text_type', - help='Text properties', - choices=['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'] -) -parser_command_set.add_argument( - '--width', - help='Width multiplier', - type=int -) -parser_command_set.add_argument( - '--height', - help='Height multiplier', - type=int -) -parser_command_set.add_argument( - '--density', - help='Print density', - type=int -) -parser_command_set.add_argument( - '--invert', - help='White on black printing', - type=bool -) -parser_command_set.add_argument( - '--smooth', - help='Text smoothing. Effective on >= 4x4 text', - type=bool -) -parser_command_set.add_argument( - '--flip', - help='Text smoothing. Effective on >= 4x4 text', - type=bool -) + parser_command_charcode = command_subparsers.add_parser('charcode', help='Set character code table') + parser_command_charcode.set_defaults(func='charcode') + parser_command_charcode.add_argument( + '--code', + help='Character code', + required=True + ) -parser_command_hw = command_subparsers.add_parser('hw', help='Hardware operations') -parser_command_hw.set_defaults(func='hw') -parser_command_hw.add_argument( - '--hw', - help='Operation', - choices=['INIT', 'SELECT', 'RESET'], - required=True -) + parser_command_set = command_subparsers.add_parser('set', help='Set text properties') + parser_command_set.set_defaults(func='set') + parser_command_set.add_argument( + '--align', + help='Horizontal alignment', + choices=['left', 'center', 'right'] + ) + parser_command_set.add_argument( + '--font', + help='Font choice', + choices=['left', 'center', 'right'] + ) + parser_command_set.add_argument( + '--text_type', + help='Text properties', + choices=['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'] + ) + parser_command_set.add_argument( + '--width', + help='Width multiplier', + type=int + ) + parser_command_set.add_argument( + '--height', + help='Height multiplier', + type=int + ) + parser_command_set.add_argument( + '--density', + help='Print density', + type=int + ) + parser_command_set.add_argument( + '--invert', + help='White on black printing', + type=bool + ) + parser_command_set.add_argument( + '--smooth', + help='Text smoothing. Effective on >= 4x4 text', + type=bool + ) + parser_command_set.add_argument( + '--flip', + help='Text smoothing. Effective on >= 4x4 text', + type=bool + ) -parser_command_control = command_subparsers.add_parser('control', help='Control sequences') -parser_command_control.set_defaults(func='control') -parser_command_control.add_argument( - '--ctl', - help='Control sequence', - choices=['LF', 'FF', 'CR', 'HT', 'VT'], - required=True -) -parser_command_control.add_argument( - '--pos', - help='Horizontal tab position (1-4)', - type=int -) + parser_command_hw = command_subparsers.add_parser('hw', help='Hardware operations') + parser_command_hw.set_defaults(func='hw') + parser_command_hw.add_argument( + '--hw', + help='Operation', + choices=['INIT', 'SELECT', 'RESET'], + required=True + ) -parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', help='Disables panel buttons') -parser_command_panel_buttons.set_defaults(func='panel_buttons') -parser_command_panel_buttons.add_argument( - '--enable', - help='Feed button enabled', - type=bool, - required=True -) + parser_command_control = command_subparsers.add_parser('control', help='Control sequences') + parser_command_control.set_defaults(func='control') + parser_command_control.add_argument( + '--ctl', + help='Control sequence', + choices=['LF', 'FF', 'CR', 'HT', 'VT'], + required=True + ) + parser_command_control.add_argument( + '--pos', + help='Horizontal tab position (1-4)', + type=int + ) + parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', help='Disables panel buttons') + parser_command_panel_buttons.set_defaults(func='panel_buttons') + parser_command_panel_buttons.add_argument( + '--enable', + help='Feed button enabled', + type=bool, + required=True + ) -if not printer: - raise Exception('No printers loaded from config') + parser_command_demo = command_subparsers.add_parser('demo', help='Demonstrates various functions') + parser_command_demo.set_defaults(func='demo') + demo_group = parser_command_demo.add_mutually_exclusive_group() + demo_group.add_argument( + '--barcodes-a', + help='Print demo barcodes for function type A', + action='store_true', + ) + demo_group.add_argument( + '--barcodes-b', + help='Print demo barcodes for function type B', + action='store_true', + ) + demo_group.add_argument( + '--qr', + help='Print some demo QR codes', + action='store_true', + ) + demo_group.add_argument( + '--text', + help='Print some demo text', + action='store_true', + ) -# Get only arguments actually passed -args_dict = vars(parser.parse_args()) -command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) + if not printer: + raise Exception('No printers loaded from config') -target_command = command_arguments.pop('func') + # Get only arguments actually passed + args_dict = vars(parser.parse_args()) + command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) -# print command with args -getattr(printer, target_command)(**command_arguments) + target_command = command_arguments.pop('func') + + if hasattr(printer, target_command): + # print command with args + getattr(printer, target_command)(**command_arguments) + else: + command_arguments['printer'] = printer + globals()[target_command](**command_arguments) + +def demo(printer, **kwargs): + for demo_choice in kwargs.keys(): + command = getattr( + printer, + demo_choice + .replace('barcodes_a', 'barcode') + .replace('barcodes_b', 'barcode') + ) + for params in demo_functions[demo_choice]: + command(**params) + +if __name__ == '__main__': + main() From 7afd5e75d40e1da1c326a803c6baa1f69b59c913 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 09:26:57 -0700 Subject: [PATCH 24/58] Add docstrings --- escpos/cli.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/escpos/cli.py b/escpos/cli.py index 5be6086..5fa0d27 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -9,6 +9,10 @@ import six import os from . import printer, config +# Used in demo method +# Key: The name of escpos function and the argument passed on the CLI. Some +# manual translation is done in the case of barcodes_a -> barcode. +# Value: A list of dictionaries to pass to the escpos function as arguments. demo_functions = { 'text': [ {'txt': 'Hello, World!',} @@ -48,6 +52,12 @@ demo_functions = { } def main(): + """ + + Handles loading of configuration and creating and processing of command + line arguments. Called when run from a CLI. + + """ c = config.Config() printer = c.printer() @@ -318,6 +328,14 @@ def main(): globals()[target_command](**command_arguments) def demo(printer, **kwargs): + """ + Prints specificed demos. Called when CLI is passed `demo`. This function + uses the demo_functions dictionary. + + :param printer: A printer from escpos.printer + :param kwargs: A dict with a key for each function you want to test. It's + in this format since it usually comes from argparse. + """ for demo_choice in kwargs.keys(): command = getattr( printer, From 3ec00ae16efce1b365f8a358ffcaa8f05b668813 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 09:29:50 -0700 Subject: [PATCH 25/58] Remove unneeded requirements --- doc/requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index cba8312..acd8cd8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,5 +4,3 @@ qrcode>=4.0 pyserial sphinx-rtd-theme setuptools-scm -appdirs -pyyaml From 9b40c0860f4943d6fe273a5e4de3d784f5dd3836 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 13:29:22 -0700 Subject: [PATCH 26/58] Change error to handle strings and files instead of list --- escpos/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index bf6e4cb..2eed0bb 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -49,8 +49,8 @@ class Config(object): with open(config_path, 'rb') as f: config = yaml.safe_load(f) except EnvironmentError as e: - raise exceptions.ConfigNotFoundError('Couldn\'t read config at one or more of {config_path}'.format( - config_path="\n".join(config_path), + raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format( + config_path=str(config_path), )) except yaml.YAMLError as e: raise exceptions.ConfigSyntaxError('Error parsing YAML') From 7a5810992855511fa8ff4394fd8f388725210968 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 13:47:51 -0700 Subject: [PATCH 27/58] Clean up per landscape --- escpos/cli.py | 5 +---- escpos/config.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 5fa0d27..5dcda69 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -3,11 +3,8 @@ from __future__ import absolute_import import argparse -import sys -import serial import six -import os -from . import printer, config +from . import config # Used in demo method # Key: The name of escpos function and the argument passed on the CLI. Some diff --git a/escpos/config.py b/escpos/config.py index 2eed0bb..54aa0ea 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -48,7 +48,7 @@ class Config(object): else: with open(config_path, 'rb') as f: config = yaml.safe_load(f) - except EnvironmentError as e: + except EnvironmentError: raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format( config_path=str(config_path), )) From 8e44c5126e52f617a6de7fdd694a9c71ffdd2086 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 14:28:53 -0700 Subject: [PATCH 28/58] Remove unsued variable --- escpos/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos/config.py b/escpos/config.py index 54aa0ea..556887d 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -52,7 +52,7 @@ class Config(object): raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format( config_path=str(config_path), )) - except yaml.YAMLError as e: + except yaml.YAMLError: raise exceptions.ConfigSyntaxError('Error parsing YAML') if 'printer' in config: From 5ba751d89b3ff98cf2a8163810bc2226e4691a1d Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 15:24:08 -0700 Subject: [PATCH 29/58] Stop checking for file to be python2/3 compatible --- escpos/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index 556887d..def8e68 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -43,11 +43,11 @@ class Config(object): ) try: - if isinstance(config_path, file): - config = yaml.safe_load(config_path) - else: + if isinstance(config_path, str): with open(config_path, 'rb') as f: config = yaml.safe_load(f) + else: + config = yaml.safe_load(config_path) except EnvironmentError: raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format( config_path=str(config_path), From 4f92247ed6ded06aee06861f28d5eb0ad70caad7 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 16:21:05 -0700 Subject: [PATCH 30/58] Fix no help display in python3 --- escpos/cli.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 5dcda69..12e2aa1 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import argparse +import sys import six from . import config @@ -308,12 +309,16 @@ def main(): action='store_true', ) + # Get only arguments actually passed + args_dict = vars(parser.parse_args()) + if not args_dict: + parser.print_help() + sys.exit() + command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) + if not printer: raise Exception('No printers loaded from config') - # Get only arguments actually passed - args_dict = vars(parser.parse_args()) - command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) target_command = command_arguments.pop('func') From ca3b4665a23d626110d15b4d4de61b6547381a7b Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 16:24:23 -0700 Subject: [PATCH 31/58] pylint changes --- escpos/cli.py | 80 +++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 12e2aa1..8ae9b2e 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -11,7 +11,7 @@ from . import config # Key: The name of escpos function and the argument passed on the CLI. Some # manual translation is done in the case of barcodes_a -> barcode. # Value: A list of dictionaries to pass to the escpos function as arguments. -demo_functions = { +DEMO_FUNCTIONS = { 'text': [ {'txt': 'Hello, World!',} ], @@ -20,32 +20,32 @@ demo_functions = { {'text': 'https://en.wikipedia.org/'} ], 'barcodes_a': [ - {'bc': 'UPC-A', 'code': '13243546576'}, - {'bc': 'UPC-E', 'code': '132435'}, - {'bc': 'EAN13', 'code': '1324354657687'}, - {'bc': 'EAN8', 'code': '13243546'}, + {'bc': 'UPC-A', 'code': '13243546576'}, + {'bc': 'UPC-E', 'code': '132435'}, + {'bc': 'EAN13', 'code': '1324354657687'}, + {'bc': 'EAN8', 'code': '13243546'}, {'bc': 'CODE39', 'code': '*TEST*'}, - {'bc': 'ITF', 'code': '55867492279103'}, - {'bc': 'NW7', 'code': 'A00000000A'}, + {'bc': 'ITF', 'code': '55867492279103'}, + {'bc': 'NW7', 'code': 'A00000000A'}, ], 'barcodes_b': [ - {'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'}, - {'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'}, - {'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'}, - {'bc': 'EAN8', 'code': '13243546', 'function_type': 'B'}, - {'bc': 'CODE39', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'}, - {'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'}, - {'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'}, - {'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'}, - {'bc': 'CODE128A', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'CODE128B', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'CODE128C', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'}, - {'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'}, - {'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'}, - {'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'}, - {'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'}, + {'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'}, + {'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'}, + {'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'}, + {'bc': 'EAN8', 'code': '13243546', 'function_type': 'B'}, + {'bc': 'CODE39', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'}, + {'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'}, + {'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'}, + {'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'}, + {'bc': 'CODE128A', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE128B', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE128C', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'}, + {'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'}, ] } @@ -53,15 +53,16 @@ def main(): """ Handles loading of configuration and creating and processing of command - line arguments. Called when run from a CLI. - + line arguments. Called when run from a CLI. + """ - c = config.Config() - printer = c.printer() + saved_config = config.Config() + printer = saved_config.printer() parser = argparse.ArgumentParser( description='CLI for python-escpos', - epilog='Printer configuration is defined in the python-escpos config file. See documentation for details.', + epilog='Printer configuration is defined in the python-escpos config' + 'file. See documentation for details.', ) command_subparsers = parser.add_subparsers( @@ -128,7 +129,8 @@ def main(): required=True ) - parser_command_block_text = command_subparsers.add_parser('block_text', help='Print wrapped text') + parser_command_block_text = command_subparsers.add_parser('block_text', + help='Print wrapped text') parser_command_block_text.set_defaults(func='block_text') parser_command_block_text.add_argument( '--txt', @@ -194,10 +196,12 @@ def main(): ) # Not supported - # parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', help='Print an direct_image') + # parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', + # help='Print an direct_image') # parser_command_direct_image.set_defaults(func='direct_image') - parser_command_charcode = command_subparsers.add_parser('charcode', help='Set character code table') + parser_command_charcode = command_subparsers.add_parser('charcode', + help='Set character code table') parser_command_charcode.set_defaults(func='charcode') parser_command_charcode.add_argument( '--code', @@ -276,7 +280,8 @@ def main(): type=int ) - parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', help='Disables panel buttons') + parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', + help='Disables panel buttons') parser_command_panel_buttons.set_defaults(func='panel_buttons') parser_command_panel_buttons.add_argument( '--enable', @@ -285,7 +290,8 @@ def main(): required=True ) - parser_command_demo = command_subparsers.add_parser('demo', help='Demonstrates various functions') + parser_command_demo = command_subparsers.add_parser('demo', + help='Demonstrates various functions') parser_command_demo.set_defaults(func='demo') demo_group = parser_command_demo.add_mutually_exclusive_group() demo_group.add_argument( @@ -331,8 +337,8 @@ def main(): def demo(printer, **kwargs): """ - Prints specificed demos. Called when CLI is passed `demo`. This function - uses the demo_functions dictionary. + Prints specificed demos. Called when CLI is passed `demo`. This function + uses the DEMO_FUNCTIONS dictionary. :param printer: A printer from escpos.printer :param kwargs: A dict with a key for each function you want to test. It's @@ -345,7 +351,7 @@ def demo(printer, **kwargs): .replace('barcodes_a', 'barcode') .replace('barcodes_b', 'barcode') ) - for params in demo_functions[demo_choice]: + for params in DEMO_FUNCTIONS[demo_choice]: command(**params) if __name__ == '__main__': From 14ae1a7d89da8f440ca487c4c807f039f9b0b1fd Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 16 Mar 2016 16:26:13 -0700 Subject: [PATCH 32/58] Fix python3 barcode type b length encoding --- escpos/escpos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos/escpos.py b/escpos/escpos.py index 467cf4d..59ce938 100644 --- a/escpos/escpos.py +++ b/escpos/escpos.py @@ -442,7 +442,7 @@ class Escpos(object): self._raw(bc_types[bc.upper()]) if function_type.upper() == "B": - self._raw(chr(len(code))) + self._raw(six.int2byte(len(code))) # Print Code if code: From 8c186d912d4c774b535d0b08c89b5f4480e5eea6 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Fri, 18 Mar 2016 11:42:56 -0700 Subject: [PATCH 33/58] Remove EAN8 code check digit --- escpos/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 8ae9b2e..3681834 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -23,7 +23,7 @@ DEMO_FUNCTIONS = { {'bc': 'UPC-A', 'code': '13243546576'}, {'bc': 'UPC-E', 'code': '132435'}, {'bc': 'EAN13', 'code': '1324354657687'}, - {'bc': 'EAN8', 'code': '13243546'}, + {'bc': 'EAN8', 'code': '1324354'}, {'bc': 'CODE39', 'code': '*TEST*'}, {'bc': 'ITF', 'code': '55867492279103'}, {'bc': 'NW7', 'code': 'A00000000A'}, @@ -32,7 +32,7 @@ DEMO_FUNCTIONS = { {'bc': 'UPC-A', 'code': '13243546576', 'function_type': 'B'}, {'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'}, {'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'}, - {'bc': 'EAN8', 'code': '13243546', 'function_type': 'B'}, + {'bc': 'EAN8', 'code': '1324354', 'function_type': 'B'}, {'bc': 'CODE39', 'code': '*TEST*', 'function_type': 'B'}, {'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'}, {'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'}, From 00ef7f129b25abd87d5b2f9d9b8cf0a5d3bceb31 Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 19 Mar 2016 20:03:16 -0700 Subject: [PATCH 34/58] Add raw option when using cli --- escpos/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/escpos/cli.py b/escpos/cli.py index 3681834..55823df 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -290,6 +290,14 @@ def main(): required=True ) + parser_command_raw = command_subparsers.add_parser('raw', help='Raw text') + parser_command_raw.set_defaults(func='_raw') + parser_command_raw.add_argument( + '--msg', + help='Raw data to send', + required=True + ) + parser_command_demo = command_subparsers.add_parser('demo', help='Demonstrates various functions') parser_command_demo.set_defaults(func='demo') From a445c4205a66b2c4a96d3122469c760e370f2f7f Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 19 Mar 2016 20:03:34 -0700 Subject: [PATCH 35/58] Add cut after printing demos --- escpos/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/escpos/cli.py b/escpos/cli.py index 55823df..190b053 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -361,6 +361,7 @@ def demo(printer, **kwargs): ) for params in DEMO_FUNCTIONS[demo_choice]: command(**params) + printer.cut() if __name__ == '__main__': main() From ee223670bf79ef27b23c31c9864f506536ad761d Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 19 Mar 2016 20:08:20 -0700 Subject: [PATCH 36/58] Attempt to flush before quitting --- escpos/printer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/escpos/printer.py b/escpos/printer.py index baff92e..5a4d126 100644 --- a/escpos/printer.py +++ b/escpos/printer.py @@ -151,6 +151,7 @@ class Serial(Escpos): def __del__(self): """ Close Serial interface """ if self.device is not None: + self.device.flush() self.device.close() @@ -209,6 +210,7 @@ class Network(Escpos): def __del__(self): """ Close TCP connection """ + self.device.shutdown(socket.SHUT_RDWR) self.device.close() @@ -256,4 +258,5 @@ class File(Escpos): def __del__(self): """ Close system file """ + self.device.flush() self.device.close() From 28b82fb54f585f6d5d18d69fd54843087fcae1e1 Mon Sep 17 00:00:00 2001 From: davisgoglin Date: Sat, 19 Mar 2016 20:11:58 -0700 Subject: [PATCH 37/58] Fix CODE39 barcodes. Assume Code128 are the same --- escpos/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index 190b053..f8b949c 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -24,7 +24,7 @@ DEMO_FUNCTIONS = { {'bc': 'UPC-E', 'code': '132435'}, {'bc': 'EAN13', 'code': '1324354657687'}, {'bc': 'EAN8', 'code': '1324354'}, - {'bc': 'CODE39', 'code': '*TEST*'}, + {'bc': 'CODE39', 'code': 'TEST'}, {'bc': 'ITF', 'code': '55867492279103'}, {'bc': 'NW7', 'code': 'A00000000A'}, ], @@ -33,14 +33,14 @@ DEMO_FUNCTIONS = { {'bc': 'UPC-E', 'code': '132435', 'function_type': 'B'}, {'bc': 'EAN13', 'code': '1324354657687', 'function_type': 'B'}, {'bc': 'EAN8', 'code': '1324354', 'function_type': 'B'}, - {'bc': 'CODE39', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE39', 'code': 'TEST', 'function_type': 'B'}, {'bc': 'ITF', 'code': '55867492279103', 'function_type': 'B'}, {'bc': 'NW7', 'code': 'A00000000A', 'function_type': 'B'}, {'bc': 'CODE93', 'code': 'A00000000A', 'function_type': 'B'}, {'bc': 'CODE93', 'code': '1324354657687', 'function_type': 'B'}, - {'bc': 'CODE128A', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'CODE128B', 'code': '*TEST*', 'function_type': 'B'}, - {'bc': 'CODE128C', 'code': '*TEST*', 'function_type': 'B'}, + {'bc': 'CODE128A', 'code': 'TEST', 'function_type': 'B'}, + {'bc': 'CODE128B', 'code': 'TEST', 'function_type': 'B'}, + {'bc': 'CODE128C', 'code': 'TEST', 'function_type': 'B'}, {'bc': 'GS1-128', 'code': '00123456780000000001', 'function_type': 'B'}, {'bc': 'GS1 DataBar Omnidirectional', 'code': '0000000000000', 'function_type': 'B'}, {'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'}, From 1a000d29fde887402c596cebacb6bce97c59cfcf Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 09:53:45 -0700 Subject: [PATCH 38/58] Add a newline for text demo --- escpos/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos/cli.py b/escpos/cli.py index f8b949c..d0bff18 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -13,7 +13,7 @@ from . import config # Value: A list of dictionaries to pass to the escpos function as arguments. DEMO_FUNCTIONS = { 'text': [ - {'txt': 'Hello, World!',} + {'txt': 'Hello, World!\n',} ], 'qr': [ {'text': 'This tests a QR code'}, From 8101e1ec9f9a2bfa7f930b2f4124e4859a4c48ae Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 11:16:37 -0700 Subject: [PATCH 39/58] Reduce many lines of code to a loop --- escpos/cli.py | 583 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 354 insertions(+), 229 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index d0bff18..bec1b66 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -46,9 +46,350 @@ DEMO_FUNCTIONS = { {'bc': 'GS1 DataBar Truncated', 'code': '0000000000000', 'function_type': 'B'}, {'bc': 'GS1 DataBar Limited', 'code': '0000000000000', 'function_type': 'B'}, {'bc': 'GS1 DataBar Expanded', 'code': '00AAAAAAA', 'function_type': 'B'}, - ] + ], } +# Used to build the CLI +# A list of dictionaries. Each dict is a CLI argument. +# Keys: +# parser: A dict of args for command_parsers.add_parser +# defaults: A dict of args for subparser.set_defaults +# arguments: A list of dicts of args for subparser.add_argument +ESCPOS_COMMANDS = [ + { + 'parser': { + 'name': 'qr', + 'help': 'Print a QR code', + }, + 'defaults': { + 'func': 'qr', + }, + 'arguments': [ + { + 'option_strings': ('--text',), + 'help': 'Text to print as a qr code', + 'required': True, + } + ], + }, + { + 'parser': { + 'name': 'barcode', + 'help': 'Print a barcode', + }, + 'defaults': { + 'func': 'barcode', + }, + 'arguments': [ + { + 'option_strings': ('--code',), + 'help': 'Barcode data to print', + 'required': True, + }, + { + 'option_strings': ('--bc',), + 'help': 'Barcode format', + 'required': True, + }, + { + 'option_strings': ('--height',), + 'help': 'Barcode height in px', + 'type': int, + }, + { + 'option_strings': ('--width',), + 'help': 'Barcode width', + 'type': int, + }, + { + 'option_strings': ('--pos',), + 'help': 'Label position', + 'choices': ['BELOW', 'ABOVE', 'BOTH', 'OFF'], + }, + { + 'option_strings': ('--font',), + 'help': 'Label font', + 'choices': ['A', 'B'], + }, + { + 'option_strings': ('--align_ct',), + 'help': 'Align barcode center', + 'type': bool, + }, + { + 'option_strings': ('--function_type',), + 'help': 'ESCPOS function type', + 'choices': ['A', 'B'], + }, + ], + }, + { + 'parser': { + 'name': 'text', + 'help': 'Print plain text', + }, + 'defaults': { + 'func': 'text', + }, + 'arguments': [ + { + 'option_strings': ('--text',), + 'help': 'Text to print as a qr code', + 'required': True, + } + ], + }, + { + 'parser': { + 'name': 'block_text', + 'help': 'Print wrapped text', + }, + 'defaults': { + 'func': 'block_text', + }, + 'arguments': [ + { + 'option_strings': ('--txt',), + 'help': 'block_text to print', + 'required': True, + }, + { + 'option_strings': ('--columns',), + 'help': 'Number of columns', + 'type': int, + }, + ], + }, + { + 'parser': { + 'name': 'cut', + 'help': 'Cut the paper', + }, + 'defaults': { + 'func': 'cut', + }, + 'arguments': [ + { + 'option_strings': ('--mode',), + 'help': 'Type of cut', + 'choices': ['FULL', 'PART'], + }, + ], + }, + { + 'parser': { + 'name': 'cashdraw', + 'help': 'Kick the cash drawer', + }, + 'defaults': { + 'func': 'cashdraw', + }, + 'arguments': [ + { + 'option_strings': ('--pin',), + 'help': 'Which PIN to kick', + 'choices': [2, 5], + }, + ], + }, + { + 'parser': { + 'name': 'image', + 'help': 'Print an image', + }, + 'defaults': { + 'func': 'image', + }, + 'arguments': [ + { + 'option_strings': ('--path_img',), + 'help': 'Path to image', + 'required': True, + }, + ], + }, + { + 'parser': { + 'name': 'fullimage', + 'help': 'Print a fullimage', + }, + 'defaults': { + 'func': 'fullimage', + }, + 'arguments': [ + { + 'option_strings': ('--img',), + 'help': 'Path to img', + 'required': True, + }, + { + 'option_strings': ('--max_height',), + 'help': 'Max height of image in px', + 'type': int, + }, + { + 'option_strings': ('--width',), + 'help': 'Max width of image in px', + 'type': int, + }, + { + 'option_strings': ('--histeq',), + 'help': 'Equalize the histrogram', + 'type': bool, + }, + { + 'option_strings': ('--bandsize',), + 'help': 'Size of bands to divide into when printing', + 'type': int, + }, + ], + }, + { + 'parser': { + 'name': 'charcode', + 'help': 'Set character code table', + }, + 'defaults': { + 'func': 'charcode', + }, + 'arguments': [ + { + 'option_strings': ('--code',), + 'help': 'Character code', + 'required': True, + }, + ], + }, + { + 'parser': { + 'name': 'set', + 'help': 'Set text properties', + }, + 'defaults': { + 'func': 'set', + }, + 'arguments': [ + { + 'option_strings': ('--align',), + 'help': 'Horizontal alignment', + 'choices': ['left', 'center', 'right'], + }, + { + 'option_strings': ('--font',), + 'help': 'Font choice', + 'choices': ['left', 'center', 'right'], + }, + { + 'option_strings': ('--text_type',), + 'help': 'Text properties', + 'choices': ['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'], + }, + { + 'option_strings': ('--width',), + 'help': 'Width multiplier', + 'type': int, + }, + { + 'option_strings': ('--height',), + 'help': 'Height multiplier', + 'type': int, + }, + { + 'option_strings': ('--density',), + 'help': 'Print density', + 'type': int, + }, + { + 'option_strings': ('--invert',), + 'help': 'White on black printing', + 'type': bool, + }, + { + 'option_strings': ('--smooth',), + 'help': 'Text smoothing. Effective on >: 4x4 text', + 'type': bool, + }, + { + 'option_strings': ('--flip',), + 'help': 'Text smoothing. Effective on >: 4x4 text', + 'type': bool, + }, + ], + }, + { + 'parser': { + 'name': 'hw', + 'help': 'Hardware operations', + }, + 'defaults': { + 'func': 'hw', + }, + 'arguments': [ + { + 'option_strings': ('--hw',), + 'help': 'Operation', + 'choices': ['INIT', 'SELECT', 'RESET'], + 'required': True, + }, + ], + }, + { + 'parser': { + 'name': 'control', + 'help': 'Control sequences', + }, + 'defaults': { + 'func': 'control', + }, + 'arguments': [ + { + 'option_strings': ('--ctl',), + 'help': 'Control sequence', + 'choices': ['LF', 'FF', 'CR', 'HT', 'VT'], + 'required': True, + }, + { + 'option_strings': ('--pos',), + 'help': 'Horizontal tab position (1-4)', + 'type': int, + }, + ], + }, + { + 'parser': { + 'name': 'panel_buttons', + 'help': 'Controls panel buttons', + }, + 'defaults': { + 'func': 'panel_buttons', + }, + 'arguments': [ + { + 'option_strings': ('--enable',), + 'help': 'Feed button enabled', + 'type': bool, + 'required': True, + }, + ], + }, + { + 'parser': { + 'name': 'raw', + 'help': 'Raw data', + }, + 'defaults': { + 'func': '_raw', + }, + 'arguments': [ + { + 'option_strings': ('--msg',), + 'help': 'Raw data to send', + 'required': True, + }, + ], + }, +] + def main(): """ @@ -56,6 +397,8 @@ def main(): line arguments. Called when run from a CLI. """ + + # Load the configuration and defined printer saved_config = config.Config() printer = saved_config.printer() @@ -65,239 +408,21 @@ def main(): 'file. See documentation for details.', ) + # Everything runs off of a subparser so we can use the format + #cli [subparser] -args command_subparsers = parser.add_subparsers( title='ESCPOS Command', ) - # From here on func needs to be a string, since we don't have a printer to work on yet - parser_command_qr = command_subparsers.add_parser('qr', help='Print a QR code') - parser_command_qr.set_defaults(func='qr') - parser_command_qr.add_argument( - '--text', - help='Text to print as a qr code', - required=True - ) - - parser_command_barcode = command_subparsers.add_parser('barcode', help='Print a barcode') - parser_command_barcode.set_defaults(func='barcode') - parser_command_barcode.add_argument( - '--code', - help='Barcode data to print', - required=True - ) - parser_command_barcode.add_argument( - '--bc', - help='Barcode format', - required=True - ) - parser_command_barcode.add_argument( - '--height', - help='Barcode height in px', - type=int - ) - parser_command_barcode.add_argument( - '--width', - help='Barcode width', - type=int - ) - parser_command_barcode.add_argument( - '--pos', - help='Label position', - choices=['BELOW', 'ABOVE', 'BOTH', 'OFF'] - ) - parser_command_barcode.add_argument( - '--font', - help='Label font', - choices=['A', 'B'] - ) - parser_command_barcode.add_argument( - '--align_ct', - help='Align barcode center', - type=bool - ) - parser_command_barcode.add_argument( - '--function_type', - help='ESCPOS function type', - choices=['A', 'B'] - ) - - parser_command_text = command_subparsers.add_parser('text', help='Print plain text') - parser_command_text.set_defaults(func='text') - parser_command_text.add_argument( - '--txt', - help='Text to print', - required=True - ) - - parser_command_block_text = command_subparsers.add_parser('block_text', - help='Print wrapped text') - parser_command_block_text.set_defaults(func='block_text') - parser_command_block_text.add_argument( - '--txt', - help='block_text to print', - required=True - ) - parser_command_block_text.add_argument( - '--columns', - help='Number of columns', - type=int - ) - - parser_command_cut = command_subparsers.add_parser('cut', help='Cut the paper') - parser_command_cut.set_defaults(func='cut') - parser_command_cut.add_argument( - '--mode', - help='Type of cut', - choices=['FULL', 'PART'] - ) - - parser_command_cashdraw = command_subparsers.add_parser('cashdraw', help='Kick the cash drawer') - parser_command_cashdraw.set_defaults(func='cashdraw') - parser_command_cashdraw.add_argument( - '--pin', - help='Which PIN to kick', - choices=[2, 5] - ) - - parser_command_image = command_subparsers.add_parser('image', help='Print an image') - parser_command_image.set_defaults(func='image') - parser_command_image.add_argument( - '--path_img', - help='Path to image', - required=True - ) - - parser_command_fullimage = command_subparsers.add_parser('fullimage', help='Print an fullimage') - parser_command_fullimage.set_defaults(func='fullimage') - parser_command_fullimage.add_argument( - '--img', - help='Path to img', - required=True - ) - parser_command_fullimage.add_argument( - '--max_height', - help='Max height of image in px', - type=int - ) - parser_command_fullimage.add_argument( - '--width', - help='Max width of image in px', - type=int - ) - parser_command_fullimage.add_argument( - '--histeq', - help='Equalize the histrogram', - type=bool - ) - parser_command_fullimage.add_argument( - '--bandsize', - help='Size of bands to divide into when printing', - type=int - ) - - # Not supported - # parser_command_direct_image = command_subparsers.add_parser('direct_direct_image', - # help='Print an direct_image') - # parser_command_direct_image.set_defaults(func='direct_image') - - parser_command_charcode = command_subparsers.add_parser('charcode', - help='Set character code table') - parser_command_charcode.set_defaults(func='charcode') - parser_command_charcode.add_argument( - '--code', - help='Character code', - required=True - ) - - parser_command_set = command_subparsers.add_parser('set', help='Set text properties') - parser_command_set.set_defaults(func='set') - parser_command_set.add_argument( - '--align', - help='Horizontal alignment', - choices=['left', 'center', 'right'] - ) - parser_command_set.add_argument( - '--font', - help='Font choice', - choices=['left', 'center', 'right'] - ) - parser_command_set.add_argument( - '--text_type', - help='Text properties', - choices=['B', 'U', 'U2', 'BU', 'BU2', 'NORMAL'] - ) - parser_command_set.add_argument( - '--width', - help='Width multiplier', - type=int - ) - parser_command_set.add_argument( - '--height', - help='Height multiplier', - type=int - ) - parser_command_set.add_argument( - '--density', - help='Print density', - type=int - ) - parser_command_set.add_argument( - '--invert', - help='White on black printing', - type=bool - ) - parser_command_set.add_argument( - '--smooth', - help='Text smoothing. Effective on >= 4x4 text', - type=bool - ) - parser_command_set.add_argument( - '--flip', - help='Text smoothing. Effective on >= 4x4 text', - type=bool - ) - - parser_command_hw = command_subparsers.add_parser('hw', help='Hardware operations') - parser_command_hw.set_defaults(func='hw') - parser_command_hw.add_argument( - '--hw', - help='Operation', - choices=['INIT', 'SELECT', 'RESET'], - required=True - ) - - parser_command_control = command_subparsers.add_parser('control', help='Control sequences') - parser_command_control.set_defaults(func='control') - parser_command_control.add_argument( - '--ctl', - help='Control sequence', - choices=['LF', 'FF', 'CR', 'HT', 'VT'], - required=True - ) - parser_command_control.add_argument( - '--pos', - help='Horizontal tab position (1-4)', - type=int - ) - - parser_command_panel_buttons = command_subparsers.add_parser('panel_buttons', - help='Disables panel buttons') - parser_command_panel_buttons.set_defaults(func='panel_buttons') - parser_command_panel_buttons.add_argument( - '--enable', - help='Feed button enabled', - type=bool, - required=True - ) - - parser_command_raw = command_subparsers.add_parser('raw', help='Raw text') - parser_command_raw.set_defaults(func='_raw') - parser_command_raw.add_argument( - '--msg', - help='Raw data to send', - required=True - ) + # Build the ESCPOS command arguments + for command in ESCPOS_COMMANDS: + parser_command = command_subparsers.add_parser(**command['parser']) + parser_command.set_defaults(**command['defaults']) + for argument in command['arguments']: + option_strings = argument.pop('option_strings') + parser_command.add_argument(*option_strings, **argument) + # Build any custom arguments parser_command_demo = command_subparsers.add_parser('demo', help='Demonstrates various functions') parser_command_demo.set_defaults(func='demo') From 5a2ca108745691e837bd9cd2fc5737bdebe1e700 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 11:19:09 -0700 Subject: [PATCH 40/58] Add docstring --- escpos/cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/escpos/cli.py b/escpos/cli.py index bec1b66..bf1c1b9 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -1,4 +1,12 @@ #!/usr/bin/env python +""" CLI + +This module acts as a command line interface for python-escpos. It mirrors +closely the available ESCPOS commands while adding a couple extra ones for convience. + +It requires you to have a configuration file. See documentation for details. + +""" from __future__ import absolute_import From f504d2dc15971a043e0d64416be273491a48b1d4 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 11:49:12 -0700 Subject: [PATCH 41/58] Allow config path to be passed --- escpos/cli.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index bf1c1b9..c84a4b8 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -406,18 +406,20 @@ def main(): """ - # Load the configuration and defined printer - saved_config = config.Config() - printer = saved_config.printer() - parser = argparse.ArgumentParser( description='CLI for python-escpos', epilog='Printer configuration is defined in the python-escpos config' 'file. See documentation for details.', ) - # Everything runs off of a subparser so we can use the format - #cli [subparser] -args + # Allow config file location to be passed + parser.add_argument( + '-c', '--config', + help='Altnerate path to the configuration file', + ) + + # Everything interesting runs off of a subparser so we can use the format + # cli [subparser] -args command_subparsers = parser.add_subparsers( title='ESCPOS Command', ) @@ -463,6 +465,15 @@ def main(): sys.exit() command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) + # If there was a config path passed, grab it + config_path = command_arguments.pop('config', None) + + # Load the configuration and defined printer + saved_config = config.Config() + saved_config.load(config_path) + printer = saved_config.printer() + + if not printer: raise Exception('No printers loaded from config') From 76f300ea18057dc8a779bca3c33241f765236058 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 14:23:18 -0700 Subject: [PATCH 42/58] Fix text argument --- escpos/cli.py | 4 +- setup.py | 2 +- test/test_cli.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 test/test_cli.py diff --git a/escpos/cli.py b/escpos/cli.py index c84a4b8..0f2f55a 100755 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -141,8 +141,8 @@ ESCPOS_COMMANDS = [ }, 'arguments': [ { - 'option_strings': ('--text',), - 'help': 'Text to print as a qr code', + 'option_strings': ('--txt',), + 'help': 'Plain text to print', 'required': True, } ], diff --git a/setup.py b/setup.py index d68d869..2eddb08 100755 --- a/setup.py +++ b/setup.py @@ -78,6 +78,6 @@ setup( setup_requires=[ 'setuptools_scm', ], - tests_require=['tox', 'nose'], + tests_require=['tox', 'nose', 'scripttest'], cmdclass={'test': Tox}, ) diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..11018b6 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,99 @@ +"""Test for the CLI + +""" + +import os +import sys +from scripttest import TestFileEnvironment +from nose.tools import assert_equals + +TEST_DIR = os.path.abspath('test/test-cli-output') + +DEVFILE_NAME = 'testfile' + +DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME) +CONFIGFILE = 'testconfig.yaml' +CONFIG_YAML = ''' +--- + +printer: + type: file + devfile: {testfile} +'''.format( + testfile=DEVFILE, +) + +class TestCLI: + + @classmethod + def setup_class(cls): + """ Create a config file to read from """ + with open(CONFIGFILE, 'w') as config: + config.write(CONFIG_YAML) + + @classmethod + def teardown_class(cls): + """ Remove config file """ + os.remove(CONFIGFILE) + + def setup(self): + """ Create a file to print to and set up env""" + self.env = TestFileEnvironment( + base_path=TEST_DIR, + cwd=os.getcwd(), + ) + + self.default_args = ( + sys.executable, + '-mescpos.cli', + '-c', + CONFIGFILE, + ) + + fhandle = open(DEVFILE, 'a') + try: + os.utime(DEVFILE, None) + finally: + fhandle.close() + + def teardown(self): + """ Destroy printer file """ + os.remove(DEVFILE) + + def test_cli_help(self): + """ Test getting help from cli """ + result = self.env.run(sys.executable, '-mescpos.cli', '-h') + assert not result.stderr + assert 'usage' in result.stdout + + def test_cli_text(self): + """ Make sure text returns what we sent it """ + test_text = 'this is some text' + result = self.env.run( + *(self.default_args + ( + 'text', + '--txt', + test_text, + )) + ) + assert not result.stderr + assert DEVFILE_NAME in result.files_updated.keys() + assert_equals( + result.files_updated[DEVFILE_NAME].bytes, + test_text + ) + + def test_cli_text_inavlid_args(self): + """ Test a failure to send valid arguments """ + result = self.env.run( + *(self.default_args + ( + 'text', + '--invalid-param', + 'some data' + )), + expect_error=True, + expect_stderr=True + ) + assert_equals(result.returncode, 2) + assert 'error:' in result.stderr + assert not result.files_updated diff --git a/tox.ini b/tox.ini index 0cee432..6c1b7bc 100644 --- a/tox.ini +++ b/tox.ini @@ -4,4 +4,5 @@ envlist = py27, py34, py35 [testenv] deps = nose coverage + scripttest commands = nosetests --with-coverage --cover-erase --cover-branches From 0c3f273fa12b11b4e00a57dd32a7ebe9f0af0278 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 14:57:06 -0700 Subject: [PATCH 43/58] Clear test dir --- test/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_cli.py b/test/test_cli.py index 11018b6..690f93e 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -57,8 +57,9 @@ class TestCLI: fhandle.close() def teardown(self): - """ Destroy printer file """ + """ Destroy printer file and env """ os.remove(DEVFILE) + self.env.clear() def test_cli_help(self): """ Test getting help from cli """ From cabb2c930aea1c944c9908b547aa7308323b5903 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 15:01:40 -0700 Subject: [PATCH 44/58] Add docstring to TestCLI class --- test/test_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_cli.py b/test/test_cli.py index 690f93e..abb7bb0 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -24,6 +24,8 @@ printer: ) class TestCLI: + """ Contains setups, teardowns, and tests for CLI + """ @classmethod def setup_class(cls): From 38b58eb39ae2257d94584cc0caf6ea0e00e1f620 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 15:07:16 -0700 Subject: [PATCH 45/58] Remove executable flag so we hopefully get code coverage --- escpos/cli.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 escpos/cli.py diff --git a/escpos/cli.py b/escpos/cli.py old mode 100755 new mode 100644 From fd6a0e4bda7fe96e592d656aa84cf1a13eb13a9f Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 15:15:53 -0700 Subject: [PATCH 46/58] Add an automatic newline for some methods --- escpos/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/escpos/cli.py b/escpos/cli.py index 0f2f55a..21b884b 100644 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -15,6 +15,9 @@ import sys import six from . import config +# A list of functions that work better with a newline to be sent after them. +REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text') + # Used in demo method # Key: The name of escpos function and the argument passed on the CLI. Some # manual translation is done in the case of barcodes_a -> barcode. @@ -477,12 +480,13 @@ def main(): if not printer: raise Exception('No printers loaded from config') - target_command = command_arguments.pop('func') if hasattr(printer, target_command): # print command with args getattr(printer, target_command)(**command_arguments) + if target_command in REQUIRES_NEWLINE: + printer.text("\n") else: command_arguments['printer'] = printer globals()[target_command](**command_arguments) From 39165fcb41e65dbaa27fb0ee139e84509d52e622 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Mon, 28 Mar 2016 15:21:16 -0700 Subject: [PATCH 47/58] Fix tests I just wrote then broke --- test/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_cli.py b/test/test_cli.py index abb7bb0..e22ba5f 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -83,7 +83,7 @@ class TestCLI: assert DEVFILE_NAME in result.files_updated.keys() assert_equals( result.files_updated[DEVFILE_NAME].bytes, - test_text + test_text + '\n' ) def test_cli_text_inavlid_args(self): From b6b30d7c82951d2a5af01a0c34671f121fdbeaf3 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:29:28 -0700 Subject: [PATCH 48/58] Add future imports --- escpos/cli.py | 3 +++ escpos/config.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/escpos/cli.py b/escpos/cli.py index 21b884b..fa0c6af 100644 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -9,6 +9,9 @@ It requires you to have a configuration file. See documentation for details. """ from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals import argparse import sys diff --git a/escpos/config.py b/escpos/config.py index def8e68..448f825 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -5,6 +5,9 @@ This module contains the implentations of abstract base class :py:class:`Config` """ from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals import os import appdirs From 3831665da4eccf79336da1eb15861cb136a5fa75 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:30:44 -0700 Subject: [PATCH 49/58] Fix pylint messages --- escpos/config.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index 448f825..aba4e86 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -47,8 +47,8 @@ class Config(object): try: if isinstance(config_path, str): - with open(config_path, 'rb') as f: - config = yaml.safe_load(f) + with open(config_path, 'rb') as config_file: + config = yaml.safe_load(config_file) else: config = yaml.safe_load(config_path) except EnvironmentError: @@ -63,9 +63,11 @@ class Config(object): self._printer_name = self._printer_config.pop('type').title() if not self._printer_name or not hasattr(printer, self._printer_name): - raise exceptions.ConfigSyntaxError('Printer type "{printer_name}" is invalid'.format( - printer_name=self._printer_name, - )) + raise exceptions.ConfigSyntaxError( + 'Printer type "{printer_name}" is invalid'.format( + printer_name=self._printer_name, + ) + ) self._has_loaded = True @@ -79,7 +81,7 @@ class Config(object): self.load() if not self._printer: - # We could catch init errors and make them a ConfigSyntaxError, + # We could catch init errors and make them a ConfigSyntaxError, # but I'll just let them pass self._printer = getattr(printer, self._printer_name)(**self._printer_config) From 3017c14df23f25ae2e43b478c2871586f3693d04 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:31:21 -0700 Subject: [PATCH 50/58] Add future imports --- test/test_cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_cli.py b/test/test_cli.py index e22ba5f..84572cd 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -2,6 +2,11 @@ """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + import os import sys from scripttest import TestFileEnvironment From e545999aa2eb9fa2c84b53c5c980ae805fae5c5d Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:38:03 -0700 Subject: [PATCH 51/58] Convert class methods to static --- test/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_cli.py b/test/test_cli.py index 84572cd..f1331e0 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -32,14 +32,14 @@ class TestCLI: """ Contains setups, teardowns, and tests for CLI """ - @classmethod - def setup_class(cls): + @staticmethod + def setup_class(): """ Create a config file to read from """ with open(CONFIGFILE, 'w') as config: config.write(CONFIG_YAML) - @classmethod - def teardown_class(cls): + @staticmethod + def teardown_class(): """ Remove config file """ os.remove(CONFIGFILE) From 39da32ca85d25abe6d170fa234140d1309639c9c Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:38:27 -0700 Subject: [PATCH 52/58] Define class vars first in __init__ --- test/test_cli.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_cli.py b/test/test_cli.py index f1331e0..331eff6 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -32,6 +32,14 @@ class TestCLI: """ Contains setups, teardowns, and tests for CLI """ + def __init__(self): + """ Initalize the tests. + Just define some vars here since most of them get set during + setup_class and teardown_class + """ + self.env = None + self.default_args = None + @staticmethod def setup_class(): """ Create a config file to read from """ From 5ecae9d58566c8e2c1bbb62e357b7de4f5d6d4f6 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 13:47:16 -0700 Subject: [PATCH 53/58] Add more testing directories to .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index c2841b4..925ae40 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,10 @@ temp # packaging and testing .tox/ *.egg-info/ +.eggs/ build/ dist/ +.coverage + +# testing temporary directories +test/test-cli-output/ From b9c9189ca7857075698fcc93a9a6feb7205d035b Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 17:10:21 -0700 Subject: [PATCH 54/58] Add an exception for missing configuration sections --- escpos/config.py | 16 ++++++++++++---- escpos/exceptions.py | 17 ++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/escpos/config.py b/escpos/config.py index aba4e86..bac19e0 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -46,11 +46,15 @@ class Config(object): ) try: - if isinstance(config_path, str): + # First check if it's file like. If it is, pyyaml can load it. + # I'm checking type instead of catching excpetions to keep the + # 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. with open(config_path, 'rb') as config_file: config = yaml.safe_load(config_file) - else: - config = yaml.safe_load(config_path) except EnvironmentError: raise exceptions.ConfigNotFoundError('Couldn\'t read config at {config_path}'.format( config_path=str(config_path), @@ -72,7 +76,8 @@ class Config(object): self._has_loaded = True def printer(self): - """ Returns a printer that was defined in the config, or None. + """ Returns a printer that was defined in the config, or throws an + exception. This method loads the default config if one hasn't beeen already loaded. @@ -80,6 +85,9 @@ class Config(object): if not self._has_loaded: self.load() + if not self._printer_name: + raise exceptions.ConfigSectionMissingError('printer') + if not self._printer: # We could catch init errors and make them a ConfigSyntaxError, # but I'll just let them pass diff --git a/escpos/exceptions.py b/escpos/exceptions.py index 3b1ac1a..0a51782 100644 --- a/escpos/exceptions.py +++ b/escpos/exceptions.py @@ -15,6 +15,7 @@ Result/Exit codes: - `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError` - `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError` - `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 :organization: Bashlinux and `python-escpos `_ @@ -211,7 +212,7 @@ class ConfigNotFoundError(Error): class ConfigSyntaxError(Error): """ The configuration file is invalid - The syntax is incorrect or there is a section missing + The syntax is incorrect Ths returncode for this exception is `210`. """ def __init__(self, msg=""): @@ -221,3 +222,17 @@ class ConfigSyntaxError(Error): def __str__(self): return "Configuration syntax is invalid ({msg})".format(msg=self.msg) + +class ConfigSectionMissingError(Error): + """ The configuration file is missing a section + + The part of the config asked for doesn't exist in the loaded configuration + Ths returncode for this exception is `220`. + """ + def __init__(self, msg=""): + Error.__init__(self, msg) + self.msg = msg + self.resultcode = 220 + + def __str__(self): + return "Configuration section is missing ({msg})".format(msg=self.msg) From 3fe4589b8bdde83e36f459d8f7be8776f46ab48e Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 17:11:05 -0700 Subject: [PATCH 55/58] Add the reset of configurations between loads --- escpos/config.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/escpos/config.py b/escpos/config.py index bac19e0..a598393 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -26,6 +26,23 @@ class Config(object): _config_file = 'config.yaml' def __init__(self): + """ Initialize configuration. + + Remember to add anything that needs to be reset between configurations + to self._reset_config + """ + self._has_loaded = False + self._printer = None + + self._printer_name = None + self._printer_config = None + + def _reset_config(self): + """ Clear the loaded configuration. + + If we are loading a changed config, we don't want to have leftover + data. + """ self._has_loaded = False self._printer = None @@ -39,6 +56,9 @@ class Config(object): for the configuration file. """ + + self._reset_config() + if not config_path: config_path = os.path.join( appdirs.user_config_dir(self._app_name), From 062282bf475fccadf3be14667b3b7a013f542d12 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 17:13:20 -0700 Subject: [PATCH 56/58] Fix Spelling --- escpos/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos/config.py b/escpos/config.py index a598393..8d5b915 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -67,7 +67,7 @@ class Config(object): try: # First check if it's file like. If it is, pyyaml can load it. - # I'm checking type instead of catching excpetions to keep the + # I'm checking type instead of catching exceptions to keep the # exception handling simple if hasattr(config_path, 'read'): config = yaml.safe_load(config_path) From 8ed1441c4c3d50c4d9cbfe9cdf1782487b489515 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Wed, 30 Mar 2016 17:13:59 -0700 Subject: [PATCH 57/58] Clairify config loading comments --- escpos/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/escpos/config.py b/escpos/config.py index 8d5b915..4a5d077 100644 --- a/escpos/config.py +++ b/escpos/config.py @@ -72,7 +72,8 @@ class Config(object): 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. + # 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: From fca363119c93f9086bdc7281a42bdf116e7d82d6 Mon Sep 17 00:00:00 2001 From: Davis Goglin Date: Fri, 1 Apr 2016 10:31:28 -0700 Subject: [PATCH 58/58] Fix converting of passed bool values properly --- escpos/cli.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/escpos/cli.py b/escpos/cli.py index fa0c6af..d250852 100644 --- a/escpos/cli.py +++ b/escpos/cli.py @@ -18,6 +18,13 @@ import sys import six from . import config +# Must be defined before it's used in DEMO_FUNCTIONS +def str_to_bool(string): + """ Used as a type in argparse so that we get back a proper + bool instead of always True + """ + return string.lower() in ('y', 'yes', '1', 'true') + # A list of functions that work better with a newline to be sent after them. REQUIRES_NEWLINE = ('qr', 'barcode', 'text', 'block_text') @@ -128,7 +135,7 @@ ESCPOS_COMMANDS = [ { 'option_strings': ('--align_ct',), 'help': 'Align barcode center', - 'type': bool, + 'type': str_to_bool, }, { 'option_strings': ('--function_type',), @@ -249,7 +256,7 @@ ESCPOS_COMMANDS = [ { 'option_strings': ('--histeq',), 'help': 'Equalize the histrogram', - 'type': bool, + 'type': str_to_bool, }, { 'option_strings': ('--bandsize',), @@ -316,17 +323,17 @@ ESCPOS_COMMANDS = [ { 'option_strings': ('--invert',), 'help': 'White on black printing', - 'type': bool, + 'type': str_to_bool, }, { 'option_strings': ('--smooth',), 'help': 'Text smoothing. Effective on >: 4x4 text', - 'type': bool, + 'type': str_to_bool, }, { 'option_strings': ('--flip',), 'help': 'Text smoothing. Effective on >: 4x4 text', - 'type': bool, + 'type': str_to_bool, }, ], }, @@ -381,7 +388,7 @@ ESCPOS_COMMANDS = [ { 'option_strings': ('--enable',), 'help': 'Feed button enabled', - 'type': bool, + 'type': str_to_bool, 'required': True, }, ], @@ -418,6 +425,8 @@ def main(): 'file. See documentation for details.', ) + parser.register('type', 'bool', str_to_bool) + # Allow config file location to be passed parser.add_argument( '-c', '--config', @@ -469,7 +478,7 @@ def main(): if not args_dict: parser.print_help() sys.exit() - command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v) + command_arguments = dict([k, v] for k, v in six.iteritems(args_dict) if v is not None) # If there was a config path passed, grab it config_path = command_arguments.pop('config', None)