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

112 Commits

Author SHA1 Message Date
Gertjan van den Burg
11d46fdb66 Make logging.basicConfig conditional (#670)
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2025-08-25 01:36:17 +02:00
Hasan Sezer Taşan
1a780e8f80 ref(package) replace appdirs with platformdirs in configuration and requirements files (#697)
* ref(package) replace appdirs with platformdirs in configuration and requirements files

* ref: remove types-appdirs from dependencies in tox.ini
2025-08-25 01:30:25 +02:00
dependabot[bot]
c702204231 Bump actions/checkout from 4 to 5 (#694)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 01:22:31 +02:00
Benito López
00d3a1301f Switch to python 3.13 (#693)
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2025-08-11 01:25:11 +02:00
Benito López
ebd6f88acf Add justify to text alignment of software_columns() Fixes #689 (#690)
* Add method: justify

* Add justify test

* Add another justify test

* Please the linter

* Allow single-item text_list in _rearrange_into_cols()

* Add parameter checks to software_columns

* Test for specific errors
2025-08-11 01:21:29 +02:00
dependabot[bot]
b85d5b907d Bump actions/setup-python from 5.5.0 to 5.6.0 (#682)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.5.0 to 5.6.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.5.0...v5.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-24 18:13:59 +02:00
dependabot[bot]
3dfbf15fa5 Bump actions/setup-python from 5.4.0 to 5.5.0 (#681)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.4.0...v5.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 13:16:11 +01:00
Patrick Kanzler
9b695d698b exclude .venv from flake8 2025-03-16 15:52:23 +01:00
dependabot[bot]
7ec59c41a2 Bump jinja2 from 3.1.5 to 3.1.6 in /examples/docker-flask (#679)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-06 13:35:25 +01:00
Benito López
a394826d69 Docs: profile usage additions and clarifications (#677)
* Add link to github

* Add profile params to 'Documentation and Usage'

* More profile additions and clarifications

* Fix code style

* Fix Include link to github in documentation topbar

* Fix Black code style

* Fix GH link

* Fix GH link path

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2025-02-21 21:01:40 +01:00
Benito López
e383b7a397 Fix wrong font choices (#676)
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2025-02-21 10:59:43 +01:00
dependabot[bot]
e39ec9e50d Bump codecov/codecov-action from 4 to 5 (#668)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-21 10:53:59 +01:00
dependabot[bot]
dc0d9e6bf6 Bump jinja2 from 3.1.4 to 3.1.5 in /examples/docker-flask (#671)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.5)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-21 10:45:27 +01:00
dependabot[bot]
5d943566c9 Bump actions/setup-python from 5.3.0 to 5.4.0 (#674)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.3.0...v5.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2025-02-21 10:37:15 +01:00
Benito López
3e8525673b New feature: Software columns - Part 4: Examples (#673)
* Add software_colums example

* Fix docs build
2025-02-21 10:33:28 +01:00
dependabot[bot]
0a1d3841f1 Bump sphinx-rtd-theme from 3.0.1 to 3.0.2 (#666)
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 3.0.1 to 3.0.2.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/3.0.1...3.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 16:40:30 +01:00
dependabot[bot]
27def759ba Bump werkzeug from 3.0.3 to 3.0.6 in /examples/docker-flask (#664)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.3 to 3.0.6.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.3...3.0.6)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 05:51:10 +02:00
dependabot[bot]
8c44d8e64e Bump actions/setup-python from 5.2.0 to 5.3.0 (#663)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.2.0...v5.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 20:51:49 +02:00
dependabot[bot]
7d42f11716 Bump sphinx-rtd-theme from 2.0.0 to 3.0.1 (#661)
* Bump sphinx-rtd-theme from 2.0.0 to 3.0.1

Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 2.0.0 to 3.0.1.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

* remove call to get_html_theme_path

according to deprection warning of sphinx-rtd-theme>=3

* disable broken spelling integration (pypi) and fix spelling

* fix spelling

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
2024-10-10 15:00:28 +02:00
dependabot[bot]
5cdff0b56e Bump actions/setup-python from 5.1.1 to 5.2.0 (#655)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.1.1 to 5.2.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.1.1...v5.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-30 14:52:10 +02:00
aerialist
f42410603d Add sleep in sending fragments (#624)
* Add sleep in sending fragments

Adding sleep prevents "USBTimeoutError: [Errno 110] Operation timed out".

* change sorting

* make sleep configurable

* add spelling

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
2024-08-25 00:50:27 +02:00
Patrick Kanzler
22982fbd12 update organize import trigger, update capabilities data, fix mypy (#654)
* update organize import trigger

* update capabilities data

* fix mypy error (jaconv is using a import hack)
2024-08-25 00:21:47 +02:00
Benito López
82386f7496 New feature: Software columns - Part 3: Tests (#651)
* Add test_function_software_columns.py

* Improve coverage
2024-08-24 23:29:00 +02:00
Benito López
99501cc2c1 New feature: Software columns - Part 2: CLI (#649)
* Add software_columns CLI parameter

* Fix sorting

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2024-07-19 22:23:52 +02:00
Psychpsyo
3aaf203ceb Fix typo in README (#650) 2024-07-19 22:18:09 +02:00
Benito López
a8753a1121 New feature: Software columns (#645)
* add type hint Alignment

* Add static method padding()

* Add static method truncate()

* Add static method _repeat_last()

* Add private method _rearrange_into_cols()

* Add private method _add_padding_into_cols()

* Add public method software_columns

* Make truncate and padding private staticmethods

* Revert "add type hint Alignment"

This reverts commit 546391cb9c.

* Add type hint Alignment

* Fix typo in docstring

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2024-07-11 17:13:34 +02:00
dependabot[bot]
5af01641d9 Bump actions/setup-python from 5.1.0 to 5.1.1 (#648)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.1.0...v5.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-11 16:41:42 +02:00
Benito López
fe3cdde424 Clarify set() width and height documentation (#644) 2024-05-24 23:21:54 +02:00
dependabot[bot]
4c02881fe7 Bump jinja2 from 3.1.3 to 3.1.4 in /examples/docker-flask (#642)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 22:54:24 +02:00
dependabot[bot]
82f5a00b8d Bump werkzeug from 3.0.1 to 3.0.3 in /examples/docker-flask (#641)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.1...3.0.3)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 21:40:03 +02:00
Benito López
1e9adb80f3 Fix CLI not working for some connectors Fixes #639 (#640) 2024-04-29 23:50:52 +02:00
dependabot[bot]
640f6089ac Bump pillow from 10.2.0 to 10.3.0 in /examples/docker-flask (#634)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 21:04:35 +02:00
dependabot[bot]
a8a9d0f0ad Bump codecov/codecov-action from 3 to 4 (#625)
* Bump codecov/codecov-action from 3 to 4

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>

* add codecov token

* add codecov token

* remove directory config

* adapt excludes

* exclude mypy_cache

* glob

* exclude

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
2024-04-02 01:23:56 +02:00
dependabot[bot]
f781e28a69 Bump actions/setup-python from 5.0.0 to 5.1.0 (#632)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.0.0 to 5.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.0.0...v5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 00:03:22 +02:00
dependabot[bot]
62c234f6f1 Bump pillow from 10.0.1 to 10.2.0 in /examples/docker-flask (#623)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.0.1 to 10.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.0.1...10.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 23:25:38 +01:00
dependabot[bot]
4ba98c0017 Bump jinja2 from 3.1.2 to 3.1.3 in /examples/docker-flask (#620)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-12 12:24:41 +01:00
Wesley Appler
a865b715f3 Fixed lack of spacing for multi-line strings (#619)
Co-authored-by: Wes Appler <wes@lamemakes>
2024-01-11 12:35:03 +01:00
dependabot[bot]
776b8a26ad Bump capabilities-data from 4006299 to 375135d (#616)
Bumps [capabilities-data](https://github.com/receipt-print-hq/escpos-printer-db) from `4006299` to `375135d`.
- [Commits](4006299c0f...375135d552)

---
updated-dependencies:
- dependency-name: capabilities-data
  dependency-type: direct:production
...

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

---------

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

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

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

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

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

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

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

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

* remove unused internal interfaces

* enable pytest in VS Code

* simplify and fix path logic in loader

* increase test coverage

* test missing config

* check for wrong printer names

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-30 08:38:40 +01:00
Patrick Kanzler
33d17615a0 prepare next release development cycle (#594) 2023-11-17 01:12:31 +01:00
Patrick Kanzler
90c89e66f0 filter for ValueError too in dpi logic 2023-11-17 00:48:31 +01:00
Patrick Kanzler
8f44ae7480 update changelog 2023-11-17 00:48:31 +01:00
Patrick Kanzler
ff5631c989 update authors 2023-11-17 00:48:31 +01:00
Patrick Kanzler
ecf3fbcc25 update capabilities data 2023-11-17 00:48:31 +01:00
Patrick Kanzler
678f8d2451 Merge pull request #579 from python-escpos:update-changelog
update changelog for new v3 release
2023-11-16 23:39:30 +01:00
Patrick Kanzler
a127c7b9c4 Merge branch 'master' into update-changelog 2023-11-16 23:32:22 +01:00
Patrick Kanzler
72d26879a0 Apply suggestions from code review 2023-11-01 19:37:38 +01:00
Benito López
3b6551004f Restore types-pywin32 (#591)
* Restore types-pywin32 dependency

* Restore pywin32 type annotations in win32raw

* Add return type to _raw()
2023-10-31 13:55:48 +01:00
Patrick Kanzler
5d3d2ca34b import TypedDict from typing, not from extensions (#589)
TypedDict is in the supported python versions
available in typing. Therefore an import from
potentially uninstalled typing_extensions is
not necessary.

fixes #560
2023-10-28 21:11:38 +02:00
Patrick Kanzler
d2ef5159c5 Merge branch 'master' into update-changelog 2023-10-28 20:53:21 +02:00
Benito López
a50a3b7167 Separate method open() and constructor, enhance consistency between connectors: Rework printer tests (#587)
* Add fixtures
* Add test_printer_file.py
* Remove old broken printer tests
* Include a close_on_reopen test
* Add test_printer_network.py
* Add test_printer_serial.py
* Add test_printer_usb.py
* Add test_printer_lp.py
* Add test_printer_cups.py
* Add test_printer_win32raw.py
* Test the 'printers' property
* Fix conftest import formatting
* Fix failing LP tests
* Cancel close only if literal False|None _device
* Fix win32raw failing tests (maybe)
* Include win32raw close_on_reopen test
* Include test _raw methods to win32raw
* Replace general exceptions in win32raw
* Replace wrong exception in cups
* Include more tests to cups
* Extend cups tests
2023-10-28 20:52:59 +02:00
dependabot[bot]
e7dd97554c Bump werkzeug from 2.3.4 to 3.0.1 in /examples/docker-flask (#588)
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.4 to 3.0.1.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.3.4...3.0.1)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 18:55:56 +02:00
Benito López
425d3d209f Update capabilities data (#586) 2023-10-18 13:20:08 +02:00
Benito López
a00b98937b Separate method open() and constructor, enhance consistency between connectors. (#584)
* Add self-open mechanism
* self-open mechanism through the 'device' property
* Separate open() and store connection in 'device'
* Restore device status to False on close()
* Add default value to all params + type annotations
* Add generic DeviceNotFoundError exception
* Update USBNotFoundError return code
* Enhance connectors consistency
* Fix LP printer stall
* Fix LP waste of paper due to auto-flush + flush on close
* Move platform dependent printers' guard to init

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
2023-10-16 11:36:07 +02:00
Patrick Kanzler
a8d789fe64 update change log 2023-10-15 23:03:10 +02:00
Patrick Kanzler
246c4ea6c5 add template for new release 2023-10-09 00:16:38 +02:00
Patrick Kanzler
3a8af8a6f5 switch to python 3.12 (#582)
* switch to python 3.12
* 3.11 on RTD
* fix SyntaxWarning (regex strings were invalid partially)
2023-10-09 00:13:39 +02:00
Patrick Kanzler
e4f4844983 add action for windows build (#581)
* copy action for windows
* exclude cups on windows
* update changelog
2023-10-08 23:27:37 +02:00
Patrick Kanzler
a70e1604d6 Magic encoder: fix codepage usage (#580)
* add unit test for issue pointed out by @scott-r in #570

* swap order of encoder search

As described by @scott-r in
Magic encoder does not use
previously used code pages
when possible #570
Thank you!
2023-10-05 14:55:12 +02:00
Patrick Kanzler
5018f7f377 add checkpoint to release checklist for changelog (#578) 2023-10-05 14:22:21 +02:00
Patrick Kanzler
ecfeeb9b13 Improve diagnostic output (#577)
* add extended version information

* autodocument argparser

* add spelling exception

* fix docstrings

* add annotations

* use typing types

* add test
2023-10-05 14:15:19 +02:00
dependabot[bot]
ba0167f1fd Bump pillow from 9.5.0 to 10.0.1 in /examples/docker-flask (#576)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.0.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-04 08:44:44 +02:00
dependabot[bot]
87e488ce11 Bump actions/setup-python from 4.7.0 to 4.7.1 (#575)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.7.0 to 4.7.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.7.0...v4.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 20:10:41 +02:00
Patrick Kanzler
6d0c475b9a refactor set method (#547)
* add type annotations
* Update setup.cfg
* improve mypy and test config
* get_profile_class returns a baseProfile
* mute mypy type issue
* add set_with_default
* docstring
* add test for empty call
* correct type annotations in set_with_default
* improve tests
* test for exception
* sort with isort
* add default value to get call
* empty string has the same effect: will not be found --> None
* add mypy test to workflow
* explicitely call mypy
* update spelling
2023-09-19 07:51:39 +02:00
Patrick Kanzler
adcde8abda Install types-pywin32 for mypy (#568) 2023-09-17 23:39:16 +02:00
Patrick Kanzler
89aaae0186 get mypy configuration change from #547 (#567) 2023-09-17 23:24:16 +02:00
Patrick Kanzler
f3ee049ee1 Update spelling_wordlist.txt (#566)
* Update spelling_wordlist.txt

* update apt
2023-09-17 23:00:38 +02:00
Patrick Kanzler
cfa9ecf16d Enable spell checking (#563)
* add spellchecking

* improve spelling

* improve spelling config

* extend word list

* improve spelling

* improve spelling

* escalate warning in spell check to failure

* fix spelling

* fix spelling

* add plural

* Update doc/spelling_wordlist.txt

* do not stop on warning

* require newest sphinxcontrib spelling

* remove old comment

* add authors as single line entry to spelling list

* reenable stop on warning
2023-09-07 22:08:31 +02:00
Patrick Kanzler
8f07c1da0f isort imports on save (#564) 2023-09-06 23:42:01 +02:00
Patrick Kanzler
24217756f7 558 improve capabilities handling (#562)
* add handling for missing capabilities file

* improve documentation

* Update doc/user/installation.rst
2023-09-06 00:05:38 +02:00
dependabot[bot]
e3e1500d35 Bump actions/checkout from 3 to 4 (#561)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 13:07:02 +02:00
Patrick Kanzler
44444f3c51 Maintenance: fix read the docs and some annotations (#557) 2023-09-03 09:57:56 +02:00
Patrick Kanzler
c7c01cdbff fix rtd theme (#553)
* use rtd_theme

* add types for serial

* annotate types where possible

* fix imports
2023-08-25 01:19:23 +02:00
dependabot[bot]
6c49e4a057 Bump sphinx-rtd-theme from 1.2.2 to 1.3.0 (#550) 2023-08-24 21:30:22 +00:00
Benito López
f23bcacabe fix typo in lp connector (#548) 2023-08-24 23:17:34 +02:00
Patrick Kanzler
c11d192e65 Fix import issues with qrcode in sphinx 7.2 (#552)
* 7.2 introduces the issue

* mock module qrcode for documentation build

This prevents issues with sphinx 7.2
2023-08-24 23:02:33 +02:00
Patrick Kanzler
3177c8d411 split off dependencies for optional installation (#546)
* add inheritance diagrams to all printers and exceptions
* split off printer implementations into separate files
* add wrapper that thros RuntimeError if not importable
* add dependency check for lp
* add dependency check for pyserial
* added check for usability
* import Win32Raw
* include WIn32Raw in documentation
* enable all extras on tox
* update github workflow
2023-08-17 01:37:50 +02:00
Patrick Kanzler
fbabd8ed88 Drop Py37, improve typing and docstrings (#544)
Drops Py3.7, improves typing and adds a mypy config, improves the docstrings and isorts the imports.

* configure isort
* sort with isort
* add github action
* enable flake8-docstrings
* fix docstrings
* add mypy env
* no implicit optional
* add type for raw
* add some type hints
2023-08-15 01:03:36 +02:00
Patrick Kanzler
2b62c8e28d modernize and cleanup documentation (#542)
* disable system packages on rtd

* install pycups on rtd

* enable cups binding in documentation

* document CupsPrinter

* fix formatting

* revise methods and installation

* revise user/printers

* revise raspi section

* further revise
2023-08-10 01:38:47 +02:00
Patrick Kanzler
4c2dcdfac6 Clean up tests and migrate (#540)
* migrate
  * abstract base class test
  * remove assert_equal in test_cli
  * remove nose from test_cli
  * remove nose dependencies
  * use tempfile
* configure coverage
  * flag python version in name
  * enable comment
* drop EOL py37
2023-08-10 00:18:02 +02:00
Patrick Kanzler
31daabcbea Improve documentation build (#539)
* use imgconverter
* enable pdf build with xelatex
* change heading levels
* restructure headings
* add chapter intro
* remove extensions from index
2023-08-09 00:52:01 +02:00
Patrick Kanzler
4a0f5855ef disable pdf build for now (#538) 2023-07-29 21:05:18 +02:00
Patrick Kanzler
60c4f481ae Add printer profile list to documentation (#536)
* add first draft of printer profile listing

* add todos

* Update doc/capability_templates/capabilities-template.jinja

* Update doc/capability_templates/capabilities-template.jinja

* restructure documentation

* add encoding list

* add color and encoding table

* add notes on usage

* add feature table
2023-07-29 02:02:13 +02:00
Patrick Kanzler
09a598883c 351 ean example fix (#537)
* bug/doc: Fix invalid EAN-13 barcode in examples

In the various examples and python-escpos CLI there are a number of uses
of `13243546557687` as an EAN-13 example.  This EAN barcode is invalid
as the checksum should be `0` and not `7`.

```
$ python test_print.py
Traceback (most recent call last):
  File "test_print.py", line 5, in <module>
    p.barcode('13243546557687', 'EAN13', 64, 2, '', '')
  File "/home/pi/fatt-display/lib/python3.7/site-packages/escpos/escpos.py", line 433, in barcode
    bc=bc,
escpos.exceptions.BarcodeCodeError: No Barcode code was supplied (Barcode '13243546557687' not in a valid format for type 'EAN13')
```

This patch set removes `13243546557687` and replaces it with the valid
number `40063813339310`.

In researching the list of [assigned prefixes issued by
G1][g1-prefixes] there seemed to be no "officially" defined test  prefix, so
this change was made to be minimally invasive using the number from the
existing test cases.

Resolves #350

Affects #176

[g1-prefixes]: https://www.gs1.org/standards/id-keys/company-prefix
[test-code]: https://www.barcodelookup.com/4006381333931

---------

Co-authored-by: Brian 'Redbeard' Harrington <redbeard@dead-city.org>
2023-07-28 18:23:18 +02:00
Alfredo orozco
df9e8ff394 Feature(escpos) Add buzzer function (#535)
* Add buzzer function
* Add `buzzer(time, duration)` function to control the buzzer on supported
   printers.
* Add unit tests for buzzer function.
* Update test_function_buzzer.py to pass black
* Run black in tests files
---------

Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
2023-07-27 19:10:19 +02:00
Benito López
9ff327d967 Remove sleep when quering status. Fixes #407 (#534)
* Remove sleep when quering status
* Remove unused import time
2023-07-23 23:22:27 +02:00
Patrick Kanzler
e9e8b10582 Fixes from ci (#533)
* break line

* remove unused imports

* remove unused os import

* make flake8 more strict

* configure flake for black

* fix action

* use importlib_resources

* rename deprecated methods
2023-07-21 23:03:46 +02:00
Florent de Labarre
cb3f4e856b [FIX] doesn't work in multi user env (#532)
1/ launch code with user A, a temp dir is create with user A
2/ launch code with user B --> permission denied
2023-07-21 22:10:29 +02:00
dependabot[bot]
756bf2c6c2 Bump actions/setup-python from 4.6.1 to 4.7.0 (#531)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.1 to 4.7.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.6.1...v4.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 17:59:00 +02:00
Benito López
3c11c1b9ab New high level barcode method. Closes #245, #244. (#527)
* Merge software and hardware barcodes to one method

* Fix wrong sw barcode heigh/width

* Add missing param to _sw_barcode call

* Make barcode() smarter, improvements and clean up

* Use param font_size in sw_barcode()

* Update docstrings

* Update barcode examples and docs

* Add --force_software option to CLI

* Attempt to match the sw and hw barcode sizes

* Better approximation to native font size

* Fix docs build

* Update tests at test_function_softbarcode

* Fix exception

* Move image dpi setting to writter_options

* Fix _sw_barcode() docstring param

* Fix wrong default param in docstring

* improve linkage in documentation

---------

Co-authored-by: Patrick Kanzler <4189642+patkan@users.noreply.github.com>
Co-authored-by: Patrick Kanzler <dev@pkanzler.de>
2023-07-12 20:45:41 +02:00
dependabot[bot]
676d2840de Bump sphinx-rtd-theme from 1.2.1 to 1.2.2 (#528)
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.2.1 to 1.2.2.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.2.1...1.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-09 01:24:17 +02:00
dependabot[bot]
e72ebc7986 Bump actions/setup-python from 4.6.0 to 4.6.1 (#526)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4.6.0...v4.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-25 14:03:26 +02:00
dependabot[bot]
c67c077a4d Bump sphinx-rtd-theme from 1.2.0 to 1.2.1 (#524)
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 1.2.0 to 1.2.1.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/1.2.0...1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 15:22:50 +02:00
Patrick Kanzler
fc17391b63 remove unused badges (#522) 2023-05-18 16:38:40 +02:00
Patrick Kanzler
905e37e52e update read the docs config (#520)
* update read the docs config

* add hints on black code style

* add link to black

* fix path to requirements

* include submodules in build

* add submodule capabilities-data

* format as list

* post on execution on RTD

* use sphinx rtd theme 1.2.0

* add apt dependencies
2023-05-18 16:33:20 +02:00
Ricardo Sánchez Alba
66348ccc0a Added example with Docker and Flask (#519)
* Added example with Docker and Flask

* set flask debug flag to False

* new line at the end of file

* format with black

---------

Co-authored-by: Ricardo Sanchez Alba <r2sanchezalba@gmail.com>
2023-05-18 15:06:06 +02:00
111 changed files with 5668 additions and 1927 deletions

View File

@@ -18,7 +18,8 @@ I have:
**Printer:** Manufacturer Model XVI **Printer:** Manufacturer Model XVI
<!-- since version 2.0.1 you can type 'python-escpos version' in your shell. <!-- since version 2.0.1 you can type 'python-escpos version' in your shell.
Alternatively you could use '__version__' in module escpos. --> Starting with python-escpos version 3.0, please replace the information below
with the output of `python-escpos version_extended`. -->
**python-escpos version:** 0.0.0 **python-escpos version:** 0.0.0
**python version:** 0.0 **python version:** 0.0

View File

@@ -6,8 +6,8 @@ jobs:
black-code-style: black-code-style:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
- uses: psf/black@stable - uses: psf/black@stable
with: with:
version: "23.3.0" version: "23.12.0"

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job # Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Install packages - name: Install packages
@@ -27,6 +27,6 @@ jobs:
sudo apt-get update -y && sudo apt-get update -y &&
sudo apt-get install -y git python3-sphinx graphviz libenchant-2-2 && sudo apt-get install -y git python3-sphinx graphviz libenchant-2-2 &&
sudo apt-get install -y gcc libcups2-dev python3-dev python3-setuptools && sudo apt-get install -y gcc libcups2-dev python3-dev python3-setuptools &&
sudo pip install tox pycups sudo pip install --ignore-installed tox pycups
- name: Test doc build - name: Test doc build
run: tox -e docs run: tox -e docs

View File

@@ -0,0 +1,57 @@
name: Python package on Windows
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v5
with:
submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest tox tox-gh-actions
If (Test-Path .\requirements.txt) { pip install -r .\requirements.txt }
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E,F,W --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with tox
run: |
tox
env:
ESCPOS_CAPABILITIES_FILE: D:\a\python-escpos\python-escpos\capabilities-data\dist\capabilities.json
- name: Test mypy with tox
run: |
tox -e mypy
env:
ESCPOS_CAPABILITIES_FILE: D:\a\python-escpos\python-escpos\capabilities-data\dist\capabilities.json
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
files: ./coverage.xml
exclude: "**/.mypy_cache"
flags: unittests
name: coverage-tox-${{ matrix.python-version }}
verbose: true

View File

@@ -15,25 +15,32 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update
sudo apt-get install -y graphviz libenchant-2-2 gcc libcups2-dev python3-dev xindy
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install flake8 pytest tox tox-gh-actions pip install flake8 pytest tox tox-gh-actions
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Check sorting of imports
uses: isort/isort-action@master
with:
requirementsFiles: "requirements.txt doc/requirements.txt"
sortPaths: "./doc ./src ./examples ./test ./setup.py"
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E,F,W --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with tox - name: Test with tox
@@ -41,3 +48,20 @@ jobs:
tox tox
env: env:
ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json
- name: Test mypy with tox
run: |
tox -e mypy
env:
ESCPOS_CAPABILITIES_FILE: /home/runner/work/python-escpos/python-escpos/capabilities-data/dist/capabilities.json
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
files: ./coverage.xml
exclude: "**/.mypy_cache"
flags: unittests
name: coverage-tox-${{ matrix.python-version }}
verbose: true

2
.gitignore vendored
View File

@@ -22,6 +22,7 @@ dist/
src/escpos/version.py src/escpos/version.py
.hypothesis .hypothesis
.pytest_cache/ .pytest_cache/
coverage.xml
# pyenv # pyenv
.python-version .python-version
@@ -40,3 +41,4 @@ test/test-cli-output/
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json

View File

@@ -17,3 +17,4 @@ Alex Debiasio <alex.debiasio@thinkin.io> <alex.debiasio@studenti.uni
Maximilian Wagenbach <maximilian.wagenbach@native-instruments.de> Maximilian Wagenbach <maximilian.wagenbach@native-instruments.de>
<belono@users.noreply.github.com> <tiotil.lindeman@gmail.com> <belono@users.noreply.github.com> <tiotil.lindeman@gmail.com>
belono <belono@users.noreply.github.com> Benito López <belono@users.noreply.github.com> belono <belono@users.noreply.github.com> Benito López <belono@users.noreply.github.com>
Alfredo Orozco <alfredoopa@gmail.com> Alfredo orozco <alfreedom@users.noreply.github.com>

27
.readthedocs.yml Normal file
View File

@@ -0,0 +1,27 @@
version: 2
formats:
- epub
- pdf
build:
os: ubuntu-22.04
tools:
python: "3.11"
apt_packages:
- graphviz
- libenchant-2-2
- gcc
- libcups2-dev
- python3-dev
- xindy
sphinx:
configuration: doc/conf.py
submodules:
include:
- capabilities-data
recursive: true
python:
install:
- requirements: doc/requirements.txt
- method: pip
path: .

View File

@@ -11,4 +11,9 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"python.formatting.provider": "black", "python.formatting.provider": "black",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
} }

View File

@@ -2,7 +2,9 @@ Ahmed Tahri
akeonly akeonly
Alejandro Hernández Alejandro Hernández
Alexander Bougakov Alexander Bougakov
Alexandre Detiste
Alex Debiasio Alex Debiasio
Alfredo Orozco
Asuki Kono Asuki Kono
belono belono
B. Howell B. Howell
@@ -15,6 +17,7 @@ Davis Goglin
Dean Rispin Dean Rispin
dependabot[bot] dependabot[bot]
Dmytro Katyukha Dmytro Katyukha
Florent de Labarre
Gerard Marull-Paretas Gerard Marull-Paretas
Hark Hark
Joel Lehtonen Joel Lehtonen
@@ -37,12 +40,15 @@ Qian Linfeng
Ramon Poca Ramon Poca
reck31 reck31
Renato Lorenzi Renato Lorenzi
Ricardo Sánchez Alba
Romain Porte Romain Porte
Sam Cheng Sam Cheng
Sergio Pulgarin Sergio Pulgarin
Stephan Sokolow Stephan Sokolow
Thijs Triemstra Thijs Triemstra
Thomas van den Berg Thomas van den Berg
tuxmaster
vendryan
Yaisel Hurtado Yaisel Hurtado
ysuolmai ysuolmai
白月秋见心 白月秋见心

View File

@@ -1,6 +1,130 @@
*********
Changelog Changelog
********* =========
202x-xx-xx - Version 3.x - ""
-------------------------------------------
changes
^^^^^^^
contributors
^^^^^^^^^^^^
2023-12-17 - Version 3.1 - "Rubric Of Ruin"
-------------------------------------------
This is the minor release of the new version 3.1.
It adds a modification of the API of the qr-method,
hence the minor release.
changes
^^^^^^^
- extend API of the qr-method to allow passing image
parameters in non-native mode
- use version 0.15 and upwards of python-barcode
- fix an issue in the config provider that prevented
config files to be found when only a path was supplied
- Improve type annotations, usage of six and other
packaging relevant parts.
contributors
^^^^^^^^^^^^
- Patrick Kanzler
- Alexandre Detiste
- tuxmaster
- belono
2023-11-17 - Version 3.0 - "Quietly Confident"
----------------------------------------------
This is the major release of the new version 3.0.
A big thank you to @belono for their many contributions
for the finalization of v3!
The third major release of this library drops support for
Python 2 and requires a Python version of at least 3.8.
The API has been reworked to be more consistent and two
new concepts have been introduced:
`Capabilities` allow the library to know which features
the currently used printer implements and send fitting
commands.
`Magic Encode` is a new feature that uses the
capability information and encodes Unicode automatically
with the correct code page while sending also the
code page change commands.
The license of the project has been changed to MIT
in accordance with its contributors.
The changes listed here are a summary of the changes
of the previous alpha releases. For details please
read the changelog of the alpha releases.
changes
^^^^^^^
- change the project's license to MIT in accordance with the contributors (see python-escpos/python-escpos#171)
- feature: add "capabilities" which are shared with escpos-php, capabilities are stored in
`escpos-printer-db <https://github.com/receipt-print-hq/escpos-printer-db>`_
- feature: the driver tries now to guess the appropriate code page and sets it automatically (called "magic encode")
- as an alternative you can force the code page with the old API
- fix the encoding search so that lower encodings are found first
- automatically handle cases where full cut or partial cut is not available
- refactor of the set-method
- preliminary support of POS "line display" printing
- add support for software-based barcode-rendering
- make feed for cut optional
- implemented paper sensor querying command
- Include support for CUPS based printer interfaces
- add Win32Raw-Printer on Windows-platforms
- add and improve Windows support of USB-class
- pickle capabilities for faster startup
contributors
^^^^^^^^^^^^
This is a list of contributors since the last v2 release.
- Ahmed Tahri
- akeonly
- Alexander Bougakov
- AlexandroJaez
- Asuki Kono
- belono
- brendanhowell
- Brian
- Christoph Heuel
- csoft2k
- Curtis // mashedkeyboard
- Dmytro Katyukha
- Foaly
- Gerard Marull-Paretas
- Justin Vieira
- kedare
- kennedy
- Lucy Linder
- Maximilian Wagenbach
- Michael Billington
- Michael Elsdörfer
- mrwunderbar666
- NullYing
- Omer Akram
- Patrick Kanzler
- primax79
- Ramon Poca
- reck31
- Romain Porte
- Sam Cheng
- Scott Rotondo
- Sergio Pulgarin
- Thijs Triemstra
- Yaisel Hurtado
- ysuolmai
and others
2023-05-11 - Version 3.0a9 - "Pride Comes Before A Fall" 2023-05-11 - Version 3.0a9 - "Pride Comes Before A Fall"
-------------------------------------------------------- --------------------------------------------------------
This release is the 10th alpha release of the new version 3.0. This release is the 10th alpha release of the new version 3.0.
@@ -67,7 +191,7 @@ contributors
- Alexander Bougakov - Alexander Bougakov
- Brian - Brian
- Yaisel Hurtado - Yaisel Hurtado
- Maximilan Wagenbach - Maximilian Wagenbach
- Patrick Kanzler - Patrick Kanzler
2019-06-19 - Version 3.0a6 - "Mistake not..." 2019-06-19 - Version 3.0a6 - "Mistake not..."
@@ -79,7 +203,7 @@ released.
changes changes
^^^^^^^ ^^^^^^^
- fix inclusion of the capabilities-file - fix inclusion of the capabilities-file
- execute CI jobs also on Windows and macOS-targets - execute CI jobs also on Windows and MacOS-targets
- improve documentation - improve documentation
contributors contributors
@@ -214,11 +338,11 @@ changes
- feature: the driver tries now to guess the appropriate code page and sets it automatically (called "magic encode") - feature: the driver tries now to guess the appropriate code page and sets it automatically (called "magic encode")
- as an alternative you can force the code page with the old API - as an alternative you can force the code page with the old API
- updated and improved documentation - updated and improved documentation
- changed constructor of main class due to introduction of capablities - changed constructor of main class due to introduction of capabilities
- changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation - changed interface of method `blocktext`, changed behavior of multiple methods, for details refer to the documentation
on `python-escpos.readthedocs.io <https://python-escpos.readthedocs.io>`_ on `python-escpos.readthedocs.io <https://python-escpos.readthedocs.io>`_
- add support for custom cash drawer sequence - add support for custom cash drawer sequence
- enforce flake8 on the src-files, test py36 and py37 on travis - enforce flake8 on the src-files, test py36 and py37 on Travis
contributors contributors
^^^^^^^^^^^^ ^^^^^^^^^^^^
@@ -251,7 +375,7 @@ contributors
changes changes
^^^^^^^ ^^^^^^^
- configure readthedocs and travis - configure readthedocs and Travis
- update doc with hint on image preprocessing - update doc with hint on image preprocessing
- add fix for printing large images (by splitting them into multiple images) - add fix for printing large images (by splitting them into multiple images)
@@ -293,8 +417,8 @@ changes
- packaging: configured the coverage-analysis codecov.io - packaging: configured the coverage-analysis codecov.io
- GitHub: improved issues-template - GitHub: improved issues-template
- documentation: add troubleshooting tip to network-interface - documentation: add troubleshooting tip to network-interface
- the module, cli and documentation is now aware of the version of python-escpos - the module, CLI and documentation is now aware of the version of python-escpos
- the cli does now support basic tabcompletion - the CLI does now support basic tab completion
contributors contributors
^^^^^^^^^^^^ ^^^^^^^^^^^^
@@ -312,11 +436,11 @@ changes
- refactor complete code in order to be compatible with Python 2 and 3 - refactor complete code in order to be compatible with Python 2 and 3
- modernize packaging - modernize packaging
- add testing and CI - add testing and CI
- merge various forks into codebase, fixing multiple issues with barcode-, QR-printing, cashdraw and structure - merge various forks into codebase, fixing multiple issues with barcode-, QR-printing, cash-draw and structure
- improve the documentation - improve the documentation
- extend support of barcode-codes to type B - extend support of barcode-codes to type B
- add function to disable panel-buttons - add function to disable panel-buttons
- the text-functions are now intended for unicode, the driver will automatically encode the string based on the selected - the text-functions are now intended for Unicode, the driver will automatically encode the string based on the selected
code page code page
- the image-functions are now much more flexible - the image-functions are now much more flexible
- added a CLI - added a CLI
@@ -355,7 +479,7 @@ contributors
-------------------------- --------------------------
- Merge pull request #53 from ldos/master - Merge pull request #53 from ldos/master
- Extended params for serial printers - Extended parameters for serial printers
- Sent by ldos <cafeteria.ldosalzira@gmail.com> - Sent by ldos <cafeteria.ldosalzira@gmail.com>
2015-04-21 - Version 1.0.5 2015-04-21 - Version 1.0.5
@@ -383,7 +507,7 @@ contributors
-------------------------- --------------------------
- Issue #5: Fixed vertical tab - Issue #5: Fixed vertical tab
- Issue #9: Fixed identation inconsistence - Issue #9: Fixed indentation inconsistency
2013-03-14 - Version 1.0.1 2013-03-14 - Version 1.0.1
-------------------------- --------------------------
@@ -394,6 +518,6 @@ contributors
2012-11-15 - Version 1.0 2012-11-15 - Version 1.0
------------------------ ------------------------
- Issue #2: Added ethernet support - Issue #2: Added Ethernet support
- Issue #3: Added compatibility with libusb-1.0.1 - Issue #3: Added compatibility with libusb-1.0.1
- Issue #4: Fixed typo in escpos.py - Issue #4: Fixed typo in escpos.py

View File

@@ -1,68 +1,83 @@
************
Contributing Contributing
************ ============
This project is open to any kind of contribution. You can help with improving the documentation, adding fixes to the :Last Reviewed: 2023-08-10
code, providing test cases in code or as a description or just spreading the word. Please feel free to create an
issue or pull request.
In order to reduce the amount of work for everyone please try to adhere to good practice.
The pull requests and issues will be prefilled with templates. Please fill in your information where applicable. This project is open to any kind of contribution.
You can help with improving the documentation, adding fixes to the
code, providing test cases in code or as a description or just
spreading the word.
Please feel free to create an issue or pull request.
In order to reduce the amount of work for everyone please try
to adhere to good practice.
This project uses `semantic versioning <https://semver.org/>`_ and tries to adhere to the proposed rules as The pull requests and issues will be prefilled with templates.
well as possible. Please fill in your information where applicable.
This project uses `semantic versioning <https://semver.org/>`_
and tries to adhere to the proposed rules as well as possible.
Author-list Author-list
----------- -----------
This project keeps a list of authors. This can be auto-generated by calling `./doc/generate-authors.sh`. This project keeps a list of authors.
When contributing the first time, please include a commit with the output of this script in place. This can be auto-generated by calling `./doc/generate-authors.sh`.
Otherwise the integration-check will fail. When contributing the first time, please include a commit with
the output of this script in place.
When you change your username or mail-address, please also update the `.mailmap` and the authors-list. When you change your username or mail-address, please also
You can find a good documentation on the mapping-feature in the `documentation of git-shortlog <https://git-scm.com/docs/git-shortlog#_mapping_authors>`_. update the `.mailmap` and the authors-list.
You can find a good documentation on the mapping-feature in the
`documentation of git-shortlog <https://git-scm.com/docs/git-shortlog#_mapping_authors>`_.
Style-Guide Style-Guide
----------- -----------
When writing code please try to stick to these rules. When writing code please try to stick to these rules.
PEP8 Black Code Style
^^^^ ^^^^^^^^^^^^^^^^
The entire codebase adheres to the rules of PEP8. This project is formatted with the auto formatter `black <https://github.com/psf/black>`_.
These rules are enforced by running `flake8` in the integration-checks. Please format your contributions with black, otherwise they will be rejected.
Please adhere to these rules as your contribution can only be merged if the check succeeds.
You can use flake8 or similar tools locally in order to check your code.
Apart from that the travis-log and the check by Landscape will provide you with hints.
GIT GIT
^^^ ^^^
The master-branch contains the main development of the project. A release to PyPi is marked with a tag The master-branch contains the main development of the project.
corresponding to the version. Issues are closed when they have been resolved by merging into the master-branch. A release to PyPi is marked with a tag corresponding to the version.
When you have a change to make, begin by creating a new branch from the HEAD of `python-escpos/master`. Issues are closed when they have been resolved by merging
into the master-branch.
When you have a change to make, begin by creating a new branch
from the HEAD of `python-escpos/master`.
Please try to group your commits into logical units. If you need to tidy up your branch, you can make use of a Please try to group your commits into logical units.
git feature called an 'interactive rebase' before making a pull request. A small, self-contained change-set is If you need to tidy up your branch, you can make use of a
easier to review, and improves the chance of your code being merged. git feature called an 'interactive rebase' before making a pull request.
Please also make sure that before creating your PR, your branch is rebased on a recent commit or you merged a recent A small, self-contained change-set is easier to review, and
commit into your branch. This way you can ensure that your PR is without merge conflicts. improves the chance of your code being merged.
Please also make sure that before creating your PR, your branch
is rebased on a recent commit or you merged a recent
commit into your branch.
This way you can ensure that your PR is without merge conflicts.
Docstrings Docstrings
^^^^^^^^^^ ^^^^^^^^^^
This project tries to have a good documentation. This project tries to have a good documentation.
Please add a docstring to every method and class. Have a look at existing methods and classes for the style. Please add a docstring to every method and class.
Have a look at existing methods and classes for the style.
We use basically standard rst-docstrings for Sphinx. We use basically standard rst-docstrings for Sphinx.
Test Test
^^^^ ^^^^
Try to write tests whenever possible. Our goal for the future is 100% coverage. Try to write tests whenever possible.
We are currently using `nose` but might change in the future. Our goal for the future is 100% coverage.
You can copy the structure from other testcases. Please remember to adapt the docstrings. You can copy the structure from other testcases.
Please remember to adapt the docstrings.
Further reading Further reading
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
For further best practices and hints on contributing please see the For further best practices and hints on contributing please see the
`contribution-guide <https://www.contribution-guide.org/>`_. Should there be any contradictions between this guide `contribution-guide <https://www.contribution-guide.org/>`_.
Should there be any contradictions between this guide
and the linked one, please stick to this text. and the linked one, please stick to this text.
Aside from that feel free to create an issue or write an email if anything is unclear. Aside from that feel free to create an issue or write an email if anything is unclear.

View File

@@ -4,7 +4,7 @@ python-escpos
This library is available over pypi. So for most of the use-cases it should be sufficient to run This library is available over pypi. So for most of the use-cases it should be sufficient to run
``` ```
pip install python-escpos --user # add --pre if you want to install pre-releases pip install python-escpos[all] --user # add --pre if you want to install pre-releases
``` ```
For more information please read the documentation at https://python-escpos.readthedocs.io/en/latest/user/installation.html For more information please read the documentation at https://python-escpos.readthedocs.io/en/latest/user/installation.html

View File

@@ -2,26 +2,17 @@
python-escpos - Python library to manipulate ESC/POS Printers python-escpos - Python library to manipulate ESC/POS Printers
############################################################# #############################################################
.. image:: https://travis-ci.org/python-escpos/python-escpos.svg?branch=master Description
:target: https://travis-ci.org/python-escpos/python-escpos ===========
:alt: Continous Integration
.. image:: https://codecov.io/github/python-escpos/python-escpos/coverage.svg?branch=master
:target: https://codecov.io/github/python-escpos/python-escpos?branch=master
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/python-escpos/badge/?version=latest .. image:: https://readthedocs.org/projects/python-escpos/badge/?version=latest
:target: https://python-escpos.readthedocs.io/en/latest/?badge=latest :target: https://python-escpos.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status :alt: Documentation Status
Description
-----------
Python ESC/POS is a library which lets the user have access to all those printers handled Python ESC/POS is a library which lets the user have access to all those printers handled
by ESC/POS commands, as defined by Epson, from a Python application. by ESC/POS commands, as defined by Epson, from a Python application.
The library tries to implement the functions provided by the ESC/POS-commandset and supports sending text, images, The library tries to implement the functions provided by the ESC/POS-command-set and supports sending text, images,
barcodes and qr-codes to the printer. barcodes and qr-codes to the printer.
Text can be aligned/justified and fonts can be changed by size, type and weight. Text can be aligned/justified and fonts can be changed by size, type and weight.
@@ -58,7 +49,7 @@ The basic usage is:
p = Usb(0x04b8, 0x0202, 0, profile="TM-T88III") p = Usb(0x04b8, 0x0202, 0, profile="TM-T88III")
p.text("Hello World\n") p.text("Hello World\n")
p.image("logo.gif") p.image("logo.gif")
p.barcode('1324354657687', 'EAN13', 64, 2, '', '') p.barcode('4006381333931', 'EAN13', 64, 2, '', '')
p.cut() p.cut()
@@ -68,9 +59,9 @@ Another example based on the Network printer class:
from escpos.printer import Network from escpos.printer import Network
kitchen = Network("192.168.1.100") #Printer IP Address kitchen = Network("192.168.1.100", profile="TM-T88III") #Printer IP Address
kitchen.text("Hello World\n") kitchen.text("Hello World\n")
kitchen.barcode('1324354657687', 'EAN13', 64, 2, '', '') kitchen.barcode('4006381333931', 'EAN13', 64, 2, '', '')
kitchen.cut() kitchen.cut()
Another example based on the Serial printer class: Another example based on the Serial printer class:
@@ -80,29 +71,37 @@ Another example based on the Serial printer class:
from escpos.printer import Serial from escpos.printer import Serial
""" 9600 Baud, 8N1, Flow Control Enabled """ """ 9600 Baud, 8N1, Flow Control Enabled """
p = Serial(devfile='/dev/tty.usbserial', p = Serial(
devfile='/dev/tty.usbserial',
baudrate=9600, baudrate=9600,
bytesize=8, bytesize=8,
parity='N', parity='N',
stopbits=1, stopbits=1,
timeout=1.00, timeout=1.00,
dsrdtr=True) dsrdtr=True,
profile="TM-T88III"
)
p.text("Hello World\n") p.text("Hello World\n")
p.qr("You can readme from your smartphone") p.qr("You can readme from your smartphone")
p.cut() p.cut()
.. note:: It is highly recommended to include a matching profile to inform python-escpos about the printer's capabilities.
The full project-documentation is available on `Read the Docs <https://python-escpos.readthedocs.io>`_. The full project-documentation is available on
`Read the Docs <https://python-escpos.readthedocs.io>`_.
Contributing Contributing
------------ ------------
This project is open for any contribution! Please see `CONTRIBUTING.rst <https://python-escpos.readthedocs.io/en/latest/dev/contributing.html>`_ for more information. This project is open for any contribution! Please see
`CONTRIBUTING.rst <https://python-escpos.readthedocs.io/en/latest/dev/contributing.html>`_
for more information.
Disclaimer Disclaimer
---------- ----------
None of the vendors cited in this project agree or endorse any of the patterns or implementations. None of the vendors cited in this project agree or endorse any of the
Its names are used only to maintain context. patterns or implementations.
Their names are used only to maintain context.

View File

@@ -8,5 +8,10 @@ coverage:
changes: off changes: off
range: "60...100" range: "60...100"
comment: off comment:
layout: " diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: false # [true :: must have a base report to post]
require_head: true # [true :: must have a head report to post]

View File

@@ -0,0 +1,28 @@
{% for item in data.encodings %}
{% set encoding = data.encodings[item] %}
{% macro draw_with_underline(text, symbol='-') -%}
{{ escape_rst(text) }}
{{ escape_rst(text) | length * symbol }}
{%- endmacro %}
{{ '.. _encoding-label-' + item + ':' }}
{{ draw_with_underline(encoding.name) }}
{{ escape_rst(encoding.notes) }}
Mapping Information
^^^^^^^^^^^^^^^^^^^
====================== ================================================================
identifier {{ escape_rst(item) }}
Name {{ escape_rst(encoding.name|default('Unknown')) }}
Iconv Name {{ escape_rst(encoding.iconv|default('Unknown')) }}
``python_encode`` Name {{ escape_rst(encoding.python_encode|default('Unknown')) }}
====================== ================================================================
{% if encoding.data is defined %}
{{ draw_with_underline('Code page data', symbol='^') }}
{{ encoding.data }}
{% endif %}
{% endfor %}

View File

@@ -0,0 +1,83 @@
{% for item in data.profiles %}
{% set printer = data.profiles[item] %}
{% macro draw_with_underline(text, symbol='-') -%}
{{ escape_rst(text) }}
{{ escape_rst(text) | length * symbol }}
{%- endmacro %}
{% macro abort(error) %}
{{ None['[ERROR] ' ~ error][0] }}
{% endmacro %}
{% macro fill_line(text, total, symbol=' ') -%}
{%- if total < text|length -%}
{{- abort("Line cannot be filled: must be longer") -}}
{%- endif -%}
{{- text + ((total - text|length ) * symbol ) -}}
{%- endmacro %}
{{ '.. _printer-label-' + item + ':' }}
{{ draw_with_underline(printer.name) }}
{{ escape_rst(printer.notes) }}
You can select this profile in python-escpos with this identifier: ``{{ item }}``.
(Set parameter to `profile='{{ item }}'`.)
Basic information
^^^^^^^^^^^^^^^^^
====================== ================================================================
Name {{ escape_rst(printer.name|default('Unknown')) }}
Vendor {{ escape_rst(printer.vendor|default('Unknown')) }}
Media width (mm) {{ escape_rst(printer.media.width.mm|default('Unknown')|string) }}
Media width (pixels) {{ escape_rst(printer.media.width.pixels|default('Unknown')|string) }}
DPI {{ escape_rst(printer.media.dpi|default('Unknown')|string) }}
====================== ================================================================
Fonts
^^^^^
+------------------+------------------------------+-----------------------+
| ID | Name | Columns |
+==================+==============================+=======================+
{% for id in printer.fonts -%}
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(escape_rst(printer.fonts[id].name), 28) }} | {{ fill_line(printer.fonts[id].columns|string, 21) }} |
+------------------+------------------------------+-----------------------+
{% endfor %}
Colors
^^^^^^
+------------------+----------------------------------------------------------------+
| ID | Color |
+==================+================================================================+
{% for id in printer.colors -%}
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(escape_rst(printer.colors[id]), 62) }} |
+------------------+----------------------------------------------------------------+
{% endfor %}
Feature support
^^^^^^^^^^^^^^^
+-----------------------------------+----------------------------+
| Feature | Supported |
+===================================+============================+
{% for feature in printer.features -%}
| {{ fill_line(escape_rst(feature), 33) }} | {{ fill_line(escape_rst(printer.features[feature]|string), 26) }} |
+-----------------------------------+----------------------------+
{% endfor %}
Text code pages
^^^^^^^^^^^^^^^
+------------------+----------------------------------------------------------------+
| ID | Encoding |
+==================+================================================================+
{% for id in printer.codePages -%}
| {{ fill_line(escape_rst(id), 16) }} | {{ fill_line(':ref:`encoding-label-'+printer.codePages[id]+'`', 62) }} |
+------------------+----------------------------------------------------------------+
{% endfor %}
{% endfor %}

View File

@@ -12,9 +12,8 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os import os
import sys
from importlib.metadata import version as imp_version from importlib.metadata import version as imp_version
on_rtd = os.getenv("READTHEDOCS") == "True" on_rtd = os.getenv("READTHEDOCS") == "True"
@@ -35,6 +34,7 @@ root = os.path.relpath(os.path.join(os.path.dirname(__file__), ".."))
# ones. # ones.
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.doctest", "sphinx.ext.doctest",
"sphinx.ext.todo", "sphinx.ext.todo",
"sphinx.ext.coverage", "sphinx.ext.coverage",
@@ -42,9 +42,15 @@ extensions = [
"sphinx.ext.todo", "sphinx.ext.todo",
"sphinx.ext.graphviz", "sphinx.ext.graphviz",
"sphinx.ext.inheritance_diagram", "sphinx.ext.inheritance_diagram",
"sphinx.ext.imgconverter",
"sphinxarg.ext",
"sphinxcontrib.datatemplates",
"sphinxcontrib.spelling", "sphinxcontrib.spelling",
] ]
# mock the following modules for autodoc
autodoc_mock_imports = ["qrcode"]
# supress warnings for external images # supress warnings for external images
suppress_warnings = [ suppress_warnings = [
"image.nonlocal_uri", "image.nonlocal_uri",
@@ -54,7 +60,7 @@ suppress_warnings = [
todo_include_todos = True todo_include_todos = True
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates", "capability_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = ".rst" source_suffix = ".rst"
@@ -67,7 +73,7 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "python-escpos" project = "python-escpos"
copyright = "2016, Manuel F Martinez and others" copyright = "2024, python-escpos developers"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@@ -121,13 +127,13 @@ pygments_style = "sphinx"
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
if on_rtd: if on_rtd:
html_theme = "default" html_theme = "sphinx_rtd_theme"
print("recognized execution on RTD")
else: else:
try: try:
import sphinx_rtd_theme import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme" html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError: except ImportError:
print("no sphinx_rtd_theme found, switching to nature") print("no sphinx_rtd_theme found, switching to nature")
html_theme = "default" html_theme = "default"
@@ -137,6 +143,14 @@ else:
# documentation. # documentation.
# html_theme_options = {} # html_theme_options = {}
# Show a 'Edit on GitHub' link instead of 'View page source'
html_context = {
"display_github": True,
"github_user": "python-escpos",
"github_repo": "python-escpos",
"github_version": "master/doc/",
}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = [] # html_theme_path = []
@@ -222,6 +236,8 @@ latex_elements = {
#'preamble': '', #'preamble': '',
} }
latex_engine = "xelatex"
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
@@ -230,7 +246,7 @@ latex_documents = [
"index", "index",
"python-escpos.tex", "python-escpos.tex",
"python-escpos Documentation", "python-escpos Documentation",
"Manuel F Martinez and others", "python-escpos developers",
"manual", "manual",
), ),
] ]
@@ -265,7 +281,7 @@ man_pages = [
"index", "index",
"python-escpos", "python-escpos",
"python-escpos Documentation", "python-escpos Documentation",
["Manuel F Martinez and others"], ["python-escpos developers"],
1, 1,
) )
] ]
@@ -284,7 +300,7 @@ texinfo_documents = [
"index", "index",
"python-escpos", "python-escpos",
"python-escpos Documentation", "python-escpos Documentation",
"Manuel F Martinez and others", "python-escpos developers",
"python-escpos", "python-escpos",
"One line description of project.", "One line description of project.",
"Miscellaneous", "Miscellaneous",
@@ -304,7 +320,19 @@ texinfo_documents = [
# texinfo_no_detailmenu = False # texinfo_no_detailmenu = False
# spellchecker # spellchecker
spelling_ignore_pypi_package_names = True spelling_ignore_pypi_package_names = False
spelling_ignore_wiki_words = True spelling_ignore_wiki_words = True
spelling_ignore_python_builtins = True spelling_ignore_python_builtins = True
spelling_ignore_importable_modules = True spelling_ignore_importable_modules = True
spelling_ignore_contributor_names = True
spelling_word_list_filename = ["spelling_wordlist.txt", "../AUTHORS"]
spelling_show_suggestions = True
spelling_suggestion_limit = 3
spelling_warning = True
spelling_exclude_patterns = [
"**/capabilities.json",
"../../capabilities-data/dist/capabilities.json",
"**/available-encodings.rst",
"**/available-profiles.rst",
"dev/todo.rst",
]

View File

@@ -0,0 +1,11 @@
Release process
===============
:Last Reviewed: 2023-08-10
* Update authors file
* Update changelog
* Set annotated tag for release and push to public GitHub
* Build wheel
* Load wheel to PyPi
* Prepare project for next release with an empty changelog entry

17
doc/dev/repository.rst Normal file
View File

@@ -0,0 +1,17 @@
.. _developer-manual-repository:
Repository
==========
:Last Reviewed: 2023-09-05
This project uses sub-projects and retrieves its versioning
information from version control.
Therefore it is crucial that you follow these rules when
working with the project (e.g. for packaging a
development version).
* Make sure that the git project is complete. A call to git status for example should succeed.
* Make sure that you have checked out all available sub-projects.
* Proper initialization of submodules can be ensured with ``git submodule update --init --recursive``

View File

@@ -1,6 +1,7 @@
****
TODO TODO
**** ====
:Last Reviewed: 2023-08-10
Open points and issues of the project are tracked in the GitHub issues. Open points and issues of the project are tracked in the GitHub issues.
Some annotations still remain in the code and should be moved over time Some annotations still remain in the code and should be moved over time

View File

@@ -5,8 +5,15 @@
.. include:: ../README.rst .. include:: ../README.rst
#######
Content Content
------- #######
User Documentation
==================
This chapter describes the central points that
are relevant to the user of this library.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
@@ -16,16 +23,46 @@ Content
user/methods user/methods
user/printers user/printers
user/raspi user/raspi
user/todo
user/usage user/usage
user/cli-user
user/barcode user/barcode
Printer profiles
================
This chapter gives a listing of the available
printer profiles. Details are described in
:ref:`capabilities-profile-intro`.
.. toctree::
:maxdepth: 1
:caption: Printer profiles
printer_profiles/capabilities
printer_profiles/available-profiles
printer_profiles/available-encodings
Developer Documentation
=======================
This chapter summarizes information for
developers of this library.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
:caption: Developer Documentation :caption: Developer Documentation
dev/release-process
dev/contributing dev/contributing
dev/repository
dev/changelog dev/changelog
dev/todo
API Documentation
=================
This chapter contains an auto-generated
documentation of the API of this library.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
@@ -43,8 +80,9 @@ Content
api/codepages api/codepages
api/katakana api/katakana
##################
Indices and tables Indices and tables
================== ##################
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`

View File

@@ -0,0 +1,11 @@
Available Encodings
-------------------
:Last Reviewed: 2023-08-10
If you find any issues with the described encodings,
please open an issue in the
`ESC/POS printer database <https://github.com/receipt-print-hq/escpos-printer-db>`_.
The data shown here is directly taken from there.
.. datatemplate:json:: ../../capabilities-data/dist/capabilities.json
:template: capabilities-template-encoding.jinja

View File

@@ -0,0 +1,18 @@
.. _available-profiles:
Available Profiles
------------------
:Last Reviewed: 2023-08-10
The following list describes which printer profiles are
available in this release.
The existence of a profile is a hint, but no guarantee
that this printer actually can be controlled by this library.
If you find any issues with the described capabilities,
please open an issue in the
`ESC/POS printer database <https://github.com/receipt-print-hq/escpos-printer-db>`_.
The data shown here is directly taken from there.
.. datatemplate:json:: ../../capabilities-data/dist/capabilities.json
:template: capabilities-template.jinja

View File

@@ -0,0 +1,26 @@
.. _capabilities-profile-intro:
Capabilities
------------
:Last Reviewed: 2023-08-10
Since the used command set often differs between printers,
a model for supporting different printers is implemented.
This feature is called `capabilities`.
The `capabilities`-feature allows this library to know
which features are supported.
If no further information is specified, python-escpos will
try to automatically use features based on the supplied information.
In order to use the `capabilities`-database, the printer instance
simply has to be created with the parameter `profile` set to the
relevant identifier.
The identifier can be found in :ref:`available-profiles`.
This documentation describes the profiles in the database file that
is bundled with this release.
If another configuration is to be used, this chapter can be followed
for information on how to side-load another `capabilities`-database:
:ref:`advanced-usage-change-capabilities-profile`.

View File

@@ -2,10 +2,15 @@ pyusb
Pillow>=2.0 Pillow>=2.0
qrcode>=4.0 qrcode>=4.0
pyserial pyserial
sphinx-rtd-theme sphinx-rtd-theme==3.0.2
setuptools setuptools
setuptools-scm setuptools-scm
docutils>=0.12 docutils>=0.12
sphinxcontrib-spelling>=7.2.0 sphinxcontrib-spelling>=8.0.0
python-barcode>=0.11.0,<1 python-barcode>=0.15.0,<1
importlib-metadata importlib-metadata
importlib_resources
sphinxcontrib.datatemplates
sphinx-argparse
sphinx-autodoc-typehints
pycups

View File

@@ -1,7 +1,145 @@
Raspbian
ESC ESC
Esc
POS POS
Pos
Escpos Escpos
Escpos Escpos
escpos
bitImageColumn
bitImageRaster
ep
pc
cp
csoft
Frédéric
Headcrash
Krispy
primax
Tahri
reck
mrwunderbar
zouppen
kedare
Foaly
brendanhowell
der
fvdsn
Marull
Paretas
Kakistocrat
Billington
patkan
Romain
Bougakov
Yaisel
Hurtado
Wagenbach
Poca
Akram
Vieira
Christoph
Heuel
Thijs
Triemstra
Linder
Romain
Pulgarin
Romain
Cheng
Dmytro
Katyukha
Elsdörfer
Asuki
Kono
López
mashedkeyboard
Thijs
Triemstra
Elsdörfer
Renato
Lorenzi
Bookham
Goglin
Christoph
Heuel
Qian
Lehtonen
Kanzler
Rotondo
Alexandre
Detiste
barcode
barcodes
baudrate baudrate
Bashlinux
capabilities
cashdraw
charcode
changelog
cheque
codebase
codecov
codepages
config
del
dev
dialout
docstring
docstrings
ean
Ean
encodable
Errno
fff
formatter
fullimage
hw
io
img
json
latin
libusb
lp
lsusb lsusb
natively
php
pre
prefilled
preprocess
preprocessing
printcap
programmatically
py
pypy
pyserial
pyusb
pyyaml
pywin
px
qrcode
Raspbian
readthedocs
rebase
rebased
renderer
resetted
rst
submodule
submodules
src
testcases
th
Todo
tox
traceback
udev
usb
USBTimeoutError
usec
virtualenvs
viivakoodi
whitespaces
xml

View File

@@ -1,17 +1,29 @@
Printing Barcodes Printing Barcodes
----------------- -----------------
:Last Reviewed: 2016-07-31
Most ESC/POS-printers implement barcode-printing. :Last Reviewed: 2023-08-10
The barcode-commandset is implemented in the barcode-method.
For a list of compatible barcodes you should check the manual of your printer. Many printers implement barcode printing natively.
As a rule of thumb: even older Epson-models support most 1D-barcodes. These hardware rendered barcodes are fast but the supported
To be sure just try some implementations and have a look at the notices below. formats are limited by the printer itself and different between models.
However, almost all printers support printing images, so barcode
rendering can be performed externally by software and then sent
to the printer as an image.
As a drawback, this operation is much slower and the user needs
to know and choose the image implementation method supported by
the printer's command-set.
barcode-method barcode-method
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
The barcode-method is rather low-level and orients itself on the implementation of ESC/POS. Since version 3.0, the ``barcode`` method unifies the previous
In the future this class could be supplemented by a high-level class that helps the user generating the payload. ``barcode`` (hardware) and ``soft_barcode`` (software) methods.
It is able to choose automatically the best printer implementation
for barcode printing based on the capabilities of the printer
and the type of barcode desired.
To achieve this, it relies on the information contained in the
escpos-printer-db profiles.
The chosen profile needs to match the capabilities of the printer
as closely as possible.
.. py:currentmodule:: escpos.escpos .. py:currentmodule:: escpos.escpos
@@ -31,4 +43,5 @@ For alphanumeric CODE128 you have to preface your payload with `{B`.
# print CODE128 012ABCDabcd # print CODE128 012ABCDabcd
p.barcode("{B012ABCDabcd", "CODE128", function_type="B") p.barcode("{B012ABCDabcd", "CODE128", function_type="B")
A very good description on CODE128 is also on `Wikipedia <https://en.wikipedia.org/wiki/Code_128>`_. A very good description on CODE128 is also on
`Wikipedia <https://en.wikipedia.org/wiki/Code_128>`_.

10
doc/user/cli-user.rst Normal file
View File

@@ -0,0 +1,10 @@
CLI
===
:Last Reviewed: 2023-09-25
Documentation of the command line interface, callable with ``python-escpos``.
.. argparse::
:ref: escpos.cli.generate_parser
:prog: python-escpos

View File

@@ -1,25 +1,53 @@
************ .. _user_installation:
Installation
************
:Last Reviewed: 2016-07-23 Installation
============
:Last Reviewed: 2023-08-10
Installation with PIP Installation with PIP
--------------------- ---------------------
Installation should be rather straight-forward. python-escpos is on PyPi, so you can simply enter: Installation should be rather straight-forward. python-escpos is on PyPi,
so you can simply enter:
:: ::
pip install python-escpos pip install python-escpos[all]
This should install all necessary dependencies. Apart from that python-escpos should also be This should install all necessary dependencies. Apart from that
available as a Debian package. If you want to always benefit from the newest stable releases you should probably python-escpos is for some versions also available as a Debian package.
install from PyPi. If you want to always benefit from the newest stable releases you should
always install from PyPi.
If you use the ``--pre`` parameter for ``pip``, you will get the latest
pre-release.
The following installation options exist:
* `all`: install all packages available for this platform
* `usb`: install packages required for USB printers
* `serial`: install packages required for serial printers
* `win32`: install packages required for win32 printing (only Windows)
* `cups`: install packages required for CUPS printing
Other installation methods
--------------------------
Officially, no other installation methods are supplied.
If you want to install nevertheless from another source,
please make sure that you have received the correct package
and that the capabilities data is properly integrated.
When packaging from source please read the developer
information in :ref:`developer-manual-repository`.
If your packaging method breaks the resource system from setuptools,
it might be necessary to supply the path to the capabilities file:
:ref:`advanced-usage-change-capabilities-profile`.
Setup udev for USB-Printers Setup udev for USB-Printers
--------------------------- ---------------------------
1. Get the *Product ID* and *Vendor ID* from the lsusb command 1. Get the *Product ID* and *Vendor ID* from the lsusb command
``# lsusb Bus 002 Device 001: ID 1a2b:1a2b Device name`` ``# lsusb Bus 002 Device 001: ID 1a2b:1a2b Device name``.
(Or whichever way your system supplies to get the PID and VID.)
2. Create a udev rule to let users belonging to *dialout* group use the 2. Create a udev rule to let users belonging to *dialout* group use the
printer. You can create the file printer. You can create the file
@@ -35,7 +63,8 @@ Setup udev for USB-Printers
Enabling tab-completion in CLI Enabling tab-completion in CLI
------------------------------ ------------------------------
python-escpos has a CLI with tab-completion. This is realised with ``argcomplete``. python-escpos has a CLI with tab-completion.
This is realized with ``argcomplete``.
In order for this to work you have to enable tab-completion, which is described in In order for this to work you have to enable tab-completion, which is described in
the `manual of argcomplete <https://argcomplete.readthedocs.io>`__. the `manual of argcomplete <https://argcomplete.readthedocs.io>`__.

View File

@@ -1,12 +1,12 @@
*******
Methods Methods
******* =======
:Last Reviewed: 2017-01-25
:Last Reviewed: 2023-08-10
Escpos class Escpos class
------------ ------------
The core part of this libraries API is the Escpos class. The core part of the API of this library is the Escpos class.
You use it by instantiating a :doc:`printer <printers>` which is a child of Escpos. You use it by instantiating a :doc:`printer <printers>` which is a child of Escpos.
The following methods are available: The following methods are available:

View File

@@ -1,15 +1,18 @@
********
Printers Printers
******** ========
:Last Reviewed: 2022-11-25
As of now there are 7 different type of printer implementations. :Last Reviewed: 2023-08-23
As of now there are 8 different types of printer implementations.
USB USB
--- ---
The USB-class uses pyusb and libusb to communicate with USB-based The USB-class uses pyusb and libusb to communicate with USB-based
printers. Note that this driver is not suited for USB-to-Serial-adapters printers.
and similiar devices, but only for those implementing native USB.
.. note::
This driver is not suited for USB-to-Serial-adapters
and similar devices, but only for those implementing native USB.
.. autoclass:: escpos.printer.Usb .. autoclass:: escpos.printer.Usb
:members: :members:
@@ -44,20 +47,24 @@ This driver is based on the socket class.
Troubleshooting Troubleshooting
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
Problems with a network-attached printer can have numerous causes. Make sure that your device has a proper IP address. Problems with a network-attached printer can have numerous causes.
Often you can check the IP address by triggering the self-test of the device. As a next step try to send text Make sure that your device has a proper IP address.
manually to the device. You could use for example: Often you can check the IP address by triggering the self-test of the device.
As a next step try to send text manually to the device.
You could use for example:
:: ::
echo "OK\n" | nc IPADDRESS 9100 echo "OK\n" | nc IPADDRESS 9100
# the port number is often 9100 # the port number is often 9100
As a last resort try to reset the interface of the printer. This should be described in its manual. As a last resort try to reset the interface of the printer.
This should be described in its manual.
File File
---- ----
This printer "prints" just into a file-handle. Especially on \*nix-systems this comes very handy. This printer "prints" just into a file-handle.
Especially on \*nix-systems this comes very handy.
.. autoclass:: escpos.printer.File .. autoclass:: escpos.printer.File
:members: :members:
@@ -67,8 +74,8 @@ This printer "prints" just into a file-handle. Especially on \*nix-systems this
Dummy Dummy
----- -----
The Dummy-printer is mainly for testing- and debugging-purposes. It stores The Dummy-printer is mainly for testing- and debugging-purposes.
all of the "output" as raw ESC/POS in a string and returns that. It stores all of the "output" as raw ESC/POS in a string and returns that.
.. autoclass:: escpos.printer.Dummy .. autoclass:: escpos.printer.Dummy
:members: :members:
@@ -82,7 +89,10 @@ Supports both local and remote CUPS printers and servers.
The printer must be properly configured in CUPS administration. The printer must be properly configured in CUPS administration.
The connector generates a print job that is added to the CUPS queue. The connector generates a print job that is added to the CUPS queue.
.. todo:: fix import in documentation .. autoclass:: escpos.printer.CupsPrinter
:members:
:member-order: bysource
:noindex:
LP LP
---- ----
@@ -91,10 +101,23 @@ Supports local and remote CUPS printers.
The printer must be properly configured in CUPS administration. The printer must be properly configured in CUPS administration.
The connector spawns a new sub-process where the command lp is executed. The connector spawns a new sub-process where the command lp is executed.
No dependencies required, but somehow the print queue will affect some print job such as barcode. No dependencies required, but somehow the print queue will affect some
print job such as barcode.
.. autoclass:: escpos.printer.LP .. autoclass:: escpos.printer.LP
:members: :members:
:special-members: :special-members:
:member-order: bysource :member-order: bysource
:noindex: :noindex:
Win32Raw
--------
This driver uses a native WIN32 interface of Windows in order to print.
Please refer to the code for documentation as this driver is currently
not included in the documentation build.
.. autoclass:: escpos.printer.Win32Raw
:members:
:special-members:
:member-order: bysource
:noindex:

View File

@@ -1,42 +1,26 @@
************
Raspberry Pi Raspberry Pi
************ ============
:Last Reviewed: 2017-01-05 :Last Reviewed: 2023-08-10
This instructions were tested on Raspbian Jessie. .. warning:: You should **never** directly connect an printer with RS232-interface
(serial port) directly to a Raspberry PI or similar interface
.. warning:: You should **never** directly connect an printer with RS232-interface (serial port) directly to (e.g. those simple USB-sticks without encasing).
a Raspberry PI or similar interface (e.g. those simple USB-sticks without encasing). Those interfaces are Those interfaces are based on 5V- or 3,3V-logic
based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). Classical RS232 uses 12V-logic and would (the latter in the case of Raspberry PI).
**thus destroy your interface**. Connect both systems with an appropriate *level shifter*. Classical RS232 uses 12V-logic and would **thus destroy your interface**.
Connect both systems with an appropriate *level shifter*.
Dependencies
------------
First, install the packages available on Raspbian.
::
sudo apt-get install python3 python3-setuptools python3-pip libjpeg8-dev
Installation Installation
------------ ------------
You can install by using pip3. The installation should be performed as described in :ref:`user_installation`.
::
sudo pip3 install --upgrade pip
sudo pip3 install python-escpos
Run Run
--- ---
You need sudo and python3 to run your program. You can run this software as on any other Linux system.
:: Attach your printer and test it with the example code in the project's set of examples.
You can find that in the
sudo python3 your-program.py `project-repository <https://github.com/python-escpos/python-escpos>`__.
Now you can attach your printer and and test it with the example code in the project's set of examples.
You can find that in the `project-repository <https://github.com/python-escpos/python-escpos>`__.
For more details on this check the :doc:`installation-manual <installation>`. For more details on this check the :doc:`installation-manual <installation>`.

View File

@@ -1,7 +1,7 @@
*****
Usage Usage
***** =====
:Last Reviewed: 2017-06-10
:Last Reviewed: 2025-02-16
Define your printer Define your printer
------------------- -------------------
@@ -113,7 +113,7 @@ on a USB interface.
from escpos import * from escpos import *
""" Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """ """ Seiko Epson Corp. Receipt Printer M129 Definitions (EPSON TM-T88IV) """
p = printer.Usb(0x04b8,0x0202) p = printer.Usb(0x04b8,0x0202, profile="TM-T88IV")
# Print text # Print text
p.text("Hello World\n") p.text("Hello World\n")
# Print image # Print image
@@ -121,7 +121,7 @@ on a USB interface.
# Print QR Code # Print QR Code
p.qr("You can readme from your smartphone") p.qr("You can readme from your smartphone")
# Print barcode # Print barcode
p.barcode('1324354657687','EAN13',64,2,'','') p.barcode('4006381333931','EAN13',64,2,'','')
# Cut paper # Cut paper
p.cut() p.cut()
@@ -142,11 +142,11 @@ format. For windows it is probably at::
%appdata%/python-escpos/config.yaml %appdata%/python-escpos/config.yaml
And for linux:: And for Linux::
$HOME/.config/python-escpos/config.yaml $HOME/.config/python-escpos/config.yaml
If you aren't sure, run:: If you are not sure, run::
from escpos import config from escpos import config
c = config.Config() c = config.Config()
@@ -180,6 +180,7 @@ An example file printer::
printer: printer:
type: File type: File
devfile: /dev/someprinter devfile: /dev/someprinter
profile: TM-U220
And for a network printer:: And for a network printer::
@@ -187,6 +188,7 @@ And for a network printer::
type: Network type: Network
host: 127.0.0.1 host: 127.0.0.1
port: 9000 port: 9000
profile: TM-U220
An USB-printer could be defined by:: An USB-printer could be defined by::
@@ -196,20 +198,31 @@ An USB-printer could be defined by::
idProduct: 0x5678 idProduct: 0x5678
in_ep: 0x66 in_ep: 0x66
out_ep: 0x01 out_ep: 0x01
profile: TM-U220
Printing text right Printing text right
------------------- -------------------
Python-escpos is designed to accept unicode. Python-escpos is designed to accept Unicode.
For normal usage you can simply pass your text to the printers ``text()``-function. It will automatically guess For normal usage you can simply pass your text to the printers ``text()``-function. It will automatically guess
the right code page and then send the encoded data to the printer. If this feature does not work, please try to the right code page and then send the encoded data to the printer. If this feature does not work, please try to
isolate the error and then create an issue on the GitHub project page. isolate the error and then create an issue on the GitHub project page.
If you want or need to you can manually set the codepage. For this please use the ``charcode()``-function. You can set If you want or need to you can manually set the code page.
any key-value that is in ``CHARCODE``. If something is wrong, an ``CharCodeError`` will be raised. For this please use the ``charcode()``-function.
After you have manually set the codepage the printer won't change it anymore. You can revert to normal behaviour You can set any key-value that is in ``CHARCODE``.
by setting charcode to ``AUTO``. If something is wrong, an ``CharCodeError`` will be raised.
After you have manually set the code page the printer won't change it anymore.
You can revert to normal behavior by setting charcode to ``AUTO``.
Resolving bus timeout issues during printing images
---------------------------------------------------
If an error message such as "USBTimeoutError: [Errno 110] Operation timed out" occurs,
setting a sleep time between printing fragments can help.
This can be done with the :meth:`.set_sleep_in_fragment()` method.
Advanced Usage: Print from binary blob Advanced Usage: Print from binary blob
-------------------------------------- --------------------------------------
@@ -238,8 +251,10 @@ Here you can download an example, that will print a set of common barcodes:
* :download:`barcode.bin </download/barcode.bin>` by `@mike42 <https://github.com/mike42>`_ * :download:`barcode.bin </download/barcode.bin>` by `@mike42 <https://github.com/mike42>`_
Advanced Usage: change capabilities-profile .. _advanced-usage-change-capabilities-profile:
-------------------------------------------
Advanced Usage: change where is the capabilities-profile
--------------------------------------------------------
Packaged together with the escpos-code is a capabilities-file. This file in Packaged together with the escpos-code is a capabilities-file. This file in
JSON-format describes the capabilities of different printers. It is developed and hosted in JSON-format describes the capabilities of different printers. It is developed and hosted in

View File

@@ -1,11 +1,11 @@
"""Example for printing barcodes."""
from escpos.printer import Usb from escpos.printer import Usb
# Adapt to your needs # Adapt to your needs
p = Usb(0x0416, 0x5011, profile="POS-5890") p = Usb(0x0416, 0x5011, profile="TM-T88II")
# Print software and then hardware barcode with the same content # Print software and then hardware barcode with the same content
p.soft_barcode("code39", "123456") p.barcode("123456", "CODE39", width=2, force_software=True)
p.text("\n") p.text("\n")
p.text("\n") p.text("\n")
p.barcode("123456", "CODE39") p.barcode("123456", "CODE39")

View File

@@ -1,23 +1,22 @@
"""Prints code page tables. """Prints code page tables."""
"""
import six
import sys import sys
from escpos import printer from escpos import printer
from escpos.constants import ( from escpos.constants import (
CODEPAGE_CHANGE, CODEPAGE_CHANGE,
ESC,
CTL_LF,
CTL_FF,
CTL_CR, CTL_CR,
CTL_FF,
CTL_HT, CTL_HT,
CTL_LF,
CTL_VT, CTL_VT,
ESC,
) )
def main(): def main():
"""Init printer and print codepage tables."""
dummy = printer.Dummy() dummy = printer.Dummy()
dummy.hw("init") dummy.hw("init")
@@ -34,9 +33,10 @@ def main():
def print_codepage(printer, codepage): def print_codepage(printer, codepage):
"""Print a code page."""
if codepage.isdigit(): if codepage.isdigit():
codepage = int(codepage) codepage = int(codepage)
printer._raw(CODEPAGE_CHANGE + six.int2byte(codepage)) printer._raw(CODEPAGE_CHANGE + bytes((codepage,)))
printer._raw("after") printer._raw("after")
else: else:
printer.charcode(codepage) printer.charcode(codepage)
@@ -45,18 +45,20 @@ def print_codepage(printer, codepage):
# Table header # Table header
printer.set(font="b") printer.set(font="b")
printer._raw(" {}\n".format(sep.join(map(lambda s: hex(s)[2:], range(0, 16))))) printer._raw(f" {sep.join(map(lambda s: hex(s)[2:], range(0, 16)))}\n")
printer.set() printer.set()
# The table # The table
for x in range(0, 16): for x in range(0, 16):
# First column # First column
printer.set(font="b") printer.set(font="b")
printer._raw("{} ".format(hex(x)[2:])) printer._raw(f"{hex(x)[2:]} ")
printer.set() printer.set()
for y in range(0, 16): for y in range(0, 16):
byte = six.int2byte(x * 16 + y) byte = bytes(
(x * 16 + y),
)
if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT): if byte in (ESC, CTL_LF, CTL_FF, CTL_CR, CTL_HT, CTL_VT):
byte = " " byte = " "

View File

@@ -0,0 +1,28 @@
# Use the official Python image as the base image
FROM python:3.9-slim
# Set the working directory
WORKDIR /app
# Copy the requirements file
COPY requirements.txt .
#Install the libcups library
RUN apt-get update -y && apt-get install libcups2-dev -y
# Install the Python packages
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install python-escpos --pre
# Install Git
RUN apt-get update && \
apt-get install -y git && \
rm -rf /var/lib/apt/lists/*
# Copy the Flask app
COPY app.py .
# Expose the port the Flask app will run on
EXPOSE 8080
# Run the Flask app
CMD ["python", "app.py"]

View File

@@ -0,0 +1,6 @@
Simple example on how to use it inside a web service
```
docker build . -t escpos-web
docker run --network=host -p 9999:9999 escpos
```

View File

@@ -0,0 +1,22 @@
"""Example for a flask application."""
from flask import Flask
from escpos.printer import CupsPrinter
# Initialize Flask app
app = Flask(__name__)
@app.route("/", methods=["GET"])
def do_print():
"""Print."""
# p = Usb(0x04b8, 0x0e28, 0)
p = CupsPrinter(host="localhost", port=631, printer_name="TM-T20III")
p.text("Hello World\n")
p.cut()
p.close()
return "OK"
if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=9999)

View File

@@ -0,0 +1,20 @@
platformdirs==4.3.8
argcomplete==3.0.8
blinker==1.6.2
click==8.1.3
Flask==2.3.2
itsdangerous==2.1.2
Jinja2==3.1.6
MarkupSafe==2.1.2
Pillow==10.3.0
pycups==2.0.1
pypng==0.20220715.0
pyserial==3.5
python-barcode==0.14.0
python-escpos==3.0a9
pyusb==1.2.1
PyYAML==6.0
qrcode==7.4.2
six==1.16.0
typing_extensions==4.5.0
Werkzeug==3.0.6

View File

@@ -1,9 +1,11 @@
"""Print example QR codes."""
import sys import sys
from escpos.printer import Usb from escpos.printer import Usb
def usage(): def usage():
"""Print information on usage."""
print("usage: qr_code.py <content>") print("usage: qr_code.py <content>")

View File

@@ -1,9 +1,9 @@
"""Example file for software barcodes."""
from escpos.printer import Usb from escpos.printer import Usb
# Adapt to your needs # Adapt to your needs
p = Usb(0x0416, 0x5011, profile="POS-5890") p = Usb(0x0416, 0x5011, profile="POS-5890")
# Some software barcodes # Some software barcodes
p.soft_barcode("code128", "Hello") p.barcode("Hello", "code128", width=2, force_software="bitImageRaster")
p.soft_barcode("code39", "1234") p.barcode("1234", "code39", width=2, force_software=True)

View File

@@ -0,0 +1,25 @@
""" Example for software_columns: Print text arranged into columns."""
from escpos import printer
p = printer.Dummy(profile="TM-U220")
font = "a"
p.set(font=font)
# Default: Automatic column width given the characters per line of the printer.
text_list = ["col1", "col2", "col3"]
charsxline = p.profile.get_columns(font)
p.software_columns(text_list=text_list, widths=charsxline, align="center")
# Tuning some columns:
text_list = ["col1", "col2", "col3"]
widths = [5, 20] # col1 = 5 chars width, col2 + col3 = 20 chars width
align = ["left", "center"] # col1 = left aligned, col2 + col3 = center aligned
p.software_columns(text_list=text_list, widths=widths, align=align)
# Tuning them all:
text_list = ["col1", "col2", "col3"]
widths = [5, 20, 15]
align = ["left", "center", "right"]
p.software_columns(text_list=text_list, widths=widths, align=align)

View File

@@ -1,28 +1,29 @@
#!/usr/bin/python #!/usr/bin/python
"""Weather forecast example.
Adapted script from Adafruit
Weather forecast for Raspberry Pi w/Adafruit Mini Thermal Printer.
Retrieves data from DarkSky.net's API, prints current conditions and
forecasts for next two days.
Weather example using nice bitmaps.
Written by Adafruit Industries. MIT license.
Adapted and enhanced for escpos library by MrWunderbar666
Icons taken from https://adamwhitcroft.com/climacons/
Check out his github: https://github.com/AdamWhitcroft/climacons
"""
# Adapted script from Adafruit
# Weather forecast for Raspberry Pi w/Adafruit Mini Thermal Printer.
# Retrieves data from DarkSky.net's API, prints current conditions and
# forecasts for next two days.
# Weather example using nice bitmaps.
# Written by Adafruit Industries. MIT license.
# Adapted and enhanced for escpos library by MrWunderbar666
# Icons taken from https://adamwhitcroft.com/climacons/
# Check out his github: https://github.com/AdamWhitcroft/climacons
from datetime import datetime
import calendar import calendar
import urllib
import json import json
import time
import os import os
import time
from datetime import datetime
from urllib.request import urlopen
from escpos.printer import Usb from escpos.printer import Usb
""" Setting up the main pathing """ """Set up the main pathing."""
this_dir, this_filename = os.path.split(__file__) this_dir, this_filename = os.path.split(__file__)
GRAPHICS_PATH = os.path.join(this_dir, "graphics/climacons/") GRAPHICS_PATH = os.path.join(this_dir, "graphics/climacons/")
@@ -38,13 +39,14 @@ LONG = "114.189945" # Your Location
def forecast_icon(idx): def forecast_icon(idx):
"""Get right icon for forecast."""
icon = data["daily"]["data"][idx]["icon"] icon = data["daily"]["data"][idx]["icon"]
image = GRAPHICS_PATH + icon + ".png" image = GRAPHICS_PATH + icon + ".png"
return image return image
# Dumps one forecast line to the printer
def forecast(idx): def forecast(idx):
"""Dump one forecast line to the printer."""
date = datetime.fromtimestamp(int(data["daily"]["data"][idx]["time"])) date = datetime.fromtimestamp(int(data["daily"]["data"][idx]["time"]))
day = calendar.day_name[date.weekday()] day = calendar.day_name[date.weekday()]
lo = data["daily"]["data"][idx]["temperatureMin"] lo = data["daily"]["data"][idx]["temperatureMin"]
@@ -67,12 +69,13 @@ def forecast(idx):
printer.text(" high " + str(hi)) printer.text(" high " + str(hi))
printer.text(deg) printer.text(deg)
printer.text("\n") printer.text("\n")
# take care of pesky unicode dash # take care of pesky Unicode dash
printer.text(cond.replace("\u2013", "-").encode("utf-8")) printer.text(cond.replace("\u2013", "-").encode("utf-8"))
printer.text("\n \n") printer.text("\n \n")
def icon(): def icon():
"""Get icon."""
icon = data["currently"]["icon"] icon = data["currently"]["icon"]
image = GRAPHICS_PATH + icon + ".png" image = GRAPHICS_PATH + icon + ".png"
return image return image
@@ -90,7 +93,7 @@ url = (
+ LONG + LONG
+ "?exclude=[alerts,minutely,hourly,flags]&units=si" + "?exclude=[alerts,minutely,hourly,flags]&units=si"
) # change last bit to 'us' for Fahrenheit ) # change last bit to 'us' for Fahrenheit
response = urllib.urlopen(url) response = urlopen(url)
data = json.loads(response.read()) data = json.loads(response.read())
printer.print_and_feed(n=1) printer.print_and_feed(n=1)

View File

@@ -1,3 +1,27 @@
[tool.black] [tool.black]
extend-exclude = 'capabilities-data' extend-exclude = 'capabilities-data'
[tool.isort]
profile = "black"
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--doctest-modules --cov escpos --cov-report=xml"
testpaths = [
"test",
"src",
"src/escpos",
"escpos",
]
[[tool.mypy.overrides]]
module = ["pytest",
"jaconv",
"scripttest",
"barcode.*",
"qrcode",
"usb.*",
"cups",
"win32print"
]
ignore_missing_imports = true

View File

@@ -1,7 +0,0 @@
formats:
- pdf
- epub
requirements_file: doc/requirements.txt
python:
version: 3
setup_py_install: true

View File

@@ -3,9 +3,10 @@ name = python-escpos
url = https://github.com/python-escpos/python-escpos url = https://github.com/python-escpos/python-escpos
description = Python library to manipulate ESC/POS Printers description = Python library to manipulate ESC/POS Printers
long_description = file: README.rst long_description = file: README.rst
long_description_content_type = text/x-rst
license = MIT license = MIT
license_file = LICENSE license_file = LICENSE
author = Manuel F Martinez and others author = python-escpos developers
author_email = dev@pkanzler.de author_email = dev@pkanzler.de
maintainer = Patrick Kanzler maintainer = Patrick Kanzler
maintainer_email = dev@pkanzler.de maintainer_email = dev@pkanzler.de
@@ -18,11 +19,12 @@ classifiers =
Operating System :: OS Independent Operating System :: OS Independent
Programming Language :: Python Programming Language :: Python
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
Topic :: Software Development :: Libraries :: Python Modules Topic :: Software Development :: Libraries :: Python Modules
Topic :: Office/Business :: Financial :: Point-Of-Sale Topic :: Office/Business :: Financial :: Point-Of-Sale
@@ -32,41 +34,48 @@ project_urls =
Release Notes = https://github.com/python-escpos/python-escpos/releases Release Notes = https://github.com/python-escpos/python-escpos/releases
[options] [options]
python_requires = >=3.6 python_requires = >=3.8
zip_safe = false zip_safe = false
include_package_data = true include_package_data = true
install_requires = install_requires =
pyusb>=1.0.0
Pillow>=2.0 Pillow>=2.0
qrcode>=4.0 qrcode>=4.0
pyserial python-barcode>=0.15.0,<1
python-barcode>=0.9.1,<1
setuptools setuptools
six six
appdirs platformdirs
PyYAML PyYAML
argparse
argcomplete argcomplete
future importlib_resources
setup_requires = setuptools_scm setup_requires = setuptools_scm
tests_require = tests_require =
jaconv jaconv
tox tox>=4.11
pytest!=3.2.0,!=3.3.0 pytest>=7.4
pytest-cov pytest-cov
pytest-mock pytest-mock
nose
scripttest scripttest
mock mock
hypothesis>4 hypothesis>=6.83
flake8 flake8
sphinxcontrib-spelling>=7.2.0 sphinxcontrib-spelling>=8.0.0
[nosetests] [options.extras_require]
verbosity=3 usb =
with-doctest=1 pyusb>=1.0.0
serial =
pyserial
cups =
pycups; platform_system!='Windows'
win32 =
pywin32; platform_system=='Windows'
all =
pyusb>=1.0.0
pyserial
pycups; platform_system!='Windows'
pywin32; platform_system=='Windows'
[flake8] [flake8]
exclude = .git,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py exclude = .git,.venv,.tox,.github,.eggs,__pycache__,doc/conf.py,build,dist,capabilities-data,test,src/escpos/constants.py
max-line-length = 120 max-line-length = 120
# future-imports = absolute_import, division, print_function, unicode_literals # we are not there yet extend-ignore = E203, W503

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Setup script for python package."""
import os import os
import sys import sys
from setuptools import find_packages, setup
from setuptools import find_packages, setup
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
src_dir = os.path.join(base_dir, "src") src_dir = os.path.join(base_dir, "src")
@@ -14,14 +15,18 @@ sys.path.insert(0, src_dir)
def read(fname): def read(fname):
"""read file from same path as setup.py""" """Read file from same path as setup.py."""
return open(os.path.join(os.path.dirname(__file__), fname)).read() return open(os.path.join(os.path.dirname(__file__), fname)).read()
setuptools_scm_template = """\ setuptools_scm_template = """\
# coding: utf-8 #!/usr/bin/python
# file generated by setuptools_scm # -*- coding: utf-8 -*-
# don't change, don't track in version control \"\"\"Version identifier.
file generated by setuptools_scm
don't change, don't track in version control
\"\"\"
version = '{version}' version = '{version}'
""" """

View File

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

View File

@@ -1,29 +1,31 @@
import re """Handler for capabilities data."""
from os import environ, path import atexit
import pkg_resources
import pickle
import logging import logging
import pickle
import platform
import re
import time import time
from contextlib import ExitStack
from os import environ, path
from tempfile import mkdtemp
from typing import Any, Dict, Optional, Type
import six import importlib_resources
import yaml import yaml
from tempfile import gettempdir if environ.get("ESCPOS_CAPABILITIES_DEBUG", 0):
import platform
from typing import Any, Dict
logging.basicConfig() logging.basicConfig()
logger = logging.getLogger(__name__)
pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", gettempdir()) logger = logging.getLogger(__name__)
pickle_path = path.join( pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", mkdtemp())
pickle_dir, "{v}.capabilities.pickle".format(v=platform.python_version()) pickle_path = path.join(pickle_dir, f"{platform.python_version()}.capabilities.pickle")
) # get a temporary file from importlib_resources if no file is specified in env
# get a temporary file from pkg_resources if no file is specified in env file_manager = ExitStack()
atexit.register(file_manager.close)
ref = importlib_resources.files(__name__) / "capabilities.json"
capabilities_path = environ.get( capabilities_path = environ.get(
"ESCPOS_CAPABILITIES_FILE", "ESCPOS_CAPABILITIES_FILE",
pkg_resources.resource_filename(__name__, "capabilities.json"), file_manager.enter_context(importlib_resources.as_file(ref)),
) )
# Load external printer database # Load external printer database
@@ -46,18 +48,43 @@ if full_load:
logger.debug("Loading and pickling capabilities") logger.debug("Loading and pickling capabilities")
with open(capabilities_path) as cp, open(pickle_path, "wb") as pp: with open(capabilities_path) as cp, open(pickle_path, "wb") as pp:
CAPABILITIES = yaml.safe_load(cp) CAPABILITIES = yaml.safe_load(cp)
if not CAPABILITIES:
# yaml could not be loaded
print(
f"Capabilities yaml from {capabilities_path} could not be loaded.\n"
"This python package seems to be broken. If it has been installed "
"from official sources, please report an issue on GitHub.\n"
"Currently loaded capabilities:\n"
f"{CAPABILITIES}"
)
CAPABILITIES = {
"profiles": {
"default": {
"name": "BrokenDefault",
"notes": "The integrated capabilities file could not be found and has been replaced.",
"codePages": {"0": "Broken"},
"features": {},
},
},
"encodings": {
"Broken": {
"name": "Broken",
"notes": "The configuration is broken.",
}
},
}
print(
"Created a minimal backup profile, "
"many functionalities of the library will not work:\n"
f"{CAPABILITIES}"
)
pickle.dump(CAPABILITIES, pp, protocol=2) pickle.dump(CAPABILITIES, pp, protocol=2)
logger.debug("Finished loading capabilities took %.2fs", time.time() - t0) logger.debug("Finished loading capabilities took %.2fs", time.time() - t0)
PROFILES: Dict[str, Any] = CAPABILITIES["profiles"]
class NotSupported(Exception): class NotSupported(Exception):
"""Raised if a requested feature is not supported by the """Raised if a requested feature is not supported by the printer profile."""
printer profile.
"""
pass pass
@@ -65,7 +92,7 @@ class NotSupported(Exception):
BARCODE_B = "barcodeB" BARCODE_B = "barcodeB"
class BaseProfile(object): class BaseProfile:
"""This represents a printer profile. """This represents a printer profile.
A printer profile knows about the number of columns, supported A printer profile knows about the number of columns, supported
@@ -75,36 +102,39 @@ class BaseProfile(object):
profile_data: Dict[str, Any] = {} profile_data: Dict[str, Any] = {}
def __getattr__(self, name): def __getattr__(self, name):
"""Get a data element from the profile."""
return self.profile_data[name] return self.profile_data[name]
def get_font(self, font) -> int: def get_font(self, font) -> int:
"""Return the escpos index for `font`. Makes sure that """Return the escpos index for `font`.
the requested `font` is valid.
Makes sure that the requested `font` is valid.
""" """
font = {"a": 0, "b": 1}.get(font, font) font = {"a": 0, "b": 1}.get(font, font)
if not six.text_type(font) in self.fonts: if not str(font) in self.fonts:
raise NotSupported( raise NotSupported(f'"{font}" is not a valid font in the current profile')
'"{}" is not a valid font in the current profile'.format(font)
)
return font return font
def get_columns(self, font): def get_columns(self, font) -> int:
"""Return the number of columns for the given font.""" """Return the number of columns for the given font."""
font = self.get_font(font) font = self.get_font(font)
return self.fonts[six.text_type(font)]["columns"] columns = self.fonts[str(font)]["columns"]
assert type(columns) is int
return columns
def supports(self, feature): def supports(self, feature) -> bool:
"""Return true/false for the given feature.""" """Return true/false for the given feature."""
return self.features.get(feature) return self.features.get(feature)
def get_code_pages(self): def get_code_pages(self) -> Dict[str, int]:
"""Return the support code pages as a ``{name: index}`` dict.""" """Return the support code pages as a ``{name: index}`` dict."""
return {v: k for k, v in self.codePages.items()} return {v: k for k, v in self.codePages.items()}
def get_profile(name: str = None, **kwargs): def get_profile(name: Optional[str] = None, **kwargs):
"""Get the profile by name; if no name is given, return the """Get a profile by name.
default profile.
If no name is given, return the default profile.
""" """
if isinstance(name, Profile): if isinstance(name, Profile):
return name return name
@@ -116,21 +146,25 @@ def get_profile(name: str = None, **kwargs):
CLASS_CACHE = {} CLASS_CACHE = {}
def get_profile_class(name: str): def get_profile_class(name: str) -> Type[BaseProfile]:
"""For the given profile name, load the data from the external """Load a profile class.
For the given profile name, load the data from the external
database, then generate dynamically a class. database, then generate dynamically a class.
""" """
if name not in CLASS_CACHE: if name not in CLASS_CACHE:
profile_data = PROFILES[name] profiles: Dict[str, Any] = CAPABILITIES["profiles"]
profile_data = profiles[name]
profile_name = clean(name) profile_name = clean(name)
class_name = "{}{}Profile".format(profile_name[0].upper(), profile_name[1:]) class_name = f"{profile_name[0].upper()}{profile_name[1:]}Profile"
new_class = type(class_name, (BaseProfile,), {"profile_data": profile_data}) new_class = type(class_name, (BaseProfile,), {"profile_data": profile_data})
CLASS_CACHE[name] = new_class CLASS_CACHE[name] = new_class
return CLASS_CACHE[name] return CLASS_CACHE[name]
def clean(s): def clean(s: str) -> str:
"""Clean profile name."""
# Remove invalid characters # Remove invalid characters
s = re.sub("[^0-9a-zA-Z_]", "", s) s = re.sub("[^0-9a-zA-Z_]", "", s)
# Remove leading characters until we find a letter or underscore # Remove leading characters until we find a letter or underscore
@@ -138,18 +172,25 @@ def clean(s):
return str(s) return str(s)
class Profile(get_profile_class("default")): # mute the mypy type issue with this dynamic base class function for now (: Any)
""" ProfileBaseClass: Any = get_profile_class("default")
For users, who want to provide their profile
class Profile(ProfileBaseClass):
"""Profile class for user usage.
For users, who want to provide their own profile.
""" """
def __init__(self, columns=None, features=None): def __init__(self, columns: Optional[int] = None, features=None) -> None:
"""Initialize profile."""
super(Profile, self).__init__() super(Profile, self).__init__()
self.columns = columns self.columns = columns
self.features = features or {} self.features = features or {}
def get_columns(self, font): def get_columns(self, font) -> int:
"""Get column count of printer."""
if self.columns is not None: if self.columns is not None:
return self.columns return self.columns

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
""" CLI """CLI.
This module acts as a command line interface for python-escpos. It mirrors 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 convenience. closely the available ESCPOS commands while adding a couple extra ones for convenience.
@@ -9,8 +9,9 @@ It requires you to have a configuration file. See documentation for details.
""" """
import argparse import argparse
import platform
from typing import Any, Dict, List
try: try:
import argcomplete import argcomplete
@@ -18,15 +19,18 @@ except ImportError:
# this CLI works nevertheless without argcomplete # this CLI works nevertheless without argcomplete
pass # noqa pass # noqa
import sys import sys
import six
from . import config from . import config, escpos
from . import printer as escpos_printer_module
from . import version from . import version
# Must be defined before it's used in DEMO_FUNCTIONS # Must be defined before it's used in DEMO_FUNCTIONS
def str_to_bool(string): def str_to_bool(string: str) -> bool:
"""Used as a type in argparse so that we get back a proper """Convert string to bool.
bool instead of always True
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") return string.lower() in ("y", "yes", "1", "true")
@@ -52,7 +56,7 @@ DEMO_FUNCTIONS = {
"barcodes_a": [ "barcodes_a": [
{"bc": "UPC-A", "code": "13243546576"}, {"bc": "UPC-A", "code": "13243546576"},
{"bc": "UPC-E", "code": "132435"}, {"bc": "UPC-E", "code": "132435"},
{"bc": "EAN13", "code": "1324354657687"}, {"bc": "EAN13", "code": "4006381333931"},
{"bc": "EAN8", "code": "1324354"}, {"bc": "EAN8", "code": "1324354"},
{"bc": "CODE39", "code": "TEST"}, {"bc": "CODE39", "code": "TEST"},
{"bc": "ITF", "code": "55867492279103"}, {"bc": "ITF", "code": "55867492279103"},
@@ -61,7 +65,7 @@ DEMO_FUNCTIONS = {
"barcodes_b": [ "barcodes_b": [
{"bc": "UPC-A", "code": "13243546576", "function_type": "B"}, {"bc": "UPC-A", "code": "13243546576", "function_type": "B"},
{"bc": "UPC-E", "code": "132435", "function_type": "B"}, {"bc": "UPC-E", "code": "132435", "function_type": "B"},
{"bc": "EAN13", "code": "1324354657687", "function_type": "B"}, {"bc": "EAN13", "code": "4006381333931", "function_type": "B"},
{"bc": "EAN8", "code": "1324354", "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": "ITF", "code": "55867492279103", "function_type": "B"},
@@ -89,7 +93,7 @@ DEMO_FUNCTIONS = {
# parser: A dict of args for command_parsers.add_parser # parser: A dict of args for command_parsers.add_parser
# defaults: A dict of args for subparser.set_defaults # defaults: A dict of args for subparser.set_defaults
# arguments: A list of dicts of args for subparser.add_argument # arguments: A list of dicts of args for subparser.add_argument
ESCPOS_COMMANDS = [ ESCPOS_COMMANDS: List[Dict[str, Any]] = [
{ {
"parser": { "parser": {
"name": "qr", "name": "qr",
@@ -161,6 +165,11 @@ ESCPOS_COMMANDS = [
"help": "ESCPOS function type", "help": "ESCPOS function type",
"choices": ["A", "B"], "choices": ["A", "B"],
}, },
{
"option_strings": ("--force_software",),
"help": "Force render and print barcode as an image",
"choices": ["graphics", "bitImageColumn", "bitImageRaster"],
},
], ],
}, },
{ {
@@ -200,6 +209,38 @@ ESCPOS_COMMANDS = [
}, },
], ],
}, },
{
"parser": {
"name": "software_columns",
"help": "Print a list of texts arranged into columns",
},
"defaults": {
"func": "software_columns",
},
"arguments": [
{
"option_strings": ("--text_list",),
"help": "list of texts to print",
"nargs": "+",
"type": str,
"required": True,
},
{
"option_strings": ("--widths",),
"help": "list of column widths",
"nargs": "+",
"type": int,
"required": True,
},
{
"option_strings": ("--align",),
"help": "list of column alignments",
"nargs": "+",
"type": str,
"required": True,
},
],
},
{ {
"parser": { "parser": {
"name": "cut", "name": "cut",
@@ -289,7 +330,7 @@ ESCPOS_COMMANDS = [
}, },
{ {
"option_strings": ("--histeq",), "option_strings": ("--histeq",),
"help": "Equalize the histrogram", "help": "Equalize the histogram",
"type": str_to_bool, "type": str_to_bool,
}, },
{ {
@@ -332,7 +373,7 @@ ESCPOS_COMMANDS = [
{ {
"option_strings": ("--font",), "option_strings": ("--font",),
"help": "Font choice", "help": "Font choice",
"choices": ["left", "center", "right"], "choices": ["A", "B"],
}, },
{ {
"option_strings": ("--text_type",), "option_strings": ("--text_type",),
@@ -446,17 +487,42 @@ ESCPOS_COMMANDS = [
] ]
def main(): def print_extended_information() -> None:
""" """Print diagnostic information for bug reports."""
print(f"* python-escpos version: `{version.version}`")
print(
f"* python version: `{platform.python_implementation()} v{platform.python_version()}`"
)
print(f"* platform: `{platform.platform()}`")
print(
f"* printer driver `USB` is usable: `{escpos_printer_module.Usb.is_usable()}`"
)
print(
f"* printer driver `File` is usable: `{escpos_printer_module.File.is_usable()}`"
)
print(
f"* printer driver `Network` is usable: `{escpos_printer_module.Network.is_usable()}`"
)
print(
f"* printer driver `Serial` is usable: `{escpos_printer_module.Serial.is_usable()}`"
)
print(f"* printer driver `LP` is usable: `{escpos_printer_module.LP.is_usable()}`")
print(
f"* printer driver `Dummy` is usable: `{escpos_printer_module.Dummy.is_usable()}`"
)
print(
f"* printer driver `CupsPrinter` is usable: `{escpos_printer_module.CupsPrinter.is_usable()}`"
)
print(
f"* printer driver `Win32Raw` is usable: `{escpos_printer_module.Win32Raw.is_usable()}`"
)
Handles loading of configuration and creating and processing of command
line arguments. Called when run from a CLI.
"""
def generate_parser() -> argparse.ArgumentParser:
"""Generate an argparse parser."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="CLI for python-escpos", description="CLI for python-escpos",
epilog="Printer configuration is defined in the python-escpos config" epilog="Printer configuration is defined in the python-escpos configuration "
"file. See documentation for details.", "file. See documentation for details.",
) )
@@ -514,10 +580,27 @@ def main():
) )
parser_command_version = command_subparsers.add_parser( parser_command_version = command_subparsers.add_parser(
"version", help="Print the version of python-escpos" "version", help="Print the version information of python-escpos"
) )
parser_command_version.set_defaults(version=True) parser_command_version.set_defaults(version=True)
parser_command_version_extended = command_subparsers.add_parser(
"version_extended",
help="Print the extended version information of python-escpos (for bug reports)",
)
parser_command_version_extended.set_defaults(version_extended=True)
return parser
def main() -> None:
"""Handle main entry point of CLI script.
Handles loading of configuration and creating and processing of command
line arguments. Called when run from a CLI.
"""
parser = generate_parser()
# hook in argcomplete # hook in argcomplete
if "argcomplete" in globals(): if "argcomplete" in globals():
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
@@ -527,9 +610,7 @@ def main():
if not args_dict: if not args_dict:
parser.print_help() parser.print_help()
sys.exit() sys.exit()
command_arguments = dict( command_arguments = dict([k, v] for k, v in args_dict.items() if v is not None)
[k, v] for k, v in six.iteritems(args_dict) if v is not None
)
# If version should be printed, do this, then exit # If version should be printed, do this, then exit
print_version = command_arguments.pop("version", None) print_version = command_arguments.pop("version", None)
@@ -537,6 +618,11 @@ def main():
print(version.version) print(version.version)
sys.exit() sys.exit()
print_version_extended = command_arguments.pop("version_extended", None)
if print_version_extended:
print_extended_information()
sys.exit()
# If there was a config path passed, grab it # If there was a config path passed, grab it
config_path = command_arguments.pop("config", None) config_path = command_arguments.pop("config", None)
@@ -563,9 +649,10 @@ def main():
globals()[target_command](**command_arguments) globals()[target_command](**command_arguments)
def demo(printer, **kwargs): def demo(printer: escpos.Escpos, **kwargs) -> None:
""" """Print demos.
Prints demos. Called when CLI is passed `demo`. This function
Called when CLI is passed `demo`. This function
uses the DEMO_FUNCTIONS dictionary. uses the DEMO_FUNCTIONS dictionary.
:param printer: A printer from escpos.printer :param printer: A printer from escpos.printer

View File

@@ -1,23 +1,27 @@
"""Helper module for code page handling."""
from .capabilities import CAPABILITIES from .capabilities import CAPABILITIES
class CodePageManager: class CodePageManager:
"""Holds information about all the code pages (as defined """Holds information about all the code pages.
in escpos-printer-db).
Information as defined in escpos-printer-db.
""" """
def __init__(self, data): def __init__(self, data):
"""Initialize code page manager."""
self.data = data self.data = data
def get_all(self):
return self.data.values()
@staticmethod @staticmethod
def get_encoding_name(encoding): def get_encoding_name(encoding):
# TODO resolve the encoding alias """Get encoding name.
.. todo:: Resolve the encoding alias.
"""
return encoding.upper() return encoding.upper()
def get_encoding(self, encoding): def get_encoding(self, encoding):
"""Return the encoding data."""
return self.data[encoding] return self.data[encoding]

View File

@@ -1,29 +1,27 @@
"""ESC/POS configuration manager. """ESC/POS configuration manager.
This module contains the implementations of abstract base class :py:class:`Config`. This module contains the implementations of abstract base class :py:class:`Config`.
""" """
import os import os
import appdirs import pathlib
import platformdirs
import yaml import yaml
from . import printer from . import exceptions, printer
from . import exceptions
class Config(object): class Config:
"""Configuration handler class. """Configuration handler class.
This class loads configuration from a default or specificed directory. It This class loads configuration from a default or specified directory. It
can create your defined printer and return it to you. can create your defined printer and return it to you.
""" """
_app_name = "python-escpos" _app_name = "python-escpos"
_config_file = "config.yaml" _config_file = "config.yaml"
def __init__(self): def __init__(self) -> None:
"""Initialize configuration. """Initialize configuration.
Remember to add anything that needs to be reset between configurations Remember to add anything that needs to be reset between configurations
@@ -35,7 +33,7 @@ class Config(object):
self._printer_name = None self._printer_name = None
self._printer_config = None self._printer_config = None
def _reset_config(self): def _reset_config(self) -> None:
"""Clear the loaded configuration. """Clear the loaded configuration.
If we are loading a changed config, we don't want to have leftover If we are loading a changed config, we don't want to have leftover
@@ -48,58 +46,62 @@ class Config(object):
self._printer_config = None self._printer_config = None
def load(self, config_path=None): def load(self, config_path=None):
"""Load and parse the configuration file using pyyaml """Load and parse the configuration file using pyyaml.
:param config_path: An optional file path, file handle, or byte string :param config_path: An optional file path, file handle, or byte string
for the configuration file. for the configuration file.
""" """
self._reset_config() self._reset_config()
if not config_path: if not config_path:
config_path = os.path.join( config_path = os.path.join(
appdirs.user_config_dir(self._app_name), self._config_file platformdirs.user_config_dir(self._app_name), self._config_file
) )
if isinstance(config_path, pathlib.Path):
# store string if posixpath
config_path = config_path.as_posix()
if not os.path.isfile(config_path):
# supplied path is not a file --> assume default file
config_path = os.path.join(config_path, self._config_file)
try: try:
# First check if it's file like. If it is, pyyaml can load it.
# 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)
else:
# If it isn't, it's a path. We have to open it first, otherwise
# pyyaml will try to read it as yaml
with open(config_path, "rb") as config_file: with open(config_path, "rb") as config_file:
config = yaml.safe_load(config_file) config = yaml.safe_load(config_file)
except EnvironmentError: except EnvironmentError:
raise exceptions.ConfigNotFoundError( raise exceptions.ConfigNotFoundError(
"Couldn't read config at {config_path}".format( f"Couldn't read config at {config_path}"
config_path=str(config_path),
)
) )
except yaml.YAMLError: except yaml.YAMLError:
raise exceptions.ConfigSyntaxError("Error parsing YAML") raise exceptions.ConfigSyntaxError("Error parsing YAML")
if "printer" in config: if "printer" in config:
self._printer_config = config["printer"] self._printer_config = config["printer"]
self._printer_name = self._printer_config.pop("type").title() printer_name = self._printer_config.pop("type")
class_names = {
"usb": "Usb",
"serial": "Serial",
"network": "Network",
"file": "File",
"dummy": "Dummy",
"cupsprinter": "CupsPrinter",
"lp": "LP",
"win32raw": "Win32Raw",
}
self._printer_name = class_names.get(printer_name.lower(), printer_name)
if not self._printer_name or not hasattr(printer, self._printer_name): if not self._printer_name or not hasattr(printer, self._printer_name):
raise exceptions.ConfigSyntaxError( raise exceptions.ConfigSyntaxError(
'Printer type "{printer_name}" is invalid'.format( f'Printer type "{self._printer_name}" is invalid'
printer_name=self._printer_name,
)
) )
self._has_loaded = True self._has_loaded = True
def printer(self): def printer(self):
"""Returns a printer that was defined in the config, or throws an """Return a printer that was defined in the config.
exception.
This method loads the default config if one hasn't beeen already loaded. Throw an exception on error.
This method loads the default config if one has not been already loaded.
""" """
if not self._has_loaded: if not self._has_loaded:

View File

@@ -1,50 +1,54 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Set of ESC/POS Commands (Constants) """ Set of ESC/POS Commands (Constants)
This module contains constants that are described in the esc/pos-documentation. This module contains constants that are described in the Esc/Pos-documentation.
Since there is no definitive and unified specification for all esc/pos-like printers the constants could later be Since there is no definitive and unified specification for all Esc/Pos-like printers the constants could later be
moved to `capabilities` as in `escpos-php by @mike42 <https://github.com/mike42/escpos-php>`_. moved to `capabilities` as in `escpos-php by @mike42 <https://github.com/mike42/escpos-php>`_.
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others :author: python-escpos developers
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_ :organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT :license: MIT
""" """
from typing import Dict
import six import six
from .types import ConstTxtStyleClass
# Control characters # Control characters
# as labelled in https://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf # as labelled in https://www.novopos.ch/client/EPSON/TM-T20/TM-T20_eng_qr.pdf
NUL = b"\x00" NUL: bytes = b"\x00"
EOT = b"\x04" EOT: bytes = b"\x04"
ENQ = b"\x05" ENQ: bytes = b"\x05"
DLE = b"\x10" DLE: bytes = b"\x10"
DC4 = b"\x14" DC4: bytes = b"\x14"
CAN = b"\x18" CAN: bytes = b"\x18"
ESC = b"\x1b" ESC: bytes = b"\x1b"
FS = b"\x1c" FS: bytes = b"\x1c"
GS = b"\x1d" GS: bytes = b"\x1d"
# Feed control sequences # Feed control sequences
CTL_LF = b"\n" # Print and line feed CTL_LF: bytes = b"\n" #: Print and line feed
CTL_FF = b"\f" # Form feed CTL_FF: bytes = b"\f" #: Form feed
CTL_CR = b"\r" # Carriage return CTL_CR: bytes = b"\r" #: Carriage return
CTL_HT = b"\t" # Horizontal tab CTL_HT: bytes = b"\t" #: Horizontal tab
CTL_SET_HT = ESC + b"\x44" # Set horizontal tab positions CTL_SET_HT: bytes = ESC + b"\x44" #: Set horizontal tab positions
CTL_VT = b"\v" # Vertical tab CTL_VT: bytes = b"\v" #: Vertical tab
# Printer hardware # Printer hardware
HW_INIT = ESC + b"@" # Clear data in buffer and reset modes HW_INIT: bytes = ESC + b"@" # Clear data in buffer and reset modes
HW_SELECT = ESC + b"=\x01" # Printer select HW_SELECT: bytes = ESC + b"=\x01" # Printer select
HW_RESET = ESC + b"\x3f\x0a\x00" # Reset printer hardware HW_RESET: bytes = ESC + b"\x3f\x0a\x00" # Reset printer hardware
# (TODO: Where is this specified?) # (TODO: Where is this specified?)
# Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>) # Cash Drawer (ESC p <pin> <on time: 2*ms> <off time: 2*ms>)
_CASH_DRAWER = ( _CASH_DRAWER = lambda m, t1="", t2="": ESC + b"p" + m + bytes((t1, t2))
lambda m, t1="", t2="": ESC + b"p" + m + six.int2byte(t1) + six.int2byte(t2)
) #: decimal cash drawer kick sequence
CD_KICK_DEC_SEQUENCE = ( CD_KICK_DEC_SEQUENCE = (
lambda esc, p, m, t1=50, t2=50: six.int2byte(esc) lambda esc, p, m, t1=50, t2=50: six.int2byte(esc)
+ six.int2byte(p) + six.int2byte(p)
@@ -52,50 +56,55 @@ CD_KICK_DEC_SEQUENCE = (
+ six.int2byte(t1) + six.int2byte(t1)
+ six.int2byte(t2) + six.int2byte(t2)
) )
CD_KICK_2 = _CASH_DRAWER(b"\x00", 50, 50) # Sends a pulse to pin 2 [] #: Sends a pulse to pin 2 []
CD_KICK_5 = _CASH_DRAWER(b"\x01", 50, 50) # Sends a pulse to pin 5 [] CD_KICK_2: bytes = _CASH_DRAWER(b"\x00", 50, 50)
#: Sends a pulse to pin 5 []
CD_KICK_5: bytes = _CASH_DRAWER(b"\x01", 50, 50)
# Paper Cutter # Paper Cutter
_CUT_PAPER = lambda m: GS + b"V" + m _CUT_PAPER = lambda m: GS + b"V" + m
PAPER_FULL_CUT = _CUT_PAPER(b"\x00") # Full cut paper PAPER_FULL_CUT: bytes = _CUT_PAPER(b"\x00") #: Full cut paper
PAPER_PART_CUT = _CUT_PAPER(b"\x01") # Partial cut paper PAPER_PART_CUT: bytes = _CUT_PAPER(b"\x01") #: Partial cut paper
# Beep (please note that the actual beep sequence may differ between devices) # Beep (please note that the actual beep sequence may differ between devices)
BEEP = b"\x07" BEEP: bytes = b"\x07"
# Internal buzzer (only supported printers)
BUZZER: bytes = ESC + b"\x42"
# Panel buttons (e.g. the FEED button) # Panel buttons (e.g. the FEED button)
_PANEL_BUTTON = lambda n: ESC + b"c5" + six.int2byte(n) _PANEL_BUTTON = lambda n: ESC + b"c5" + six.int2byte(n)
PANEL_BUTTON_ON = _PANEL_BUTTON(0) # enable all panel buttons PANEL_BUTTON_ON: bytes = _PANEL_BUTTON(0) # enable all panel buttons
PANEL_BUTTON_OFF = _PANEL_BUTTON(1) # disable all panel buttons PANEL_BUTTON_OFF: bytes = _PANEL_BUTTON(1) # disable all panel buttons
# Line display printing # Line display printing
LINE_DISPLAY_OPEN = ESC + b"\x3d\x02" LINE_DISPLAY_OPEN: bytes = ESC + b"\x3d\x02"
LINE_DISPLAY_CLEAR = ESC + b"\x40" LINE_DISPLAY_CLEAR: bytes = ESC + b"\x40"
LINE_DISPLAY_CLOSE = ESC + b"\x3d\x01" LINE_DISPLAY_CLOSE: bytes = ESC + b"\x3d\x01"
# Sheet modes # Sheet modes
SHEET_SLIP_MODE = ESC + b"\x63\x30\x04" # slip paper SHEET_SLIP_MODE: bytes = ESC + b"\x63\x30\x04" # slip paper
SHEET_ROLL_MODE = ESC + b"\x63\x30\x01" # paper roll SHEET_ROLL_MODE: bytes = ESC + b"\x63\x30\x01" # paper roll
# Slip specific codes # Slip specific codes
SLIP_EJECT = ESC + b"\x4b\xc0" # Eject the slip or cheque SLIP_EJECT: bytes = ESC + b"\x4b\xc0" # Eject the slip or cheque
SLIP_SELECT = FS # Select the slip station as default station SLIP_SELECT: bytes = FS # Select the slip station as default station
SLIP_SET_WAIT_TIME = ( SLIP_SET_WAIT_TIME: bytes = (
ESC + b"\x1b\x66" ESC + b"\x1b\x66"
) # Set timeout waiting for a slip/cheque to be inserted ) # Set timeout waiting for a slip/cheque to be inserted
SLIP_PRINT_AND_EJECT = ( SLIP_PRINT_AND_EJECT: bytes = (
b"\x0c" # Print the buffer and eject (after waiting for the paper to be inserted) b"\x0c" # Print the buffer and eject (after waiting for the paper to be inserted)
) )
# Text format # Text format
# TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll # TODO: Acquire the "ESC/POS Application Programming Guide for Paper Roll
# Printers" and tidy up this stuff too. # Printers" and tidy up this stuff too.
TXT_SIZE = GS + b"!" TXT_SIZE: bytes = GS + b"!"
TXT_NORMAL = ESC + b"!\x00" # Normal text TXT_NORMAL: bytes = ESC + b"!\x00" # Normal text
#: text style dictionary for :py:meth:`escpos.escpos.Escpos.set`
TXT_STYLE = { TXT_STYLE: ConstTxtStyleClass = {
"bold": { "bold": {
False: ESC + b"\x45\x00", # Bold font OFF False: ESC + b"\x45\x00", # Bold font OFF
True: ESC + b"\x45\x01", # Bold font ON True: ESC + b"\x45\x01", # Bold font ON
@@ -168,34 +177,34 @@ TXT_STYLE = {
# Fonts # Fonts
SET_FONT = lambda n: ESC + b"\x4d" + n SET_FONT = lambda n: ESC + b"\x4d" + n
TXT_FONT_A = SET_FONT(b"\x00") # Font type A TXT_FONT_A: bytes = SET_FONT(b"\x00") #: Font type A
TXT_FONT_B = SET_FONT(b"\x01") # Font type B TXT_FONT_B: bytes = SET_FONT(b"\x01") #: Font type B
# Spacing # Spacing
LINESPACING_RESET = ESC + b"2" LINESPACING_RESET = ESC + b"2"
LINESPACING_FUNCS = { LINESPACING_FUNCS: Dict[int, bytes] = {
60: ESC + b"A", # line_spacing/60 of an inch, 0 <= line_spacing <= 85 60: ESC + b"A", # line_spacing/60 of an inch, 0 <= line_spacing <= 85
360: ESC + b"+", # line_spacing/360 of an inch, 0 <= line_spacing <= 255 360: ESC + b"+", # line_spacing/360 of an inch, 0 <= line_spacing <= 255
180: ESC + b"3", # line_spacing/180 of an inch, 0 <= line_spacing <= 255 180: ESC + b"3", # line_spacing/180 of an inch, 0 <= line_spacing <= 255
} }
# Prefix to change the codepage. You need to attach a byte to indicate #: Prefix to change the code page. You need to attach a byte to indicate
# the codepage to use. We use escpos-printer-db as the data source. #: the code page to use. We use escpos-printer-db as the data source.
CODEPAGE_CHANGE = ESC + b"\x74" CODEPAGE_CHANGE: bytes = ESC + b"\x74"
# Barcode format # Barcode format
_SET_BARCODE_TXT_POS = lambda n: GS + b"H" + n _SET_BARCODE_TXT_POS = lambda n: GS + b"H" + n
BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b"\x00") # HRI barcode chars OFF BARCODE_TXT_OFF: bytes = _SET_BARCODE_TXT_POS(b"\x00") #: HRI barcode chars OFF
BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b"\x01") # HRI barcode chars above BARCODE_TXT_ABV: bytes = _SET_BARCODE_TXT_POS(b"\x01") #: HRI barcode chars above
BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b"\x02") # HRI barcode chars below BARCODE_TXT_BLW: bytes = _SET_BARCODE_TXT_POS(b"\x02") #: HRI barcode chars below
BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b"\x03") # HRI both above and below BARCODE_TXT_BTH: bytes = _SET_BARCODE_TXT_POS(b"\x03") #: HRI both above and below
_SET_HRI_FONT = lambda n: GS + b"f" + n _SET_HRI_FONT = lambda n: GS + b"f" + n
BARCODE_FONT_A = _SET_HRI_FONT(b"\x00") # Font type A for HRI barcode chars BARCODE_FONT_A: bytes = _SET_HRI_FONT(b"\x00") #: Font type A for HRI barcode chars
BARCODE_FONT_B = _SET_HRI_FONT(b"\x01") # Font type B for HRI barcode chars BARCODE_FONT_B: bytes = _SET_HRI_FONT(b"\x01") #: Font type B for HRI barcode chars
BARCODE_HEIGHT = GS + b"h" # Barcode Height [1-255] BARCODE_HEIGHT: bytes = GS + b"h" #: Barcode Height [1-255]
BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6] BARCODE_WIDTH: bytes = GS + b"w" #: Barcode Width [2-6]
# NOTE: This isn't actually an ESC/POS command. It's the common prefix to the # NOTE: This isn't actually an ESC/POS command. It's the common prefix to the
# two "print bar code" commands: # two "print bar code" commands:
@@ -204,8 +213,8 @@ BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6]
# The latter command supports more barcode types # The latter command supports more barcode types
_SET_BARCODE_TYPE = lambda m: GS + b"k" + six.int2byte(m) _SET_BARCODE_TYPE = lambda m: GS + b"k" + six.int2byte(m)
# Barcodes for printing function type A #: Barcodes for printing function type A
BARCODE_TYPE_A = { BARCODE_TYPE_A: Dict[str, bytes] = {
"UPC-A": _SET_BARCODE_TYPE(0), "UPC-A": _SET_BARCODE_TYPE(0),
"UPC-E": _SET_BARCODE_TYPE(1), "UPC-E": _SET_BARCODE_TYPE(1),
"EAN13": _SET_BARCODE_TYPE(2), "EAN13": _SET_BARCODE_TYPE(2),
@@ -216,9 +225,9 @@ BARCODE_TYPE_A = {
"CODABAR": _SET_BARCODE_TYPE(6), # Same as NW7 "CODABAR": _SET_BARCODE_TYPE(6), # Same as NW7
} }
# Barcodes for printing function type B #: Barcodes for printing function type B
# The first 8 are the same barcodes as type A #: The first 8 are the same barcodes as type A
BARCODE_TYPE_B = { BARCODE_TYPE_B: Dict[str, bytes] = {
"UPC-A": _SET_BARCODE_TYPE(65), "UPC-A": _SET_BARCODE_TYPE(65),
"UPC-E": _SET_BARCODE_TYPE(66), "UPC-E": _SET_BARCODE_TYPE(66),
"EAN13": _SET_BARCODE_TYPE(67), "EAN13": _SET_BARCODE_TYPE(67),
@@ -236,57 +245,61 @@ BARCODE_TYPE_B = {
"GS1 DATABAR EXPANDED": _SET_BARCODE_TYPE(78), "GS1 DATABAR EXPANDED": _SET_BARCODE_TYPE(78),
} }
#: supported barcode formats
BARCODE_FORMATS = { BARCODE_FORMATS = {
"UPC-A": ([(11, 12)], "^[0-9]{11,12}$"), "UPC-A": ([(11, 12)], r"^[0-9]{11,12}$"),
"UPC-E": ([(7, 8), (11, 12)], "^([0-9]{7,8}|[0-9]{11,12})$"), "UPC-E": ([(7, 8), (11, 12)], r"^([0-9]{7,8}|[0-9]{11,12})$"),
"EAN13": ([(12, 13)], "^[0-9]{12,13}$"), "EAN13": ([(12, 13)], r"^[0-9]{12,13}$"),
"EAN8": ([(7, 8)], "^[0-9]{7,8}$"), "EAN8": ([(7, 8)], r"^[0-9]{7,8}$"),
"CODE39": ([(1, 255)], "^([0-9A-Z \$\%\+\-\.\/]+|\*[0-9A-Z \$\%\+\-\.\/]+\*)$"), "CODE39": ([(1, 255)], r"^([0-9A-Z \$\%\+\-\.\/]+|\*[0-9A-Z \$\%\+\-\.\/]+\*)$"),
"ITF": ([(2, 255)], "^([0-9]{2})+$"), "ITF": ([(2, 255)], r"^([0-9]{2})+$"),
"NW7": ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), "NW7": ([(1, 255)], r"^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"),
"CODABAR": ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), # Same as NW7 "CODABAR": ([(1, 255)], r"^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), # Same as NW7
"CODE93": ([(1, 255)], "^[\\x00-\\x7F]+$"), "CODE93": ([(1, 255)], r"^[\x00-\x7F]+$"),
"CODE128": ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"), "CODE128": ([(2, 255)], r"^\{[A-C][\x00-\x7F]+$"),
"GS1-128": ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"), # same as CODE128 "GS1-128": ([(2, 255)], r"^\{[A-C][\x00-\x7F]+$"), # same as CODE128
"GS1 DATABAR OMNIDIRECTIONAL": ([(13, 13)], "^[0-9]{13}$"), "GS1 DATABAR OMNIDIRECTIONAL": ([(13, 13)], r"^[0-9]{13}$"),
"GS1 DATABAR TRUNCATED": ([(13, 13)], "^[0-9]{13}$"), # same as GS1 omnidirectional "GS1 DATABAR TRUNCATED": (
"GS1 DATABAR LIMITED": ([(13, 13)], "^[01][0-9]{12}$"), [(13, 13)],
r"^[0-9]{13}$",
), # same as GS1 omnidirectional
"GS1 DATABAR LIMITED": ([(13, 13)], r"^[01][0-9]{12}$"),
"GS1 DATABAR EXPANDED": ( "GS1 DATABAR EXPANDED": (
[(2, 255)], [(2, 255)],
"^\([0-9][A-Za-z0-9 \!\"\%\&'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\_\{]+$", r"^\([0-9][A-Za-z0-9 \!\"\%\&'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\_\{]+$",
), ),
} }
BARCODE_TYPES = { BARCODE_TYPES: Dict[str, Dict[str, bytes]] = {
"A": BARCODE_TYPE_A, "A": BARCODE_TYPE_A,
"B": BARCODE_TYPE_B, "B": BARCODE_TYPE_B,
} }
# QRCode error correction levels # QRCode error correction levels
QR_ECLEVEL_L = 0 QR_ECLEVEL_L: int = 0
QR_ECLEVEL_M = 1 QR_ECLEVEL_M: int = 1
QR_ECLEVEL_Q = 2 QR_ECLEVEL_Q: int = 2
QR_ECLEVEL_H = 3 QR_ECLEVEL_H: int = 3
# QRcode models # QRcode models
QR_MODEL_1 = 1 QR_MODEL_1: int = 1
QR_MODEL_2 = 2 QR_MODEL_2: int = 2
QR_MICRO = 3 QR_MICRO: int = 3
# Image format # Image format
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image" # NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"
# command. The constants include a fragment of the data's header. # command. The constants include a fragment of the data's header.
_PRINT_RASTER_IMG = lambda data: GS + b"v0" + data _PRINT_RASTER_IMG = lambda data: GS + b"v0" + data
S_RASTER_N = _PRINT_RASTER_IMG(b"\x00") # Set raster image normal size S_RASTER_N: bytes = _PRINT_RASTER_IMG(b"\x00") # Set raster image normal size
S_RASTER_2W = _PRINT_RASTER_IMG(b"\x01") # Set raster image double width S_RASTER_2W: bytes = _PRINT_RASTER_IMG(b"\x01") # Set raster image double width
S_RASTER_2H = _PRINT_RASTER_IMG(b"\x02") # Set raster image double height S_RASTER_2H: bytes = _PRINT_RASTER_IMG(b"\x02") # Set raster image double height
S_RASTER_Q = _PRINT_RASTER_IMG(b"\x03") # Set raster image quadruple S_RASTER_Q: bytes = _PRINT_RASTER_IMG(b"\x03") # Set raster image quadruple
# Status Command # Status Command
RT_STATUS = DLE + EOT RT_STATUS: bytes = DLE + EOT
RT_STATUS_ONLINE = RT_STATUS + b"\x01" RT_STATUS_ONLINE: bytes = RT_STATUS + b"\x01"
RT_STATUS_PAPER = RT_STATUS + b"\x04" RT_STATUS_PAPER: bytes = RT_STATUS + b"\x04"
RT_MASK_ONLINE = 8 RT_MASK_ONLINE: int = 8
RT_MASK_PAPER = 18 RT_MASK_PAPER: int = 18
RT_MASK_LOWPAPER = 30 RT_MASK_LOWPAPER: int = 30
RT_MASK_NOPAPER = 114 RT_MASK_NOPAPER: int = 114

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" ESC/POS Exceptions classes """ESC/POS Exceptions classes.
Result/Exit codes: Result/Exit codes:
@@ -13,30 +13,42 @@ Result/Exit codes:
- `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError` - `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError`
- `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError` - `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError`
- `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError` - `80` = Invalid char code :py:exc:`~escpos.exceptions.CharCodeError`
- `90` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError` - `90` = Device not found :py:exc:`~escpos.exceptions.DeviceNotFoundError`
- `91` = USB device not found :py:exc:`~escpos.exceptions.USBNotFoundError`
- `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError` - `100` = Set variable out of range :py:exc:`~escpos.exceptions.SetVariableError`
- `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError` - `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError`
- `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError` - `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError`
- `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError` - `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError`
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others :author: python-escpos developers
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_ :organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos :copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT :license: MIT
""" """
from typing import Optional
class Error(Exception): class Error(Exception):
"""Base class for ESC/POS errors""" """Base class for ESC/POS errors.
def __init__(self, msg, status=None): inheritance:
.. inheritance-diagram:: escpos.exceptions.Error
:parts: 1
"""
def __init__(self, msg: str, status: Optional[int] = None) -> None:
"""Initialize Error object."""
Exception.__init__(self) Exception.__init__(self)
self.msg = msg self.msg = msg
self.resultcode = 1 self.resultcode = 1
if status is not None: if status is not None:
self.resultcode = status self.resultcode = status
def __str__(self): def __str__(self) -> str:
"""Return string representation of Error."""
return self.msg return self.msg
@@ -46,15 +58,23 @@ class BarcodeTypeError(Error):
This exception indicates that no known barcode-type has been entered. The barcode-type has to be This exception indicates that no known barcode-type has been entered. The barcode-type has to be
one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`. one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`.
The returned error code is `10`. The returned error code is `10`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.BarcodeTypeError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeTypeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 10 self.resultcode = 10
def __str__(self): def __str__(self) -> str:
return "No Barcode type is defined ({msg})".format(msg=self.msg) """Return string representation of BarcodeTypeError."""
return f"No Barcode type is defined ({self.msg})"
class BarcodeSizeError(Error): class BarcodeSizeError(Error):
@@ -63,15 +83,23 @@ class BarcodeSizeError(Error):
This exception indicates that the values for the barcode size are out of range. This exception indicates that the values for the barcode size are out of range.
The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`. The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`.
The resulting return code is `20`. The resulting return code is `20`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.BarcodeSizeError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeSizeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 20 self.resultcode = 20
def __str__(self): def __str__(self) -> str:
return "Barcode size is out of range ({msg})".format(msg=self.msg) """Return string representation of BarcodeSizeError."""
return f"Barcode size is out of range ({self.msg})"
class BarcodeCodeError(Error): class BarcodeCodeError(Error):
@@ -80,47 +108,69 @@ class BarcodeCodeError(Error):
No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter
was True and the check failed. was True and the check failed.
The return code for this exception is `30`. The return code for this exception is `30`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.BarcodeCodeError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeCodeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 30 self.resultcode = 30
def __str__(self): def __str__(self) -> str:
return "No Barcode code was supplied ({msg})".format(msg=self.msg) """Return string representation of BarcodeCodeError."""
return f"No Barcode code was supplied ({self.msg})"
class ImageSizeError(Error): class ImageSizeError(Error):
"""Image height is longer than 255px and can't be printed. """Image height is longer than 255px and can't be printed.
The return code for this exception is `40`. The return code for this exception is `40`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ImageSizeError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ImageSizeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 40 self.resultcode = 40
def __str__(self): def __str__(self) -> str:
return "Image height is longer than 255px and can't be printed ({msg})".format( """Return string representation of ImageSizeError."""
msg=self.msg return f"Image height is longer than 255px and can't be printed ({self.msg})"
)
class ImageWidthError(Error): class ImageWidthError(Error):
"""Image width is too large. """Image width is too large.
The return code for this exception is `41`. The return code for this exception is `41`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ImageWidthError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ImageWidthError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 41 self.resultcode = 41
def __str__(self): def __str__(self) -> str:
return "Image width is too large ({msg})".format(msg=self.msg) """Return string representation of ImageWidthError."""
return f"Image width is too large ({self.msg})"
class TextError(Error): class TextError(Error):
@@ -128,17 +178,23 @@ class TextError(Error):
This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`. This exception is raised when an empty string is passed to :py:meth:`escpos.escpos.Escpos.text`.
The return code for this exception is `50`. The return code for this exception is `50`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.TextError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize TextError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 50 self.resultcode = 50
def __str__(self): def __str__(self) -> str:
return "Text string must be supplied to the text() method ({msg})".format( """Return string representation of TextError."""
msg=self.msg return f"Text string must be supplied to the text() method ({self.msg})"
)
class CashDrawerError(Error): class CashDrawerError(Error):
@@ -146,130 +202,218 @@ class CashDrawerError(Error):
A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`. A valid pin number has to be passed onto the method :py:meth:`escpos.escpos.Escpos.cashdraw`.
The return code for this exception is `60`. The return code for this exception is `60`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.CashDrawerError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize CashDrawerError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 60 self.resultcode = 60
def __str__(self): def __str__(self) -> str:
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg) """Return string representation of CashDrawerError."""
return f"Valid pin must be set to send pulse ({self.msg})"
class TabPosError(Error): class TabPosError(Error):
"""Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values. """Tab position is invalid.
Valid tab positions must be set by using from 1 to 32 tabs, and between 1 and 255 tab size values.
Both values multiplied must not exceed 255, since it is the maximum tab value. Both values multiplied must not exceed 255, since it is the maximum tab value.
This exception is raised by :py:meth:`escpos.escpos.Escpos.control`. This exception is raised by :py:meth:`escpos.escpos.Escpos.control`.
The return code for this exception is `70`. The return code for this exception is `70`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.TabPosError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize TabPosError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 70 self.resultcode = 70
def __str__(self): def __str__(self) -> str:
return "Valid tab positions must be in the range 0 to 16 ({msg})".format( """Return string representation of TabPosError."""
msg=self.msg return f"Valid tab positions must be in the range 0 to 16 ({self.msg})"
)
class CharCodeError(Error): class CharCodeError(Error):
"""Valid char code must be set. """Valid char code must be set.
The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown. The supplied charcode-name in :py:meth:`escpos.escpos.Escpos.charcode` is unknown.
Ths returncode for this exception is `80`. The return code for this exception is `80`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.CharCodeError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize CharCodeError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 80 self.resultcode = 80
def __str__(self): def __str__(self) -> str:
return "Valid char code must be set ({msg})".format(msg=self.msg) """Return string representation of CharCodeError."""
return f"Valid char code must be set ({self.msg})"
class USBNotFoundError(Error): class DeviceNotFoundError(Error):
"""Device wasn't found (probably not plugged in) """Device was not found.
The device seems to be not accessible.
The return code for this exception is `90`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.Error
:parts: 1
The USB device seems to be not plugged in.
Ths returncode for this exception is `90`.
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize DeviceNotFoundError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 90 self.resultcode = 90
def __str__(self): def __str__(self) -> str:
return "USB device not found ({msg})".format(msg=self.msg) """Return string representation of DeviceNotFoundError."""
return f"Device not found ({self.msg})"
class USBNotFoundError(DeviceNotFoundError):
"""USB device was not found (probably not plugged in).
The USB device seems to be not plugged in.
The return code for this exception is `91`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.USBNotFoundError
:parts: 1
"""
def __init__(self, msg: str = "") -> None:
"""Initialize USBNotFoundError object."""
Error.__init__(self, msg)
self.msg = msg
self.resultcode = 91
def __str__(self) -> str:
"""Return string representation of USBNotFoundError."""
return f"USB device not found ({self.msg})"
class SetVariableError(Error): class SetVariableError(Error):
"""A set method variable was out of range """A set method variable was out of range.
Check set variables against minimum and maximum values Check set variables against minimum and maximum values
Ths returncode for this exception is `100`. The return code for this exception is `100`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.SetVariableError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize SetVariableError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 100 self.resultcode = 100
def __str__(self): def __str__(self) -> str:
return "Set variable out of range ({msg})".format(msg=self.msg) """Return string representation of SetVariableError."""
return f"Set variable out of range ({self.msg})"
# Configuration errors # Configuration errors
class ConfigNotFoundError(Error): class ConfigNotFoundError(Error):
"""The configuration file was not found """The configuration file was not found.
The default or passed configuration file could not be read The default or passed configuration file could not be read
Ths returncode for this exception is `200`. The return code for this exception is `200`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ConfigNotFoundError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigNotFoundError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 200 self.resultcode = 200
def __str__(self): def __str__(self) -> str:
return "Configuration not found ({msg})".format(msg=self.msg) """Return string representation of ConfigNotFoundError."""
return f"Configuration not found ({self.msg})"
class ConfigSyntaxError(Error): class ConfigSyntaxError(Error):
"""The configuration file is invalid """The configuration file is invalid.
The syntax is incorrect The syntax is incorrect
Ths returncode for this exception is `210`. The return code for this exception is `210`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ConfigSyntaxError
:parts: 1
""" """
def __init__(self, msg=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigSyntaxError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 210 self.resultcode = 210
def __str__(self): def __str__(self) -> str:
return "Configuration syntax is invalid ({msg})".format(msg=self.msg) """Return string representation of ConfigSyntaxError."""
return f"Configuration syntax is invalid ({self.msg})"
class ConfigSectionMissingError(Error): class ConfigSectionMissingError(Error):
"""The configuration file is missing a section """The configuration file is missing a section.
The part of the config asked for does not exist in the loaded configuration
The return code for this exception is `220`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ConfigSectionMissingError
:parts: 1
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=""): def __init__(self, msg: str = "") -> None:
"""Initialize ConfigSectionMissingError object."""
Error.__init__(self, msg) Error.__init__(self, msg)
self.msg = msg self.msg = msg
self.resultcode = 220 self.resultcode = 220
def __str__(self): def __str__(self) -> str:
return "Configuration section is missing ({msg})".format(msg=self.msg) """Return string representation of ConfigSectionMissingError."""
return f"Configuration section is missing ({self.msg})"

View File

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

View File

@@ -4,14 +4,17 @@
I doubt that this currently works correctly. I doubt that this currently works correctly.
""" """
import types
import typing
jaconv: typing.Optional[types.ModuleType]
try: try:
import jaconv import jaconv
except ImportError: except ImportError:
jaconv = None jaconv = None
def encode_katakana(text): def encode_katakana(text: str) -> bytes:
"""I don't think this quite works yet.""" """I don't think this quite works yet."""
encoded = [] encoded = []
for char in text: for char in text:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Magic Encode """Magic Encode.
This module tries to convert an UTF-8 string to an encoded string for the printer. This module tries to convert an UTF-8 string to an encoded string for the printer.
It uses trial and error in order to guess the right code page. It uses trial and error in order to guess the right code page.
@@ -13,64 +13,63 @@ The code is based on the encoding-code in py-xml-escpos by @fvdsn.
""" """
import re
from builtins import bytes from builtins import bytes
import six
from .codepages import CodePages
from .constants import CODEPAGE_CHANGE from .constants import CODEPAGE_CHANGE
from .exceptions import Error from .exceptions import Error
from .codepages import CodePages
import six
import re
class Encoder(object): class Encoder:
"""Takes a list of available code spaces. Picks the right one for a """Take available code spaces and pick the right one for a given character.
given character.
Note: To determine the code page, it needs to do the conversion, and Note: To determine the code page, it needs to do the conversion, and
thus already knows what the final byte in the target encoding would thus already knows what the final byte in the target encoding would
be. Nevertheless, the API of this class doesn't return the byte. be. Nevertheless, the API of this class does not return the byte.
The caller use to do the character conversion itself. The caller use to do the character conversion itself.
$ python -m timeit -s "{u'ö':'a'}.get(u'ö')"
100000000 loops, best of 3: 0.0133 usec per loop
$ python -m timeit -s "u'ö'.encode('latin1')"
100000000 loops, best of 3: 0.0141 usec per loop
""" """
def __init__(self, codepage_map): def __init__(self, codepage_map):
"""Initialize encoder."""
self.codepages = codepage_map self.codepages = codepage_map
self.available_encodings = set(codepage_map.keys()) self.available_encodings = set(codepage_map.keys())
self.available_characters = {} self.available_characters = {}
self.used_encodings = set() self.used_encodings = set()
def get_sequence(self, encoding): def get_sequence(self, encoding):
"""Get a sequence."""
return int(self.codepages[encoding]) return int(self.codepages[encoding])
def get_encoding_name(self, encoding): def get_encoding_name(self, encoding):
"""Given an encoding provided by the user, will return a """Return a canonical encoding name.
Given an encoding provided by the user, will return a
canonical encoding name; and also validate that the encoding canonical encoding name; and also validate that the encoding
is supported. is supported.
TODO: Support encoding aliases: pc437 instead of cp437. .. todo:: Support encoding aliases: pc437 instead of cp437.
""" """
encoding = CodePages.get_encoding_name(encoding) encoding = CodePages.get_encoding_name(encoding)
if encoding not in self.codepages: if encoding not in self.codepages:
raise ValueError( raise ValueError(
( (
'Encoding "{}" cannot be used for the current profile. ' f'Encoding "{encoding}" cannot be used for the current profile. '
"Valid encodings are: {}" f'Valid encodings are: {",".join(self.codepages.keys())}'
).format(encoding, ",".join(self.codepages.keys())) )
) )
return encoding return encoding
@staticmethod @staticmethod
def _get_codepage_char_list(encoding): def _get_codepage_char_list(encoding):
"""Get codepage character list """Get code page character list.
Gets characters 128-255 for a given code page, as an array. Gets characters 128-255 for a given code page, as an array.
:param encoding: The name of the encoding. This must appear in the CodePage list :param encoding: The name of the encoding. This must appear in the code page list
""" """
codepage = CodePages.get_encoding(encoding) codepage = CodePages.get_encoding(encoding)
if "data" in codepage: if "data" in codepage:
@@ -89,10 +88,10 @@ class Encoder(object):
# Non-encodable character, just skip it # Non-encodable character, just skip it
pass pass
return encodable_chars return encodable_chars
raise LookupError("Can't find a known encoding for {}".format(encoding)) raise LookupError(f"Can't find a known encoding for {encoding}")
def _get_codepage_char_map(self, encoding): def _get_codepage_char_map(self, encoding):
"""Get codepage character map """Get code page character map.
Process an encoding and return a map of UTF-characters to code points Process an encoding and return a map of UTF-characters to code points
in this encoding. in this encoding.
@@ -112,7 +111,7 @@ class Encoder(object):
return codepage_char_map return codepage_char_map
def can_encode(self, encoding, char): def can_encode(self, encoding, char):
"""Determine if a character is encodeable in the given code page. """Determine if a character is encodable in the given code page.
:param encoding: The name of the encoding. :param encoding: The name of the encoding.
:param char: The character to attempt to encode. :param char: The character to attempt to encode.
@@ -130,7 +129,7 @@ class Encoder(object):
@staticmethod @staticmethod
def _encode_char(char, charmap, defaultchar): def _encode_char(char, charmap, defaultchar):
"""Encode a single character with the given encoding map """Encode a single character with the given encoding map.
:param char: char to encode :param char: char to encode
:param charmap: dictionary for mapping characters in this code page :param charmap: dictionary for mapping characters in this code page
@@ -142,7 +141,7 @@ class Encoder(object):
return ord(defaultchar) return ord(defaultchar)
def encode(self, text, encoding, defaultchar="?"): def encode(self, text, encoding, defaultchar="?"):
"""Encode text under the given encoding """Encode text under the given encoding.
:param text: Text to encode :param text: Text to encode
:param encoding: Encoding name to use (must be defined in capabilities) :param encoding: Encoding name to use (must be defined in capabilities)
@@ -156,15 +155,18 @@ class Encoder(object):
def __encoding_sort_func(self, item): def __encoding_sort_func(self, item):
key, index = item key, index = item
return (key in self.used_encodings, index) used = key in self.used_encodings
return (not used, index)
def find_suitable_encoding(self, char): def find_suitable_encoding(self, char):
"""The order of our search is a specific one: """Search in a specific order for a suitable encoding.
It is the following order:
1. code pages that we already tried before; there is a good 1. code pages that we already tried before; there is a good
chance they might work again, reducing the search space, chance they might work again, reducing the search space,
and by re-using already used encodings we might also and by re-using already used encodings we might also
reduce the number of codepage change instructiosn we have reduce the number of code page change instruction we have
to send. Still, any performance gains will presumably be to send. Still, any performance gains will presumably be
fairly minor. fairly minor.
@@ -184,7 +186,9 @@ class Encoder(object):
def split_writable_text(encoder, text, encoding): def split_writable_text(encoder, text, encoding):
"""Splits off as many characters from the beginning of text as """Split up the writable text.
Splits off as many characters from the beginning of text as
are writable with "encoding". Returns a 2-tuple (writable, rest). are writable with "encoding". Returns a 2-tuple (writable, rest).
""" """
if not encoding: if not encoding:
@@ -198,8 +202,10 @@ def split_writable_text(encoder, text, encoding):
return text, None return text, None
class MagicEncode(object): class MagicEncode:
"""A helper that helps us to automatically switch to the right """Help switching to the right code page.
A helper that helps us to automatically switch to the right
code page to encode any given Unicode character. code page to encode any given Unicode character.
This will consider the printers supported codepages, according This will consider the printers supported codepages, according
@@ -213,7 +219,7 @@ class MagicEncode(object):
def __init__( def __init__(
self, driver, encoding=None, disabled=False, defaultsymbol="?", encoder=None self, driver, encoding=None, disabled=False, defaultsymbol="?", encoder=None
): ):
""" """Initialize magic encode.
:param driver: :param driver:
:param encoding: If you know the current encoding of the printer :param encoding: If you know the current encoding of the printer
@@ -235,7 +241,7 @@ class MagicEncode(object):
self.disabled = disabled self.disabled = disabled
def force_encoding(self, encoding): def force_encoding(self, encoding):
"""Sets a fixed encoding. The change is emitted right away. """Set a fixed encoding. The change is emitted right away.
From now one, this buffer will switch the code page anymore. From now one, this buffer will switch the code page anymore.
However, it will still keep track of the current code page. However, it will still keep track of the current code page.
@@ -248,7 +254,6 @@ class MagicEncode(object):
def write(self, text): def write(self, text):
"""Write the text, automatically switching encodings.""" """Write the text, automatically switching encodings."""
if self.disabled: if self.disabled:
self.write_with_encoding(self.encoding, text) self.write_with_encoding(self.encoding, text)
return return
@@ -277,17 +282,19 @@ class MagicEncode(object):
self.write_with_encoding(encoding, to_write) self.write_with_encoding(encoding, to_write)
def _handle_character_failed(self, char): def _handle_character_failed(self, char):
"""Called when no codepage was found to render a character.""" """Write a default symbol.
Called when no code page was found to render a character.
"""
# Writing the default symbol via write() allows us to avoid # Writing the default symbol via write() allows us to avoid
# unnecesary code page switches. # unnecesary code page switches.
self.write(self.defaultsymbol) self.write(self.defaultsymbol)
def write_with_encoding(self, encoding, text): def write_with_encoding(self, encoding, text):
if text is not None and type(text) is not six.text_type: """Write the text and inject necessary code page switches."""
if text is not None and type(text) is not str:
raise Error( raise Error(
"The supplied text has to be unicode, but is of type {type}.".format( f"The supplied text has to be Unicode, but is of type {type(text)}."
type=type(text)
)
) )
# We always know the current code page; if the new code page # We always know the current code page; if the new code page

View File

@@ -1,602 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
""" This module contains the implementations of abstract base class :py:class:`Escpos`.
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT
"""
import os
import socket
import subprocess
import sys
import serial
import usb.core
import usb.util
from .escpos import Escpos
from .exceptions import USBNotFoundError
_WIN32PRINT = False
try:
import win32print
_WIN32PRINT = True
except ImportError:
pass
_CUPSPRINT = False
try:
import cups
import tempfile
_CUPSPRINT = True
except ImportError:
pass
class Usb(Escpos):
"""USB printer
This class describes a printer that natively speaks USB.
inheritance:
.. inheritance-diagram:: escpos.printer.Usb
:parts: 1
"""
def __init__(
self,
idVendor,
idProduct,
usb_args=None,
timeout=0,
in_ep=0x82,
out_ep=0x01,
*args,
**kwargs
): # noqa: N803
"""
:param idVendor: Vendor ID
:param idProduct: Product ID
:param usb_args: Optional USB arguments (e.g. custom_match)
:param timeout: Is the time limit of the USB operation. Default without timeout.
:param in_ep: Input end point
:param out_ep: Output end point
"""
Escpos.__init__(self, *args, **kwargs)
self.timeout = timeout
self.in_ep = in_ep
self.out_ep = out_ep
usb_args = usb_args or {}
if idVendor:
usb_args["idVendor"] = idVendor
if idProduct:
usb_args["idProduct"] = idProduct
self.open(usb_args)
def open(self, usb_args):
"""Search device on USB tree and set it as escpos device.
:param usb_args: USB arguments
"""
self.device = usb.core.find(**usb_args)
if self.device is None:
raise USBNotFoundError("Device not found or cable not plugged in.")
self.idVendor = self.device.idVendor
self.idProduct = self.device.idProduct
# pyusb has three backends: libusb0, libusb1 and openusb but
# only libusb1 backend implements the methods is_kernel_driver_active()
# and detach_kernel_driver().
# This helps enable this library to work on Windows.
if self.device.backend.__module__.endswith("libusb1"):
check_driver = None
try:
check_driver = self.device.is_kernel_driver_active(0)
except NotImplementedError:
pass
if check_driver is None or check_driver:
try:
self.device.detach_kernel_driver(0)
except NotImplementedError:
pass
except usb.core.USBError as e:
if check_driver is not None:
print("Could not detatch kernel driver: {0}".format(str(e)))
try:
self.device.set_configuration()
self.device.reset()
except usb.core.USBError as e:
print("Could not set configuration: {0}".format(str(e)))
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self.device.write(self.out_ep, msg, self.timeout)
def _read(self):
"""Reads a data buffer and returns it to the caller."""
return self.device.read(self.in_ep, 16)
def close(self):
"""Release USB interface"""
if self.device:
usb.util.dispose_resources(self.device)
self.device = None
class Serial(Escpos):
"""Serial printer
This class describes a printer that is connected by serial interface.
inheritance:
.. inheritance-diagram:: escpos.printer.Serial
:parts: 1
"""
def __init__(
self,
devfile="/dev/ttyS0",
baudrate=9600,
bytesize=8,
timeout=1,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
xonxoff=False,
dsrdtr=True,
*args,
**kwargs
):
"""
:param devfile: Device file under dev filesystem
:param baudrate: Baud rate for serial transmission
:param bytesize: Serial buffer size
:param timeout: Read/Write timeout
:param parity: Parity checking
:param stopbits: Number of stop bits
:param xonxoff: Software flow control
:param dsrdtr: Hardware flow control (False to enable RTS/CTS)
"""
Escpos.__init__(self, *args, **kwargs)
self.devfile = devfile
self.baudrate = baudrate
self.bytesize = bytesize
self.timeout = timeout
self.parity = parity
self.stopbits = stopbits
self.xonxoff = xonxoff
self.dsrdtr = dsrdtr
self.open()
def open(self):
"""Setup serial port and set is as escpos device"""
if self.device is not None and self.device.is_open:
self.close()
self.device = serial.Serial(
port=self.devfile,
baudrate=self.baudrate,
bytesize=self.bytesize,
parity=self.parity,
stopbits=self.stopbits,
timeout=self.timeout,
xonxoff=self.xonxoff,
dsrdtr=self.dsrdtr,
)
if self.device is not None:
print("Serial printer enabled")
else:
print("Unable to open serial printer on: {0}".format(str(self.devfile)))
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self.device.write(msg)
def _read(self):
"""Reads a data buffer and returns it to the caller."""
return self.device.read(16)
def close(self):
"""Close Serial interface"""
if self.device is not None and self.device.is_open:
self.device.flush()
self.device.close()
class Network(Escpos):
"""Network printer
This class is used to attach to a networked printer. You can also use this in order to attach to a printer that
is forwarded with ``socat``.
If you have a local printer on parallel port ``/dev/usb/lp0`` then you could start ``socat`` with:
.. code-block:: none
socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0
Then you should be able to attach to port ``4242`` with this class.
Otherwise the normal usecase would be to have a printer with ethernet interface. This type of printer should
work the same with this class. For the address of the printer check its manuals.
inheritance:
.. inheritance-diagram:: escpos.printer.Network
:parts: 1
"""
def __init__(self, host, port=9100, timeout=60, *args, **kwargs):
"""
:param host: Printer's hostname or IP address
:param port: Port to write to
:param timeout: timeout in seconds for the socket-library
"""
Escpos.__init__(self, *args, **kwargs)
self.host = host
self.port = port
self.timeout = timeout
self.open()
def open(self):
"""Open TCP socket with ``socket``-library and set it as escpos device"""
self.device = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.device.settimeout(self.timeout)
self.device.connect((self.host, self.port))
if self.device is None:
print("Could not open socket for {0}".format(self.host))
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self.device.sendall(msg)
def _read(self):
"""Read data from the TCP socket"""
return self.device.recv(16)
def close(self):
"""Close TCP connection"""
if self.device is not None:
try:
self.device.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
self.device.close()
class File(Escpos):
"""Generic file printer
This class is used for parallel port printer or other printers that are directly attached to the filesystem.
Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable
and produce arbitrary errors.
inheritance:
.. inheritance-diagram:: escpos.printer.File
:parts: 1
"""
def __init__(self, devfile="/dev/usb/lp0", auto_flush=True, *args, **kwargs):
"""
:param devfile: Device file under dev filesystem
:param auto_flush: automatically call flush after every call of _raw()
"""
Escpos.__init__(self, *args, **kwargs)
self.devfile = devfile
self.auto_flush = auto_flush
self.open()
def open(self):
"""Open system file"""
self.device = open(self.devfile, "wb")
if self.device is None:
print("Could not open the specified file {0}".format(self.devfile))
def flush(self):
"""Flush printing content"""
self.device.flush()
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self.device.write(msg)
if self.auto_flush:
self.flush()
def close(self):
"""Close system file"""
if self.device is not None:
self.device.flush()
self.device.close()
class Dummy(Escpos):
"""Dummy printer
This class is used for saving commands to a variable, for use in situations where
there is no need to send commands to an actual printer. This includes
generating print jobs for later use, or testing output.
inheritance:
.. inheritance-diagram:: escpos.printer.Dummy
:parts: 1
"""
def __init__(self, *args, **kwargs):
""" """
Escpos.__init__(self, *args, **kwargs)
self._output_list = []
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self._output_list.append(msg)
@property
def output(self):
"""Get the data that was sent to this printer"""
return b"".join(self._output_list)
def clear(self):
"""Clear the buffer of the printer
This method can be called if you send the contents to a physical printer
and want to use the Dummy printer for new output.
"""
del self._output_list[:]
def close(self):
pass
if _WIN32PRINT:
class Win32Raw(Escpos):
def __init__(self, printer_name=None, *args, **kwargs):
Escpos.__init__(self, *args, **kwargs)
if printer_name is not None:
self.printer_name = printer_name
else:
self.printer_name = win32print.GetDefaultPrinter()
self.hPrinter = None
self.open()
def open(self, job_name="python-escpos"):
if self.printer_name is None:
raise Exception("Printer not found")
self.hPrinter = win32print.OpenPrinter(self.printer_name)
self.current_job = win32print.StartDocPrinter(
self.hPrinter, 1, (job_name, None, "RAW")
)
win32print.StartPagePrinter(self.hPrinter)
def close(self):
if not self.hPrinter:
return
win32print.EndPagePrinter(self.hPrinter)
win32print.EndDocPrinter(self.hPrinter)
win32print.ClosePrinter(self.hPrinter)
self.hPrinter = None
def _raw(self, msg):
"""Print any command sent in raw format
:param msg: arbitrary code to be printed
:type msg: bytes
"""
if self.printer_name is None:
raise Exception("Printer not found")
if self.hPrinter is None:
raise Exception("Printer job not opened")
win32print.WritePrinter(self.hPrinter, msg)
if _CUPSPRINT:
class CupsPrinter(Escpos):
"""Simple CUPS printer connector.
.. note::
Requires _pycups_ which in turn needs the cups development library package:
- Ubuntu/Debian: _libcups2-dev_
- OpenSuse/Fedora: _cups-devel_
"""
def __init__(self, printer_name=None, *args, **kwargs):
"""CupsPrinter class constructor.
:param printer_name: CUPS printer name (Optional)
:type printer_name: str
:param host: CUPS server host/ip (Optional)
:type host: str
:param port: CUPS server port (Optional)
:type port: int
"""
Escpos.__init__(self, *args, **kwargs)
host, port = args or (
kwargs.get("host", cups.getServer()),
kwargs.get("port", cups.getPort()),
)
cups.setServer(host)
cups.setPort(port)
self.conn = cups.Connection()
self.tmpfile = None
self.printer_name = printer_name
self.job_name = ""
self.pending_job = False
self.open()
@property
def printers(self):
"""Available CUPS printers."""
return self.conn.getPrinters()
def open(self, job_name="python-escpos"):
"""Setup a new print job and target printer.
A call to this method is required to send new jobs to
the same CUPS connection.
Defaults to default CUPS printer.
Creates a new temporary file buffer.
"""
self.job_name = job_name
if self.printer_name not in self.printers:
self.printer_name = self.conn.getDefault()
self.tmpfile = tempfile.NamedTemporaryFile(delete=True)
def _raw(self, msg):
"""Append any command sent in raw format to temporary file
:param msg: arbitrary code to be printed
:type msg: bytes
"""
self.pending_job = True
try:
self.tmpfile.write(msg)
except ValueError:
self.pending_job = False
raise ValueError("Printer job not opened")
def send(self):
"""Send the print job to the printer."""
if self.pending_job:
# Rewind tempfile
self.tmpfile.seek(0)
# Print temporary file via CUPS printer.
self.conn.printFile(
self.printer_name,
self.tmpfile.name,
self.job_name,
{"document-format": cups.CUPS_FORMAT_RAW},
)
self._clear()
def _clear(self):
"""Finish the print job.
Remove temporary file.
"""
self.tmpfile.close()
self.pending_job = False
def _read(self):
"""Return a single-item array with the accepting state of the print queue.
states: idle = [3], printing a job = [4], stopped = [5]
"""
printer = self.printers.get(self.printer_name, {})
state = printer.get("printer-state")
if not state:
return []
return [state]
def close(self):
"""Close CUPS connection.
Send pending job to the printer if needed.
"""
if self.pending_job:
self.send()
if self.conn:
print("Closing CUPS connection to printer {}".format(self.printer_name))
self.conn = None
if not sys.platform.startswith("win"):
class LP(Escpos):
"""Simple UNIX lp command raw printing.
Thanks to `Oyami-Srk comment <https://github.com/python-escpos/python-escpos/pull/348#issuecomment-549558316>`_.
"""
def __init__(self, printer_name: str, *args, **kwargs):
"""LP class constructor.
:param printer_name: CUPS printer name (Optional)
:type printer_name: str
:param auto_flush: Automatic flush after every _raw() (Optional)
:type auto_flush: bool
"""
Escpos.__init__(self, *args, **kwargs)
self.printer_name = printer_name
self.auto_flush = kwargs.get("auto_flush", True)
self.open()
def open(self):
"""Invoke _lp_ in a new subprocess and wait for commands."""
self.lp = subprocess.Popen(
["lp", "-d", self.printer_name, "-o", "raw"],
stdin=subprocess.PIPE,
stdout=open(os.devnull, "w"),
)
def close(self):
"""Stop the subprocess."""
self.lp.terminate()
def flush(self):
"""End line and wait for new commands"""
if self.lp.stdin.writable():
self.lp.stdin.write(b"\n")
if self.lp.stdin.closed is False:
self.lp.stdin.close()
self.lp.wait()
self.open()
def _raw(self, msg):
"""Write raw command(s) to the printer.
:param msg: arbitrary code to be printed
:type msg: bytes
"""
if self.lp.stdin.writable():
self.lp.stdin.write(msg)
else:
raise Exception("Not a valid pipe for lp process")
if self.auto_flush:
self.flush()

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""printer implementations."""
from .cups import CupsPrinter
from .dummy import Dummy
from .file import File
from .lp import LP
from .network import Network
from .serial import Serial
from .usb import Usb
from .win32raw import Win32Raw
__all__ = [
"Usb",
"File",
"Network",
"Serial",
"LP",
"Dummy",
"CupsPrinter",
"Win32Raw",
]

222
src/escpos/printer/cups.py Normal file
View File

@@ -0,0 +1,222 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the CupsPrinter printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import functools
import logging
import tempfile
from typing import Literal, Optional, Type, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
#: keeps track if the pycups dependency could be loaded (:py:class:`escpos.printer.CupsPrinter`)
_DEP_PYCUPS = False
try:
import cups
_DEP_PYCUPS = True
# Store server defaults before further configuration
DEFAULT_HOST = cups.getServer()
DEFAULT_PORT = cups.getPort()
except ImportError:
pass
# TODO: dev build mode that let's the wrapper bypass?
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
usable = False
if _DEP_PYCUPS:
usable = True
return usable
def dependency_pycups(func):
"""Indicate dependency on pycups."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Throw a RuntimeError if pycups is not imported."""
if not is_usable():
raise RuntimeError(
"Printing with PyCups requires the pycups library to "
"be installed. Please refer to the documentation on "
"what to install and install the dependencies for pycups."
)
return func(*args, **kwargs)
return wrapper
class CupsPrinter(Escpos):
"""Simple CUPS printer connector.
.. note::
Requires ``pycups`` which in turn needs the cups development library package:
- Ubuntu/Debian: ``libcups2-dev``
- OpenSuse/Fedora: ``cups-devel``
inheritance:
.. inheritance-diagram:: escpos.printer.CupsPrinter
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
@dependency_pycups
def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
"""Class constructor for CupsPrinter.
:param printer_name: CUPS printer name (Optional)
:param host: CUPS server host/ip (Optional)
:type host: str
:param port: CUPS server port (Optional)
:type port: int
"""
Escpos.__init__(self, *args, **kwargs)
self.host, self.port = args or (
kwargs.get("host", DEFAULT_HOST),
kwargs.get("port", DEFAULT_PORT),
)
self.tmpfile = tempfile.NamedTemporaryFile(delete=True)
self.printer_name = printer_name
self.job_name = ""
self.pending_job = False
self._device: Union[
Literal[False], Literal[None], Type[cups.Connection]
] = False
@property
def printers(self) -> dict:
"""Available CUPS printers."""
if self.device:
return self.device.getPrinters()
return {}
def open(
self, job_name: str = "python-escpos", raise_not_found: bool = True
) -> None:
"""Set up a new print job and target the printer.
A call to this method is required to send new jobs to
the CUPS connection after close.
Defaults to default CUPS printer.
Creates a new temporary file buffer.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device:
self.close()
cups.setServer(self.host)
cups.setPort(self.port)
self.job_name = job_name
if self.tmpfile.closed:
self.tmpfile = tempfile.NamedTemporaryFile(delete=True)
try:
# Open device
self.device: Optional[Type[cups.Connection]] = cups.Connection()
if self.device:
# Name validation, set default if no given name
self.printer_name = self.printer_name or self.device.getDefault()
assert self.printer_name in self.printers, "Incorrect printer name"
except (RuntimeError, AssertionError) as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Unable to start a print job for the printer {self.printer_name}:"
+ f"\n{e}"
)
else:
logging.error(
"CupsPrinter printing %s not available", self.printer_name
)
return
logging.info("CupsPrinter printer enabled")
def _raw(self, msg: bytes) -> None:
"""Append any command sent in raw format to temporary file.
:param msg: arbitrary code to be printed
"""
self.pending_job = True
try:
self.tmpfile.write(msg)
except TypeError:
self.pending_job = False
raise TypeError("Bytes required. Printer job not opened")
def send(self) -> None:
"""Send the print job to the printer."""
assert self.device
if self.pending_job:
# Rewind tempfile
self.tmpfile.seek(0)
# Print temporary file via CUPS printer.
self.device.printFile(
self.printer_name,
self.tmpfile.name,
self.job_name,
{"document-format": cups.CUPS_FORMAT_RAW},
)
self._clear()
def _clear(self) -> None:
"""Finish the print job.
Remove temporary file.
"""
self.tmpfile.close()
self.pending_job = False
def _read(self) -> bytes:
"""Return a single-item array with the accepting state of the print queue.
states: idle = [3], printing a job = [4], stopped = [5]
"""
printer = self.printers.get(self.printer_name, {})
state = printer.get("printer-state")
if not state or state in [4, 5]:
return b"8" # offline
return b"0" # online
def close(self) -> None:
"""Close CUPS connection.
Send pending job to the printer if needed.
"""
if not self._device:
return
if self.pending_job:
self.send()
logging.info("Closing CUPS connection to printer %s", self.printer_name)
self._device = False

View File

@@ -0,0 +1,70 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the CupsPrinter printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
from typing import List
from ..escpos import Escpos
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
return True
class Dummy(Escpos):
"""Dummy printer.
This class is used for saving commands to a variable, for use in situations where
there is no need to send commands to an actual printer. This includes
generating print jobs for later use, or testing output.
inheritance:
.. inheritance-diagram:: escpos.printer.Dummy
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
def __init__(self, *args, **kwargs) -> None:
"""Init with empty output list."""
Escpos.__init__(self, *args, **kwargs)
self._output_list: List[bytes] = []
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
self._output_list.append(msg)
@property
def output(self) -> bytes:
"""Get the data that was sent to this printer."""
return b"".join(self._output_list)
def clear(self) -> None:
"""Clear the buffer of the printer.
This method can be called if you send the contents to a physical printer
and want to use the Dummy printer for new output.
"""
del self._output_list[:]
def close(self) -> None:
"""Close not implemented for Dummy printer."""
pass

109
src/escpos/printer/file.py Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the File printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import logging
from typing import IO, Literal, Optional, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
return True
class File(Escpos):
"""Generic file printer.
This class is used for parallel port printer or other printers that are directly attached to the file system.
Note that you should stay away from using USB-to-Parallel-Adapter since they are unreliable
and produce arbitrary errors.
inheritance:
.. inheritance-diagram:: escpos.printer.File
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
def __init__(self, devfile: str = "", auto_flush: bool = True, *args, **kwargs):
"""Initialize file printer with device file.
:param devfile: Device file under dev file system
:param auto_flush: automatically call flush after every call of _raw()
"""
Escpos.__init__(self, *args, **kwargs)
self.devfile = devfile
self.auto_flush = auto_flush
self._device: Union[Literal[False], Literal[None], IO[bytes]] = False
def open(self, raise_not_found: bool = True) -> None:
"""Open system file.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device:
self.close()
try:
# Open device
self.device: Optional[IO[bytes]] = open(self.devfile, "wb")
except OSError as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Could not open the specified file {self.devfile}:\n{e}"
)
else:
logging.error("File printer %s not found", self.devfile)
return
logging.info("File printer enabled")
def flush(self) -> None:
"""Flush printing content."""
if self.device:
self.device.flush()
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
assert self.device
self.device.write(msg)
if self.auto_flush:
self.flush()
def close(self) -> None:
"""Close system file."""
if not self._device:
return
logging.info("Closing File connection to printer %s", self.devfile)
if not self.auto_flush:
self.flush()
self._device.close()
self._device = False

198
src/escpos/printer/lp.py Normal file
View File

@@ -0,0 +1,198 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the LP printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import functools
import logging
import subprocess
import sys
from typing import Literal, Optional, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
usable = False
if not sys.platform.startswith("win"):
usable = True
return usable
def dependency_linux_lp(func):
"""Indicate dependency on non Windows."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Throw a RuntimeError if not on a non-Windows system."""
if not is_usable():
raise RuntimeError(
"This printer driver depends on LP which is not "
"available on Windows systems."
)
return func(*args, **kwargs)
return wrapper
class LP(Escpos):
"""Simple UNIX lp command raw printing.
Thanks to `Oyami-Srk comment <https://github.com/python-escpos/python-escpos/pull/348#issuecomment-549558316>`_.
inheritance:
.. inheritance-diagram:: escpos.printer.LP
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
@dependency_linux_lp
def __init__(self, printer_name: str = "", *args, **kwargs):
"""LP class constructor.
:param printer_name: CUPS printer name (Optional)
:param auto_flush: Automatic flush after every _raw() (Optional)
:type auto_flush: bool (Defaults False)
"""
Escpos.__init__(self, *args, **kwargs)
self.printer_name = printer_name
self.auto_flush = kwargs.get("auto_flush", False)
self._flushed = False
self._device: Union[Literal[False], Literal[None], subprocess.Popen] = False
@property
def printers(self) -> dict:
"""Available CUPS printers."""
p_names = subprocess.run(
["lpstat", "-e"], # Get printer names
capture_output=True,
text=True,
)
p_devs = subprocess.run(
["lpstat", "-v"], # Get attached devices
capture_output=True,
text=True,
)
# List and trim output lines
names = [name for name in p_names.stdout.split("\n") if name]
devs = [dev for dev in p_devs.stdout.split("\n") if dev]
# return a dict of {printer name: attached device} pairs
return {name: dev.split()[-1] for name in names for dev in devs if name in dev}
def _get_system_default_printer(self) -> str:
"""Return the system's default printer name."""
p_name = subprocess.run(
["lpstat", "-d"],
capture_output=True,
text=True,
)
name = p_name.stdout.split()[-1]
if name not in self.printers:
return ""
return name
def open(
self,
job_name: str = "python-escpos",
raise_not_found: bool = True,
_close_opened: bool = True,
) -> None:
"""Invoke _lp_ in a new subprocess and wait for commands.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device and _close_opened:
self.close()
self._is_closing = False
self.job_name = job_name
try:
# Name validation, set default if no given name
self.printer_name = self.printer_name or self._get_system_default_printer()
assert self.printer_name in self.printers, "Incorrect printer name"
# Open device
self.device: Optional[subprocess.Popen] = subprocess.Popen(
["lp", "-d", self.printer_name, "-t", self.job_name, "-o", "raw"],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (AssertionError, subprocess.SubprocessError) as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Unable to start a print job for the printer {self.printer_name}:"
+ f"\n{e}"
)
else:
logging.error("LP printing %s not available", self.printer_name)
return
logging.info("LP printer enabled")
def close(self) -> None:
"""Stop the subprocess."""
if not self._device:
return
logging.info("Closing LP connection to printer %s", self.printer_name)
self._is_closing = True
if not self.auto_flush:
self.flush()
self._device.terminate()
self._device = False
def flush(self) -> None:
"""End line and wait for new commands."""
if not self.device or not self.device.stdin:
return
if self._flushed:
return
if self.device.stdin.writable():
self.device.stdin.write(b"\n")
if self.device.stdin.closed is False:
self.device.stdin.close()
self.device.wait()
self._flushed = True
if not self._is_closing:
self.open(_close_opened=False)
def _raw(self, msg: bytes) -> None:
"""Write raw command(s) to the printer.
:param msg: arbitrary code to be printed
"""
assert self.device is not None
assert self.device.stdin is not None
if self.device.stdin.writable():
self.device.stdin.write(msg)
else:
raise subprocess.SubprocessError("Not a valid pipe for lp process")
self._flushed = False
if self.auto_flush:
self.flush()

View File

@@ -0,0 +1,136 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the Network printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import logging
import socket
from typing import Literal, Optional, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
return True
class Network(Escpos):
"""Network printer.
This class is used to attach to a networked printer.
You can also use this in order to attach to a printer that
is forwarded with ``socat``.
If you have a local printer on parallel port ``/dev/usb/lp0``
then you could start ``socat`` with:
.. code-block:: none
socat -u TCP4-LISTEN:4242,reuseaddr,fork OPEN:/dev/usb/lp0
Then you should be able to attach to port ``4242`` with this class.
Otherwise the normal use case would be to have a printer with
Ethernet interface.
This type of printer should work the same with this class.
For the address of the printer check its manuals.
inheritance:
.. inheritance-diagram:: escpos.printer.Network
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
def __init__(
self,
host: str = "",
port: int = 9100,
timeout: Union[int, float] = 60,
*args,
**kwargs,
):
"""Initialize network printer.
:param host: Printer's host name or IP address
:param port: Port to write to
:param timeout: timeout in seconds for the socket-library
"""
Escpos.__init__(self, *args, **kwargs)
self.host = host
self.port = port
self.timeout = timeout
self._device: Union[Literal[False], Literal[None], socket.socket] = False
def open(self, raise_not_found: bool = True) -> None:
"""Open TCP socket with ``socket``-library and set it as escpos device.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device:
self.close()
try:
# Open device
self.device: Optional[socket.socket] = socket.socket(
socket.AF_INET, socket.SOCK_STREAM
)
self.device.settimeout(self.timeout)
self.device.connect((self.host, self.port))
except OSError as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Could not open socket for {self.host}:\n{e}"
)
else:
logging.error("Network device %s not found", self.host)
return
logging.info("Network printer enabled")
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
assert self.device
self.device.sendall(msg)
def _read(self) -> bytes:
"""Read data from the TCP socket."""
assert self.device
return self.device.recv(16)
def close(self) -> None:
"""Close TCP connection."""
if not self._device:
return
logging.info("Closing Network connection to printer %s", self.host)
try:
self._device.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
self._device.close()
self._device = False

View File

@@ -0,0 +1,179 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the Serial printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import functools
import logging
from typing import Literal, Optional, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
#: keeps track if the pyserial dependency could be loaded (:py:class:`escpos.printer.Serial`)
_DEP_PYSERIAL = False
try:
import serial
_DEP_PYSERIAL = True
except ImportError:
pass
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
usable = False
if _DEP_PYSERIAL:
usable = True
return usable
def dependency_pyserial(func):
"""Indicate dependency on pyserial."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Throw a RuntimeError if pyserial not installed."""
if not is_usable():
raise RuntimeError(
"Printing with Serial requires the pyserial library to "
"be installed. Please refer to the documentation on "
"what to install and install the dependencies for pyserial."
)
return func(*args, **kwargs)
return wrapper
class Serial(Escpos):
"""Serial printer.
This class describes a printer that is connected by serial interface.
inheritance:
.. inheritance-diagram:: escpos.printer.Serial
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
@dependency_pyserial
def __init__(
self,
devfile: str = "",
baudrate: int = 9600,
bytesize: int = 8,
timeout: Union[int, float] = 1,
parity: Optional[str] = None,
stopbits: Optional[int] = None,
xonxoff: bool = False,
dsrdtr: bool = True,
*args,
**kwargs,
):
"""Initialize serial printer.
:param devfile: Device file under dev file system
:param baudrate: Baud rate for serial transmission
:param bytesize: Serial buffer size
:param timeout: Read/Write timeout
:param parity: Parity checking
:param stopbits: Number of stop bits
:param xonxoff: Software flow control
:param dsrdtr: Hardware flow control (False to enable RTS/CTS)
"""
Escpos.__init__(self, *args, **kwargs)
self.devfile = devfile
self.baudrate = baudrate
self.bytesize = bytesize
self.timeout = timeout
if parity:
self.parity = parity
else:
self.parity = serial.PARITY_NONE
if stopbits:
self.stopbits = stopbits
else:
self.stopbits = serial.STOPBITS_ONE
self.xonxoff = xonxoff
self.dsrdtr = dsrdtr
self._device: Union[Literal[False], Literal[None], serial.Serial] = False
@dependency_pyserial
def open(self, raise_not_found: bool = True) -> None:
"""Set up serial port and set is as escpos device.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device:
if self.device and self.device.is_open:
self.close()
try:
# Open device
self.device: Optional[serial.Serial] = serial.Serial(
port=self.devfile,
baudrate=self.baudrate,
bytesize=self.bytesize,
parity=self.parity,
stopbits=self.stopbits,
timeout=self.timeout,
xonxoff=self.xonxoff,
dsrdtr=self.dsrdtr,
)
except (ValueError, serial.SerialException) as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Unable to open serial printer on {self.devfile}:\n{e}"
)
else:
logging.error("Serial device %s not found", self.devfile)
return
logging.info("Serial printer enabled")
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
assert self.device
self.device.write(msg)
def _read(self) -> bytes:
"""Read the data buffer and return it to the caller."""
assert self.device
return self.device.read(16)
def close(self) -> None:
"""Close Serial interface."""
if not self._device:
return
logging.info("Closing Serial connection to printer %s", self.devfile)
if self._device and self._device.is_open:
self._device.flush()
self._device.close()
self._device = False

206
src/escpos/printer/usb.py Normal file
View File

@@ -0,0 +1,206 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the USB printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import functools
import logging
from typing import Dict, Literal, Optional, Type, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError, USBNotFoundError
#: keeps track if the usb dependency could be loaded (:py:class:`escpos.printer.Usb`)
_DEP_USB = False
try:
import usb.core
import usb.util
_DEP_USB = True
except ImportError:
pass
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
usable = False
if _DEP_USB:
usable = True
return usable
def dependency_usb(func):
"""Indicate dependency on usb."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Throw a RuntimeError if usb not installed."""
if not is_usable():
raise RuntimeError(
"Printing with USB connection requires a usb library to "
"be installed. Please refer to the documentation on "
"what to install and install the dependencies for USB."
)
return func(*args, **kwargs)
return wrapper
class Usb(Escpos):
"""USB printer.
This class describes a printer that natively speaks USB.
inheritance:
.. inheritance-diagram:: escpos.printer.Usb
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
def __init__(
self,
idVendor: Optional[int] = None,
idProduct: Optional[int] = None,
usb_args: Dict[str, Union[str, int]] = {},
timeout: Union[int, float] = 0,
in_ep: int = 0x82,
out_ep: int = 0x01,
*args,
**kwargs,
):
"""Initialize USB printer.
:param idVendor: Vendor ID
:param idProduct: Product ID
:param usb_args: Optional USB arguments (e.g. custom_match)
:param timeout: Is the time limit of the USB operation. Default without timeout.
:param in_ep: Input end point
:param out_ep: Output end point
"""
Escpos.__init__(self, *args, **kwargs)
self.timeout = timeout
self.in_ep = in_ep
self.out_ep = out_ep
self.usb_args = usb_args or {}
if idVendor:
self.usb_args["idVendor"] = idVendor
if idProduct:
self.usb_args["idProduct"] = idProduct
self._device: Union[
Literal[False], Literal[None], Type[usb.core.Device]
] = False
@dependency_usb
def open(self, raise_not_found: bool = True) -> None:
"""Search device on USB tree and set it as escpos device.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
:raises: :py:exc:`~escpos.exceptions.USBNotFoundError`
"""
if self._device:
self.close()
# Open device
try:
self.device: Optional[Type[usb.core.Device]] = usb.core.find(
**self.usb_args
)
assert self.device, USBNotFoundError(
f"Device {tuple(self.usb_args.values())} not found"
+ " or cable not plugged in."
)
self._check_driver()
self._configure_usb()
except (AssertionError, usb.core.USBError) as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Unable to open USB printer on {tuple(self.usb_args.values())}:"
+ f"\n{e}"
)
else:
logging.error("USB device %s not found", tuple(self.usb_args.values()))
return
logging.info("USB printer enabled")
def _check_driver(self) -> None:
"""Check the driver.
pyusb has three backends: libusb0, libusb1 and openusb but
only libusb1 backend implements the methods is_kernel_driver_active()
and detach_kernel_driver().
This helps enable this library to work on Windows.
"""
if self.device and self.device.backend.__module__.endswith("libusb1"):
check_driver: Optional[bool] = None
try:
check_driver = self.device.is_kernel_driver_active(0)
except NotImplementedError:
pass
if check_driver is None or check_driver:
try:
self.device.detach_kernel_driver(0)
except NotImplementedError:
pass
except usb.core.USBError as e:
if check_driver is not None:
logging.error("Could not detatch kernel driver: %s", str(e))
def _configure_usb(self) -> None:
"""Configure USB."""
if not self.device:
return
try:
self.device.set_configuration()
self.device.reset()
except usb.core.USBError as e:
logging.error("Could not set configuration: %s", str(e))
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
assert self.device
self.device.write(self.out_ep, msg, self.timeout)
def _read(self) -> bytes:
"""Read a data buffer and return it to the caller."""
assert self.device
return self.device.read(self.in_ep, 16)
@dependency_usb
def close(self) -> None:
"""Release USB interface."""
if not self._device:
return
logging.info(
"Closing Usb connection to printer %s", tuple(self.usb_args.values())
)
usb.util.dispose_resources(self._device)
self._device = False

View File

@@ -0,0 +1,164 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""This module contains the implementation of the Win32Raw printer driver.
:author: python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2023 Bashlinux and python-escpos
:license: MIT
"""
import functools
import logging
from typing import Any, Literal, Optional, Union
from ..escpos import Escpos
from ..exceptions import DeviceNotFoundError
#: keeps track if the win32print dependency could be loaded (:py:class:`escpos.printer.Win32Raw`)
_DEP_WIN32PRINT = False
try:
import pywintypes
import win32print
_DEP_WIN32PRINT = True
PyPrinterHANDLE: Any = win32print.OpenPrinter
except ImportError:
pass
def is_usable() -> bool:
"""Indicate whether this component can be used due to dependencies."""
usable = False
if _DEP_WIN32PRINT:
usable = True
return usable
def dependency_win32print(func):
"""Indicate dependency on win32print."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Throw a RuntimeError if win32print not installed."""
if not is_usable():
raise RuntimeError(
"Printing with Win32Raw requires a win32print library to "
"be installed. Please refer to the documentation on "
"what to install and install the dependencies for win32print."
)
return func(*args, **kwargs)
return wrapper
class Win32Raw(Escpos):
"""Printer binding for win32 API.
Uses the module pywin32 for printing.
inheritance:
.. inheritance-diagram:: escpos.printer.Win32Raw
:parts: 1
"""
@staticmethod
def is_usable() -> bool:
"""Indicate whether this printer class is usable.
Will return True if dependencies are available.
Will return False if not.
"""
return is_usable()
@dependency_win32print
def __init__(self, printer_name: str = "", *args, **kwargs) -> None:
"""Initialize default printer."""
Escpos.__init__(self, *args, **kwargs)
self.printer_name = printer_name
self.job_name = ""
self._device: Union[
Literal[False],
Literal[None],
"PyPrinterHANDLE",
] = False
@property
def printers(self) -> dict:
"""Available Windows printers."""
return {
printer["pPrinterName"]: printer
for printer in win32print.EnumPrinters(win32print.PRINTER_ENUM_NAME, "", 4)
}
def open(
self, job_name: str = "python-escpos", raise_not_found: bool = True
) -> None:
"""Open connection to default printer.
By default raise an exception if device is not found.
:param raise_not_found: Default True.
False to log error but do not raise exception.
:raises: :py:exc:`~escpos.exceptions.DeviceNotFoundError`
"""
if self._device:
self.close()
self.job_name = job_name
try:
# Name validation, set default if no given name
self.printer_name = self.printer_name or win32print.GetDefaultPrinter()
assert self.printer_name in self.printers, "Incorrect printer name"
# Open device
self.device: Optional["PyPrinterHANDLE"] = win32print.OpenPrinter(
self.printer_name
)
if self.device:
self.current_job = win32print.StartDocPrinter(
self.device, 1, (job_name, "", "RAW")
)
win32print.StartPagePrinter(self.device)
except (AssertionError, pywintypes.error) as e:
# Raise exception or log error and cancel
self.device = None
if raise_not_found:
raise DeviceNotFoundError(
f"Unable to start a print job for the printer {self.printer_name}:"
+ f"\n{e}"
)
else:
logging.error("Win32Raw printing %s not available", self.printer_name)
return
logging.info("Win32Raw printer enabled")
def close(self) -> None:
"""Close connection to default printer."""
if self._device is False or self._device is None: # Literal False | None
return
logging.info("Closing Win32Raw connection to printer %s", self.printer_name)
win32print.EndPagePrinter(self._device)
win32print.EndDocPrinter(self._device)
win32print.ClosePrinter(self._device)
self._device = False
def _raw(self, msg: bytes) -> None:
"""Print any command sent in raw format.
:param msg: arbitrary code to be printed
"""
if self.printer_name is None:
raise DeviceNotFoundError("Printer not found")
if not self.device:
raise DeviceNotFoundError("Printer job not opened")
win32print.WritePrinter(self.device, msg) # type: ignore
# there is a bug in the typeshed
# https://github.com/mhammond/pywin32/blob/main/win32/src/win32print/win32print.cpp#L976
# https://github.com/python/typeshed/blob/main/stubs/pywin32/win32/win32print.pyi#L27C4-L27C4

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
"""Custom types."""
from typing import Dict, TypedDict
class ConstTxtStyleClass(TypedDict):
"""Describe type of :py:data:`escpos.constants.TXT_STYLES`."""
bold: Dict[bool, bytes]
underline: Dict[int, bytes]
size: Dict[str, bytes]
font: Dict[str, bytes]
align: Dict[str, bytes]
invert: Dict[bool, bytes]
color: Dict[str, bytes]
flip: Dict[bool, bytes]
density: Dict[int, bytes]
smooth: Dict[bool, bytes]
height: Dict[int, int]
width: Dict[int, int]

View File

@@ -1,7 +1,49 @@
import pytest import pytest
from escpos.printer import Dummy
from escpos.exceptions import DeviceNotFoundError
from escpos.printer import LP, CupsPrinter, Dummy, File, Network, Serial, Usb, Win32Raw
@pytest.fixture @pytest.fixture
def driver(): def driver() -> Dummy:
return Dummy() return Dummy()
@pytest.fixture
def usbprinter() -> Usb:
return Usb()
@pytest.fixture
def serialprinter() -> Serial:
return Serial()
@pytest.fixture
def networkprinter() -> Network:
return Network()
@pytest.fixture
def fileprinter() -> File:
return File()
@pytest.fixture
def lpprinter() -> LP:
return LP()
@pytest.fixture
def win32rawprinter():
return Win32Raw()
@pytest.fixture
def cupsprinter():
return CupsPrinter()
@pytest.fixture
def devicenotfounderror():
return DeviceNotFoundError

View File

@@ -1,26 +1,27 @@
#!/usr/bin/python #!/usr/bin/python
"""verifies that the metaclass abc is properly used by ESC/POS """verifies that the metaclass abc is properly used by ESC/POS
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 Patrick Kanzler :copyright: Copyright (c) 2016 Patrick Kanzler
:license: MIT :license: MIT
""" """
from nose.tools import raises
import escpos.escpos as escpos
from abc import ABCMeta from abc import ABCMeta
import pytest
@raises(TypeError) import escpos.escpos as escpos
def test_abstract_base_class_raises():
def test_abstract_base_class_raises() -> None:
"""test whether the abstract base class raises an exception for ESC/POS""" """test whether the abstract base class raises an exception for ESC/POS"""
escpos.Escpos() # This call should raise TypeError because of abstractmethod _raw() with pytest.raises(TypeError):
# This call should raise TypeError because of abstractmethod _raw()
escpos.Escpos() # type: ignore [abstract]
def test_abstract_base_class(): def test_abstract_base_class() -> None:
"""test whether Escpos has the metaclass ABCMeta""" """test whether Escpos has the metaclass ABCMeta"""
assert issubclass(escpos.Escpos, object) assert issubclass(escpos.Escpos, object)
assert type(escpos.Escpos) is ABCMeta assert type(escpos.Escpos) is ABCMeta

View File

@@ -4,48 +4,46 @@
import os import os
import sys import shutil
from scripttest import TestFileEnvironment import tempfile
from nose.tools import assert_equal, nottest
from scripttest import TestFileEnvironment as TFE
import escpos import escpos
TEST_DIR = os.path.abspath("test/test-cli-output") TEST_DIR = tempfile.mkdtemp() + "/cli-test"
DEVFILE_NAME = "testfile" DEVFILE_NAME = "testfile"
DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME) DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME)
CONFIGFILE = "testconfig.yaml" CONFIGFILE = "testconfig.yaml"
CONFIG_YAML = """ CONFIG_YAML = f"""
--- ---
printer: printer:
type: file type: file
devfile: {testfile} devfile: {DEVFILE}
""".format( """
testfile=DEVFILE,
)
class TestCLI: class TestCLI:
"""Contains setups, teardowns, and tests for CLI""" """Contains setups, teardowns, and tests for CLI"""
@classmethod @classmethod
def setup_class(cls): def setup_class(cls) -> None:
"""Create a config file to read from""" """Create a config file to read from"""
with open(CONFIGFILE, "w") as config: with open(CONFIGFILE, "w") as config:
config.write(CONFIG_YAML) config.write(CONFIG_YAML)
@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls) -> None:
"""Remove config file""" """Remove config file"""
os.remove(CONFIGFILE) os.remove(CONFIGFILE)
shutil.rmtree(TEST_DIR)
def setup(self): def setup_method(self) -> None:
"""Create a file to print to and set up env""" """Create a file to print to and set up env"""
self.env = None self.env = TFE(
self.default_args = None
self.env = TestFileEnvironment(
base_path=TEST_DIR, base_path=TEST_DIR,
cwd=os.getcwd(), cwd=os.getcwd(),
) )
@@ -62,25 +60,32 @@ class TestCLI:
finally: finally:
fhandle.close() fhandle.close()
def teardown(self): def teardown_method(self) -> None:
"""Destroy printer file and env""" """Destroy printer file and env"""
os.remove(DEVFILE) os.remove(DEVFILE)
self.env.clear() self.env.clear()
def test_cli_help(self): def test_cli_help(self) -> None:
"""Test getting help from cli""" """Test getting help from cli"""
result = self.env.run("python-escpos", "-h") result = self.env.run("python-escpos", "-h")
assert not result.stderr assert not result.stderr
assert "usage" in result.stdout assert "usage" in result.stdout
def test_cli_version(self): def test_cli_version(self) -> None:
"""Test the version string""" """Test the version string"""
result = self.env.run("python-escpos", "version") result = self.env.run("python-escpos", "version")
assert not result.stderr assert not result.stderr
assert_equal(escpos.__version__, result.stdout.strip()) assert escpos.__version__ == result.stdout.strip()
@nottest # disable this test as it is not that easy anymore to predict the outcome of this call def test_cli_version_extended(self) -> None:
def test_cli_text(self): """Test the extended version information"""
result = self.env.run("python-escpos", "version_extended")
assert not result.stderr
assert escpos.__version__ in result.stdout
# test that additional information on e.g. Serial is printed
assert "Serial" in result.stdout
def test_cli_text(self) -> None:
"""Make sure text returns what we sent it""" """Make sure text returns what we sent it"""
test_text = "this is some text" test_text = "this is some text"
result = self.env.run( result = self.env.run(
@@ -95,15 +100,17 @@ class TestCLI:
) )
assert not result.stderr assert not result.stderr
assert DEVFILE_NAME in result.files_updated.keys() assert DEVFILE_NAME in result.files_updated.keys()
assert_equals(result.files_updated[DEVFILE_NAME].bytes, test_text + "\n") assert (
result.files_updated[DEVFILE_NAME].bytes == "\x1bt\x00" + test_text + "\n"
)
def test_cli_text_inavlid_args(self): def test_cli_text_invalid_args(self) -> None:
"""Test a failure to send valid arguments""" """Test a failure to send valid arguments"""
result = self.env.run( result = self.env.run(
*(self.default_args + ("text", "--invalid-param", "some data")), *(self.default_args + ("text", "--invalid-param", "some data")),
expect_error=True, expect_error=True,
expect_stderr=True expect_stderr=True,
) )
assert_equal(result.returncode, 2) assert result.returncode == 2
assert "error:" in result.stderr assert "error:" in result.stderr
assert not result.files_updated assert not result.files_updated

126
test/test_config.py Normal file
View File

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

View File

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

View File

@@ -1,25 +0,0 @@
#!/usr/bin/python
import escpos.printer as printer
import barcode.errors
import pytest
@pytest.fixture
def instance():
return printer.Dummy()
def test_soft_barcode_ean8_invalid(instance):
"""test with an invalid barcode"""
with pytest.raises(barcode.errors.BarcodeError):
instance.soft_barcode("ean8", "1234")
def test_soft_barcode_ean8(instance):
"""test with a valid ean8 barcode"""
instance.soft_barcode("ean8", "1234567")
def test_soft_barcode_ean8_nocenter(instance):
instance.soft_barcode("ean8", "1234567", center=False)

View File

@@ -1,10 +1,11 @@
#!/usr/bin/python #!/usr/bin/python
import escpos.printer as printer
from escpos.capabilities import Profile, BARCODE_B
from escpos.exceptions import BarcodeTypeError, BarcodeCodeError
import pytest import pytest
import escpos.printer as printer
from escpos.capabilities import BARCODE_B, Profile
from escpos.exceptions import BarcodeCodeError, BarcodeTypeError
@pytest.mark.parametrize( @pytest.mark.parametrize(
"bctype,data,expected", "bctype,data,expected",

View File

@@ -0,0 +1,55 @@
import pytest
from escpos import printer
from escpos.constants import BUZZER
def test_buzzer_function_with_default_params() -> None:
instance = printer.Dummy()
instance.buzzer()
expected = BUZZER + bytes((2, 4))
assert instance.output == expected
@pytest.mark.parametrize(
"times, duration",
[
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
],
)
def test_buzzer_function(times: int, duration: int) -> None:
instance = printer.Dummy()
instance.buzzer(times, duration)
expected = BUZZER + bytes((times, duration))
assert instance.output == expected
@pytest.mark.parametrize(
"times, duration, expected_message",
[
[0, 0, "times must be between 1 and 9"],
[-1, 0, "times must be between 1 and 9"],
[10, 0, "times must be between 1 and 9"],
[11, 0, "times must be between 1 and 9"],
[3, 0, "duration must be between 1 and 9"],
[3, -1, "duration must be between 1 and 9"],
[3, 10, "duration must be between 1 and 9"],
[3, 11, "duration must be between 1 and 9"],
],
)
def test_buzzer_fuction_with_outrange_values(
times: int, duration: int, expected_message: str
) -> None:
instance = printer.Dummy()
with pytest.raises(ValueError) as e:
instance.buzzer(times, duration)
assert str(e.value) == expected_message

View File

@@ -1,11 +1,12 @@
#!/usr/bin/python #!/usr/bin/python
import pytest
import escpos.printer as printer import escpos.printer as printer
from escpos.exceptions import CashDrawerError from escpos.exceptions import CashDrawerError
import pytest
def test_raise_CashDrawerError(): def test_raise_CashDrawerError() -> None:
"""should raise an error if the sequence is invalid.""" """should raise an error if the sequence is invalid."""
instance = printer.Dummy() instance = printer.Dummy()
with pytest.raises(CashDrawerError): with pytest.raises(CashDrawerError):

View File

@@ -2,9 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import escpos.printer as printer
import pytest import pytest
import escpos.escpos
@pytest.mark.parametrize( @pytest.mark.parametrize(
"bctype,data", "bctype,data",
@@ -47,7 +48,7 @@ import pytest
], ],
) )
def test_check_valid_barcode(bctype, data): def test_check_valid_barcode(bctype, data):
assert printer.Escpos.check_barcode(bctype, data) assert escpos.escpos.Escpos.check_barcode(bctype, data)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -103,4 +104,4 @@ def test_check_valid_barcode(bctype, data):
], ],
) )
def test_check_invalid_barcode(bctype, data): def test_check_invalid_barcode(bctype, data):
assert not printer.Escpos.check_barcode(bctype, data) assert not escpos.escpos.Escpos.check_barcode(bctype, data)

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
"""tests for line display """tests for line display
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2017 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2017 `python-escpos <https://github.com/python-escpos>`_
:license: MIT :license: MIT
@@ -11,21 +11,21 @@
import escpos.printer as printer import escpos.printer as printer
def test_function_linedisplay_select_on(): def test_function_linedisplay_select_on() -> None:
"""test the linedisplay_select function (activate)""" """test the linedisplay_select function (activate)"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_select(select_display=True) instance.linedisplay_select(select_display=True)
assert instance.output == b"\x1B\x3D\x02" assert instance.output == b"\x1B\x3D\x02"
def test_function_linedisplay_select_off(): def test_function_linedisplay_select_off() -> None:
"""test the linedisplay_select function (deactivate)""" """test the linedisplay_select function (deactivate)"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_select(select_display=False) instance.linedisplay_select(select_display=False)
assert instance.output == b"\x1B\x3D\x01" assert instance.output == b"\x1B\x3D\x01"
def test_function_linedisplay_clear(): def test_function_linedisplay_clear() -> None:
"""test the linedisplay_clear function""" """test the linedisplay_clear function"""
instance = printer.Dummy() instance = printer.Dummy()
instance.linedisplay_clear() instance.linedisplay_clear()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
"""tests for panel button function """tests for panel button function
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT :license: MIT
@@ -11,14 +11,14 @@
import escpos.printer as printer import escpos.printer as printer
def test_function_panel_button_on(): def test_function_panel_button_on() -> None:
"""test the panel button function (enabling) by comparing output""" """test the panel button function (enabling) by comparing output"""
instance = printer.Dummy() instance = printer.Dummy()
instance.panel_buttons() instance.panel_buttons()
assert instance.output == b"\x1B\x63\x35\x00" assert instance.output == b"\x1B\x63\x35\x00"
def test_function_panel_button_off(): def test_function_panel_button_off() -> None:
"""test the panel button function (disabling) by comparing output""" """test the panel button function (disabling) by comparing output"""
instance = printer.Dummy() instance = printer.Dummy()
instance.panel_buttons(False) instance.panel_buttons(False)

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import six from typing import Optional
import pytest
import escpos.printer as printer import escpos.printer as printer
from escpos.constants import TXT_NORMAL, TXT_STYLE, SET_FONT from escpos.constants import SET_FONT, TXT_NORMAL, TXT_SIZE, TXT_STYLE
from escpos.constants import TXT_SIZE from escpos.exceptions import SetVariableError
# Default test, please copy and paste this block to test set method calls def test_default_values_with_default() -> None:
"""Default test, please copy and paste this block to test set method calls"""
def test_default_values():
instance = printer.Dummy() instance = printer.Dummy()
instance.set() instance.set_with_default()
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -27,12 +27,20 @@ def test_default_values():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_default_values() -> None:
"""Default test"""
instance = printer.Dummy()
instance.set()
assert instance.output == b""
# Size tests # Size tests
def test_set_size_2h(): def test_set_size_2h() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_height=True) instance.set_with_default(double_height=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -49,9 +57,21 @@ def test_set_size_2h():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2w(): def test_set_size_2h_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_width=True) instance.set(double_height=True)
expected_sequence = (
TXT_NORMAL,
TXT_STYLE["size"]["2h"], # Double height text size
)
assert instance.output == b"".join(expected_sequence)
def test_set_size_2w() -> None:
instance = printer.Dummy()
instance.set_with_default(double_width=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -68,9 +88,21 @@ def test_set_size_2w():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_2x(): def test_set_size_2w_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(double_height=True, double_width=True) instance.set(double_width=True)
expected_sequence = (
TXT_NORMAL,
TXT_STYLE["size"]["2w"], # Double width text size
)
assert instance.output == b"".join(expected_sequence)
def test_set_size_2x() -> None:
instance = printer.Dummy()
instance.set_with_default(double_height=True, double_width=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -87,13 +119,25 @@ def test_set_size_2x():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_size_custom(): def test_set_size_2x_no_default() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(custom_size=True, width=8, height=7) instance.set(double_width=True, double_height=True)
expected_sequence = (
TXT_NORMAL,
TXT_STYLE["size"]["2x"], # Quad area text size
)
assert instance.output == b"".join(expected_sequence)
def test_set_size_custom() -> None:
instance = printer.Dummy()
instance.set_with_default(custom_size=True, width=8, height=7)
expected_sequence = ( expected_sequence = (
TXT_SIZE, # Custom text size, no normal reset TXT_SIZE, # Custom text size, no normal reset
six.int2byte(TXT_STYLE["width"][8] + TXT_STYLE["height"][7]), bytes((TXT_STYLE["width"][8] + TXT_STYLE["height"][7],)),
TXT_STYLE["flip"][False], # Flip OFF TXT_STYLE["flip"][False], # Flip OFF
TXT_STYLE["smooth"][False], # Smooth OFF TXT_STYLE["smooth"][False], # Smooth OFF
TXT_STYLE["bold"][False], # Bold OFF TXT_STYLE["bold"][False], # Bold OFF
@@ -106,12 +150,36 @@ def test_set_size_custom():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
@pytest.mark.parametrize("width", [1, 8])
@pytest.mark.parametrize("height", [1, 8])
def test_set_size_custom_no_default(width: int, height: int) -> None:
instance = printer.Dummy()
instance.set(custom_size=True, width=width, height=height)
expected_sequence = (
TXT_SIZE, # Custom text size, no normal reset
bytes((TXT_STYLE["width"][width] + TXT_STYLE["height"][height],)),
)
assert instance.output == b"".join(expected_sequence)
@pytest.mark.parametrize("width", [None, 0, 9, 10, 4444])
@pytest.mark.parametrize("height", [None, 0, 9, 10, 4444])
def test_set_size_custom_invalid_input(
width: Optional[int], height: Optional[int]
) -> None:
instance = printer.Dummy()
with pytest.raises(SetVariableError):
instance.set(custom_size=True, width=width, height=height)
# Flip # Flip
def test_set_flip(): def test_set_flip() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(flip=True) instance.set_with_default(flip=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -128,12 +196,21 @@ def test_set_flip():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_flip_no_default() -> None:
instance = printer.Dummy()
instance.set(flip=True)
expected_sequence = (TXT_STYLE["flip"][True],) # Flip ON
assert instance.output == b"".join(expected_sequence)
# Smooth # Smooth
def test_smooth(): def test_smooth() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(smooth=True) instance.set_with_default(smooth=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -153,9 +230,9 @@ def test_smooth():
# Type # Type
def test_set_bold(): def test_set_bold() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(bold=True) instance.set_with_default(bold=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -172,9 +249,9 @@ def test_set_bold():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_underline(): def test_set_underline() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(underline=1) instance.set_with_default(underline=1)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -191,9 +268,9 @@ def test_set_underline():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_set_underline2(): def test_set_underline2() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(underline=2) instance.set_with_default(underline=2)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -213,9 +290,9 @@ def test_set_underline2():
# Align # Align
def test_align_center(): def test_align_center() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(align="center") instance.set_with_default(align="center")
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -232,9 +309,9 @@ def test_align_center():
assert instance.output == b"".join(expected_sequence) assert instance.output == b"".join(expected_sequence)
def test_align_right(): def test_align_right() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(align="right") instance.set_with_default(align="right")
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -254,10 +331,10 @@ def test_align_right():
# Densities # Densities
def test_densities(): def test_densities() -> None:
for density in range(8): for density in range(8):
instance = printer.Dummy() instance = printer.Dummy()
instance.set(density=density) instance.set_with_default(density=density)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,
@@ -278,9 +355,9 @@ def test_densities():
# Invert # Invert
def test_invert(): def test_invert() -> None:
instance = printer.Dummy() instance = printer.Dummy()
instance.set(invert=True) instance.set_with_default(invert=True)
expected_sequence = ( expected_sequence = (
TXT_NORMAL, TXT_NORMAL,

View File

@@ -0,0 +1,26 @@
#!/usr/bin/python
import barcode.errors
import pytest
import escpos.printer as printer
@pytest.fixture
def instance():
return printer.Dummy()
def test_soft_barcode_ean8_invalid(instance: printer.Dummy) -> None:
"""test with an invalid barcode"""
with pytest.raises(barcode.errors.BarcodeError):
instance.barcode("1234", "ean8", force_software=True)
def test_soft_barcode_ean8(instance: printer.Dummy) -> None:
"""test with a valid ean8 barcode"""
instance.barcode("1234567", "ean8", force_software=True)
def test_soft_barcode_ean8_nocenter(instance: printer.Dummy) -> None:
instance.barcode("1234567", "ean8", align_ct=False, force_software=True)

View File

@@ -0,0 +1,80 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for software_columns
:author: Benito López and the python-escpos developers
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2024 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import pytest
def test_rearrange_into_cols(driver) -> None:
"""
GIVEN a list of columnable text
WHEN the column width is different for each column and some strings exceed the max width
THEN check the strings are properly wrapped, truncated and rearranged into some columns
"""
output = driver._rearrange_into_cols(
text_list=["fits", "row1 row2", "truncate and wrap"], widths=[4, 5, 6]
)
assert output == [["fits", "row1", "trunc."], ["", "row2", "and"], ["", "", "wrap"]]
def test_add_padding_into_cols(driver) -> None:
"""
GIVEN a list of strings
WHEN adding padding and different alignments to each string
THEN check the strings are correctly padded and aligned
"""
output = driver._add_padding_into_cols(
text_list=["col1", "col2", "col3", "col 4"],
widths=[6, 6, 6, 6],
align=["center", "left", "right", "justify"],
)
assert output == [" col1 ", "col2 ", " col3", "col 4"]
@pytest.mark.parametrize("text_list", ["", [], None])
@pytest.mark.parametrize("widths", [30.5, "30", None])
@pytest.mark.parametrize("align", ["invalid_align_name", "", None])
def test_software_columns_invalid_args(driver, text_list, widths, align) -> None:
"""
GIVEN a dummy printer object
WHEN non valid params are passed
THEN check raise exception
"""
bad_text_list = {"text_list": text_list, "widths": 5, "align": "left"}
bad_widths = {"text_list": ["valid"], "widths": widths, "align": "left"}
bad_align = {"text_list": ["valid"], "widths": 5, "align": align}
bad_args = [bad_text_list, bad_widths, bad_align]
for kwargs in bad_args:
with pytest.raises((TypeError, ValueError)):
driver.software_columns(**kwargs)
driver.close()
@pytest.mark.parametrize(
"text_list",
[
["col1", "col2", "col3"],
["wrap this string", "wrap this string", "wrap this string"],
["truncate_this_string", "truncate_this_string", "truncate_this_string"],
],
)
@pytest.mark.parametrize("widths", [[10, 10, 10], [10], 30])
@pytest.mark.parametrize("align", [["center", "left", "right"], ["center"], "justify"])
def test_software_columns_valid_args(driver, text_list, widths, align) -> None:
"""
GIVEN a dummy printer object
WHEN valid params are passed
THEN check no errors
"""
driver.software_columns(text_list=text_list, widths=widths, align=align)
driver.close()

View File

@@ -1,34 +1,34 @@
#!/usr/bin/python #!/usr/bin/python
"""tests for the text printing function """tests for the text printing function
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT :license: MIT
""" """
import pytest
import mock
from hypothesis import given, assume
import hypothesis.strategies as st import hypothesis.strategies as st
import mock
from hypothesis import given
from escpos.printer import Dummy from escpos.printer import Dummy
def get_printer(): def get_printer() -> Dummy:
return Dummy(magic_encode_args={"disabled": True, "encoding": "CP437"}) return Dummy(magic_encode_args={"disabled": True, "encoding": "CP437"})
@given(text=st.text()) @given(text=st.text())
def test_text(text): def test_text(text: str):
"""Test that text() calls the MagicEncode object.""" """Test that text() calls the MagicEncode object."""
instance = get_printer() instance = get_printer()
instance.magic.write = mock.Mock() with mock.patch.object(instance.magic, "write") as write:
instance.text(text) instance.text(text)
instance.magic.write.assert_called_with(text) write.assert_called_with(text)
def test_block_text(): def test_block_text() -> None:
printer = get_printer() printer = get_printer()
printer.block_text( printer.block_text(
"All the presidents men were eating falafel for breakfast.", font="a" "All the presidents men were eating falafel for breakfast.", font="a"
@@ -38,25 +38,25 @@ def test_block_text():
) )
def test_textln(): def test_textln() -> None:
printer = get_printer() printer = get_printer()
printer.textln("hello, world") printer.textln("hello, world")
assert printer.output == b"hello, world\n" assert printer.output == b"hello, world\n"
def test_textln_empty(): def test_textln_empty() -> None:
printer = get_printer() printer = get_printer()
printer.textln() printer.textln()
assert printer.output == b"\n" assert printer.output == b"\n"
def test_ln(): def test_ln() -> None:
printer = get_printer() printer = get_printer()
printer.ln() printer.ln()
assert printer.output == b"\n" assert printer.output == b"\n"
def test_multiple_ln(): def test_multiple_ln() -> None:
printer = get_printer() printer = get_printer()
printer.ln(3) printer.ln(3)
assert printer.output == b"\n\n\n" assert printer.output == b"\n\n\n"

View File

@@ -1,26 +1,27 @@
from nose.tools import assert_raises import pytest
from escpos.printer import Dummy from escpos.printer import Dummy
def test_line_spacing_code_gen(): def test_line_spacing_code_gen() -> None:
printer = Dummy() printer = Dummy()
printer.line_spacing(10) printer.line_spacing(10)
assert printer.output == b"\x1b3\n" assert printer.output == b"\x1b3\n"
def test_line_spacing_rest(): def test_line_spacing_rest() -> None:
printer = Dummy() printer = Dummy()
printer.line_spacing() printer.line_spacing()
assert printer.output == b"\x1b2" assert printer.output == b"\x1b2"
def test_line_spacing_error_handling(): def test_line_spacing_error_handling() -> None:
printer = Dummy() printer = Dummy()
with assert_raises(ValueError): with pytest.raises(ValueError):
printer.line_spacing(99, divisor=44) printer.line_spacing(99, divisor=44)
with assert_raises(ValueError): with pytest.raises(ValueError):
printer.line_spacing(divisor=80, spacing=86) printer.line_spacing(divisor=80, spacing=86)
with assert_raises(ValueError): with pytest.raises(ValueError):
printer.line_spacing(divisor=360, spacing=256) printer.line_spacing(divisor=360, spacing=256)
with assert_raises(ValueError): with pytest.raises(ValueError):
printer.line_spacing(divisor=180, spacing=256) printer.line_spacing(divisor=180, spacing=256)

View File

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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
"""very basic test cases that load the classes """very basic test cases that load the classes
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT :license: MIT
@@ -11,7 +11,7 @@
import escpos.printer as printer import escpos.printer as printer
def test_instantiation(): def test_instantiation() -> None:
"""test the instantiation of a escpos-printer class and basic printing""" """test the instantiation of a escpos-printer class and basic printing"""
instance = printer.Dummy() instance = printer.Dummy()
instance.text("This is a test\n") instance.text("This is a test\n")

View File

@@ -0,0 +1,29 @@
#!/usr/bin/python
"""basic test case that simulates an empty capability file
:author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import os
import tempfile
import pytest
def test_instantiation() -> None:
"""test the instantiation of a escpos-printer class"""
# inject an environment variable that points to an empty capabilities file
os.environ["ESCPOS_CAPABILITIES_FILE"] = tempfile.NamedTemporaryFile().name
import escpos.printer as printer
from escpos.exceptions import BarcodeTypeError
# remove again the variable (so that no other tests are affected)
os.environ.pop("ESCPOS_CAPABILITIES_FILE")
instance = printer.Dummy()
with pytest.raises(BarcodeTypeError):
instance.barcode("bc", "code")

View File

@@ -2,20 +2,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""tests for the magic encode module """tests for the magic encode module
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_ :author: `Patrick Kanzler <dev@pkanzler.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_ :organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_ :copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT :license: MIT
""" """
import types
import typing
import pytest
from nose.tools import raises, assert_raises
from hypothesis import given, example
import hypothesis.strategies as st import hypothesis.strategies as st
from escpos.magicencode import MagicEncode, Encoder import pytest
from hypothesis import example, given
from escpos import printer
from escpos.exceptions import Error
from escpos.katakana import encode_katakana from escpos.katakana import encode_katakana
from escpos.exceptions import CharCodeError, Error from escpos.magicencode import Encoder, MagicEncode
class TestEncoder: class TestEncoder:
@@ -23,17 +25,24 @@ class TestEncoder:
Tests the single encoders. Tests the single encoders.
""" """
def test_can_encode(self): def test_can_encode(self) -> None:
assert not Encoder({"CP437": 1}).can_encode("CP437", "") assert not Encoder({"CP437": 1}).can_encode("CP437", "")
assert Encoder({"CP437": 1}).can_encode("CP437", "á") assert Encoder({"CP437": 1}).can_encode("CP437", "á")
assert not Encoder({"foobar": 1}).can_encode("foobar", "a") assert not Encoder({"foobar": 1}).can_encode("foobar", "a")
def test_find_suitable_encoding(self): def test_find_suitable_encoding(self) -> None:
assert not Encoder({"CP437": 1}).find_suitable_encoding("") assert not Encoder({"CP437": 1}).find_suitable_encoding("")
assert Encoder({"CP858": 1}).find_suitable_encoding("") == "CP858" assert Encoder({"CP858": 1}).find_suitable_encoding("") == "CP858"
@raises(ValueError) def test_find_suitable_encoding_unnecessary_codepage_swap(self) -> None:
def test_get_encoding(self): enc = Encoder({"CP857": 1, "CP437": 2, "CP1252": 3, "CP852": 4, "CP858": 5})
# desired behavior would be that the encoder always stays in the lower
# available codepages if possible
for character in ("Á", "É", "Í", "Ó", "Ú"):
assert enc.find_suitable_encoding(character) == "CP857"
def test_get_encoding(self) -> None:
with pytest.raises(ValueError):
Encoder({}).get_encoding_name("latin1") Encoder({}).get_encoding_name("latin1")
@@ -47,7 +56,7 @@ class TestMagicEncode:
Test initialization. Test initialization.
""" """
def test_disabled_requires_encoding(self, driver): def test_disabled_requires_encoding(self, driver: printer.Dummy) -> None:
""" """
Test that disabled without encoder raises an error. Test that disabled without encoder raises an error.
@@ -57,33 +66,33 @@ class TestMagicEncode:
MagicEncode(driver, disabled=True) MagicEncode(driver, disabled=True)
class TestWriteWithEncoding: class TestWriteWithEncoding:
def test_init_from_none(self, driver): def test_init_from_none(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding=None) encode = MagicEncode(driver, encoding=None)
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\x1bt\x13\xd5 ist teuro." assert driver.output == b"\x1bt\x13\xd5 ist teuro."
def test_change_from_another(self, driver): def test_change_from_another(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP437") encode = MagicEncode(driver, encoding="CP437")
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\x1bt\x13\xd5 ist teuro." assert driver.output == b"\x1bt\x13\xd5 ist teuro."
def test_no_change(self, driver): def test_no_change(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP858") encode = MagicEncode(driver, encoding="CP858")
encode.write_with_encoding("CP858", "€ ist teuro.") encode.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\xd5 ist teuro." assert driver.output == b"\xd5 ist teuro."
class TestWrite: class TestWrite:
def test_write(self, driver): def test_write(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.write("€ ist teuro.") encode.write("€ ist teuro.")
assert driver.output == b"\x1bt\x0f\xa4 ist teuro." assert driver.output == b"\x1bt\x0f\xa4 ist teuro."
def test_write_disabled(self, driver): def test_write_disabled(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver, encoding="CP437", disabled=True) encode = MagicEncode(driver, encoding="CP437", disabled=True)
encode.write("€ ist teuro.") encode.write("€ ist teuro.")
assert driver.output == b"? ist teuro." assert driver.output == b"? ist teuro."
def test_write_no_codepage(self, driver): def test_write_no_codepage(self, driver: printer.Dummy) -> None:
encode = MagicEncode( encode = MagicEncode(
driver, driver,
defaultsymbol="_", defaultsymbol="_",
@@ -94,7 +103,7 @@ class TestMagicEncode:
assert driver.output == b"_ ist teuro." assert driver.output == b"_ ist teuro."
class TestForceEncoding: class TestForceEncoding:
def test(self, driver): def test(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver) encode = MagicEncode(driver)
encode.force_encoding("CP437") encode.force_encoding("CP437")
assert driver.output == b"\x1bt\x00" assert driver.output == b"\x1bt\x00"
@@ -103,6 +112,7 @@ class TestMagicEncode:
assert driver.output == b"\x1bt\x00? ist teuro." assert driver.output == b"\x1bt\x00? ist teuro."
jaconv: typing.Optional[types.ModuleType]
try: try:
import jaconv import jaconv
except ImportError: except ImportError:
@@ -115,9 +125,9 @@ class TestKatakana:
@example("カタカナ") @example("カタカナ")
@example("あいうえお") @example("あいうえお")
@example("ハンカクカタカナ") @example("ハンカクカタカナ")
def test_accept(self, text): def test_accept(self, text: str) -> None:
encode_katakana(text) encode_katakana(text)
def test_result(self): def test_result(self) -> None:
assert encode_katakana("カタカナ") == b"\xb6\xc0\xb6\xc5" assert encode_katakana("カタカナ") == b"\xb6\xc0\xb6\xc5"
assert encode_katakana("あいうえお") == b"\xb1\xb2\xb3\xb4\xb5" assert encode_katakana("あいうえお") == b"\xb1\xb2\xb3\xb4\xb5"

View File

@@ -1,72 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""tests for the File printer
:author: `Patrick Kanzler <patrick.kanzler@fablab.fau.de>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import six
import pytest
from hypothesis import given, settings
from hypothesis.strategies import text
import escpos.printer as printer
if six.PY3:
mock_open_call = "builtins.open"
else:
mock_open_call = "__builtin__.open"
@pytest.mark.skip("this test is broken and has to be fixed or discarded")
@given(path=text())
def test_load_file_printer(mocker, path):
"""test the loading of the file-printer"""
mock_escpos = mocker.patch("escpos.escpos.Escpos.__init__")
mock_open = mocker.patch(mock_open_call)
printer.File(devfile=path)
assert mock_escpos.called
mock_open.assert_called_with(path, "wb")
@pytest.mark.skip("this test is broken and has to be fixed or discarded")
@given(txt=text())
def test_auto_flush(mocker, txt):
"""test auto_flush in file-printer"""
mock_escpos = mocker.patch("escpos.escpos.Escpos.__init__")
mock_open = mocker.patch(mock_open_call)
mock_device = mocker.patch.object(printer.File, "device")
p = printer.File(auto_flush=False)
# inject the mocked device-object
p.device = mock_device
p._raw(txt)
assert not mock_device.flush.called
mock_device.reset_mock()
p = printer.File(auto_flush=True)
# inject the mocked device-object
p.device = mock_device
p._raw(txt)
assert mock_device.flush.called
@pytest.mark.skip("this test is broken and has to be fixed or discarded")
@given(txt=text())
def test_flush_on_close(mocker, txt):
"""test flush on close in file-printer"""
mock_open = mocker.patch(mock_open_call)
mock_device = mocker.patch.object(printer.File, "device")
p = printer.File(auto_flush=False)
# inject the mocked device-object
p.device = mock_device
p._raw(txt)
assert not mock_device.flush.called
p.close()
assert mock_device.flush.called
assert mock_device.close.called

View File

@@ -1,24 +0,0 @@
#!/usr/bin/python
import escpos.printer as printer
import pytest
import mock
import socket
@pytest.fixture
def instance():
socket.socket.connect = mock.Mock()
return printer.Network("localhost")
def test_close_without_open(instance):
"""try to close without opening (should fail gracefully)
Currently we never open from our fixture, so calling close once
should be enough. In the future this might not be enough,
therefore we have to close twice in order to provoke an error
(if possible, this should not raise)
"""
instance.close()
instance.close()

Some files were not shown because too many files have changed in this diff Show More