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

113 Commits

Author SHA1 Message Date
dependabot[bot]
1349ad85bf Bump actions/setup-python from 5.6.0 to 6.0.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.6.0...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 12:14:08 +00:00
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
<!-- 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 version:** 0.0

View File

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

View File

@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v5
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -38,7 +38,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# 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).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -63,4 +63,4 @@ jobs:
# make release
- 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:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
submodules: 'recursive'
- name: Install packages
@@ -27,6 +27,6 @@ jobs:
sudo apt-get update -y &&
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 pip install tox pycups
sudo pip install --ignore-installed tox pycups
- name: Test doc build
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@v6.0.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
strategy:
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:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
with:
submodules: 'recursive'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.6.0
uses: actions/setup-python@v6.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
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
pip install flake8 pytest tox tox-gh-actions
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
run: |
# 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
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with tox
@@ -41,3 +48,20 @@ jobs:
tox
env:
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
.hypothesis
.pytest_cache/
coverage.xml
# pyenv
.python-version
@@ -40,3 +41,4 @@ test/test-cli-output/
!.vscode/tasks.json
!.vscode/launch.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>
<belono@users.noreply.github.com> <tiotil.lindeman@gmail.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.formatOnPaste": true,
"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
Alejandro Hernández
Alexander Bougakov
Alexandre Detiste
Alex Debiasio
Alfredo Orozco
Asuki Kono
belono
B. Howell
@@ -15,6 +17,7 @@ Davis Goglin
Dean Rispin
dependabot[bot]
Dmytro Katyukha
Florent de Labarre
Gerard Marull-Paretas
Hark
Joel Lehtonen
@@ -37,12 +40,15 @@ Qian Linfeng
Ramon Poca
reck31
Renato Lorenzi
Ricardo Sánchez Alba
Romain Porte
Sam Cheng
Sergio Pulgarin
Stephan Sokolow
Thijs Triemstra
Thomas van den Berg
tuxmaster
vendryan
Yaisel Hurtado
ysuolmai
白月秋见心

View File

@@ -1,6 +1,130 @@
*********
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"
--------------------------------------------------------
This release is the 10th alpha release of the new version 3.0.
@@ -67,7 +191,7 @@ contributors
- Alexander Bougakov
- Brian
- Yaisel Hurtado
- Maximilan Wagenbach
- Maximilian Wagenbach
- Patrick Kanzler
2019-06-19 - Version 3.0a6 - "Mistake not..."
@@ -79,7 +203,7 @@ released.
changes
^^^^^^^
- 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
contributors
@@ -214,11 +338,11 @@ changes
- 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
- 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
on `python-escpos.readthedocs.io <https://python-escpos.readthedocs.io>`_
- 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
^^^^^^^^^^^^
@@ -251,7 +375,7 @@ contributors
changes
^^^^^^^
- configure readthedocs and travis
- configure readthedocs and Travis
- update doc with hint on image preprocessing
- add fix for printing large images (by splitting them into multiple images)
@@ -293,8 +417,8 @@ changes
- packaging: configured the coverage-analysis codecov.io
- GitHub: improved issues-template
- documentation: add troubleshooting tip to network-interface
- the module, cli and documentation is now aware of the version of python-escpos
- the cli does now support basic tabcompletion
- the module, CLI and documentation is now aware of the version of python-escpos
- the CLI does now support basic tab completion
contributors
^^^^^^^^^^^^
@@ -312,11 +436,11 @@ changes
- refactor complete code in order to be compatible with Python 2 and 3
- modernize packaging
- 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
- extend support of barcode-codes to type B
- 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
- the image-functions are now much more flexible
- added a CLI
@@ -355,7 +479,7 @@ contributors
--------------------------
- Merge pull request #53 from ldos/master
- Extended params for serial printers
- Extended parameters for serial printers
- Sent by ldos <cafeteria.ldosalzira@gmail.com>
2015-04-21 - Version 1.0.5
@@ -383,7 +507,7 @@ contributors
--------------------------
- Issue #5: Fixed vertical tab
- Issue #9: Fixed identation inconsistence
- Issue #9: Fixed indentation inconsistency
2013-03-14 - Version 1.0.1
--------------------------
@@ -394,6 +518,6 @@ contributors
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 #4: Fixed typo in escpos.py

View File

@@ -1,68 +1,83 @@
************
Contributing
************
============
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.
:Last Reviewed: 2023-08-10
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
well as possible.
The pull requests and issues will be prefilled with templates.
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
-----------
This project keeps a list of authors. This can be auto-generated by calling `./doc/generate-authors.sh`.
When contributing the first time, please include a commit with the output of this script in place.
Otherwise the integration-check will fail.
This project keeps a list of authors.
This can be auto-generated by calling `./doc/generate-authors.sh`.
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.
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>`_.
When you change your username or mail-address, please also
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
-----------
When writing code please try to stick to these rules.
PEP8
^^^^
The entire codebase adheres to the rules of PEP8.
These rules are enforced by running `flake8` in the integration-checks.
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.
Black Code Style
^^^^^^^^^^^^^^^^
This project is formatted with the auto formatter `black <https://github.com/psf/black>`_.
Please format your contributions with black, otherwise they will be rejected.
GIT
^^^
The master-branch contains the main development of the project. A release to PyPi is marked with a tag
corresponding to the version. 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`.
The master-branch contains the main development of the project.
A release to PyPi is marked with a tag corresponding to the version.
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
git feature called an 'interactive rebase' before making a pull request. A small, self-contained change-set is
easier to review, and 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.
Please try to group your commits into logical units.
If you need to tidy up your branch, you can make use of a
git feature called an 'interactive rebase' before making a pull request.
A small, self-contained change-set is easier to review, and
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
^^^^^^^^^^
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.
Test
^^^^
Try to write tests whenever possible. Our goal for the future is 100% coverage.
We are currently using `nose` but might change in the future.
You can copy the structure from other testcases. Please remember to adapt the docstrings.
Try to write tests whenever possible.
Our goal for the future is 100% coverage.
You can copy the structure from other testcases.
Please remember to adapt the docstrings.
Further reading
^^^^^^^^^^^^^^^
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.
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
```
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

View File

@@ -2,26 +2,17 @@
python-escpos - Python library to manipulate ESC/POS Printers
#############################################################
.. image:: https://travis-ci.org/python-escpos/python-escpos.svg?branch=master
: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
Description
===========
.. image:: https://readthedocs.org/projects/python-escpos/badge/?version=latest
:target: https://python-escpos.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
Description
-----------
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.
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.
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.text("Hello World\n")
p.image("logo.gif")
p.barcode('1324354657687', 'EAN13', 64, 2, '', '')
p.barcode('4006381333931', 'EAN13', 64, 2, '', '')
p.cut()
@@ -68,9 +59,9 @@ Another example based on the Network printer class:
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.barcode('1324354657687', 'EAN13', 64, 2, '', '')
kitchen.barcode('4006381333931', 'EAN13', 64, 2, '', '')
kitchen.cut()
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
""" 9600 Baud, 8N1, Flow Control Enabled """
p = Serial(devfile='/dev/tty.usbserial',
p = Serial(
devfile='/dev/tty.usbserial',
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1.00,
dsrdtr=True)
dsrdtr=True,
profile="TM-T88III"
)
p.text("Hello World\n")
p.qr("You can readme from your smartphone")
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
------------
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
----------
None of the vendors cited in this project agree or endorse any of the patterns or implementations.
Its names are used only to maintain context.
None of the vendors cited in this project agree or endorse any of the
patterns or implementations.
Their names are used only to maintain context.

View File

