diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml deleted file mode 100644 index 2d40d5f..0000000 --- a/.github/workflows/checks.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Checks - -on: [push, pull_request] - -jobs: - styles: - runs-on: ubuntu-18.04 - name: Linting - steps: - - uses: actions/checkout@master - - uses: actions/setup-python@v1 - - uses: whynothugo/python-linting@master diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c7077f1..baae952 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,39 +6,16 @@ on: - v* jobs: - github-release: - runs-on: ubuntu-18.04 - name: Publish GitHub Release - steps: - - uses: actions/checkout@master - - uses: actions/setup-python@v1 - with: - python-version: 3.7 - architecture: x64 - - name: Install build dependencies - run: pip install wheel - - name: Build packages - run: python setup.py sdist bdist_wheel - - name: Release - uses: softprops/action-gh-release@v1 - with: - files: dist/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pypi: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 name: Publish package on PyPI steps: - - uses: actions/checkout@master - - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 with: python-version: 3.7 - architecture: x64 - - name: Install build dependencies - run: pip install wheel - - name: Build packages - run: python setup.py sdist bdist_wheel - - name: Publish a Python distribution to PyPI - uses: pypa/gh-action-pypi-publish@master + - run: pip install build setuptools wheel build setuptools_scm + - run: python -m build --sdist --wheel --no-isolation + - uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6917728..31375c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,22 +7,29 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-20.04, macOS-10.15, windows-2019 ] - python: [ '3.6', '3.7', '3.8', '3.9' ] + os: [ ubuntu-22.04 ] + python: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] variant: [ "py", "py-images" ] + include: + - os: macOS-12 + python: "3.12" + variant: py-images + - os: windows-2022 + python: "3.12" + variant: py-images + - os: windows-2022 name: python${{ matrix.python }} on ${{ matrix.os }} ${{ matrix.variant }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + cache: pip + cache-dependency-path: | + pyproject.toml - name: Install test dependency - run: pip install tox codecov + run: pip install tox - name: Run tests run: tox env: TOXENV: ${{ matrix.variant }} - - name: Report coverage - run: codecov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 4584e1a..844a299 100755 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.kpf *.svg *.db +.idea/* barcode/__pycache* build/* dist/* @@ -17,3 +18,5 @@ MANIFEST barcode/version.py tests/index.html tests/test_outputs/ +tests/test.py +test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 20fe897..2b932c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,28 +1,19 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -# vim: set nospell: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v5.0.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: debug-statements - - repo: https://github.com/asottile/reorder_python_imports - rev: v2.3.5 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.11.9' hooks: - - id: reorder-python-imports - - repo: https://github.com/psf/black - rev: "20.8b1" + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/netromdk/vermin + rev: v1.6.0 hooks: - - id: black - - repo: https://gitlab.com/pycqa/flake8 - rev: "3.8.3" # pick a git hash / tag to point to - hooks: - - id: flake8 - additional_dependencies: [flake8-comprehensions, flake8-import-order, flake8-bugbear] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.782' # Use the sha / tag you want to point at - hooks: - - id: mypy + - id: vermin + args: ['-t=3.9-', '--violations'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..69e83c2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing to python-barcode + +## Bug reports + +Please make sure that you take the following steps when reporting issues: + +Ensure that you are using the latest version. You bug may already have been +fixed. + +Search existing issues to ensure that it was not previously included. Be sure to +search closed issues too, your issue may have been fixed recently. + +In case that the generated output is not correct, please mention: + +- What input did you provide? +- What output did you get? +- What output do you think you should have obtained? + +In case of crashes, please copy the full stack trace and **at least** the line +from your code that reproduces the crash. + +Trying to understand and debug any error is a lot easier if you provide a small +portion of code that I can run myself to reproduce the error. diff --git a/LICENCE b/LICENCE index 159bbe8..92decff 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2020 Hugo Osvaldo Barrera , et al +Copyright (c) 2017-2023 Hugo Osvaldo Barrera , et al Copyright (c) 2010-2013 Thorsten Weimann Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/README.rst b/README.rst index 6e33e32..44b25a2 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,10 @@ python-barcode ============== -.. image:: https://action-badges.now.sh/WhyNotHugo/python-barcode +.. image:: https://github.com/WhyNotHugo/python-barcode/actions/workflows/tests.yml/badge.svg :target: https://github.com/WhyNotHugo/python-barcode/actions :alt: CI status -.. image:: https://codecov.io/gh/WhyNotHugo/python-barcode/branch/master/graph/badge.svg - :target: https://codecov.io/gh/WhyNotHugo/python-barcode - :alt: Build coverage - .. image:: https://readthedocs.org/projects/python-barcode/badge/ :target: https://python-barcode.rtfd.org/ :alt: documentation @@ -22,7 +18,7 @@ python-barcode :alt: downloads .. image:: https://img.shields.io/pypi/l/python-barcode.svg - :target: https://github.com/WhyNotHugo/python-barcode/blob/master/LICENCE + :target: https://github.com/WhyNotHugo/python-barcode/blob/main/LICENCE :alt: licence **python-barcode** provides a simple way to create barcodes in Python. @@ -30,7 +26,7 @@ python-barcode There are no external dependencies when generating SVG files. Pillow is required for generating images (e.g.: PNGs). -Support Python 3.6 to 3.9. +Support Python 3.9 to 3.13. .. image:: example-ean13.png :target: https://github.com/WhyNotHugo/python-barcode @@ -41,6 +37,8 @@ Documentation Full documentation is published at http://python-barcode.rtfd.io/ +You can build the documentation locally using ``make -C docs html``. + Licence ------- diff --git a/barcode/__init__.py b/barcode/__init__.py index 571fdea..6a4d481 100755 --- a/barcode/__init__.py +++ b/barcode/__init__.py @@ -3,18 +3,24 @@ created as SVG objects. If Pillow is installed, the barcodes can also be rendered as images (all formats supported by Pillow). """ + +from __future__ import annotations + import os +from typing import TYPE_CHECKING from typing import BinaryIO -from typing import Dict -from typing import Union +from typing import overload -from barcode.codex import Code128 +from barcode.codabar import CODABAR +from barcode.codex import PZN from barcode.codex import Code39 +from barcode.codex import Code128 from barcode.codex import Gs1_128 -from barcode.codex import PZN +from barcode.ean import EAN8 +from barcode.ean import EAN8_GUARD from barcode.ean import EAN13 +from barcode.ean import EAN13_GUARD from barcode.ean import EAN14 -from barcode.ean import EAN8 from barcode.ean import JAN from barcode.errors import BarcodeNotFoundError from barcode.isxn import ISBN10 @@ -24,68 +30,95 @@ from barcode.upc import UPCA from barcode.version import version # noqa: F401 -__BARCODE_MAP = { - "ean8": EAN8, - "ean13": EAN13, +if TYPE_CHECKING: + from barcode.base import Barcode + from barcode.writer import BaseWriter + +__BARCODE_MAP: dict[str, type[Barcode]] = { + "codabar": CODABAR, + "code128": Code128, + "code39": Code39, "ean": EAN13, - "gtin": EAN14, + "ean13": EAN13, + "ean13-guard": EAN13_GUARD, "ean14": EAN14, - "jan": JAN, - "upc": UPCA, - "upca": UPCA, - "isbn": ISBN13, - "isbn13": ISBN13, + "ean8": EAN8, + "ean8-guard": EAN8_GUARD, "gs1": ISBN13, + "gs1_128": Gs1_128, + "gtin": EAN14, + "isbn": ISBN13, "isbn10": ISBN10, + "isbn13": ISBN13, "issn": ISSN, - "code39": Code39, - "pzn": PZN, - "code128": Code128, "itf": ITF, - "gs1_128": Gs1_128, + "jan": JAN, + "nw-7": CODABAR, + "pzn": PZN, + "upc": UPCA, + "upca": UPCA, } PROVIDED_BARCODES = list(__BARCODE_MAP) PROVIDED_BARCODES.sort() -def get(name, code=None, writer=None, options=None): +@overload +def get( + name: str, code: str, writer: BaseWriter | None = None, options: dict | None = None +) -> Barcode: ... + + +@overload +def get( + name: str, + code: None = None, + writer: BaseWriter | None = None, + options: dict | None = None, +) -> type[Barcode]: ... + + +def get( + name: str, + code: str | None = None, + writer: BaseWriter | None = None, + options: dict | None = None, +) -> Barcode | type[Barcode]: """Helper method for getting a generator or even a generated code. - :param str name: The name of the type of barcode desired. - :param str code: The actual information to encode. If this parameter is + :param name: The name of the type of barcode desired. + :param code: The actual information to encode. If this parameter is provided, a generated barcode is returned. Otherwise, the barcode class is returned. :param Writer writer: An alternative writer to use when generating the barcode. - :param dict options: Additional options to be passed on to the barcode when + :param options: Additional options to be passed on to the barcode when generating. """ options = options or {} + barcode: type[Barcode] try: barcode = __BARCODE_MAP[name.lower()] - except KeyError: - raise BarcodeNotFoundError( - "The barcode {!r} you requested is not known.".format(name) - ) + except KeyError as e: + raise BarcodeNotFoundError(f"The barcode {name!r} is not known.") from e if code is not None: return barcode(code, writer, **options) - else: - return barcode + + return barcode -def get_class(name): +def get_class(name: str) -> type[Barcode]: return get_barcode(name) def generate( name: str, code: str, - writer=None, - output: Union[str, os.PathLike, BinaryIO] = None, - writer_options: Dict = None, - text: str = None, -): + writer: BaseWriter | None = None, + output: str | os.PathLike | BinaryIO | None = None, + writer_options: dict | None = None, + text: str | None = None, +) -> str | None: """Shortcut to generate a barcode in one line. :param name: Name of the type of barcode to use. @@ -98,18 +131,22 @@ def generate( """ from barcode.base import Barcode + if output is None: + raise TypeError("'output' cannot be None") + writer = writer or Barcode.default_writer() writer.set_options(writer_options or {}) barcode = get(name, code, writer) if isinstance(output, str): - fullname = barcode.save(output, writer_options, text) - return fullname - elif output: - barcode.write(output, writer_options, text) - else: - raise TypeError("'output' cannot be None") + return barcode.save(output, writer_options, text) + if isinstance(output, os.PathLike): + with open(output, "wb") as fp: + barcode.write(fp, writer_options, text) + return None + barcode.write(output, writer_options, text) + return None get_barcode = get diff --git a/barcode/base.py b/barcode/base.py index 7b7b0fe..cfcbe36 100755 --- a/barcode/base.py +++ b/barcode/base.py @@ -1,18 +1,25 @@ -"""barcode.base +"""barcode.base""" -""" +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import ClassVar + +from barcode.writer import BaseWriter from barcode.writer import SVGWriter +if TYPE_CHECKING: + from typing import BinaryIO -class Barcode: +class Barcode: name = "" digits = 0 default_writer = SVGWriter - default_writer_options = { + default_writer_options: ClassVar[dict] = { "module_width": 0.2, "module_height": 15.0, "quiet_zone": 6.5, @@ -24,16 +31,26 @@ class Barcode: "text": "", } - def to_ascii(self): - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("1", "X").replace("0", " ") - return "\n".join(code) + writer: BaseWriter - def __repr__(self): - return "<{}({!r})>".format(self.__class__.__name__, self.get_fullcode()) + def __init__(self, code: str, writer: BaseWriter | None = None, **options) -> None: + raise NotImplementedError - def build(self): + def to_ascii(self) -> str: + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("1", "X").replace("0", " ") + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}({self.get_fullcode()!r})>" + + def build(self) -> list[str]: + """Return a single-element list with a string encoding the barcode. + + Typically the string consists of 1s and 0s, although it can contain + other characters such as G for guard lines (e.g. in EAN13).""" raise NotImplementedError def get_fullcode(self): @@ -44,56 +61,46 @@ def get_fullcode(self): """ raise NotImplementedError - def save(self, filename, options=None, text=None): + def save( + self, filename: str, options: dict | None = None, text: str | None = None + ) -> str: """Renders the barcode and saves it in `filename`. - :parameters: - filename : String - Filename to save the barcode in (without filename - extension). - options : Dict - The same as in `self.render`. - text : str - Text to render under the barcode. + :param filename: Filename to save the barcode in (without filename extension). + :param options: The same as in `self.render`. + :param text: Text to render under the barcode. :returns: The full filename with extension. - :rtype: String """ - if text: - output = self.render(options, text) - else: - output = self.render(options) + output = self.render(options, text) if text else self.render(options) - _filename = self.writer.save(filename, output) - return _filename + return self.writer.save(filename, output) - def write(self, fp, options=None, text=None): + def write( + self, + fp: BinaryIO, + options: dict | None = None, + text: str | None = None, + ) -> None: """Renders the barcode and writes it to the file like object `fp`. - :parameters: - fp : File like object - Object to write the raw data in. - options : Dict - The same as in `self.render`. - text : str - Text to render under the barcode. + :param fp: Object to write the raw data in. + :param options: The same as in `self.render`. + :param text: Text to render under the barcode. """ output = self.render(options, text) self.writer.write(output, fp) - def render(self, writer_options=None, text=None): + def render(self, writer_options: dict | None = None, text: str | None = None): """Renders the barcode using `self.writer`. - :parameters: - writer_options : Dict - Options for `self.writer`, see writer docs for details. - text : str - Text to render under the barcode. + :param writer_options: Options for `self.writer`, see writer docs for details. + :param text: Text to render under the barcode. :returns: Output of the writers render method. """ - options = Barcode.default_writer_options.copy() + options = self.default_writer_options.copy() options.update(writer_options or {}) if options["write_text"] or text is not None: if text is not None: @@ -101,6 +108,8 @@ def render(self, writer_options=None, text=None): else: options["text"] = self.get_fullcode() self.writer.set_options(options) - code = self.build() - raw = self.writer.render(code) - return raw + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return self.writer.render([code]) diff --git a/barcode/charsets/codabar.py b/barcode/charsets/codabar.py new file mode 100644 index 0000000..429b3ce --- /dev/null +++ b/barcode/charsets/codabar.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +# W = Wide bar +# w = wide space +# N = Narrow bar +# n = narrow space + +CODES = { + "0": "NnNnNwW", + "1": "NnNnWwN", + "2": "NnNwNnW", + "3": "WwNnNnN", + "4": "NnWnNwN", + "5": "WnNnNwN", + "6": "NwNnNnW", + "7": "NwNnWnN", + "8": "NwWnNnN", + "9": "WnNwNnN", + "-": "NnNwWnN", + "$": "NnWwNnN", + ":": "WnNnWnW", + "/": "WnWnNnW", + ".": "WnWnWnN", + "+": "NnWnWnW", +} + +STARTSTOP = {"A": "NnWwNwN", "B": "NwNwNnW", "C": "NnNwNwW", "D": "NnNwWwN"} diff --git a/barcode/charsets/code128.py b/barcode/charsets/code128.py index 4941aa2..c631f3b 100644 --- a/barcode/charsets/code128.py +++ b/barcode/charsets/code128.py @@ -1,47 +1,44 @@ +from __future__ import annotations + import string # Charsets for code 128 _common = ( - ( - " ", - "!", - '"', - "#", - "$", - "%", - "&", - "'", - "(", - ")", - "*", - "+", - ",", - "-", - ".", - "/", - ) - + tuple(string.digits) - + ( - ":", - ";", - "<", - "=", - ">", - "?", - "@", - ) - + tuple(string.ascii_uppercase) - + ( - "[", - "\\", - "]", - "^", - "_", - ) + " ", + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + *tuple(string.digits), + ":", + ";", + "<", + "=", + ">", + "?", + "@", + *tuple(string.ascii_uppercase), + "[", + "\\", + "]", + "^", + "_", ) -_charset_a = _common + ( +_charset_a = ( + *_common, "\x00", "\x01", "\x02", @@ -51,11 +48,11 @@ "\x06", "\x07", "\x08", - "\x09", - "\x0a", + "\t", + "\n", "\x0b", "\x0c", - "\x0d", + "\r", "\x0e", "\x0f", "\x10", @@ -74,33 +71,31 @@ "\x1d", "\x1e", "\x1f", - "\xf3", - "\xf2", + "ó", + "ò", "SHIFT", "TO_C", "TO_B", - "\xf4", - "\xf1", + "ô", + "ñ", ) _charset_b = ( - _common - + ("`",) - + tuple(string.ascii_lowercase) - + ( - "{", - "|", - "}", - "~", - "\x7f", - "\xf3", - "\xf2", - "SHIFT", - "TO_C", - "\xf4", - "TO_A", - "\xf1", - ) + *_common, + "`", + *tuple(string.ascii_lowercase), + "{", + "|", + "}", + "~", + "\x7f", + "ó", + "ò", + "SHIFT", + "TO_C", + "ô", + "TO_A", + "ñ", ) ALL = set(_common + _charset_a + _charset_b) diff --git a/barcode/charsets/code39.py b/barcode/charsets/code39.py index d9f679d..dcd80e7 100644 --- a/barcode/charsets/code39.py +++ b/barcode/charsets/code39.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import string # Charsets for code 39 diff --git a/barcode/charsets/ean.py b/barcode/charsets/ean.py index 199142d..afc94c1 100644 --- a/barcode/charsets/ean.py +++ b/barcode/charsets/ean.py @@ -1,3 +1,5 @@ +from __future__ import annotations + EDGE = "101" MIDDLE = "01010" CODES = { diff --git a/barcode/charsets/itf.py b/barcode/charsets/itf.py index 24eb02f..6965bae 100644 --- a/barcode/charsets/itf.py +++ b/barcode/charsets/itf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + # W = Wide bar # w = wide space # N = Narrow bar diff --git a/barcode/charsets/upc.py b/barcode/charsets/upc.py index df90deb..cb49510 100644 --- a/barcode/charsets/upc.py +++ b/barcode/charsets/upc.py @@ -1,3 +1,5 @@ +from __future__ import annotations + EDGE = "101" MIDDLE = "01010" CODES = { diff --git a/barcode/codabar.py b/barcode/codabar.py new file mode 100644 index 0000000..a70e3dd --- /dev/null +++ b/barcode/codabar.py @@ -0,0 +1,78 @@ +"""Module: barcode.codabar + +:Provided barcodes: Codabar (NW-7) +""" + +from __future__ import annotations + +__docformat__ = "restructuredtext en" + +from barcode.base import Barcode +from barcode.charsets import codabar +from barcode.errors import BarcodeError +from barcode.errors import IllegalCharacterError + + +class CODABAR(Barcode): + """Initializes a new CODABAR instance. + + :parameters: + code : String + Codabar (NW-7) string that matches [ABCD][0-9$:/.+-]+[ABCD] + writer : barcode.writer Instance + The writer to render the barcode (default: SVGWriter). + narrow: Integer + Width of the narrow elements (default: 2) + wide: Integer + Width of the wide elements (default: 5) + wide/narrow must be in the range 2..3 + """ + + name = "Codabar (NW-7)" + + def __init__(self, code, writer=None, narrow=2, wide=5) -> None: + self.code = code + self.writer = writer or self.default_writer() + self.narrow = narrow + self.wide = wide + + def __str__(self) -> str: + return self.code + + def get_fullcode(self): + return self.code + + def build(self) -> list[str]: + try: + data = ( + codabar.STARTSTOP[self.code[0]] + "n" + ) # Start with [A-D], followed by a narrow space + + except KeyError: + raise BarcodeError("Codabar should start with either A,B,C or D") from None + + try: + data += "n".join( + [codabar.CODES[c] for c in self.code[1:-1]] + ) # separated by a narrow space + except KeyError: + raise IllegalCharacterError( + "Codabar can only contain numerics or $:/.+-" + ) from None + + try: + data += "n" + codabar.STARTSTOP[self.code[-1]] # End with [A-D] + except KeyError: + raise BarcodeError("Codabar should end with either A,B,C or D") from None + + raw = "" + for e in data: + if e == "W": + raw += "1" * self.wide + if e == "w": + raw += "0" * self.wide + if e == "N": + raw += "1" * self.narrow + if e == "n": + raw += "0" * self.narrow + return [raw] diff --git a/barcode/codex.py b/barcode/codex.py index 870c8c2..9a60c31 100755 --- a/barcode/codex.py +++ b/barcode/codex.py @@ -2,13 +2,24 @@ :Provided barcodes: Code 39, Code 128, PZN """ + +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Literal + from barcode.base import Barcode -from barcode.charsets import code128 from barcode.charsets import code39 +from barcode.charsets import code128 from barcode.errors import BarcodeError from barcode.errors import IllegalCharacterError from barcode.errors import NumberOfDigitsError +if TYPE_CHECKING: + from collections.abc import Collection + + from barcode.writer import BaseWriter + __docformat__ = "restructuredtext en" # Sizes @@ -16,7 +27,7 @@ MIN_QUIET_ZONE = 2.54 -def check_code(code, name, allowed): +def check_code(code: str, name: str, allowed: Collection[str]) -> None: wrong = [] for char in code: if char not in allowed: @@ -34,7 +45,7 @@ class Code39(Barcode): name = "Code 39" - def __init__(self, code: str, writer=None, add_checksum: bool = True): + def __init__(self, code: str, writer=None, add_checksum: bool = True) -> None: r""" :param code: Code 39 string without \* and without checksum. :param writer: A ``barcode.writer`` instance used to render the barcode @@ -45,33 +56,37 @@ def __init__(self, code: str, writer=None, add_checksum: bool = True): self.code = code.upper() if add_checksum: self.code += self.calculate_checksum() - self.writer = writer or Barcode.default_writer() + self.writer = writer or self.default_writer() check_code(self.code, self.name, code39.REF) - def __str__(self): + def __str__(self) -> str: return self.code def get_fullcode(self) -> str: """:returns: The full code as it will be encoded.""" return self.code - def calculate_checksum(self): + def calculate_checksum(self) -> str: check = sum(code39.MAP[x][0] for x in self.code) % 43 for k, v in code39.MAP.items(): if check == v[0]: return k + raise RuntimeError( + "All possible values for the checksum should have been included in the map." + ) - def build(self): + def build(self) -> list[str]: chars = [code39.EDGE] for char in self.code: chars.append(code39.MAP[char][1]) chars.append(code39.EDGE) - return [code39.MIDDLE.join(chars)] + result = code39.MIDDLE.join(chars) + return [result] def render(self, writer_options=None, text=None): options = {"module_width": MIN_SIZE, "quiet_zone": MIN_QUIET_ZONE} options.update(writer_options or {}) - return Barcode.render(self, options, text) + return super().render(options, text) class PZN7(Code39): @@ -88,28 +103,28 @@ class PZN7(Code39): digits = 6 - def __init__(self, pzn, writer=None): + def __init__(self, pzn, writer=None) -> None: pzn = pzn[: self.digits] if not pzn.isdigit(): raise IllegalCharacterError("PZN can only contain numbers.") if len(pzn) != self.digits: raise NumberOfDigitsError( - "PZN must have {} digits, not {}.".format(self.digits, len(pzn)) + f"PZN must have {self.digits} digits, not {len(pzn)}." ) self.pzn = pzn - self.pzn = "{}{}".format(pzn, self.calculate_checksum()) - Code39.__init__(self, "PZN-{}".format(self.pzn), writer, add_checksum=False) + self.pzn = f"{pzn}{self.calculate_checksum()}" + super().__init__(f"PZN-{self.pzn}", writer, add_checksum=False) def get_fullcode(self): - return "PZN-{}".format(self.pzn) + return f"PZN-{self.pzn}" def calculate_checksum(self): sum_ = sum(int(x) * int(y) for x, y in enumerate(self.pzn, start=2)) checksum = sum_ % 11 if checksum == 10: raise BarcodeError("Checksum can not be 10 for PZN.") - else: - return checksum + + return checksum class PZN8(PZN7): @@ -130,25 +145,31 @@ class Code128(Barcode): """ name = "Code 128" + _charset: Literal["A", "B", "C"] + code: str + writer: BaseWriter + buffer: str - def __init__(self, code, writer=None): + def __init__(self, code: str, writer=None) -> None: self.code = code - self.writer = writer or Barcode.default_writer() - self._charset = "B" - self._buffer = "" + self.writer = writer or self.default_writer() + self._charset = "C" + self._digit_buffer = "" # Accumulate pairs of digits for charset C check_code(self.code, self.name, code128.ALL) - def __str__(self): + def __str__(self) -> str: return self.code @property - def encoded(self): + def encoded(self) -> list[int]: return self._build() - def get_fullcode(self): + def get_fullcode(self) -> str: return self.code - def _new_charset(self, which): + def _new_charset(self, which: Literal["A", "B", "C"]) -> list[int]: + if which == self._charset: + raise ValueError(f"Already in charset {which}") if which == "A": code = self._convert("TO_A") elif which == "B": @@ -158,84 +179,120 @@ def _new_charset(self, which): self._charset = which return [code] - def _maybe_switch_charset(self, pos): + # to be redefined in subclass if required + def _is_char_fnc1_char(self, char): + """Whether a character is the FNC1 character. + + May be redefined by subclasses if required. FNC1 char is defined in GS1-128 + specification and it is defined just the same for all encodings therefore this + sign should be treated in a special way. + """ + return False + + def _maybe_switch_charset(self, pos: int) -> list[int]: char = self.code[pos] next_ = self.code[pos : pos + 10] - def look_next(): + def look_next() -> bool: digits = 0 for c in next_: if c.isdigit(): digits += 1 else: break - return digits > 3 and (digits % 2) == 0 + return digits > 3 - codes = [] + codes: list[int] = [] if self._charset == "C" and not char.isdigit(): + if self._is_char_fnc1_char(char) and not self._digit_buffer: + return codes if char in code128.B: codes = self._new_charset("B") elif char in code128.A: codes = self._new_charset("A") - if len(self._buffer) == 1: - codes.append(self._convert(self._buffer[0])) - self._buffer = "" + assert self._charset != "C" + if len(self._digit_buffer) == 1: + # Flush the remaining single digit from the buffer + codes.append(self._convert(self._digit_buffer[0])) + self._digit_buffer = "" elif self._charset == "B": if look_next(): codes = self._new_charset("C") - elif char not in code128.B: - if char in code128.A: - codes = self._new_charset("A") + elif char not in code128.B and char in code128.A: + codes = self._new_charset("A") elif self._charset == "A": if look_next(): codes = self._new_charset("C") - elif char not in code128.A: - if char in code128.B: - codes = self._new_charset("B") + elif char not in code128.A and char in code128.B: + codes = self._new_charset("B") return codes - def _convert(self, char): + def _convert(self, char: str) -> int: + """Convert a character to a code number for the current charset. + + NOTE: encoding digits with charset C requires buffering and is not supported + here. Use _convert_or_buffer instead. + """ if self._charset == "A": return code128.A[char] - elif self._charset == "B": + if self._charset == "B": return code128.B[char] - elif self._charset == "C": - if char in code128.C: + if self._charset == "C": + if char in ["TO_A", "TO_B"]: return code128.C[char] - elif char.isdigit(): - self._buffer += char - if len(self._buffer) == 2: - value = int(self._buffer) - self._buffer = "" - return value - - def _try_to_optimize(self, encoded): + raise RuntimeError("Use _convert_or_buffer for charset C.") + raise RuntimeError( + f"Character {char} could not be converted in charset {self._charset}." + ) + + def _convert_or_buffer(self, char: str) -> int | None: + """Convert a character to a code number for the current charset. + + If charset C is active then digits are encoded in pairs. When the first digit + is encountered, it is buffered and None is returned. + """ + if self._charset != "C": + return self._convert(char) + if char in code128.C: + return code128.C[char] + if char.isdigit(): + self._digit_buffer += char + if len(self._digit_buffer) == 1: + # Wait for the second digit to group in pairs + return None + assert len(self._digit_buffer) == 2 + value = int(self._digit_buffer) + self._digit_buffer = "" + return value + raise RuntimeError(f"Character {char} could not be converted in charset C.") + + def _try_to_optimize(self, encoded: list[int]) -> list[int]: if encoded[1] in code128.TO: encoded[:2] = [code128.TO[encoded[1]]] return encoded - def _calculate_checksum(self, encoded): + def _calculate_checksum(self, encoded: list[int]) -> int: cs = [encoded[0]] for i, code_num in enumerate(encoded[1:], start=1): cs.append(i * code_num) return sum(cs) % 103 - def _build(self): - encoded = [code128.START_CODES[self._charset]] + def _build(self) -> list[int]: + encoded: list[int] = [code128.START_CODES[self._charset]] for i, char in enumerate(self.code): encoded.extend(self._maybe_switch_charset(i)) - code_num = self._convert(char) + code_num = self._convert_or_buffer(char) if code_num is not None: encoded.append(code_num) - # Finally look in the buffer - if len(self._buffer) == 1: + # If we finish in charset C with a single digit remaining in the buffer, + # switch to charset B and flush out the buffer. + if len(self._digit_buffer) == 1: encoded.extend(self._new_charset("B")) - encoded.append(self._convert(self._buffer[0])) - self._buffer = "" - encoded = self._try_to_optimize(encoded) - return encoded + encoded.append(self._convert(self._digit_buffer[0])) + self._digit_buffer = "" + return self._try_to_optimize(encoded) - def build(self): + def build(self) -> list[str]: encoded = self._build() encoded.append(self._calculate_checksum(encoded)) code = "" @@ -248,10 +305,10 @@ def build(self): def render(self, writer_options=None, text=None): options = {"module_width": MIN_SIZE, "quiet_zone": MIN_QUIET_ZONE} options.update(writer_options or {}) - return Barcode.render(self, options, text) + return super().render(options, text) -class Gs1_128(Code128): +class Gs1_128(Code128): # noqa: N801 """ following the norm, a gs1-128 barcode is a subset of code 128 barcode, it can be generated by prepending the code with the FNC1 character @@ -263,13 +320,16 @@ class Gs1_128(Code128): FNC1_CHAR = "\xf1" - def __init__(self, code, writer=None): + def __init__(self, code, writer=None) -> None: code = self.FNC1_CHAR + code super().__init__(code, writer) def get_fullcode(self): return super().get_fullcode()[1:] + def _is_char_fnc1_char(self, char): + return char == self.FNC1_CHAR + # For pre 0.8 compatibility PZN = PZN7 diff --git a/barcode/ean.py b/barcode/ean.py index 0b4ab5b..e001ccc 100755 --- a/barcode/ean.py +++ b/barcode/ean.py @@ -2,17 +2,17 @@ :Provided barcodes: EAN-14, EAN-13, EAN-8, JAN """ + +from __future__ import annotations + __docformat__ = "restructuredtext en" -from functools import reduce from barcode.base import Barcode from barcode.charsets import ean as _ean -from barcode.errors import ( - IllegalCharacterError, - NumberOfDigitsError, - WrongCountryCodeError, -) +from barcode.errors import IllegalCharacterError +from barcode.errors import NumberOfDigitsError +from barcode.errors import WrongCountryCodeError # EAN13 Specs (all sizes in mm) SIZES = { @@ -32,90 +32,112 @@ class EuropeanArticleNumber13(Barcode): """Initializes EAN13 object. - :parameters: - ean : String - The ean number as string. - writer : barcode.writer Instance - The writer to render the barcode (default: SVGWriter). + :param ean: The ean number as string. If the value is too long, it is trimmed. + :param writer: The writer to render the barcode (default: SVGWriter). + :param no_checksum: Don't calculate the checksum. Use the provided input instead. """ name = "EAN-13" digits = 12 - def __init__(self, ean, writer=None, no_checksum=False): - ean = ean[: self.digits] - if not ean.isdigit(): - raise IllegalCharacterError("EAN code can only contain numbers.") - if len(ean) != self.digits: + def __init__( + self, + ean: str, + writer=None, + no_checksum: bool = False, + guardbar: bool = False, + ) -> None: + if not ean[: self.digits].isdigit(): + raise IllegalCharacterError(f"EAN code can only contain numbers {ean}.") + + if len(ean) < self.digits: raise NumberOfDigitsError( - "EAN must have {} digits, not {}.".format( - self.digits, - len(ean), - ) + f"EAN must have {self.digits} digits, received {len(ean)}." ) - self.ean = ean - # If no checksum + + base = ean[: self.digits] if no_checksum: - # Add a thirteen char if given in parameter, - # otherwise pad with zero - self.ean = "{}{}".format( - ean, ean[self.digits] if len(ean) > self.digits else 0 - ) + # Use the thirteenth digit if given in parameter, otherwise pad with zero + if len(ean) > self.digits and ean[self.digits].isdigit(): + last = int(ean[self.digits]) + else: + last = 0 + else: + last = self.calculate_checksum(base) + + self.ean = f"{base}{last}" + + self.guardbar = guardbar + if guardbar: + self.EDGE = _ean.EDGE.replace("1", "G") + self.MIDDLE = _ean.MIDDLE.replace("1", "G") else: - self.ean = "{}{}".format(ean, self.calculate_checksum()) - self.writer = writer or Barcode.default_writer() + self.EDGE = _ean.EDGE + self.MIDDLE = _ean.MIDDLE + self.writer = writer or self.default_writer() - def __str__(self): + def __str__(self) -> str: return self.ean - def get_fullcode(self): + def get_fullcode(self) -> str: + if self.guardbar: + return self.ean[0] + " " + self.ean[1:7] + " " + self.ean[7:] + " >" return self.ean - def calculate_checksum(self): - """Calculates the checksum for EAN13-Code. + def calculate_checksum(self, value: str | None = None) -> int: + """Calculates and returns the checksum for EAN13-Code. - :returns: The checksum for `self.ean`. - :rtype: Integer + Calculates the checksum for the supplied `value` (if any) or for this barcode's + internal ``self.ean`` property. """ - def sum_(x, y): - return int(x) + int(y) + ean_without_checksum = value or self.ean[: self.digits] - evensum = reduce(sum_, self.ean[-2::-2]) - oddsum = reduce(sum_, self.ean[-1::-2]) + evensum = sum(int(x) for x in ean_without_checksum[-2::-2]) + oddsum = sum(int(x) for x in ean_without_checksum[-1::-2]) return (10 - ((evensum + oddsum * 3) % 10)) % 10 - def build(self): + def build(self) -> list[str]: """Builds the barcode pattern from `self.ean`. :returns: The pattern as string - :rtype: String + :rtype: List containing the string as a single element """ - code = _ean.EDGE[:] + code = self.EDGE[:] pattern = _ean.LEFT_PATTERN[int(self.ean[0])] for i, number in enumerate(self.ean[1:7]): code += _ean.CODES[pattern[i]][int(number)] - code += _ean.MIDDLE + code += self.MIDDLE for number in self.ean[7:]: code += _ean.CODES["C"][int(number)] - code += _ean.EDGE + code += self.EDGE return [code] - def to_ascii(self): + def to_ascii(self) -> str: """Returns an ascii representation of the barcode. :rtype: String """ - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("1", "|").replace("0", " ") - return "\n".join(code) + code_list = self.build() + if not len(code_list) == 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("G", "|").replace("1", "|").replace("0", " ") - def render(self, writer_options=None, text=None): + def render(self, writer_options: dict | None = None, text: str | None = None): options = {"module_width": SIZES["SC2"]} options.update(writer_options or {}) - return Barcode.render(self, options, text) + return super().render(options, text) + + +class EuropeanArticleNumber13WithGuard(EuropeanArticleNumber13): + """A shortcut to EAN-13 with ``guardbar=True``.""" + + name = "EAN-13 with guards" + + def __init__(self, ean, writer=None, no_checksum=False, guardbar=True) -> None: + super().__init__(ean, writer, no_checksum, guardbar) class JapanArticleNumber(EuropeanArticleNumber13): @@ -132,77 +154,89 @@ class JapanArticleNumber(EuropeanArticleNumber13): valid_country_codes = list(range(450, 460)) + list(range(490, 500)) - def __init__(self, jan, writer=None): - if int(jan[:3]) not in JapanArticleNumber.valid_country_codes: + def __init__(self, jan, *args, **kwargs) -> None: + if int(jan[:3]) not in self.valid_country_codes: raise WrongCountryCodeError( "Country code isn't between 450-460 or 490-500." ) - EuropeanArticleNumber13.__init__(self, jan, writer) + super().__init__(jan, *args, **kwargs) class EuropeanArticleNumber8(EuropeanArticleNumber13): """Represents an EAN-8 barcode. See EAN13's __init__ for details. - :parameters: - ean : String - The ean number as string. - writer : barcode.writer Instance - The writer to render the barcode (default: SVGWriter). + :param ean: The ean number as string. + :param writer: The writer to render the barcode (default: SVGWriter). """ name = "EAN-8" digits = 7 - def __init__(self, ean, writer=None): - EuropeanArticleNumber13.__init__(self, ean, writer) - - def build(self): + def build(self) -> list[str]: """Builds the barcode pattern from `self.ean`. - :returns: The pattern as string - :rtype: String + :returns: A list containing the string as a single element """ - code = _ean.EDGE[:] + code = self.EDGE[:] for number in self.ean[:4]: code += _ean.CODES["A"][int(number)] - code += _ean.MIDDLE + code += self.MIDDLE for number in self.ean[4:]: code += _ean.CODES["C"][int(number)] - code += _ean.EDGE + code += self.EDGE return [code] + def get_fullcode(self): + if self.guardbar: + return "< " + self.ean[:4] + " " + self.ean[4:] + " >" + return self.ean + + +class EuropeanArticleNumber8WithGuard(EuropeanArticleNumber8): + """A shortcut to EAN-8 with ``guardbar=True``.""" + + name = "EAN-8 with guards" + + def __init__( + self, + ean: str, + writer=None, + no_checksum: bool = False, + guardbar: bool = True, + ) -> None: + super().__init__(ean, writer, no_checksum, guardbar) + class EuropeanArticleNumber14(EuropeanArticleNumber13): """Represents an EAN-14 barcode. See EAN13's __init__ for details. - :parameters: - ean : String - The ean number as string. - writer : barcode.writer Instance - The writer to render the barcode (default: SVGWriter). + :param ean: The ean number as string. + :param writer: The writer to render the barcode (default: SVGWriter). + :param no_checksum: Don't calculate the checksum. Use the provided input instead. """ name = "EAN-14" digits = 13 - def calculate_checksum(self): - """Calculates the checksum for EAN13-Code. + def calculate_checksum(self, value: str | None = None) -> int: + """Calculates and returns the checksum for EAN14-Code. - :returns: The checksum for `self.ean`. - :rtype: Integer + Calculates the checksum for the supplied `value` (if any) or for this barcode's + internal ``self.ean`` property. """ - def sum_(x, y): - return int(x) + int(y) + ean_without_checksum = value or self.ean[: self.digits] - evensum = reduce(sum_, self.ean[::2]) - oddsum = reduce(sum_, self.ean[1::2]) + evensum = sum(int(x) for x in ean_without_checksum[::2]) + oddsum = sum(int(x) for x in ean_without_checksum[1::2]) return (10 - (((evensum * 3) + oddsum) % 10)) % 10 # Shortcuts EAN14 = EuropeanArticleNumber14 EAN13 = EuropeanArticleNumber13 +EAN13_GUARD = EuropeanArticleNumber13WithGuard EAN8 = EuropeanArticleNumber8 +EAN8_GUARD = EuropeanArticleNumber8WithGuard JAN = JapanArticleNumber diff --git a/barcode/errors.py b/barcode/errors.py index 7fb448f..4725187 100755 --- a/barcode/errors.py +++ b/barcode/errors.py @@ -1,12 +1,15 @@ """barcode.errors""" + +from __future__ import annotations + __docformat__ = "restructuredtext en" class BarcodeError(Exception): - def __init__(self, msg): + def __init__(self, msg) -> None: self.msg = msg - def __str__(self): + def __str__(self) -> str: return self.msg diff --git a/barcode/isxn.py b/barcode/isxn.py index 3d73ce1..e74ea58 100755 --- a/barcode/isxn.py +++ b/barcode/isxn.py @@ -21,6 +21,9 @@ '0132354187' """ + +from __future__ import annotations + from barcode.ean import EuropeanArticleNumber13 from barcode.errors import BarcodeError from barcode.errors import WrongCountryCodeError @@ -40,15 +43,14 @@ class InternationalStandardBookNumber13(EuropeanArticleNumber13): name = "ISBN-13" - def __init__(self, isbn, writer=None): + def __init__(self, isbn, writer=None, no_checksum=False, guardbar=False) -> None: isbn = isbn.replace("-", "") self.isbn13 = isbn if isbn[:3] not in ("978", "979"): raise WrongCountryCodeError("ISBN must start with 978 or 979.") - if isbn[:3] == "979": - if isbn[3:5] not in ("10", "11"): - raise BarcodeError("ISBN must start with 97910 or 97911.") - EuropeanArticleNumber13.__init__(self, isbn, writer) + if isbn[:3] == "979" and isbn[3:4] not in ("1", "8"): + raise BarcodeError("ISBN must start with 97910 or 97911.") + super().__init__(isbn, writer, no_checksum, guardbar) class InternationalStandardBookNumber10(InternationalStandardBookNumber13): @@ -66,21 +68,21 @@ class InternationalStandardBookNumber10(InternationalStandardBookNumber13): digits = 9 - def __init__(self, isbn, writer=None): + def __init__(self, isbn, writer=None) -> None: isbn = isbn.replace("-", "") isbn = isbn[: self.digits] + super().__init__("978" + isbn, writer) self.isbn10 = isbn - self.isbn10 = "{}{}".format(isbn, self._calculate_checksum()) - InternationalStandardBookNumber13.__init__(self, "978" + isbn, writer) + self.isbn10 = f"{isbn}{self._calculate_checksum()}" def _calculate_checksum(self): tmp = sum(x * int(y) for x, y in enumerate(self.isbn10[:9], start=1)) % 11 if tmp == 10: return "X" - else: - return tmp - def __str__(self): + return tmp + + def __str__(self) -> str: return self.isbn10 @@ -99,12 +101,12 @@ class InternationalStandardSerialNumber(EuropeanArticleNumber13): digits = 7 - def __init__(self, issn, writer=None): + def __init__(self, issn, writer=None) -> None: issn = issn.replace("-", "") issn = issn[: self.digits] self.issn = issn - self.issn = "{}{}".format(issn, self._calculate_checksum()) - EuropeanArticleNumber13.__init__(self, self.make_ean(), writer) + self.issn = f"{issn}{self._calculate_checksum()}" + super().__init__(self.make_ean(), writer) def _calculate_checksum(self): tmp = ( @@ -114,13 +116,13 @@ def _calculate_checksum(self): ) if tmp == 10: return "X" - else: - return tmp + + return tmp def make_ean(self): - return "977{}00{}".format(self.issn[:7], self._calculate_checksum()) + return f"977{self.issn[:7]}00{self._calculate_checksum()}" - def __str__(self): + def __str__(self) -> str: return self.issn diff --git a/barcode/itf.py b/barcode/itf.py index 4978dcd..f40d9cb 100644 --- a/barcode/itf.py +++ b/barcode/itf.py @@ -2,6 +2,9 @@ :Provided barcodes: Interleaved 2 of 5 """ + +from __future__ import annotations + __docformat__ = "restructuredtext en" from barcode.base import Barcode @@ -29,24 +32,24 @@ class ITF(Barcode): name = "ITF" - def __init__(self, code, writer=None, narrow=2, wide=5): + def __init__(self, code, writer=None, narrow=2, wide=5) -> None: if not code.isdigit(): raise IllegalCharacterError("ITF code can only contain numbers.") # Length must be even, prepend 0 if necessary if len(code) % 2 != 0: code = "0" + code self.code = code - self.writer = writer or Barcode.default_writer() + self.writer = writer or self.default_writer() self.narrow = narrow self.wide = wide - def __str__(self): + def __str__(self) -> str: return self.code def get_fullcode(self): return self.code - def build(self): + def build(self) -> list[str]: data = itf.START for i in range(0, len(self.code), 2): bars_digit = int(self.code[i]) @@ -73,4 +76,4 @@ def render(self, writer_options, text=None): "quiet_zone": MIN_QUIET_ZONE, } options.update(writer_options or {}) - return Barcode.render(self, options, text) + return super().render(options, text) diff --git a/barcode/pybarcode.py b/barcode/pybarcode.py index c44723c..35d69b0 100644 --- a/barcode/pybarcode.py +++ b/barcode/pybarcode.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import os from argparse import ArgumentParser import barcode from barcode.version import version +from barcode.writer import BaseWriter from barcode.writer import ImageWriter from barcode.writer import SVGWriter IMG_FORMATS = ("BMP", "GIF", "JPEG", "MSP", "PCX", "PNG", "TIFF", "XBM") -def list_types(args, parser=None): +def list_types(args, parser=None) -> None: print("\npython-barcode available barcode formats:") print(", ".join(barcode.PROVIDED_BARCODES)) print("\n") @@ -22,33 +25,28 @@ def list_types(args, parser=None): print("\n") -def create_barcode(args, parser): +def create_barcode(args, parser) -> None: args.type = args.type.upper() if args.type != "SVG" and args.type not in IMG_FORMATS: - parser.error( - "Unknown type {type}. Try list action for available types.".format( - type=args.type - ) - ) + parser.error(f"Unknown type {args.type}. Try list action for available types.") args.barcode = args.barcode.lower() if args.barcode not in barcode.PROVIDED_BARCODES: parser.error( - "Unknown barcode {bc}. Try list action for available barcodes.".format( - bc=args.barcode - ) + f"Unknown barcode {args.barcode}. Try list action for available barcodes." ) if args.type != "SVG": + assert ImageWriter is not None opts = {"format": args.type} - writer = ImageWriter() + writer: BaseWriter = ImageWriter() else: opts = {"compress": args.compress} writer = SVGWriter() out = os.path.normpath(os.path.abspath(args.output)) name = barcode.generate(args.barcode, args.code, writer, out, opts, args.text) - print("New barcode saved as {}.".format(name)) + print(f"New barcode saved as {name}.") -def main(): +def main() -> None: msg = [] if ImageWriter is None: msg.append("Image output disabled (Pillow not found), --type option disabled.") diff --git a/barcode/upc.py b/barcode/upc.py index c3ad3c8..060f19f 100755 --- a/barcode/upc.py +++ b/barcode/upc.py @@ -2,13 +2,17 @@ :Provided barcodes: UPC-A """ + +from __future__ import annotations + __docformat__ = "restructuredtext en" from functools import reduce from barcode.base import Barcode from barcode.charsets import upc as _upc -from barcode.errors import IllegalCharacterError, NumberOfDigitsError +from barcode.errors import IllegalCharacterError +from barcode.errors import NumberOfDigitsError class UniversalProductCodeA(Barcode): @@ -21,7 +25,7 @@ class UniversalProductCodeA(Barcode): digits = 11 - def __init__(self, upc, writer=None, make_ean=False): + def __init__(self, upc, writer=None, make_ean=False) -> None: """Initializes new UPC-A barcode. :param str upc: The upc number as string. @@ -37,23 +41,23 @@ def __init__(self, upc, writer=None, make_ean=False): raise IllegalCharacterError("UPC code can only contain numbers.") if len(upc) != self.digits: raise NumberOfDigitsError( - "UPC must have {} digits, not {}.".format(self.digits, len(upc)) + f"UPC must have {self.digits} digits, not {len(upc)}." ) self.upc = upc - self.upc = "{}{}".format(upc, self.calculate_checksum()) - self.writer = writer or Barcode.default_writer() + self.upc = f"{upc}{self.calculate_checksum()}" + self.writer = writer or self.default_writer() - def __str__(self): + def __str__(self) -> str: if self.ean: return "0" + self.upc - else: - return self.upc + + return self.upc def get_fullcode(self): if self.ean: return "0" + self.upc - else: - return self.upc + + return self.upc def calculate_checksum(self): """Calculates the checksum for UPCA/UPC codes @@ -71,14 +75,14 @@ def sum_(x, y): check = (evensum + oddsum * 3) % 10 if check == 0: return 0 - else: - return 10 - check - def build(self): + return 10 - check + + def build(self) -> list[str]: """Builds the barcode pattern from 'self.upc' :return: The pattern as string - :rtype: str + :rtype: List containing the string as a single element """ code = _upc.EDGE[:] @@ -94,21 +98,22 @@ def build(self): return [code] - def to_ascii(self): + def to_ascii(self) -> str: """Returns an ascii representation of the barcode. :rtype: str """ - code = self.build() - for i, line in enumerate(code): - code[i] = line.replace("1", "|").replace("0", "_") - return "\n".join(code) + code_list = self.build() + if len(code_list) != 1: + raise RuntimeError("Code list must contain a single element.") + code = code_list[0] + return code.replace("1", "|").replace("0", "_") def render(self, writer_options=None, text=None): options = {"module_width": 0.33} options.update(writer_options or {}) - return Barcode.render(self, options, text) + return super().render(options, text) UPCA = UniversalProductCodeA diff --git a/barcode/writer.py b/barcode/writer.py index b7fd6b4..1bed3fc 100755 --- a/barcode/writer.py +++ b/barcode/writer.py @@ -1,46 +1,73 @@ +from __future__ import annotations + import gzip import os -import xml.dom +import xml.dom.minidom +from typing import TYPE_CHECKING from typing import BinaryIO +from typing import Callable +from typing import TypedDict from barcode.version import version +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Literal + + from PIL.Image import Image as T_Image + from PIL.ImageDraw import ImageDraw as T_ImageDraw + + class InternalText(TypedDict): + start: list + end: list + xpos: list + was_guard: bool + + class Callbacks(TypedDict): + initialize: Callable | None + paint_module: Callable + paint_text: Callable | None + finish: Callable + + try: - import Image - import ImageDraw - import ImageFont + from PIL import Image + from PIL import ImageDraw + from PIL import ImageFont except ImportError: - try: - from PIL import Image, ImageDraw, ImageFont # lint:ok - except ImportError: - import logging + import logging - log = logging.getLogger("pyBarcode") - log.info("Pillow not found. Image output disabled") - Image = ImageDraw = ImageFont = None # lint:ok + log = logging.getLogger("pyBarcode") + log.info("Pillow not found. Image output disabled") + Image = ImageDraw = ImageFont = None # type: ignore[assignment] -def mm2px(mm, dpi=300): +def mm2px(mm: float, dpi: int) -> float: return (mm * dpi) / 25.4 -def pt2mm(pt): +def pt2mm(pt: float) -> float: return pt * 0.352777778 -def _set_attributes(element, **attributes): +def _set_attributes( + element: xml.dom.minidom.Element, + **attributes: str, +) -> None: for key, value in attributes.items(): element.setAttribute(key, value) -def create_svg_object(with_doctype=False): - imp = xml.dom.getDOMImplementation() +def create_svg_object(with_doctype: bool = False) -> xml.dom.minidom.Document: + imp = xml.dom.minidom.getDOMImplementation() + assert imp is not None doctype = imp.createDocumentType( "svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", ) document = imp.createDocument(None, "svg", doctype if with_doctype else None) + assert document.documentElement is not None _set_attributes( document.documentElement, version="1.1", xmlns="http://www.w3.org/2000/svg" ) @@ -48,38 +75,53 @@ def create_svg_object(with_doctype=False): SIZE = "{0:.3f}mm" -COMMENT = "Autogenerated with python-barcode {}".format(version) +COMMENT = f"Autogenerated with python-barcode {version}" PATH = os.path.dirname(os.path.abspath(__file__)) class BaseWriter: """Baseclass for all writers. - Initializes the basic writer options. Childclasses can add more - attributes and can set them directly or using - `self.set_options(option=value)`. - - :parameters: - initialize : Function - Callback for initializing the inheriting writer. - Is called: `callback_initialize(raw_code)` - paint_module : Function - Callback for painting one barcode module. - Is called: `callback_paint_module(xpos, ypos, width, color)` - paint_text : Function - Callback for painting the text under the barcode. - Is called: `callback_paint_text(xpos, ypos)` using `self.text` - as text. - finish : Function - Callback for doing something with the completely rendered - output. - Is called: `return callback_finish()` and must return the - rendered output. + Initializes the basic writer options. Child classes can add more attributes and can + set them directly or using ``self.set_options(option=value)``. + + :param initialize: Callback for initializing the inheriting writer. + Is called: ``callback_initialize(raw_code)`` + :param paint_module: + Callback for painting one barcode module. + Is called: ``callback_paint_module(xpos, ypos, width, color)`` + :param paint_text: Callback for painting the text under the barcode. + Is called: ``callback_paint_text(xpos, ypos)`` using `self.text` + as text. + :param finish: Callback for doing something with the completely rendered + output. Is called: ``return callback_finish()`` and must return the + rendered output. """ + _callbacks: Callbacks + module_width: float + module_height: float + font_path: str + font_size: float + quiet_zone: float + background: str | int + foreground: str | int + text: str + human: str + text_distance: float + text_line_distance: float + center_text: bool + guard_height_factor: float + margin_top: float + margin_bottom: float + def __init__( - self, initialize=None, paint_module=None, paint_text=None, finish=None - ): + self, + initialize: Callable | None, + paint_module: Callable, + paint_text: Callable | None, + finish: Callable, + ) -> None: self._callbacks = { "initialize": initialize, "paint_module": paint_module, @@ -98,21 +140,22 @@ def __init__( self.text_distance = 5 self.text_line_distance = 1 self.center_text = True + self.guard_height_factor = 1.1 + self.margin_top = 1 + self.margin_bottom = 1 - def calculate_size(self, modules_per_line, number_of_lines): + def calculate_size(self, modules_per_line: int, number_of_lines: int) -> tuple: """Calculates the size of the barcode in pixel. - :parameters: - modules_per_line : Integer - Number of modules in one line. - number_of_lines : Integer - Number of lines of the barcode. + :param modules_per_line: Number of modules in one line. + :param number_of_lines: Number of lines of the barcode. :returns: Width and height of the barcode in pixel. - :rtype: Tuple """ width = 2 * self.quiet_zone + modules_per_line * self.module_width - height = 2.0 + self.module_height * number_of_lines + height = ( + self.margin_bottom + self.margin_top + self.module_height * number_of_lines + ) number_of_text_lines = len(self.text.splitlines()) if self.font_size and self.text: height += ( @@ -121,174 +164,224 @@ def calculate_size(self, modules_per_line, number_of_lines): height += self.text_line_distance * (number_of_text_lines - 1) return width, height - def save(self, filename, output): + def save(self, filename: str, output) -> str: """Saves the rendered output to `filename`. - :parameters: - filename : String - Filename without extension. - output : String - The rendered output. + :param filename: Filename without extension. + :param output: The rendered output. :returns: The full filename with extension. - :rtype: String """ raise NotImplementedError - def register_callback(self, action, callback): - """Register one of the three callbacks if not given at instance - creation. + def register_callback( + self, + action: Literal["initialize", "paint_module", "paint_text", "finish"], + callback: Callable, + ) -> None: + """Register one of the three callbacks if not given at instance creation. - :parameters: - action : String - One of 'initialize', 'paint_module', 'paint_text', 'finish'. - callback : Function - The callback function for the given action. + :param action: One of 'initialize', 'paint_module', 'paint_text', 'finish'. + :param callback: The callback function for the given action. """ self._callbacks[action] = callback - def set_options(self, options): + def set_options(self, options: dict) -> None: """Sets the given options as instance attributes (only if they are known). - :parameters: - options : Dict - All known instance attributes and more if the childclass - has defined them before this call. - - :rtype: None + :param options: All known instance attributes and more if the child class + has defined them before this call. """ for key, val in options.items(): key = key.lstrip("_") if hasattr(self, key): setattr(self, key, val) - def render(self, code): + def packed(self, line: str) -> Generator[tuple[int, float], str, None]: + """ + Pack line to list give better gfx result, otherwise in can + result in aliasing gaps + '11010111' -> [2, -1, 1, -1, 3] + + This method will yield a sequence of pairs (width, height_factor). + + :param line: A string matching the writer spec (only contain 0 or 1 or G). + """ + line += " " + c = 1 + for i in range(len(line) - 1): + if line[i] == line[i + 1]: + c += 1 + else: + if line[i] == "1": + yield (c, 1) + elif line[i] == "G": + yield (c, self.guard_height_factor) + else: + yield (-c, self.guard_height_factor) + c = 1 + + def render(self, code: list[str]): """Renders the barcode to whatever the inheriting writer provides, using the registered callbacks. :parameters: code : List - List of strings matching the writer spec - (only contain 0 or 1). + List consisting of a single string matching the writer spec + (only contain 0 or 1 or G). """ if self._callbacks["initialize"] is not None: self._callbacks["initialize"](code) - ypos = 1.0 - for cc, line in enumerate(code): - """ - Pack line to list give better gfx result, otherwise in can - result in aliasing gaps - '11010111' -> [2, -1, 1, -1, 3] - """ - line += " " - c = 1 - mlist = [] - for i in range(0, len(line) - 1): - if line[i] == line[i + 1]: - c += 1 - else: - if line[i] == "1": - mlist.append(c) - else: - mlist.append(-c) - c = 1 - # Left quiet zone is x startposition - xpos = self.quiet_zone - bxs = xpos # x start of barcode - for mod in mlist: - if mod < 1: - color = self.background - else: - color = self.foreground - # remove painting for background colored tiles? - self._callbacks["paint_module"]( - xpos, ypos, self.module_width * abs(mod), color - ) - xpos += self.module_width * abs(mod) - bxe = xpos - # Add right quiet zone to every line, except last line, - # quiet zone already provided with background, - # should it be removed completely? - if (cc + 1) != len(code): - self._callbacks["paint_module"]( - xpos, ypos, self.quiet_zone, self.background - ) - ypos += self.module_height + ypos = self.margin_top + base_height = self.module_height + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + # Left quiet zone is x startposition + xpos = self.quiet_zone + bxs = xpos # x start of barcode + text: InternalText = { + "start": [], # The x start of a guard + "end": [], # The x end of a guard + "xpos": [], # The x position where to write a text block + # Flag that indicates if the previous mod was part of an guard block: + "was_guard": False, + } + for mod, height_factor in self.packed(line): + if mod < 1: + color = self.background + else: + color = self.foreground + + if text["was_guard"] and height_factor == 1: + # The current guard ended, store its x position + text["end"].append(xpos) + text["was_guard"] = False + elif not text["was_guard"] and height_factor != 1: + # A guard started, store its x position + text["start"].append(xpos) + text["was_guard"] = True + + self.module_height = base_height * height_factor + # remove painting for background colored tiles? + self._callbacks["paint_module"]( + xpos, ypos, self.module_width * abs(mod), color + ) + xpos += self.module_width * abs(mod) + else: + if height_factor != 1: + text["end"].append(xpos) + self.module_height = base_height + + bxe = xpos + ypos += self.module_height + if self.text and self._callbacks["paint_text"] is not None: - ypos += self.text_distance - if self.center_text: - # better center position for text - xpos = bxs + ((bxe - bxs) / 2.0) + if not text["start"]: + # If we don't have any start value, print the entire ean + ypos += self.text_distance + xpos = bxs + (bxe - bxs) / 2.0 if self.center_text else bxs + self._callbacks["paint_text"](xpos, ypos) else: - xpos = bxs - self._callbacks["paint_text"](xpos, ypos) + # Else, divide the ean into blocks and print each block + # in the expected position. + text["xpos"] = [bxs - 4 * self.module_width] + + # Calculates the position of the text by getting the difference + # between a guard end and the next start + text["start"].pop(0) + for s, e in zip(text["start"], text["end"]): + text["xpos"].append(e + (s - e) / 2) + + # The last text block is always put after the last guard end + text["xpos"].append(text["end"][-1] + 4 * self.module_width) + + ypos += pt2mm(self.font_size) + + # Split the ean into its blocks + blocks = self.text.split(" ") + for text_, xpos in zip(blocks, text["xpos"]): + self.text = text_ + self._callbacks["paint_text"](xpos, ypos) + return self._callbacks["finish"]() + def write(self, content, fp: BinaryIO) -> None: + raise NotImplementedError + class SVGWriter(BaseWriter): - def __init__(self): - BaseWriter.__init__( - self, self._init, self._create_module, self._create_text, self._finish + def __init__(self) -> None: + super().__init__( + self._init, + self._create_module, + self._create_text, + self._finish, ) - self.compress = False - self.with_doctype = True - self._document = None - self._root = None - self._group = None - - def _init(self, code): - width, height = self.calculate_size(len(code[0]), len(code)) + self.compress: bool = False + self.with_doctype: bool = True + self._document: xml.dom.minidom.Document + self._root: xml.dom.minidom.Element + self._group: xml.dom.minidom.Element + + def _init(self, code: list[str]): + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + width, height = self.calculate_size(len(line), 1) self._document = create_svg_object(self.with_doctype) + assert self._document.documentElement is not None self._root = self._document.documentElement attributes = { "width": SIZE.format(width), "height": SIZE.format(height), } _set_attributes(self._root, **attributes) - self._root.appendChild(self._document.createComment(COMMENT)) + if COMMENT: + self._root.appendChild(self._document.createComment(COMMENT)) # create group for easier handling in 3rd party software # like corel draw, inkscape, ... group = self._document.createElement("g") attributes = {"id": "barcode_group"} _set_attributes(group, **attributes) self._group = self._root.appendChild(group) - background = self._document.createElement("rect") - attributes = { - "width": "100%", - "height": "100%", - "style": "fill:{}".format(self.background), - } - _set_attributes(background, **attributes) - self._group.appendChild(background) + if self.background is not None: + background = self._document.createElement("rect") + attributes = { + "width": "100%", + "height": "100%", + "style": f"fill:{self.background}", + } + _set_attributes(background, **attributes) + self._group.appendChild(background) def _create_module(self, xpos, ypos, width, color): - element = self._document.createElement("rect") - attributes = { - "x": SIZE.format(xpos), - "y": SIZE.format(ypos), - "width": SIZE.format(width), - "height": SIZE.format(self.module_height), - "style": "fill:{};".format(color), - } - _set_attributes(element, **attributes) - self._group.appendChild(element) + # Background rect has been provided already, so skipping "spaces" + if color != self.background: + element = self._document.createElement("rect") + attributes = { + "x": SIZE.format(xpos), + "y": SIZE.format(ypos), + "width": SIZE.format(width), + "height": SIZE.format(self.module_height), + "style": f"fill:{color};", + } + _set_attributes(element, **attributes) + self._group.appendChild(element) def _create_text(self, xpos, ypos): # check option to override self.text with self.human (barcode as # human readable data, can be used to print own formats) - if self.human != "": - barcodetext = self.human - else: - barcodetext = self.text + barcodetext = self.human if self.human != "" else self.text for subtext in barcodetext.split("\n"): element = self._document.createElement("text") attributes = { "x": SIZE.format(xpos), "y": SIZE.format(ypos), - "style": "fill:{};font-size:{}pt;text-anchor:middle;".format( - self.foreground, - self.font_size, + "style": ( + f"fill:{self.foreground};" + f"font-size:{self.font_size}pt;text-anchor:middle;" ), } _set_attributes(element, **attributes) @@ -297,27 +390,27 @@ def _create_text(self, xpos, ypos): self._group.appendChild(element) ypos += pt2mm(self.font_size) + self.text_line_distance - def _finish(self): + def _finish(self) -> bytes: if self.compress: return self._document.toxml(encoding="UTF-8") - else: - return self._document.toprettyxml( - indent=4 * " ", newl=os.linesep, encoding="UTF-8" - ) - def save(self, filename, output): + return self._document.toprettyxml( + indent=4 * " ", newl=os.linesep, encoding="UTF-8" + ) + + def save(self, filename: str, output) -> str: if self.compress: - _filename = "{}.svgz".format(filename) - f = gzip.open(_filename, "wb") - f.write(output) - f.close() + _filename = f"{filename}.svgz" + with gzip.open(_filename, "wb") as f: + f.write(output) + f.close() else: - _filename = "{}.svg".format(filename) + _filename = f"{filename}.svg" with open(_filename, "wb") as f: f.write(output) return _filename - def write(self, content, fp: BinaryIO): + def write(self, content, fp: BinaryIO) -> None: """Write `content` into a file-like object. Content should be a barcode rendered by this writer. @@ -326,15 +419,20 @@ def write(self, content, fp: BinaryIO): if Image is None: - ImageWriter = None + ImageWriter: type | None = None else: - class ImageWriter(BaseWriter): # type: ignore + class ImageWriter(BaseWriter): # type: ignore[no-redef] format: str mode: str dpi: int - def __init__(self, format="PNG", mode="RGB"): + def __init__( + self, + format: str = "PNG", + mode: str = "RGB", + dpi: int = 300, + ) -> None: """Initialise a new write instance. :params format: The file format for the generated image. This parameter can @@ -342,53 +440,69 @@ def __init__(self, format="PNG", mode="RGB"): :params mode: The colour-mode for the generated image. Set this to RGBA if you wish to use colours with transparency. """ - BaseWriter.__init__( - self, self._init, self._paint_module, self._paint_text, self._finish + super().__init__( + self._init, + self._paint_module, + self._paint_text, + self._finish, ) self.format = format self.mode = mode - self.dpi = 300 - self._image = None - self._draw = None - - def _init(self, code): - width, height = self.calculate_size(len(code[0]), len(code)) + self.dpi = dpi + self._image: T_Image + self._draw: T_ImageDraw + + def _init(self, code: list[str]) -> None: + if ImageDraw is None: + raise RuntimeError("Pillow not found. Cannot create image.") + if len(code) != 1: + raise NotImplementedError("Only one line of code is supported") + line = code[0] + width, height = self.calculate_size(len(line), 1) size = (int(mm2px(width, self.dpi)), int(mm2px(height, self.dpi))) self._image = Image.new(self.mode, size, self.background) self._draw = ImageDraw.Draw(self._image) - def _paint_module(self, xpos, ypos, width, color): + def _paint_module(self, xpos: float, ypos: float, width: float, color): size = [ (mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)), ( - mm2px(xpos + width, self.dpi), + mm2px(xpos + width, self.dpi) - 1, mm2px(ypos + self.module_height, self.dpi), ), ] self._draw.rectangle(size, outline=color, fill=color) def _paint_text(self, xpos, ypos): + assert ImageFont is not None + + # check option to override self.text with self.human (barcode as + # human readable data, can be used to print own formats) + barcodetext = self.human if self.human != "" else self.text + font_size = int(mm2px(pt2mm(self.font_size), self.dpi)) + if font_size <= 0: + return font = ImageFont.truetype(self.font_path, font_size) - for subtext in self.text.split("\n"): - width, height = font.getsize(subtext) - # determine the maximum width of each line + for subtext in barcodetext.split("\n"): pos = ( - mm2px(xpos, self.dpi) - width // 2, - mm2px(ypos, self.dpi) - height, + mm2px(xpos, self.dpi), + mm2px(ypos, self.dpi), + ) + self._draw.text( + pos, subtext, font=font, fill=self.foreground, anchor="md" ) - self._draw.text(pos, subtext, font=font, fill=self.foreground) ypos += pt2mm(self.font_size) / 2 + self.text_line_distance - def _finish(self): + def _finish(self) -> T_Image: return self._image - def save(self, filename, output): - filename = "{}.{}".format(filename, self.format.lower()) + def save(self, filename: str, output) -> str: + filename = f"{filename}.{self.format.lower()}" output.save(filename, self.format.upper()) return filename - def write(self, content, fp: BinaryIO): + def write(self, content, fp: BinaryIO) -> None: """Write `content` into a file-like object. Content should be a barcode rendered by this writer. diff --git a/docs/changelog.rst b/docs/changelog.rst index b44e87d..6309ee6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,64 @@ Changelog --------- +v0.16.2 +~~~~~~~ +* Add support for Python 3.13. + +v0.16.1 +~~~~~~~ +* Switch from ``setup.py`` to ``pyproject.toml``. Only affects how installation + from source is performed, and has no runtime impact. + +v0.16.0 +~~~~~~~ + +* **Breaking** Drop support for Python 3.7 and 3.8. +* Make image DPI configurable. +* Fixed inconsistent checksum calculation when calculating the checksum + multiple times for EAN barcodes. +* Update the documentation with some barcodes that were not previously + documented. +* Specifying ``None`` as a background for the ``SVGWriter``, no background is + included resulting in a transparent background. +* Do not paint text if its size would be zero, to avoid an "invalid ppem value" + error with newer versions of Pillow. +* Optimization of code creation, avoiding to many charset switch. + This results in shorter codes; according to GS1 codes should not + be longer than 165 mm (6.5"). (#232) + +v0.15.1 +~~~~~~~ + +* Add missing dependency to release script. + +v0.15.0 +~~~~~~~ + +* **Breaking** Dropped support for Python 3.6 and 3.7. +* Added support for Python 3.11. +* Fixed compatibility with Pillow 10.0. +* Updated ISBN to support newer allocated ranges. +* Improved type hints. + +v0.14.0 +~~~~~~~ + +* **Breaking**: The default dimensions have changed slightly. This is so that + the results of generating a PNG and an SVG look more alike. +* Previous versions included an empty text element for SVGs with no comment. + This is no longer the case. +* Some internals have been improved so as to allow better subclassing. + Subclasses of ``Barcode`` can now override ``default_writer_options`` and + ``default_writer()``. +* A ``guardbar`` parameter has been added to EAN barcodes. This renders + barcodes with guardars (longer bars). +* Added support for Python 3.10. +* The documentation setup has been redone, hopefully squashing a lot of legacy + quirks. +* Previous versions installed the `tests` module. This was not intentional and + have been fixed. + v0.13.1 ~~~~~~~ @@ -11,6 +69,8 @@ v0.13.0 * Added support for transparent backgrounds. This is done by setting the ``mode`` option for a writer to ``RGBA``. +* Dropped support for Python 3.5. +* Added support for Python 3.9. v0.12.0 ~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index e19d18e..81e3338 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +# noqa: INP001 # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full @@ -8,15 +9,14 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +from __future__ import annotations + import barcode # -- Project information ----------------------------------------------------- project = "python-barcode" -copyright = "2020, Hugo Osvaldo Barrera, et al" +copyright = "2017-2023, Hugo Osvaldo Barrera, et al" author = "Hugo Osvaldo Barrera, et al" # The short X.Y version. @@ -61,7 +61,6 @@ "github_user": "WhyNotHugo", "github_repo": "python-barcode", "github_banner": "true", - "sidebar_collapse": False, } graphviz_output_format = "svg" diff --git a/docs/contents.rst b/docs/contents.rst index 1ddf7c4..de71562 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -36,7 +36,7 @@ Issues and source code are all in `GitHub `_ for further +Donations are welcome. See `here `_ for further details. Licence diff --git a/docs/getting-started.rst b/docs/getting-started.rst index e819933..49cc47a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -43,7 +43,7 @@ Generating SVG files # Write to a file-like object: rv = BytesIO() - EAN13(str("100000902922"), writer=SVGWriter()).write(rv) + EAN13("100000902922", writer=SVGWriter()).write(rv) # Or to an actual file: with open("somefile.svg", "wb") as f: diff --git a/docs/index.rst b/docs/index.rst new file mode 120000 index 0000000..a3419c6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1 @@ +contents.rst \ No newline at end of file diff --git a/docs/supported-formats.rst b/docs/supported-formats.rst index 5dc8548..20d8601 100644 --- a/docs/supported-formats.rst +++ b/docs/supported-formats.rst @@ -4,6 +4,15 @@ Supported Formats The following are the supported barcode formats. PRs for other code formats are welcome! +Codabar +------- + +.. inheritance-diagram:: barcode.codabar.CODABAR + :parts: 1 + +.. autoclass:: barcode.codabar.CODABAR + :members: + Code 39 ------- @@ -24,34 +33,48 @@ Code 128 .. autoclass:: barcode.codex.Code128 :members: -PZN7 (aka: PZN) +PZN (aka: PZN7) --------------- -.. inheritance-diagram:: barcode.codex.PZN7 +.. inheritance-diagram:: barcode.codex.PZN :parts: 1 -.. autoclass:: barcode.codex.PZN7 +.. autoclass:: barcode.codex.PZN :members: - EAN-13 ------ -.. inheritance-diagram:: barcode.ean.EuropeanArticleNumber13 +.. inheritance-diagram:: barcode.ean.EuropeanArticleNumber13WithGuard :parts: 1 .. autoclass:: barcode.ean.EuropeanArticleNumber13 :members: +.. autoclass:: barcode.ean.EuropeanArticleNumber13WithGuard + :members: + EAN-8 ----- -.. inheritance-diagram:: barcode.ean.EuropeanArticleNumber8 +.. inheritance-diagram:: barcode.ean.EuropeanArticleNumber8WithGuard :parts: 1 .. autoclass:: barcode.ean.EuropeanArticleNumber8 :members: +.. autoclass:: barcode.ean.EuropeanArticleNumber8WithGuard + :members: + +EAN-14 +------ + +.. inheritance-diagram:: barcode.ean.EuropeanArticleNumber14 + :parts: 1 + +.. autoclass:: barcode.ean.EuropeanArticleNumber14 + :members: + JAN --- @@ -61,8 +84,8 @@ JAN .. autoclass:: barcode.ean.JapanArticleNumber :members: -ISBN-13 -------- +ISBN-13 (aka: GS1, ISBN) +------------------------ .. inheritance-diagram:: barcode.isxn.InternationalStandardBookNumber13 :parts: 1 @@ -97,8 +120,8 @@ UPC-A .. autoclass:: barcode.upc.UniversalProductCodeA :members: -EAN14 ------ +EAN14 (aka: GTIN) +----------------- .. inheritance-diagram:: barcode.ean.EuropeanArticleNumber14 :parts: 1 @@ -116,3 +139,23 @@ GS1-128 .. autoclass:: barcode.codex.Gs1_128 :members: + +ITF +--- + +.. versionadded:: 0.8.0 + +.. inheritance-diagram:: barcode.itf.ITF + :parts: 1 + +.. autoclass:: barcode.itf.ITF + :members: + +UPCA (aka UPC) +-------------- + +.. inheritance-diagram:: barcode.upc.UPCA + :parts: 1 + +.. autoclass:: barcode.upc.UPCA + :members: diff --git a/docs/writers.rst b/docs/writers.rst index f433921..c5399c1 100644 --- a/docs/writers.rst +++ b/docs/writers.rst @@ -32,6 +32,7 @@ be set). :font_size: Font size of the text under the barcode in pt as *integer*. + Font size zero suppresses text. Defaults to **10**. :text_distance: @@ -56,6 +57,14 @@ be set). Some barcode classes change the above defaults to fit in some kind of specification. +BaseWriter +---------- + +Both ``ImageWriter`` and ``SVGWriter`` are subclasses of ``BaseWriter``: + +.. autoclass:: barcode.writer.BaseWriter + :members: + SVGWriter --------- @@ -89,7 +98,7 @@ In addition to the common writer options you can give the following special opti Custom writers -------------- -It's possible to create your own writer by inheriting from `barcode.writer.BaseWriter`. +It's possible to create your own writer by inheriting from ``barcode.writer.BaseWriter``. In your ``__init__`` method call BaseWriter's ``__init__`` and give your callbacks for: @@ -121,5 +130,5 @@ Saving a compressed SVG (SVGZ): >>> filename 'ean13.svgz' -Now you have ean13.svg and the compressed ean13.svgz in your current +Now you have ``ean13.svg`` and the compressed ``ean13.svgz`` in your current working directory. Open it and see the result. diff --git a/publish-release.yaml b/publish-release.yaml new file mode 100644 index 0000000..3555260 --- /dev/null +++ b/publish-release.yaml @@ -0,0 +1,25 @@ +# Run this with: +# hut builds submit -f publish-release.yaml +image: archlinux +packages: + - python-build + - python-setuptools-scm + - python-wheel + - twine +sources: + - https://github.com/WhyNotHugo/python-barcode/ +secrets: + - 0dd39b49-3530-4002-a197-e0ca7fc3fde7 # PyPI token. +tasks: + - check: | + cd python-barcode + git fetch --tags + + # Stop here unless this is a tag. + git describe --exact-match --tags || complete-build + - build: | + cd python-barcode + python -m build --no-isolation + - publish: | + cd python-barcode + twine upload --non-interactive dist/* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..891d6c3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[build-system] +requires = ["setuptools>=61", "wheel", "setuptools_scm>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-barcode" +description = "Create standard barcodes with Python. No external modules needed. (optional Pillow support included)." +readme = "README.rst" +requires-python = ">=3.9" +license = { text = "MIT" } +authors = [ + { name = "Hugo Osvaldo Barrera et al", email = "hugo@whynothugo.nl" } +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dynamic = ["version"] + +[project.optional-dependencies] +images = ["pillow"] + +[project.scripts] +python-barcode = "barcode.pybarcode:main" + +[project.urls] +documentation = "https://python-barcode.readthedocs.io/" +repository = "https://github.com/WhyNotHugo/python-barcode" +issues = "https://github.com/WhyNotHugo/python-barcode/issues" +funding= "https://whynothugo.nl/sponsor/" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.setuptools_scm] +write_to = "barcode/version.py" +version_scheme = "post-release" + +[tool.ruff.lint] +extend-select = [ + "E", + "W", + "I", + "N", + "UP", + "YTT", + "BLE", + "B", + "C4", + "ISC", + "ICN", + "G", + "INP", + "PIE", + "PYI", + "PT", + "Q", + "RSE", + "RET", + "SIM", + "TID", + "TCH", + "INT", + "ERA", + "PGH", + "PLE", + "RUF", +] + +[tool.ruff.lint.isort] +force-single-line = true +required-imports = ["from __future__ import annotations"] + +[tool.coverage.report] +exclude_lines = [ + "if TYPE_CHECKING:", +] + +[tool.pytest.ini_options] +addopts = [ + "-vv", + "--cov=barcode", + "--cov-report=term-missing:skip-covered", + "--no-cov-on-fail", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1a7de2b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -exclude = docs/conf.py -max-line-length = 88 -extend-ignore = E203, W503 - -[tool:pytest] -addopts = - -vv - --cov=barcode - --cov-report=term-missing:skip-covered - --no-cov-on-fail diff --git a/setup.py b/setup.py deleted file mode 100755 index 6231b73..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from pathlib import Path - -from setuptools import find_packages -from setuptools import setup - -setup( - name="python-barcode", - packages=find_packages(), - url="https://github.com/WhyNotHugo/python-barcode", - license="MIT", - author="Hugo Osvaldo Barrera et al", - author_email="hugo@barrera.io", - description=( - "Create standard barcodes with Python. No external modules needed. " - "(optional Pillow support included)." - ), - long_description=Path("README.rst").read_text(), - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Topic :: Multimedia :: Graphics", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - entry_points={"console_scripts": ["python-barcode = barcode.pybarcode:main"]}, - use_scm_version={ - "version_scheme": "post-release", - "write_to": "barcode/version.py", - }, - setup_requires=["setuptools_scm"], - extras_require={"images": ["pillow"]}, - include_package_data=True, -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_builds.py b/tests/test_builds.py index fa99476..aac11eb 100755 --- a/tests/test_builds.py +++ b/tests/test_builds.py @@ -1,8 +1,17 @@ +from __future__ import annotations + from barcode import get_barcode -def test_ean8_builds(): +def test_ean8_builds() -> None: ref = "1010100011000110100100110101111010101000100100010011100101001000101" ean = get_barcode("ean8", "40267708") bc = ean.build() assert ref == bc[0] + + +def test_ean8_builds_with_longer_bars() -> None: + ref = "G0G01000110001101001001101011110G0G01000100100010011100101001000G0G" + ean = get_barcode("ean8", "40267708", options={"guardbar": True}) + bc = ean.build() + assert ref == bc[0] diff --git a/tests/test_checksums.py b/tests/test_checksums.py index f2d4451..b97fef7 100755 --- a/tests/test_checksums.py +++ b/tests/test_checksums.py @@ -1,46 +1,48 @@ +from __future__ import annotations + from barcode import get_barcode -def test_code39_checksum(): +def test_code39_checksum() -> None: code39 = get_barcode("code39", "Code39") - assert "CODE39W" == code39.get_fullcode() + assert code39.get_fullcode() == "CODE39W" -def test_pzn_checksum(): +def test_pzn_checksum() -> None: pzn = get_barcode("pzn", "103940") - assert "PZN-1039406" == pzn.get_fullcode() + assert pzn.get_fullcode() == "PZN-1039406" -def test_ean13_checksum(): +def test_ean13_checksum() -> None: ean = get_barcode("ean13", "400614457735") - assert "4006144577350" == ean.get_fullcode() + assert ean.get_fullcode() == "4006144577350" -def test_ean8_checksum(): +def test_ean8_checksum() -> None: ean = get_barcode("ean8", "6032299") - assert "60322999" == ean.get_fullcode() + assert ean.get_fullcode() == "60322999" -def test_jan_checksum(): +def test_jan_checksum() -> None: jan = get_barcode("jan", "491400614457") - assert "4914006144575" == jan.get_fullcode() + assert jan.get_fullcode() == "4914006144575" -def test_ean14_checksum(): +def test_ean14_checksum() -> None: ean = get_barcode("ean14", "1234567891258") - assert "12345678912589" == ean.get_fullcode() + assert ean.get_fullcode() == "12345678912589" -def test_isbn10_checksum(): +def test_isbn10_checksum() -> None: isbn = get_barcode("isbn10", "376926085") - assert "3769260856" == isbn.isbn10 + assert isbn.isbn10 == "3769260856" # type: ignore[attr-defined] -def test_isbn13_checksum(): +def test_isbn13_checksum() -> None: isbn = get_barcode("isbn13", "978376926085") - assert "9783769260854" == isbn.get_fullcode() + assert isbn.get_fullcode() == "9783769260854" -def test_gs1_128_checksum(): +def test_gs1_128_checksum() -> None: gs1_128 = get_barcode("gs1_128", "00376401856400470087") - assert "00376401856400470087" == gs1_128.get_fullcode() + assert gs1_128.get_fullcode() == "00376401856400470087" diff --git a/tests/test_ean.py b/tests/test_ean.py new file mode 100644 index 0000000..95097de --- /dev/null +++ b/tests/test_ean.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import sys + +import pytest + +from barcode.ean import EAN13 + + +def test_ean_checksum_generated() -> None: + ean = EAN13("842167143322") # input has 12 digits + assert ean.calculate_checksum() == 5 + assert ean.ean == "8421671433225" + + +def test_ean_checksum_zeroed() -> None: + ean = EAN13("842167143322", no_checksum=True) # input has 12 digits + assert ean.calculate_checksum() == 5 + assert ean.ean == "8421671433220" + + +def test_ean_checksum_supplied_and_generated() -> None: + ean = EAN13("8421671433225") # input has 13 digits + assert ean.calculate_checksum() == 5 + assert ean.ean == "8421671433225" + + +def test_ean_checksum_supplied_and_matching() -> None: + ean = EAN13("8421671433225", no_checksum=True) # input has 13 digits + assert ean.calculate_checksum() == 5 + assert ean.ean == "8421671433225" + + +def test_ean_checksum_supplied_and_different() -> None: + ean = EAN13("8421671433229", no_checksum=True) # input has 13 digits + assert ean.calculate_checksum() == 5 + assert ean.ean == "8421671433229" + + +def test_ean_checksum_generated_placeholder() -> None: + ean = EAN13("977114487500X") # input has 13 digits + assert ean.calculate_checksum() == 7 + assert ean.ean == "9771144875007" + + +@pytest.mark.skipif(sys.platform == "win32", reason="no /dev/null") +def test_ean_checksum_supplied_placeholder() -> None: + ean = EAN13("977114487500X", no_checksum=True) # input has 13 digits + assert ean.calculate_checksum() == 7 + assert ean.ean == "9771144875000" + + with open("/dev/null", "wb") as f: + ean.write(f) diff --git a/tests/test_gs1_128.py b/tests/test_gs1_128.py new file mode 100644 index 0000000..3352bf0 --- /dev/null +++ b/tests/test_gs1_128.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import pytest + +from barcode.codex import Gs1_128 + +FNC1_CHAR = "\xf1" +FNC1 = 102 +START_B = 104 +START_C = 105 +FROM_AC_TO_B = 100 +FROM_AB_TO_C = 99 +FROM_BC_TO_A = 101 +CODE_BUILD_TEST = ( + # '(01)01234567891011(11)200622(17)240622(21)88888888' # noqa: ERA001 + ( + "010123456789101111200622172406222188888888", + [ + START_C, + FNC1, + 1, + 1, + 23, + 45, + 67, + 89, + 10, + 11, + 11, + 20, + 6, + 22, + 17, + 24, + 6, + 22, + 21, + 88, + 88, + 88, + 88, + ], + ), + # '(01)01234567891011(11)200622(17)240622(21)888888888' # noqa: ERA001 + ( + "0101234567891011112006221724062221888888888", + [ + START_C, + FNC1, + 1, + 1, + 23, + 45, + 67, + 89, + 10, + 11, + 11, + 20, + 6, + 22, + 17, + 24, + 6, + 22, + 21, + 88, + 88, + 88, + 88, + 100, + 24, + ], + ), + # '(01)01234567891011(11)200622(10)12345(21)1234' # noqa: ERA001 + ( + "0101234567891011112006221012345" + FNC1_CHAR + "211234", + [ + START_C, + FNC1, + 1, + 1, + 23, + 45, + 67, + 89, + 10, + 11, + 11, + 20, + 6, + 22, + 10, + 12, + 34, + FROM_AC_TO_B, + 21, + FNC1, + FROM_AB_TO_C, + 21, + 12, + 34, + ], + ), + # '(01)01234567891011(11)200622(10)1234(21)1234' # noqa: ERA001 + ( + "010123456789101111200622101234" + FNC1_CHAR + "211234", + [ + START_C, + FNC1, + 1, + 1, + 23, + 45, + 67, + 89, + 10, + 11, + 11, + 20, + 6, + 22, + 10, + 12, + 34, + FNC1, + 21, + 12, + 34, + ], + ), + # '(01)01234567891011(11)200622(10)240622(21)888888888' # noqa: ERA001 + ( + "01012345678910111120062210240622" + FNC1_CHAR + "21888888888", + [ + START_C, + FNC1, + 1, + 1, + 23, + 45, + 67, + 89, + 10, + 11, + 11, + 20, + 6, + 22, + 10, + 24, + 6, + 22, + FNC1, + 21, + 88, + 88, + 88, + 88, + 100, + 24, + ], + ), + # '(01)08720299927469(11)240621(17)250621(10)20240621/0001(21)xyz' # noqa: ERA001 + ( + "010872029992746911240621172506211020240621/0001" + FNC1_CHAR + "21xyz", + [ + 105, + 102, + 1, + 8, + 72, + 2, + 99, + 92, + 74, + 69, + 11, + 24, + 6, + 21, + 17, + 25, + 6, + 21, + 10, + 20, + 24, + 6, + 21, + 100, + 15, + 99, + 0, + 1, + 102, + 21, + 100, + 88, + 89, + 90, + ], + ), +) + + +@pytest.mark.parametrize(("target", "answer"), CODE_BUILD_TEST) +def test_code_build(target, answer): + gs1_128 = Gs1_128(target) + assert gs1_128._build() == answer diff --git a/tests/test_init.py b/tests/test_init.py index 33a845d..b78703a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from io import BytesIO @@ -10,17 +12,17 @@ TESTPATH = os.path.join(PATH, "test_outputs") -def test_generate_without_output(): +def test_generate_without_output() -> None: with pytest.raises(TypeError, match="'output' cannot be None"): barcode.generate("ean13", "123455559121112") -def test_generate_with_file(): +def test_generate_with_file() -> None: with open(os.path.join(TESTPATH, "generate_with_file.jpeg"), "wb") as f: barcode.generate("ean13", "123455559121112", output=f) -def test_generate_with_filepath(): +def test_generate_with_filepath() -> None: # FIXME: extension is added to the filepath even if you include it. rv = barcode.generate( "ean13", @@ -30,13 +32,13 @@ def test_generate_with_filepath(): assert rv == os.path.abspath(os.path.join(TESTPATH, "generate_with_filepath.svg")) -def test_generate_with_file_and_writer(): +def test_generate_with_file_and_writer() -> None: with open(os.path.join(TESTPATH, "generate_with_file_and_writer.jpeg"), "wb") as f: barcode.generate("ean13", "123455559121112", output=f, writer=SVGWriter()) -def test_generate_with_bytesio(): +def test_generate_with_bytesio() -> None: bio = BytesIO() barcode.generate("ean13", "123455559121112", output=bio) # XXX: File is not 100% deterministic; needs to be addressed at some point. - # assert len(bio.getvalue()) == 6127 + # assert len(bio.getvalue()) == 6127 # noqa: ERA001 diff --git a/tests/test_manually.py b/tests/test_manually.py index 2e9a886..802fb03 100755 --- a/tests/test_manually.py +++ b/tests/test_manually.py @@ -1,12 +1,21 @@ """Generates barcodes for visually inspecting the results.""" + +from __future__ import annotations + import codecs import os +from typing import TYPE_CHECKING + +import pytest from barcode import get_barcode from barcode import get_barcode_class from barcode import version from barcode.writer import ImageWriter +if TYPE_CHECKING: + from collections.abc import Iterator + PATH = os.path.dirname(os.path.abspath(__file__)) TESTPATH = os.path.join(PATH, "test_outputs") HTMLFILE = os.path.join(TESTPATH, "index.html") @@ -32,7 +41,9 @@ TESTCODES = ( ("ean8", "40267708"), + ("ean8-guard", "40267708"), ("ean13", "5901234123457"), + ("ean13-guard", "5901234123457"), ("ean14", "12345678911230"), ("upca", "36000291453"), ("jan", "4901234567894"), @@ -46,46 +57,51 @@ ) -def test_generating_barcodes(): +@pytest.mark.parametrize(("codename", "code"), TESTCODES) +def test_generating_barcodes( + codename: str, code: str, gather_image_elements_into_html: list[str] +) -> None: os.makedirs(TESTPATH, exist_ok=True) - objects = [] + image_elements = gather_image_elements_into_html - def append(x, y): - objects.append(OBJECTS.format(filename=x, name=y)) + def append(x, y) -> None: + image_elements.append(OBJECTS.format(filename=x, name=y)) - def append_img(x, y): - objects.append(IMAGES.format(filename=x, name=y)) + def append_img(x, y) -> None: + image_elements.append(IMAGES.format(filename=x, name=y)) options = {} - for codename, code in TESTCODES: - bcode = get_barcode(codename, code) + bcode = get_barcode(codename, code) + if codename.startswith("i"): + options["center_text"] = False + else: + options["center_text"] = True + filename = bcode.save(os.path.join(TESTPATH, codename), options=options) + print(f"Code: {bcode.name}, Input: {code}, Output: {bcode.get_fullcode()}") + append(os.path.basename(filename), bcode.name) + if ImageWriter is not None: + bcodec = get_barcode_class(codename) + bcode = bcodec(code, writer=ImageWriter()) + opts = {} if codename.startswith("i"): - options["center_text"] = False + opts["center_text"] = False else: - options["center_text"] = True - filename = bcode.save(os.path.join(TESTPATH, codename), options=options) - print( - "Code: {}, Input: {}, Output: {}".format( - bcode.name, code, bcode.get_fullcode() - ) - ) - append(os.path.basename(filename), bcode.name) - if ImageWriter is not None: - bcodec = get_barcode_class(codename) - bcode = bcodec(code, writer=ImageWriter()) - opts = {} - if codename.startswith("i"): - opts["center_text"] = False - else: - opts["center_text"] = True - filename = bcode.save(os.path.join(TESTPATH, codename), options=opts) - append_img(os.path.basename(filename), bcode.name) - else: - objects.append(NO_PIL) + opts["center_text"] = True + filename = bcode.save(os.path.join(TESTPATH, codename), options=opts) + append_img(os.path.basename(filename), bcode.name) + else: + image_elements.append(NO_PIL) + + +@pytest.fixture(scope="module") +def gather_image_elements_into_html() -> Iterator[list[str]]: + image_elements: list[str] = [] + yield image_elements + # Save htmlfile with all objects with codecs.open(HTMLFILE, "w", encoding="utf-8") as f: - obj = "\n".join(objects) + obj = "\n".join(image_elements) f.write(HTML.format(version=version, body=obj)) - print("\nNow open {htmlfile} in your browser.".format(htmlfile=HTMLFILE)) + print(f"\nNow open {HTMLFILE} in your browser.") diff --git a/tests/test_writers.py b/tests/test_writers.py index df178b2..27454bb 100644 --- a/tests/test_writers.py +++ b/tests/test_writers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from io import BytesIO @@ -5,20 +7,23 @@ from barcode.writer import ImageWriter from barcode.writer import SVGWriter - PATH = os.path.dirname(os.path.abspath(__file__)) TESTPATH = os.path.join(PATH, "test_outputs") -if ImageWriter: +if ImageWriter is not None: + + def test_saving_image_to_byteio() -> None: + assert ImageWriter is not None # workaround for mypy - def test_saving_image_to_byteio(): rv = BytesIO() EAN13(str(100000902922), writer=ImageWriter()).write(rv) with open(f"{TESTPATH}/somefile.jpeg", "wb") as f: EAN13("100000011111", writer=ImageWriter()).write(f) - def test_saving_rgba_image(): + def test_saving_rgba_image() -> None: + assert ImageWriter is not None # workaround for mypy + rv = BytesIO() EAN13(str(100000902922), writer=ImageWriter()).write(rv) @@ -30,9 +35,17 @@ def test_saving_rgba_image(): ) -def test_saving_svg_to_byteio(): +def test_saving_svg_to_byteio() -> None: rv = BytesIO() EAN13(str(100000902922), writer=SVGWriter()).write(rv) with open(f"{TESTPATH}/somefile.svg", "wb") as f: EAN13("100000011111", writer=SVGWriter()).write(f) + + +def test_saving_svg_to_byteio_with_guardbar() -> None: + rv = BytesIO() + EAN13(str(100000902922), writer=SVGWriter(), guardbar=True).write(rv) + + with open(f"{TESTPATH}/somefile_guardbar.svg", "wb") as f: + EAN13("100000011111", writer=SVGWriter(), guardbar=True).write(f) diff --git a/tox.ini b/tox.ini index 09e934c..ba2c87a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py36,py37,py38,py39}{,-images} +envlist = py,py-images,mypy,mypy-images skip_missing_interpreters = True [testenv] @@ -10,7 +10,9 @@ deps = commands = pytest --cov barcode usedevelop = True -[flake8] -exclude=.tox,build,.eggs -application-import-names=barcode,tests -import-order-style=smarkets +[testenv:mypy] +deps = + mypy + images: Pillow +commands = mypy . +usedevelop = True