Merge pull request #165 from miracle2k/capabilities

Capabilities profiles
This commit is contained in:
Patrick Kanzler 2016-09-02 14:02:10 +02:00 committed by GitHub
commit 9662ca6efe
12 changed files with 263 additions and 67 deletions

2
.coveragerc Normal file
View File

@ -0,0 +1,2 @@
[run]
branch = True

View File

@ -83,7 +83,8 @@ setup(
platforms='any',
package_dir={"": "src"},
packages=find_packages(where="src", exclude=["tests", "tests.*"]),
package_data={'': ['COPYING']},
package_data={'': ['COPYING', 'src/escpos/capabilities.json']},
include_package_data=True,
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
@ -116,7 +117,7 @@ setup(
setup_requires=[
'setuptools_scm',
],
tests_require=['tox', 'nose', 'scripttest', 'mock', 'hypothesis'],
tests_require=['tox', 'pytest', 'pytest-cov', 'nose', 'scripttest', 'mock', 'hypothesis'],
cmdclass={'test': Tox},
entry_points={
'console_scripts': [

File diff suppressed because one or more lines are too long

112
src/escpos/capabilities.py Normal file
View File

@ -0,0 +1,112 @@
import re
import six
from os import path
import yaml
# Load external printer database
with open(path.join(path.dirname(__file__), 'capabilities.json')) as f:
CAPABILITIES = yaml.load(f)
PROFILES = CAPABILITIES['profiles']
ENCODINGS = CAPABILITIES['encodings']
class NotSupported(Exception):
"""Raised if a requested feature is not suppored by the
printer profile.
"""
pass
BARCODE_B = 'barcodeB'
class BaseProfile(object):
"""This respresents a printer profile.
A printer profile knows about the number of columns, supported
features, colors and more.
"""
profile_data = {}
def __getattr__(self, name):
return self.profile_data[name]
def get_font(self, font):
"""Return the escpos index for `font`. Makes sure that
the requested `font` is valid.
"""
font = {'a': 0, 'b': 1}.get(font, font)
if not six.text_type(font) in self.fonts:
raise NotSupported(
'"{}" is not a valid font in the current profile'.format(font))
return font
def get_columns(self, font):
""" Return the number of columns for the given font.
"""
font = self.get_font(font)
return self.fonts[six.text_type(font)]['columns']
def supports(self, feature):
"""Return true/false for the given feature.
"""
return self.features.get(feature)
def get_profile(name=None, **kwargs):
"""Get the profile by name; if no name is given, return the
default profile.
"""
if isinstance(name, Profile):
return name
clazz = get_profile_class(name or 'default')
return clazz(**kwargs)
CLASS_CACHE = {}
def get_profile_class(name):
"""For the given profile name, load the data from the external
database, then generate dynamically a class.
"""
if not name in CLASS_CACHE:
profile_data = PROFILES[name]
profile_name = clean(name)
class_name = '{}{}Profile'.format(
profile_name[0].upper(), profile_name[1:])
new_class = type(class_name, (BaseProfile,), {'profile_data': profile_data})
CLASS_CACHE[name] = new_class
return CLASS_CACHE[name]
def clean(s):
# Remove invalid characters
s = re.sub('[^0-9a-zA-Z_]', '', s)
# Remove leading characters until we find a letter or underscore
s = re.sub('^[^a-zA-Z_]+', '', s)
return str(s)
# For users, who want to provide their profile
class Profile(get_profile_class('default')):
def __init__(self, columns=None, features=None):
super(Profile, self).__init__()
self.columns = columns
self.features = features or {}
def get_columns(self, font):
if self.columns is not None:
return self.columns
return super(Profile, self).get_columns(font)

View File

@ -100,14 +100,17 @@ TXT_UNDERL_ON = ESC + b'\x2d\x01' # Underline font 1-dot ON
TXT_UNDERL2_ON = ESC + b'\x2d\x02' # Underline font 2-dot ON
TXT_BOLD_OFF = ESC + b'\x45\x00' # Bold font OFF
TXT_BOLD_ON = ESC + b'\x45\x01' # Bold font ON
TXT_FONT_A = ESC + b'\x4d\x00' # Font type A
TXT_FONT_B = ESC + b'\x4d\x01' # Font type B
TXT_ALIGN_LT = ESC + b'\x61\x00' # Left justification
TXT_ALIGN_CT = ESC + b'\x61\x01' # Centering
TXT_ALIGN_RT = ESC + b'\x61\x02' # Right justification
TXT_INVERT_ON = GS + b'\x42\x01' # Inverse Printing ON
TXT_INVERT_OFF = GS + b'\x42\x00' # Inverse Printing OFF
# Fonts
SET_FONT = lambda n: ESC + b'\x4d' + n
TXT_FONT_A = SET_FONT(b'\x00') # Font type A
TXT_FONT_B = SET_FONT(b'\x01') # Font type B
# Text colors
TXT_COLOR_BLACK = ESC + b'\x72\x00' # Default Color
TXT_COLOR_RED = ESC + b'\x72\x01' # Alternative Color (Usually Red)

View File

@ -23,6 +23,7 @@ from .exceptions import *
from abc import ABCMeta, abstractmethod # abstract base class support
from escpos.image import EscposImage
from escpos.capabilities import get_profile, BARCODE_B
@six.add_metaclass(ABCMeta)
@ -35,11 +36,11 @@ class Escpos(object):
device = None
codepage = None
def __init__(self, columns=32):
def __init__(self, profile=None):
""" Initialize ESCPOS Printer
:param columns: Text columns used by the printer. Defaults to 32."""
self.columns = columns
:param profile: Printer profile"""
self.profile = get_profile(profile)
def __del__(self):
""" call self.close upon deletion """
@ -292,7 +293,8 @@ class Escpos(object):
else:
raise CharCodeError()
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", align_ct=True, function_type="A"):
def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A",
align_ct=True, function_type=None):
""" Print Barcode
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
@ -363,14 +365,40 @@ class Escpos(object):
issued.
:type align_ct: bool
:param function_type: Choose between ESCPOS function type A or B, depending on printer support and desired
barcode.
:param function_type: Choose between ESCPOS function type A or B,
depending on printer support and desired barcode. If not given,
the printer will attempt to automatically choose the correct
function based on the current profile.
*default*: A
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
:py:exc:`~escpos.exceptions.BarcodeCodeError`
"""
if function_type is None:
# Choose the function type automatically.
if bc in BARCODE_TYPES['A']:
function_type = 'A'
else:
if bc in BARCODE_TYPES['B']:
if not self.profile.supports(BARCODE_B):
raise BarcodeTypeError((
"Barcode type '{bc} not supported for "
"the current printer profile").format(bc=bc))
function_type = 'B'
else:
raise BarcodeTypeError((
"Barcode type '{bc} is not valid").format(bc=bc))
bc_types = BARCODE_TYPES[function_type.upper()]
if bc.upper() not in bc_types.keys():
raise BarcodeTypeError((
"Barcode type '{bc}' not valid for barcode function type "
"{function_type}").format(
bc=bc,
function_type=function_type,
))
# Align Bar Code()
if align_ct:
self._raw(TXT_ALIGN_CT)
@ -399,14 +427,6 @@ class Escpos(object):
else: # DEFAULT POSITION: BELOW
self._raw(BARCODE_TXT_BLW)
bc_types = BARCODE_TYPES[function_type.upper()]
if bc.upper() not in bc_types.keys():
# TODO: Raise a better error, or fix the message of this error type
raise BarcodeTypeError("Barcode type {bc} not valid for barcode function type {function_type}".format(
bc=bc,
function_type=function_type,
))
self._raw(bc_types[bc.upper()])
if function_type.upper() == "B":
@ -439,7 +459,7 @@ class Escpos(object):
# TODO: why is it problematic to print an empty string?
raise TextError()
def block_text(self, txt, columns=None):
def block_text(self, txt, font=None, columns=None):
""" Text is printed wrapped to specified columns
Text has to be encoded in unicode.
@ -448,11 +468,11 @@ class Escpos(object):
:param columns: amount of columns
:return: None
"""
col_count = self.columns if columns is None else columns
col_count = self.profile.get_columns(font) if columns is None else columns
self.text(textwrap.fill(txt, col_count))
def set(self, align='left', font='a', text_type='normal', width=1, height=1, density=9, invert=False, smooth=False,
flip=False):
def set(self, align='left', font='a', text_type='normal', width=1,
height=1, density=9, invert=False, smooth=False, flip=False):
""" Set text properties by sending them to the printer
:param align: horizontal position for text, possible values are:
@ -462,7 +482,9 @@ class Escpos(object):
* RIGHT
*default*: LEFT
:param font: font type, possible values are A or B, *default*: A
:param font: font given as an index, a name, or one of the
special values 'a' or 'b', refering to fonts 0 and 1.
:param text_type: text type, possible values are:
* B for bold
@ -527,10 +549,8 @@ class Escpos(object):
self._raw(TXT_BOLD_OFF)
self._raw(TXT_UNDERL_OFF)
# Font
if font.upper() == "B":
self._raw(TXT_FONT_B)
else: # DEFAULT FONT: A
self._raw(TXT_FONT_A)
self._raw(SET_FONT(six.int2byte(self.profile.get_font(font))))
# Align
if align.upper() == "CENTER":
self._raw(TXT_ALIGN_CT)

View File

@ -1 +0,0 @@
Dies ist ein Test.

View File

@ -34,27 +34,22 @@ 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():
@classmethod
def setup_class(cls):
""" Create a config file to read from """
with open(CONFIGFILE, 'w') as config:
config.write(CONFIG_YAML)
@staticmethod
def teardown_class():
@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 = None
self.default_args = None
self.env = TestFileEnvironment(
base_path=TEST_DIR,
cwd=os.getcwd(),

View File

@ -0,0 +1,38 @@
#!/usr/bin/python
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import escpos.printer as printer
from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B
from escpos.capabilities import Profile, BARCODE_B
from escpos.exceptions import BarcodeTypeError
import pytest
@pytest.mark.parametrize("bctype,data,expected", [
('EAN13', '4006381333931',
b'\x1ba\x01\x1dh@\x1dw\x03\x1df\x00\x1dH\x02\x1dk\x024006381333931\x00')
])
def test_barcode(bctype, data, expected):
"""should generate different barcode types correctly.
"""
instance = printer.Dummy()
instance.barcode(data, bctype)
assert instance.output == expected
@pytest.mark.parametrize("bctype,supports_b", [
('invalid', True),
('CODE128', False),
])
def test_lacks_support(bctype, supports_b):
"""should raise an error if the barcode type is not supported.
"""
profile = Profile(features={BARCODE_B: supports_b})
instance = printer.Dummy(profile=profile)
with pytest.raises(BarcodeTypeError):
instance.barcode('test', bctype)
assert instance.output == b''

View File

@ -12,34 +12,19 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from nose.tools import with_setup
import escpos.printer as printer
import os
import filecmp
devfile = 'testfile'
from escpos.printer import Dummy
def setup_testfile():
"""create a testfile as devfile"""
fhandle = open(devfile, 'a')
try:
os.utime(devfile, None)
finally:
fhandle.close()
def teardown_testfile():
"""destroy testfile again"""
os.remove(devfile)
@with_setup(setup_testfile, teardown_testfile)
def test_function_text_dies_ist_ein_test_lf():
"""test the text printing function with simple string and compare output"""
instance = printer.File(devfile=devfile)
instance = Dummy()
instance.text('Dies ist ein Test.\n')
instance.flush()
assert(filecmp.cmp('test/Dies ist ein Test.LF.txt', devfile))
assert instance.output == b'Dies ist ein Test.\n'
def test_block_text():
printer = Dummy()
printer.block_text(
"All the presidents men were eating falafel for breakfast.", font='a')
assert printer.output == \
b'All the presidents men were eating falafel\nfor breakfast.'

38
test/test_profile.py Normal file
View File

@ -0,0 +1,38 @@
import pytest
from escpos.capabilities import get_profile, NotSupported, BARCODE_B, Profile
@pytest.fixture
def profile():
return get_profile('default')
class TestBaseProfile:
"""Test the `BaseProfile` class.
"""
def test_get_font(self, profile):
with pytest.raises(NotSupported):
assert profile.get_font('3')
assert profile.get_font(1) == 1
assert profile.get_font('a') == 0
def test_supports(self, profile):
assert not profile.supports('asdf asdf')
assert profile.supports(BARCODE_B)
def test_get_columns(self, profile):
assert profile.get_columns('a') > 5
with pytest.raises(NotSupported):
assert profile.get_columns('asdfasdf')
class TestCustomProfile:
"""Test custom profile options with the `Profile` class.
"""
def test_columns(self):
assert Profile(columns=10).get_columns('sdfasdf') == 10
def test_features(self):
assert Profile(features={'foo': True}).supports('foo')

View File

@ -6,8 +6,10 @@ deps = nose
coverage
scripttest
mock
pytest
pytest-cov
hypothesis
commands = nosetests --with-coverage --cover-erase --cover-branches
commands = py.test --cov escpos
[testenv:docs]
basepython = python