@@ -8,5 +8,10 @@ coverage:
changes: off
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
# serve to show the default.
import sys
import os
import sys
from importlib.metadata import version as imp_version
on_rtd = os.getenv("READTHEDOCS") == "True"
@@ -35,6 +34,7 @@ root = os.path.relpath(os.path.join(os.path.dirname(__file__), ".."))
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
"sphinx.ext.doctest",
"sphinx.ext.todo",
"sphinx.ext.coverage",
@@ -42,9 +42,15 @@ extensions = [
"sphinx.ext.todo",
"sphinx.ext.graphviz",
"sphinx.ext.inheritance_diagram",
"sphinx.ext.imgconverter",
"sphinxarg.ext",
"sphinxcontrib.datatemplates",
"sphinxcontrib.spelling",
]
# mock the following modules for autodoc
autodoc_mock_imports = ["qrcode"]
# supress warnings for external images
suppress_warnings = [
"image.nonlocal_uri",
@@ -54,7 +60,7 @@ suppress_warnings = [
todo_include_todos = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
templates_path = ["_templates", "capability_templates"]
# The suffix of source filenames.
source_suffix = ".rst"
@@ -67,7 +73,7 @@ master_doc = "index"
# General information about the project.
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
# |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
# a list of builtin themes.
if on_rtd:
html_theme = "default"
html_theme = "sphinx_rtd_theme"
print("recognized execution on RTD")
else:
try:
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
except ImportError:
print("no sphinx_rtd_theme found, switching to nature")
html_theme = "default"
@@ -137,6 +143,14 @@ else:
# documentation.
# 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.
# html_theme_path = []
@@ -222,6 +236,8 @@ latex_elements = {
#'preamble': '',
}
latex_engine = "xelatex"
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
@@ -230,7 +246,7 @@ latex_documents = [
"index",
"python-escpos.tex",
"python-escpos Documentation",
"Manuel F Martinez and others",
"python-escpos developers",
"manual",
),
]
@@ -265,7 +281,7 @@ man_pages = [
"index",
"python-escpos",
"python-escpos Documentation",
["Manuel F Martinez and others"],
["python-escpos developers"],
1,
)
]
@@ -284,7 +300,7 @@ texinfo_documents = [
"index",
"python-escpos",
"python-escpos Documentation",
"Manuel F Martinez and others",
"python-escpos developers",
"python-escpos",
"One line description of project.",
"Miscellaneous",
@@ -304,7 +320,19 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
# spellchecker
spelling_ignore_pypi_package_names = True
spelling_ignore_pypi_package_names = False
spelling_ignore_wiki_words = True
spelling_ignore_python_builtins = 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
****
====
:Last Reviewed: 2023-08-10
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

View File

@@ -5,8 +5,15 @@
.. include:: ../README.rst
#######
Content
-------
#######
User Documentation
==================
This chapter describes the central points that
are relevant to the user of this library.
.. toctree::
:maxdepth: 1
@@ -16,16 +23,46 @@ Content
user/methods
user/printers
user/raspi
user/todo
user/usage
user/cli-user
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::
:maxdepth: 1
:caption: Developer Documentation
dev/release-process
dev/contributing
dev/repository
dev/changelog
dev/todo
API Documentation
=================
This chapter contains an auto-generated
documentation of the API of this library.
.. toctree::
:maxdepth: 1
@@ -43,8 +80,9 @@ Content
api/codepages
api/katakana
##################
Indices and tables
==================
##################
* :ref:`genindex`
* :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
qrcode>=4.0
pyserial
sphinx-rtd-theme
sphinx-rtd-theme==3.0.2
setuptools
setuptools-scm
docutils>=0.12
sphinxcontrib-spelling>=7.2.0
python-barcode>=0.11.0,<1
sphinxcontrib-spelling>=8.0.0
python-barcode>=0.15.0,<1
importlib-metadata
importlib_resources
sphinxcontrib.datatemplates
sphinx-argparse
sphinx-autodoc-typehints
pycups

View File

@@ -1,7 +1,145 @@
Raspbian
ESC
Esc
POS
Pos
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
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
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
-----------------
:Last Reviewed: 2016-07-31
Most ESC/POS-printers implement barcode-printing.
The barcode-commandset is implemented in the barcode-method.
For a list of compatible barcodes you should check the manual of your printer.
As a rule of thumb: even older Epson-models support most 1D-barcodes.
To be sure just try some implementations and have a look at the notices below.
:Last Reviewed: 2023-08-10
Many printers implement barcode printing natively.
These hardware rendered barcodes are fast but the supported
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
~~~~~~~~~~~~~~
The barcode-method is rather low-level and orients itself on the implementation of ESC/POS.
In the future this class could be supplemented by a high-level class that helps the user generating the payload.
Since version 3.0, the ``barcode`` method unifies the previous
``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
@@ -31,4 +43,5 @@ For alphanumeric CODE128 you have to preface your payload with `{B`.
# print CODE128 012ABCDabcd
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 @@
************
Installation
************
.. _user_installation:
:Last Reviewed: 2016-07-23
Installation
============
:Last Reviewed: 2023-08-10
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
available as a Debian package. If you want to always benefit from the newest stable releases you should probably
install from PyPi.
This should install all necessary dependencies. Apart from that
python-escpos is for some versions also available as a Debian package.
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
---------------------------
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
printer. You can create the file
@@ -35,7 +63,8 @@ Setup udev for USB-Printers
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
the `manual of argcomplete <https://argcomplete.readthedocs.io>`__.

View File

@@ -1,12 +1,12 @@
*******
Methods
*******
:Last Reviewed: 2017-01-25
=======
:Last Reviewed: 2023-08-10
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.
The following methods are available:

View File

@@ -1,15 +1,18 @@
********
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
---
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
and similiar devices, but only for those implementing native USB.
printers.
.. 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
:members:
@@ -44,20 +47,24 @@ This driver is based on the socket class.
Troubleshooting
^^^^^^^^^^^^^^^
Problems with a network-attached printer can have numerous causes. Make sure that your device has a proper IP address.
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:
Problems with a network-attached printer can have numerous causes.
Make sure that your device has a proper IP address.
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
# 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
----
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
:members:
@@ -67,8 +74,8 @@ This printer "prints" just into a file-handle. Especially on \*nix-systems this
Dummy
-----
The Dummy-printer is mainly for testing- and debugging-purposes. It stores
all of the "output" as raw ESC/POS in a string and returns that.
The Dummy-printer is mainly for testing- and debugging-purposes.
It stores all of the "output" as raw ESC/POS in a string and returns that.
.. autoclass:: escpos.printer.Dummy
:members:
@@ -82,7 +89,10 @@ Supports both local and remote CUPS printers and servers.
The printer must be properly configured in CUPS administration.
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
----
@@ -91,10 +101,23 @@ Supports local and remote CUPS printers.
The printer must be properly configured in CUPS administration.
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
:members:
:special-members:
:member-order: bysource
: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
************
============
: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 (e.g. those simple USB-sticks without encasing). Those interfaces are
based on 5V- or 3,3V-logic (the latter in the case of Raspberry PI). 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
.. warning:: You should **never** directly connect an printer with RS232-interface
(serial port) directly to a Raspberry PI or similar interface
(e.g. those simple USB-sticks without encasing).
Those interfaces are based on 5V- or 3,3V-logic
(the latter in the case of Raspberry PI).
Classical RS232 uses 12V-logic and would **thus destroy your interface**.
Connect both systems with an appropriate *level shifter*.
Installation
------------
You can install by using pip3.
::
sudo pip3 install --upgrade pip
sudo pip3 install python-escpos
The installation should be performed as described in :ref:`user_installation`.
Run
---
You need sudo and python3 to run your program.
You can run this software as on any other Linux system.
::
sudo python3 your-program.py
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>`__.
Attach your printer 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>`.

View File

@@ -1,7 +1,7 @@
*****
Usage
*****
:Last Reviewed: 2017-06-10
=====
:Last Reviewed: 2025-02-16
Define your printer
-------------------
@@ -113,7 +113,7 @@ on a USB interface.
from escpos import *
""" 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
p.text("Hello World\n")
# Print image
@@ -121,7 +121,7 @@ on a USB interface.
# Print QR Code
p.qr("You can readme from your smartphone")
# Print barcode
p.barcode('1324354657687','EAN13',64,2,'','')
p.barcode('4006381333931','EAN13',64,2,'','')
# Cut paper
p.cut()
@@ -142,11 +142,11 @@ format. For windows it is probably at::
%appdata%/python-escpos/config.yaml
And for linux::
And for Linux::
$HOME/.config/python-escpos/config.yaml
If you aren't sure, run::
If you are not sure, run::
from escpos import config
c = config.Config()
@@ -180,6 +180,7 @@ An example file printer::
printer:
type: File
devfile: /dev/someprinter
profile: TM-U220
And for a network printer::
@@ -187,6 +188,7 @@ And for a network printer::
type: Network
host: 127.0.0.1
port: 9000
profile: TM-U220
An USB-printer could be defined by::
@@ -196,20 +198,31 @@ An USB-printer could be defined by::
idProduct: 0x5678
in_ep: 0x66
out_ep: 0x01
profile: TM-U220
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
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.
If you want or need to you can manually set the codepage. For this please use the ``charcode()``-function. You can set
any key-value that is in ``CHARCODE``. If something is wrong, an ``CharCodeError`` will be raised.
After you have manually set the codepage the printer won't change it anymore. You can revert to normal behaviour
by setting charcode to ``AUTO``.
If you want or need to you can manually set the code page.
For this please use the ``charcode()``-function.
You can set any key-value that is in ``CHARCODE``.
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
--------------------------------------
@@ -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>`_
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
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
# 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
p.soft_barcode("code39", "123456")
p.barcode("123456", "CODE39", width=2, force_software=True)
p.text("\n")
p.text("\n")
p.barcode("123456", "CODE39")

View File

@@ -1,23 +1,22 @@
"""Prints code page tables.
"""
"""Prints code page tables."""
import six
import sys
from escpos import printer
from escpos.constants import (
CODEPAGE_CHANGE,
ESC,
CTL_LF,
CTL_FF,
CTL_CR,
CTL_FF,
CTL_HT,
CTL_LF,
CTL_VT,
ESC,
)
def main():
"""Init printer and print codepage tables."""
dummy = printer.Dummy()
dummy.hw("init")
@@ -34,9 +33,10 @@ def main():
def print_codepage(printer, codepage):
"""Print a code page."""
if codepage.isdigit():
codepage = int(codepage)
printer._raw(CODEPAGE_CHANGE + six.int2byte(codepage))
printer._raw(CODEPAGE_CHANGE + bytes((codepage,)))
printer._raw("after")
else:
printer.charcode(codepage)
@@ -45,18 +45,20 @@ def print_codepage(printer, codepage):
# Table header
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()
# The table
for x in range(0, 16):
# First column
printer.set(font="b")
printer._raw("{} ".format(hex(x)[2:]))
printer._raw(f"{hex(x)[2:]} ")
printer.set()
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):
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
from escpos.printer import Usb
def usage():
"""Print information on usage."""
print("usage: qr_code.py <content>")

View File

@@ -1,9 +1,9 @@
"""Example file for software barcodes."""
from escpos.printer import Usb
# Adapt to your needs
p = Usb(0x0416, 0x5011, profile="POS-5890")
# Some software barcodes
p.soft_barcode("code128", "Hello")
p.soft_barcode("code39", "1234")
p.barcode("Hello", "code128", width=2, force_software="bitImageRaster")
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
"""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 urllib
import json
import time
import os
import time
from datetime import datetime
from urllib.request import urlopen
from escpos.printer import Usb
""" Setting up the main pathing """
"""Set up the main pathing."""
this_dir, this_filename = os.path.split(__file__)
GRAPHICS_PATH = os.path.join(this_dir, "graphics/climacons/")
@@ -38,13 +39,14 @@ LONG = "114.189945" # Your Location
def forecast_icon(idx):
"""Get right icon for forecast."""
icon = data["daily"]["data"][idx]["icon"]
image = GRAPHICS_PATH + icon + ".png"
return image
# Dumps one forecast line to the printer
def forecast(idx):
"""Dump one forecast line to the printer."""
date = datetime.fromtimestamp(int(data["daily"]["data"][idx]["time"]))
day = calendar.day_name[date.weekday()]
lo = data["daily"]["data"][idx]["temperatureMin"]
@@ -67,12 +69,13 @@ def forecast(idx):
printer.text(" high " + str(hi))
printer.text(deg)
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("\n \n")
def icon():
"""Get icon."""
icon = data["currently"]["icon"]
image = GRAPHICS_PATH + icon + ".png"
return image
@@ -90,7 +93,7 @@ url = (
+ LONG
+ "?exclude=[alerts,minutely,hourly,flags]&units=si"
) # change last bit to 'us' for Fahrenheit
response = urllib.urlopen(url)
response = urlopen(url)
data = json.loads(response.read())
printer.print_and_feed(n=1)

View File

@@ -1,3 +1,27 @@
[tool.black]
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
description = Python library to manipulate ESC/POS Printers
long_description = file: README.rst
long_description_content_type = text/x-rst
license = MIT
license_file = LICENSE
author = Manuel F Martinez and others
author = python-escpos developers
author_email = dev@pkanzler.de
maintainer = Patrick Kanzler
maintainer_email = dev@pkanzler.de
@@ -18,11 +19,12 @@ classifiers =
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: Implementation :: CPython
Topic :: Software Development :: Libraries :: Python Modules
Topic :: Office/Business :: Financial :: Point-Of-Sale
@@ -32,41 +34,48 @@ project_urls =
Release Notes = https://github.com/python-escpos/python-escpos/releases
[options]
python_requires = >=3.6
python_requires = >=3.8
zip_safe = false
include_package_data = true
install_requires =
pyusb>=1.0.0
Pillow>=2.0
qrcode>=4.0
pyserial
python-barcode>=0.9.1,<1
python-barcode>=0.15.0,<1
setuptools
six
appdirs
platformdirs
PyYAML
argparse
argcomplete
future
importlib_resources
setup_requires = setuptools_scm
tests_require =
jaconv
tox
pytest!=3.2.0,!=3.3.0
tox>=4.11
pytest>=7.4
pytest-cov
pytest-mock
nose
scripttest
mock
hypothesis>4
hypothesis>=6.83
flake8
sphinxcontrib-spelling>=7.2.0
sphinxcontrib-spelling>=8.0.0
[nosetests]
verbosity=3
with-doctest=1
[options.extras_require]
usb =
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]
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
# 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
"""Setup script for python package."""
import os
import sys
from setuptools import find_packages, setup
from setuptools import find_packages, setup
base_dir = os.path.dirname(__file__)
src_dir = os.path.join(base_dir, "src")
@@ -14,14 +15,18 @@ sys.path.insert(0, src_dir)
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()
setuptools_scm_template = """\
# coding: utf-8
# file generated by setuptools_scm
# don't change, don't track in version control
#!/usr/bin/python
# -*- coding: utf-8 -*-
\"\"\"Version identifier.
file generated by setuptools_scm
don't change, don't track in version control
\"\"\"
version = '{version}'
"""

View File

@@ -1,9 +1,7 @@
# -*- 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:
from .version import version as __version__ # noqa

View File

@@ -1,29 +1,31 @@
import re
from os import environ, path
import pkg_resources
import pickle
"""Handler for capabilities data."""
import atexit
import logging
import pickle
import platform
import re
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
from tempfile import gettempdir
import platform
from typing import Any, Dict
if environ.get("ESCPOS_CAPABILITIES_DEBUG", 0):
logging.basicConfig()
logger = logging.getLogger(__name__)
pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", gettempdir())
pickle_path = path.join(
pickle_dir, "{v}.capabilities.pickle".format(v=platform.python_version())
)
# get a temporary file from pkg_resources if no file is specified in env
logger = logging.getLogger(__name__)
pickle_dir = environ.get("ESCPOS_CAPABILITIES_PICKLE_DIR", mkdtemp())
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
file_manager = ExitStack()
atexit.register(file_manager.close)
ref = importlib_resources.files(__name__) / "capabilities.json"
capabilities_path = environ.get(
"ESCPOS_CAPABILITIES_FILE",
pkg_resources.resource_filename(__name__, "capabilities.json"),
file_manager.enter_context(importlib_resources.as_file(ref)),
)
# Load external printer database
@@ -46,18 +48,43 @@ if full_load:
logger.debug("Loading and pickling capabilities")
with open(capabilities_path) as cp, open(pickle_path, "wb") as pp:
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)
logger.debug("Finished loading capabilities took %.2fs", time.time() - t0)
PROFILES: Dict[str, Any] = CAPABILITIES["profiles"]
class NotSupported(Exception):
"""Raised if a requested feature is not supported by the
printer profile.
"""
"""Raised if a requested feature is not supported by the printer profile."""
pass
@@ -65,7 +92,7 @@ class NotSupported(Exception):
BARCODE_B = "barcodeB"
class BaseProfile(object):
class BaseProfile:
"""This represents a printer profile.
A printer profile knows about the number of columns, supported
@@ -75,36 +102,39 @@ class BaseProfile(object):
profile_data: Dict[str, Any] = {}
def __getattr__(self, name):
"""Get a data element from the profile."""
return self.profile_data[name]
def get_font(self, font) -> int:
"""Return the escpos index for `font`. Makes sure that
the requested `font` is valid.
"""Return the escpos index for `font`.
Makes sure that the requested `font` is valid.
"""
font = {"a": 0, "b": 1}.get(font, font)
if not six.text_type(font) in self.fonts:
raise NotSupported(
'"{}" is not a valid font in the current profile'.format(font)
)
if not str(font) in self.fonts:
raise NotSupported(f'"{font}" is not a valid font in the current profile')
return font
def get_columns(self, font):
def get_columns(self, font) -> int:
"""Return the number of columns for the given 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 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 {v: k for k, v in self.codePages.items()}
def get_profile(name: str = None, **kwargs):
"""Get the profile by name; if no name is given, return the
default profile.
def get_profile(name: Optional[str] = None, **kwargs):
"""Get a profile by name.
If no name is given, return the default profile.
"""
if isinstance(name, Profile):
return name
@@ -116,21 +146,25 @@ def get_profile(name: str = None, **kwargs):
CLASS_CACHE = {}
def get_profile_class(name: str):
"""For the given profile name, load the data from the external
def get_profile_class(name: str) -> Type[BaseProfile]:
"""Load a profile class.
For the given profile name, load the data from the external
database, then generate dynamically a class.
"""
if name not in CLASS_CACHE:
profile_data = PROFILES[name]
profiles: Dict[str, Any] = CAPABILITIES["profiles"]
profile_data = profiles[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})
CLASS_CACHE[name] = new_class
return CLASS_CACHE[name]
def clean(s):
def clean(s: str) -> str:
"""Clean profile name."""
# Remove invalid characters
s = re.sub("[^0-9a-zA-Z_]", "", s)
# Remove leading characters until we find a letter or underscore
@@ -138,18 +172,25 @@ def clean(s):
return str(s)
class Profile(get_profile_class("default")):
"""
For users, who want to provide their profile
# mute the mypy type issue with this dynamic base class function for now (: Any)
ProfileBaseClass: Any = get_profile_class("default")
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__()
self.columns = columns
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:
return self.columns

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
""" CLI
"""CLI.
This module acts as a command line interface for python-escpos. It mirrors
closely the available ESCPOS commands while adding a couple extra ones for convenience.
@@ -9,8 +9,9 @@ It requires you to have a configuration file. See documentation for details.
"""
import argparse
import platform
from typing import Any, Dict, List
try:
import argcomplete
@@ -18,15 +19,18 @@ except ImportError:
# this CLI works nevertheless without argcomplete
pass # noqa
import sys
import six
from . import config
from . import config, escpos
from . import printer as escpos_printer_module
from . import version
# Must be defined before it's used in DEMO_FUNCTIONS
def str_to_bool(string):
"""Used as a type in argparse so that we get back a proper
bool instead of always True
def str_to_bool(string: str) -> bool:
"""Convert string to bool.
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")
@@ -52,7 +56,7 @@ DEMO_FUNCTIONS = {
"barcodes_a": [
{"bc": "UPC-A", "code": "13243546576"},
{"bc": "UPC-E", "code": "132435"},
{"bc": "EAN13", "code": "1324354657687"},
{"bc": "EAN13", "code": "4006381333931"},
{"bc": "EAN8", "code": "1324354"},
{"bc": "CODE39", "code": "TEST"},
{"bc": "ITF", "code": "55867492279103"},
@@ -61,7 +65,7 @@ DEMO_FUNCTIONS = {
"barcodes_b": [
{"bc": "UPC-A", "code": "13243546576", "function_type": "B"},
{"bc": "UPC-E", "code": "132435", "function_type": "B"},
{"bc": "EAN13", "code": "1324354657687", "function_type": "B"},
{"bc": "EAN13", "code": "4006381333931", "function_type": "B"},
{"bc": "EAN8", "code": "1324354", "function_type": "B"},
{"bc": "CODE39", "code": "TEST", "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
# defaults: A dict of args for subparser.set_defaults
# arguments: A list of dicts of args for subparser.add_argument
ESCPOS_COMMANDS = [
ESCPOS_COMMANDS: List[Dict[str, Any]] = [
{
"parser": {
"name": "qr",
@@ -161,6 +165,11 @@ ESCPOS_COMMANDS = [
"help": "ESCPOS function type",
"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": {
"name": "cut",
@@ -289,7 +330,7 @@ ESCPOS_COMMANDS = [
},
{
"option_strings": ("--histeq",),
"help": "Equalize the histrogram",
"help": "Equalize the histogram",
"type": str_to_bool,
},
{
@@ -332,7 +373,7 @@ ESCPOS_COMMANDS = [
{
"option_strings": ("--font",),
"help": "Font choice",
"choices": ["left", "center", "right"],
"choices": ["A", "B"],
},
{
"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(
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.",
)
@@ -514,10 +580,27 @@ def main():
)
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_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
if "argcomplete" in globals():
argcomplete.autocomplete(parser)
@@ -527,9 +610,7 @@ def main():
if not args_dict:
parser.print_help()
sys.exit()
command_arguments = dict(
[k, v] for k, v in six.iteritems(args_dict) if v is not None
)
command_arguments = dict([k, v] for k, v in args_dict.items() if v is not None)
# If version should be printed, do this, then exit
print_version = command_arguments.pop("version", None)
@@ -537,6 +618,11 @@ def main():
print(version.version)
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
config_path = command_arguments.pop("config", None)
@@ -563,9 +649,10 @@ def main():
globals()[target_command](**command_arguments)
def demo(printer, **kwargs):
"""
Prints demos. Called when CLI is passed `demo`. This function
def demo(printer: escpos.Escpos, **kwargs) -> None:
"""Print demos.
Called when CLI is passed `demo`. This function
uses the DEMO_FUNCTIONS dictionary.
:param printer: A printer from escpos.printer

View File

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

View File

@@ -1,29 +1,27 @@
"""ESC/POS configuration manager.
This module contains the implementations of abstract base class :py:class:`Config`.
"""
import os
import appdirs
import pathlib
import platformdirs
import yaml
from . import printer
from . import exceptions
from . import exceptions, printer
class Config(object):
class Config:
"""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.
"""
_app_name = "python-escpos"
_config_file = "config.yaml"
def __init__(self):
def __init__(self) -> None:
"""Initialize configuration.
Remember to add anything that needs to be reset between configurations
@@ -35,7 +33,7 @@ class Config(object):
self._printer_name = None
self._printer_config = None
def _reset_config(self):
def _reset_config(self) -> None:
"""Clear the loaded configuration.
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
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
for the configuration file.
"""
self._reset_config()
if not config_path:
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:
# 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:
config = yaml.safe_load(config_file)
except EnvironmentError:
raise exceptions.ConfigNotFoundError(
"Couldn't read config at {config_path}".format(
config_path=str(config_path),
)
f"Couldn't read config at {config_path}"
)
except yaml.YAMLError:
raise exceptions.ConfigSyntaxError("Error parsing YAML")
if "printer" in config:
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):
raise exceptions.ConfigSyntaxError(
'Printer type "{printer_name}" is invalid'.format(
printer_name=self._printer_name,
)
f'Printer type "{self._printer_name}" is invalid'
)
self._has_loaded = True
def printer(self):
"""Returns a printer that was defined in the config, or throws an
exception.
"""Return a printer that was defined in the config.
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:

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
""" ESC/POS Exceptions classes
"""ESC/POS Exceptions classes.
Result/Exit codes:
@@ -13,30 +13,42 @@ Result/Exit codes:
- `60` = Invalid pin to send Cash Drawer pulse :py:exc:`~escpos.exceptions.CashDrawerError`
- `70` = Invalid number of tab positions :py:exc:`~escpos.exceptions.TabPosError`
- `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`
- `200` = Configuration not found :py:exc:`~escpos.exceptions.ConfigNotFoundError`
- `210` = Configuration syntax error :py:exc:`~escpos.exceptions.ConfigSyntaxError`
- `220` = Configuration section not found :py:exc:`~escpos.exceptions.ConfigSectionMissingError`
:author: `Manuel F Martinez <manpaz@bashlinux.com>`_ and others
:author: python-escpos developers
:organization: Bashlinux and `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2012-2017 Bashlinux and python-escpos
:license: MIT
"""
from typing import Optional
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)
self.msg = msg
self.resultcode = 1
if status is not None:
self.resultcode = status
def __str__(self):
def __str__(self) -> str:
"""Return string representation of Error."""
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
one of those specified in :py:meth:`escpos.escpos.Escpos.barcode`.
The returned error code is `10`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.BarcodeTypeError
:parts: 1
"""
def __init__(self, msg=""):
def __init__(self, msg: str = "") -> None:
"""Initialize BarcodeTypeError object."""
Error.__init__(self, msg)
self.msg = msg
self.resultcode = 10
def __str__(self):
return "No Barcode type is defined ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of BarcodeTypeError."""
return f"No Barcode type is defined ({self.msg})"
class BarcodeSizeError(Error):
@@ -63,15 +83,23 @@ class BarcodeSizeError(Error):
This exception indicates that the values for the barcode size are out of range.
The size of the barcode has to be in the range that is specified in :py:meth:`escpos.escpos.Escpos.barcode`.
The resulting 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)
self.msg = msg
self.resultcode = 20
def __str__(self):
return "Barcode size is out of range ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of BarcodeSizeError."""
return f"Barcode size is out of range ({self.msg})"
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
was True and the check failed.
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)
self.msg = msg
self.resultcode = 30
def __str__(self):
return "No Barcode code was supplied ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of BarcodeCodeError."""
return f"No Barcode code was supplied ({self.msg})"
class ImageSizeError(Error):
"""Image height is longer than 255px and can't be printed.
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)
self.msg = msg
self.resultcode = 40
def __str__(self):
return "Image height is longer than 255px and can't be printed ({msg})".format(
msg=self.msg
)
def __str__(self) -> str:
"""Return string representation of ImageSizeError."""
return f"Image height is longer than 255px and can't be printed ({self.msg})"
class ImageWidthError(Error):
"""Image width is too large.
The return code for this exception is `41`.
inheritance:
.. inheritance-diagram:: escpos.exceptions.ImageWidthError
:parts: 1
"""
def __init__(self, msg=""):
def __init__(self, msg: str = "") -> None:
"""Initialize ImageWidthError object."""
Error.__init__(self, msg)
self.msg = msg
self.resultcode = 41
def __str__(self):
return "Image width is too large ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of ImageWidthError."""
return f"Image width is too large ({self.msg})"
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`.
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)
self.msg = msg
self.resultcode = 50
def __str__(self):
return "Text string must be supplied to the text() method ({msg})".format(
msg=self.msg
)
def __str__(self) -> str:
"""Return string representation of TextError."""
return f"Text string must be supplied to the text() method ({self.msg})"
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`.
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)
self.msg = msg
self.resultcode = 60
def __str__(self):
return "Valid pin must be set to send pulse ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of CashDrawerError."""
return f"Valid pin must be set to send pulse ({self.msg})"
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.
This exception is raised by :py:meth:`escpos.escpos.Escpos.control`.
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)
self.msg = msg
self.resultcode = 70
def __str__(self):
return "Valid tab positions must be in the range 0 to 16 ({msg})".format(
msg=self.msg
)
def __str__(self) -> str:
"""Return string representation of TabPosError."""
return f"Valid tab positions must be in the range 0 to 16 ({self.msg})"
class CharCodeError(Error):
"""Valid char code must be set.
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)
self.msg = msg
self.resultcode = 80
def __str__(self):
return "Valid char code must be set ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of CharCodeError."""
return f"Valid char code must be set ({self.msg})"
class USBNotFoundError(Error):
"""Device wasn't found (probably not plugged in)
class DeviceNotFoundError(Error):
"""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)
self.msg = msg
self.resultcode = 90
def __str__(self):
return "USB device not found ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""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):
"""A set method variable was out of range
"""A set method variable was out of range.
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)
self.msg = msg
self.resultcode = 100
def __str__(self):
return "Set variable out of range ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of SetVariableError."""
return f"Set variable out of range ({self.msg})"
# Configuration errors
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
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)
self.msg = msg
self.resultcode = 200
def __str__(self):
return "Configuration not found ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of ConfigNotFoundError."""
return f"Configuration not found ({self.msg})"
class ConfigSyntaxError(Error):
"""The configuration file is invalid
"""The configuration file is invalid.
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)
self.msg = msg
self.resultcode = 210
def __str__(self):
return "Configuration syntax is invalid ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""Return string representation of ConfigSyntaxError."""
return f"Configuration syntax is invalid ({self.msg})"
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)
self.msg = msg
self.resultcode = 220
def __str__(self):
return "Configuration section is missing ({msg})".format(msg=self.msg)
def __str__(self) -> str:
"""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`.
@@ -10,10 +10,12 @@ This module contains the image format handler :py:class:`EscposImage`.
import math
from typing import Iterator, Union
from PIL import Image, ImageOps
class EscposImage(object):
class EscposImage:
"""
Load images in, and output ESC/POS formats.
@@ -21,9 +23,8 @@ class EscposImage(object):
PIL, rather than spend CPU cycles looping over pixels.
"""
def __init__(self, img_source):
"""
Load in an image
def __init__(self, img_source: Union[Image.Image, str]) -> None:
"""Load in an image.
:param img_source: PIL.Image, or filename to load one from.
"""
@@ -48,31 +49,24 @@ class EscposImage(object):
self._im = im.convert("1")
@property
def width(self):
"""
Width of image in pixels
"""
def width(self) -> int:
"""Return width of image in pixels."""
width_pixels, _ = self._im.size
return width_pixels
@property
def width_bytes(self):
"""
Width of image if you use 8 pixels per byte and 0-pad at the end.
"""
def width_bytes(self) -> int:
"""Return width of image if you use 8 pixels per byte and 0-pad at the end."""
return (self.width + 7) >> 3
@property
def height(self):
"""
Height of image in pixels
"""
def height(self) -> int:
"""Height of image in pixels."""
_, height_pixels = self._im.size
return height_pixels
def to_column_format(self, high_density_vertical=True):
"""
Extract slices of an image as equal-sized blobs of column-format data.
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.
:param high_density_vertical: Printed line height in dots
"""
@@ -88,15 +82,12 @@ class EscposImage(object):
yield (im_bytes)
left += line_height
def to_raster_format(self):
"""
Convert image to raster-format binary
"""
def to_raster_format(self) -> bytes:
"""Convert image to raster-format binary."""
return self._im.tobytes()
def split(self, fragment_height):
"""
Split an image into multiple fragments after fragment_height pixels
def split(self, fragment_height: int):
"""Split an image into multiple fragments after fragment_height pixels.
:param fragment_height: height of fragment
:return: list of PIL objects
@@ -112,8 +103,8 @@ class EscposImage(object):
fragments.append(self.img_original.crop(box))
return fragments
def center(self, max_width):
"""In-place image centering
def center(self, max_width: int) -> None:
"""Center image in place.
:param: Maximum width in order to deduce x offset for centering
:return: None

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
""" Magic Encode
"""Magic Encode.
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.
@@ -13,64 +13,63 @@ The code is based on the encoding-code in py-xml-escpos by @fvdsn.
"""
import re
from builtins import bytes
import six
from .codepages import CodePages
from .constants import CODEPAGE_CHANGE
from .exceptions import Error
from .codepages import CodePages
import six
import re
class Encoder(object):
"""Takes a list of available code spaces. Picks the right one for a
given character.
class Encoder:
"""Take available code spaces and pick the right one for a given character.
Note: To determine the code page, it needs to do the conversion, and
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.
$ 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):
"""Initialize encoder."""
self.codepages = codepage_map
self.available_encodings = set(codepage_map.keys())
self.available_characters = {}
self.used_encodings = set()
def get_sequence(self, encoding):
"""Get a sequence."""
return int(self.codepages[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
is supported.
TODO: Support encoding aliases: pc437 instead of cp437.
.. todo:: Support encoding aliases: pc437 instead of cp437.
"""
encoding = CodePages.get_encoding_name(encoding)
if encoding not in self.codepages:
raise ValueError(
(
'Encoding "{}" cannot be used for the current profile. '
"Valid encodings are: {}"
).format(encoding, ",".join(self.codepages.keys()))
f'Encoding "{encoding}" cannot be used for the current profile. '
f'Valid encodings are: {",".join(self.codepages.keys())}'
)
)
return encoding
@staticmethod
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.
: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)
if "data" in codepage:
@@ -89,10 +88,10 @@ class Encoder(object):
# Non-encodable character, just skip it
pass
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):
"""Get codepage character map
"""Get code page character map.
Process an encoding and return a map of UTF-characters to code points
in this encoding.
@@ -112,7 +111,7 @@ class Encoder(object):
return codepage_char_map
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 char: The character to attempt to encode.
@@ -130,7 +129,7 @@ class Encoder(object):
@staticmethod
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 charmap: dictionary for mapping characters in this code page
@@ -142,7 +141,7 @@ class Encoder(object):
return ord(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 encoding: Encoding name to use (must be defined in capabilities)
@@ -156,15 +155,18 @@ class Encoder(object):
def __encoding_sort_func(self, 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):
"""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
chance they might work again, reducing the search space,
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
fairly minor.
@@ -184,7 +186,9 @@ class Encoder(object):
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).
"""
if not encoding:
@@ -198,8 +202,10 @@ def split_writable_text(encoder, text, encoding):
return text, None
class MagicEncode(object):
"""A helper that helps us to automatically switch to the right
class MagicEncode:
"""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.
This will consider the printers supported codepages, according
@@ -213,7 +219,7 @@ class MagicEncode(object):
def __init__(
self, driver, encoding=None, disabled=False, defaultsymbol="?", encoder=None
):
"""
"""Initialize magic encode.
:param driver:
:param encoding: If you know the current encoding of the printer
@@ -235,7 +241,7 @@ class MagicEncode(object):
self.disabled = disabled
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.
However, it will still keep track of the current code page.
@@ -248,7 +254,6 @@ class MagicEncode(object):
def write(self, text):
"""Write the text, automatically switching encodings."""
if self.disabled:
self.write_with_encoding(self.encoding, text)
return
@@ -277,17 +282,19 @@ class MagicEncode(object):
self.write_with_encoding(encoding, to_write)
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
# unnecesary code page switches.
self.write(self.defaultsymbol)
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(
"The supplied text has to be unicode, but is of type {type}.".format(
type=type(text)
)
f"The supplied text has to be Unicode, but is of type {type(text)}."
)
# 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
from escpos.printer import Dummy
from escpos.exceptions import DeviceNotFoundError
from escpos.printer import LP, CupsPrinter, Dummy, File, Network, Serial, Usb, Win32Raw
@pytest.fixture
def driver():
def driver() -> 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
"""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>`_
:copyright: Copyright (c) 2016 Patrick Kanzler
:license: MIT
"""
from nose.tools import raises
import escpos.escpos as escpos
from abc import ABCMeta
import pytest
@raises(TypeError)
def test_abstract_base_class_raises():
import escpos.escpos as escpos
def test_abstract_base_class_raises() -> None:
"""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"""
assert issubclass(escpos.Escpos, object)
assert type(escpos.Escpos) is ABCMeta

View File

@@ -4,48 +4,46 @@
import os
import sys
from scripttest import TestFileEnvironment
from nose.tools import assert_equal, nottest
import shutil
import tempfile
from scripttest import TestFileEnvironment as TFE
import escpos
TEST_DIR = os.path.abspath("test/test-cli-output")
TEST_DIR = tempfile.mkdtemp() + "/cli-test"
DEVFILE_NAME = "testfile"
DEVFILE = os.path.join(TEST_DIR, DEVFILE_NAME)
CONFIGFILE = "testconfig.yaml"
CONFIG_YAML = """
CONFIG_YAML = f"""
---
printer:
type: file
devfile: {testfile}
""".format(
testfile=DEVFILE,
)
devfile: {DEVFILE}
"""
class TestCLI:
"""Contains setups, teardowns, and tests for CLI"""
@classmethod
def setup_class(cls):
def setup_class(cls) -> None:
"""Create a config file to read from"""
with open(CONFIGFILE, "w") as config:
config.write(CONFIG_YAML)
@classmethod
def teardown_class(cls):
def teardown_class(cls) -> None:
"""Remove config file"""
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"""
self.env = None
self.default_args = None
self.env = TestFileEnvironment(
self.env = TFE(
base_path=TEST_DIR,
cwd=os.getcwd(),
)
@@ -62,25 +60,32 @@ class TestCLI:
finally:
fhandle.close()
def teardown(self):
def teardown_method(self) -> None:
"""Destroy printer file and env"""
os.remove(DEVFILE)
self.env.clear()
def test_cli_help(self):
def test_cli_help(self) -> None:
"""Test getting help from cli"""
result = self.env.run("python-escpos", "-h")
assert not result.stderr
assert "usage" in result.stdout
def test_cli_version(self):
def test_cli_version(self) -> None:
"""Test the version string"""
result = self.env.run("python-escpos", "version")
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_text(self):
def test_cli_version_extended(self) -> None:
"""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"""
test_text = "this is some text"
result = self.env.run(
@@ -95,15 +100,17 @@ class TestCLI:
)
assert not result.stderr
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"""
result = self.env.run(
*(self.default_args + ("text", "--invalid-param", "some data")),
expect_error=True,
expect_stderr=True
expect_stderr=True,
)
assert_equal(result.returncode, 2)
assert result.returncode == 2
assert "error:" in result.stderr
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
import escpos.printer as printer
from escpos.capabilities import Profile, BARCODE_B
from escpos.exceptions import BarcodeTypeError, BarcodeCodeError
import pytest
import escpos.printer as printer
from escpos.capabilities import BARCODE_B, Profile
from escpos.exceptions import BarcodeCodeError, BarcodeTypeError
@pytest.mark.parametrize(
"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
import pytest
import escpos.printer as printer
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."""
instance = printer.Dummy()
with pytest.raises(CashDrawerError):

View File

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

View File

@@ -1,12 +1,10 @@
import six
import escpos.printer as printer
from escpos.constants import GS
def test_cut_without_feed():
def test_cut_without_feed() -> None:
"""Test cut without feeding paper"""
instance = printer.Dummy()
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

View File

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

View File

@@ -9,7 +9,6 @@
import pytest
from PIL import Image
import escpos.printer as printer
@@ -17,7 +16,7 @@ from escpos.exceptions import ImageWidthError
# Raster format print
def test_bit_image_black():
def test_bit_image_black() -> None:
"""
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"
def test_bit_image_white():
def test_bit_image_white() -> None:
"""
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"
def test_bit_image_both():
def test_bit_image_both() -> None:
"""
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"
def test_bit_image_transparent():
def test_bit_image_transparent() -> None:
"""
Test printing black/transparent bit image (raster)
"""
@@ -59,7 +58,7 @@ def test_bit_image_transparent():
# Column format print
def test_bit_image_colfmt_black():
def test_bit_image_colfmt_black() -> None:
"""
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"
def test_bit_image_colfmt_white():
def test_bit_image_colfmt_white() -> None:
"""
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"
def test_bit_image_colfmt_both():
def test_bit_image_colfmt_both() -> None:
"""
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)
"""
@@ -100,7 +99,7 @@ def test_bit_image_colfmt_transparent():
# Graphics print
def test_graphics_black():
def test_graphics_black() -> None:
"""
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
"""
@@ -124,7 +123,7 @@ def test_graphics_white():
)
def test_graphics_both():
def test_graphics_both() -> None:
"""
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
"""
@@ -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.
"""
@@ -163,13 +162,13 @@ def test_large_graphics():
@pytest.fixture
def dummy_with_width():
def dummy_with_width() -> printer.Dummy:
instance = printer.Dummy()
instance.profile.profile_data = {"media": {"width": {"pixels": 384}}}
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.
"""
@@ -181,7 +180,7 @@ def test_width_too_large(dummy_with_width):
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
with pytest.raises(ImageWidthError):

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
"""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>`_
:copyright: Copyright (c) 2017 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
@@ -11,21 +11,21 @@
import escpos.printer as printer
def test_function_linedisplay_select_on():
def test_function_linedisplay_select_on() -> None:
"""test the linedisplay_select function (activate)"""
instance = printer.Dummy()
instance.linedisplay_select(select_display=True)
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)"""
instance = printer.Dummy()
instance.linedisplay_select(select_display=False)
assert instance.output == b"\x1B\x3D\x01"
def test_function_linedisplay_clear():
def test_function_linedisplay_clear() -> None:
"""test the linedisplay_clear function"""
instance = printer.Dummy()
instance.linedisplay_clear()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
"""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>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
@@ -11,14 +11,14 @@
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"""
instance = printer.Dummy()
instance.panel_buttons()
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"""
instance = printer.Dummy()
instance.panel_buttons(False)

View File

@@ -3,19 +3,18 @@
:author: `Michael Billington <michael.billington@gmail.com>`_
: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
"""
from nose.tools import raises
import pytest
import escpos.printer as printer
from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
def test_defaults():
def test_defaults() -> None:
"""Test QR code with defaults"""
instance = printer.Dummy()
instance.qr("1234", native=True)
@@ -26,14 +25,14 @@ def test_defaults():
assert instance.output == expected
def test_empty():
def test_empty() -> None:
"""Test QR printing blank code"""
instance = printer.Dummy()
instance.qr("", native=True)
assert instance.output == b""
def test_ec():
def test_ec() -> None:
"""Test QR error correction setting"""
instance = printer.Dummy()
instance.qr("1234", native=True, ec=QR_ECLEVEL_H)
@@ -44,7 +43,7 @@ def test_ec():
assert instance.output == expected
def test_size():
def test_size() -> None:
"""Test QR box size"""
instance = printer.Dummy()
instance.qr("1234", native=True, size=7)
@@ -55,7 +54,7 @@ def test_size():
assert instance.output == expected
def test_model():
def test_model() -> None:
"""Test QR model"""
instance = printer.Dummy()
instance.qr("1234", native=True, model=QR_MODEL_1)
@@ -66,47 +65,31 @@ def test_model():
assert instance.output == expected
@raises(ValueError)
def test_invalid_ec():
def test_invalid_ec() -> None:
"""Test invalid QR error correction"""
instance = printer.Dummy()
with pytest.raises(ValueError):
instance.qr("1234", native=True, ec=-1)
@raises(ValueError)
def test_invalid_size():
def test_invalid_size() -> None:
"""Test invalid QR size"""
instance = printer.Dummy()
with pytest.raises(ValueError):
instance.qr("1234", native=True, size=0)
@raises(ValueError)
def test_invalid_model():
def test_invalid_model() -> None:
"""Test invalid QR model"""
instance = printer.Dummy()
with pytest.raises(ValueError):
instance.qr("1234", native=True, model="Hello")
@pytest.mark.skip("this test has to be debugged")
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():
def test_image_invalid_model() -> None:
"""Test unsupported QR model as image"""
instance = printer.Dummy()
with pytest.raises(ValueError):
instance.qr("1234", native=False, model=QR_MODEL_1)
@@ -115,6 +98,6 @@ def instance():
return printer.Dummy()
def test_center_not_implementer(instance):
def test_center_not_implementer(instance: printer.Dummy) -> None:
with pytest.raises(NotImplementedError):
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
from escpos.constants import TXT_NORMAL, TXT_STYLE, SET_FONT
from escpos.constants import TXT_SIZE
from escpos.constants import SET_FONT, TXT_NORMAL, TXT_SIZE, TXT_STYLE
from escpos.exceptions import SetVariableError
# Default test, please copy and paste this block to test set method calls
def test_default_values():
def test_default_values_with_default() -> None:
"""Default test, please copy and paste this block to test set method calls"""
instance = printer.Dummy()
instance.set()
instance.set_with_default()
expected_sequence = (
TXT_NORMAL,
@@ -27,12 +27,20 @@ def test_default_values():
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
def test_set_size_2h():
def test_set_size_2h() -> None:
instance = printer.Dummy()
instance.set(double_height=True)
instance.set_with_default(double_height=True)
expected_sequence = (
TXT_NORMAL,
@@ -49,9 +57,21 @@ def test_set_size_2h():
assert instance.output == b"".join(expected_sequence)
def test_set_size_2w():
def test_set_size_2h_no_default() -> None:
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 = (
TXT_NORMAL,
@@ -68,9 +88,21 @@ def test_set_size_2w():
assert instance.output == b"".join(expected_sequence)
def test_set_size_2x():
def test_set_size_2w_no_default() -> None:
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 = (
TXT_NORMAL,
@@ -87,13 +119,25 @@ def test_set_size_2x():
assert instance.output == b"".join(expected_sequence)
def test_set_size_custom():
def test_set_size_2x_no_default() -> None:
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 = (
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["smooth"][False], # Smooth OFF
TXT_STYLE["bold"][False], # Bold OFF
@@ -106,12 +150,36 @@ def test_set_size_custom():
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
def test_set_flip():
def test_set_flip() -> None:
instance = printer.Dummy()
instance.set(flip=True)
instance.set_with_default(flip=True)
expected_sequence = (
TXT_NORMAL,
@@ -128,12 +196,21 @@ def test_set_flip():
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
def test_smooth():
def test_smooth() -> None:
instance = printer.Dummy()
instance.set(smooth=True)
instance.set_with_default(smooth=True)
expected_sequence = (
TXT_NORMAL,
@@ -153,9 +230,9 @@ def test_smooth():
# Type
def test_set_bold():
def test_set_bold() -> None:
instance = printer.Dummy()
instance.set(bold=True)
instance.set_with_default(bold=True)
expected_sequence = (
TXT_NORMAL,
@@ -172,9 +249,9 @@ def test_set_bold():
assert instance.output == b"".join(expected_sequence)
def test_set_underline():
def test_set_underline() -> None:
instance = printer.Dummy()
instance.set(underline=1)
instance.set_with_default(underline=1)
expected_sequence = (
TXT_NORMAL,
@@ -191,9 +268,9 @@ def test_set_underline():
assert instance.output == b"".join(expected_sequence)
def test_set_underline2():
def test_set_underline2() -> None:
instance = printer.Dummy()
instance.set(underline=2)
instance.set_with_default(underline=2)
expected_sequence = (
TXT_NORMAL,
@@ -213,9 +290,9 @@ def test_set_underline2():
# Align
def test_align_center():
def test_align_center() -> None:
instance = printer.Dummy()
instance.set(align="center")
instance.set_with_default(align="center")
expected_sequence = (
TXT_NORMAL,
@@ -232,9 +309,9 @@ def test_align_center():
assert instance.output == b"".join(expected_sequence)
def test_align_right():
def test_align_right() -> None:
instance = printer.Dummy()
instance.set(align="right")
instance.set_with_default(align="right")
expected_sequence = (
TXT_NORMAL,
@@ -254,10 +331,10 @@ def test_align_right():
# Densities
def test_densities():
def test_densities() -> None:
for density in range(8):
instance = printer.Dummy()
instance.set(density=density)
instance.set_with_default(density=density)
expected_sequence = (
TXT_NORMAL,
@@ -278,9 +355,9 @@ def test_densities():
# Invert
def test_invert():
def test_invert() -> None:
instance = printer.Dummy()
instance.set(invert=True)
instance.set_with_default(invert=True)
expected_sequence = (
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
"""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>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
"""
import pytest
import mock
from hypothesis import given, assume
import hypothesis.strategies as st
import mock
from hypothesis import given
from escpos.printer import Dummy
def get_printer():
def get_printer() -> Dummy:
return Dummy(magic_encode_args={"disabled": True, "encoding": "CP437"})
@given(text=st.text())
def test_text(text):
def test_text(text: str):
"""Test that text() calls the MagicEncode object."""
instance = get_printer()
instance.magic.write = mock.Mock()
with mock.patch.object(instance.magic, "write") as write:
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.block_text(
"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.textln("hello, world")
assert printer.output == b"hello, world\n"
def test_textln_empty():
def test_textln_empty() -> None:
printer = get_printer()
printer.textln()
assert printer.output == b"\n"
def test_ln():
def test_ln() -> None:
printer = get_printer()
printer.ln()
assert printer.output == b"\n"
def test_multiple_ln():
def test_multiple_ln() -> None:
printer = get_printer()
printer.ln(3)
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
def test_line_spacing_code_gen():
def test_line_spacing_code_gen() -> None:
printer = Dummy()
printer.line_spacing(10)
assert printer.output == b"\x1b3\n"
def test_line_spacing_rest():
def test_line_spacing_rest() -> None:
printer = Dummy()
printer.line_spacing()
assert printer.output == b"\x1b2"
def test_line_spacing_error_handling():
def test_line_spacing_error_handling() -> None:
printer = Dummy()
with assert_raises(ValueError):
with pytest.raises(ValueError):
printer.line_spacing(99, divisor=44)
with assert_raises(ValueError):
with pytest.raises(ValueError):
printer.line_spacing(divisor=80, spacing=86)
with assert_raises(ValueError):
with pytest.raises(ValueError):
printer.line_spacing(divisor=360, spacing=256)
with assert_raises(ValueError):
with pytest.raises(ValueError):
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>`_
:license: MIT
"""
from typing import List
from escpos.image import EscposImage
def test_image_black():
def test_image_black() -> None:
"""
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"])
def test_image_black_transparent():
def test_image_black_transparent() -> None:
"""
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
"""
@@ -39,7 +40,7 @@ def test_image_black_white():
)
def test_image_white():
def test_image_white() -> None:
"""
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"])
def test_split():
def test_split() -> None:
"""
test whether the split-function works as expected
"""
@@ -62,12 +63,12 @@ def test_split():
def _load_and_check_img(
filename,
width_expected,
height_expected,
raster_format_expected,
column_format_expected,
):
filename: str,
width_expected: int,
height_expected: int,
raster_format_expected: bytes,
column_format_expected: List[bytes],
) -> None:
"""
Load an image, and test whether raster & column formatted output, sizes, etc match expectations.
"""

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
"""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>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
:license: MIT
@@ -11,7 +11,7 @@
import escpos.printer as printer
def test_instantiation():
def test_instantiation() -> None:
"""test the instantiation of a escpos-printer class and basic printing"""
instance = printer.Dummy()
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 -*-
"""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>`_
:copyright: Copyright (c) 2016 `python-escpos <https://github.com/python-escpos>`_
: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
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.exceptions import CharCodeError, Error
from escpos.magicencode import Encoder, MagicEncode
class TestEncoder:
@@ -23,17 +25,24 @@ class TestEncoder:
Tests the single encoders.
"""
def test_can_encode(self):
def test_can_encode(self) -> None:
assert not Encoder({"CP437": 1}).can_encode("CP437", "")
assert Encoder({"CP437": 1}).can_encode("CP437", "á")
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 Encoder({"CP858": 1}).find_suitable_encoding("") == "CP858"
@raises(ValueError)
def test_get_encoding(self):
def test_find_suitable_encoding_unnecessary_codepage_swap(self) -> None:
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")
@@ -47,7 +56,7 @@ class TestMagicEncode:
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.
@@ -57,33 +66,33 @@ class TestMagicEncode:
MagicEncode(driver, disabled=True)
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.write_with_encoding("CP858", "€ 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.write_with_encoding("CP858", "€ 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.write_with_encoding("CP858", "€ ist teuro.")
assert driver.output == b"\xd5 ist teuro."
class TestWrite:
def test_write(self, driver):
def test_write(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver)
encode.write("€ 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.write("€ 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(
driver,
defaultsymbol="_",
@@ -94,7 +103,7 @@ class TestMagicEncode:
assert driver.output == b"_ ist teuro."
class TestForceEncoding:
def test(self, driver):
def test(self, driver: printer.Dummy) -> None:
encode = MagicEncode(driver)
encode.force_encoding("CP437")
assert driver.output == b"\x1bt\x00"
@@ -103,6 +112,7 @@ class TestMagicEncode:
assert driver.output == b"\x1bt\x00? ist teuro."
jaconv: typing.Optional[types.ModuleType]
try:
import jaconv
except ImportError:
@@ -115,9 +125,9 @@ class TestKatakana:
@example("カタカナ")
@example("あいうえお")
@example("ハンカクカタカナ")
def test_accept(self, text):
def test_accept(self, text: str) -> None:
encode_katakana(text)
def test_result(self):
def test_result(self) -> None:
assert encode_katakana("カタカナ") == b"\xb6\xc0\xb6\xc5"
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