diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 086094d..2101c27 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -15,10 +15,10 @@ jobs: - uses: actions/checkout@v4 with: ref: master - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install setuptools run: | diff --git a/.github/workflows/run_tox.yml b/.github/workflows/run_tox.yml index abda4eb..264345b 100644 --- a/.github/workflows/run_tox.yml +++ b/.github/workflows/run_tox.yml @@ -10,8 +10,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.10' - - uses: pre-commit/action@v3.0.0 + python-version: '3.12' + - uses: pre-commit/action@v3.0.1 tests: @@ -20,7 +20,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f3d81a..96ce04c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-builtin-literals @@ -14,9 +14,27 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.7.2 hooks: - id: ruff + name: ruff unused imports + # F401 [*] {name} imported but unused + args: [ "--select", "F401", "--extend-exclude", "__init__.py", "--fix"] + + - id: ruff + # I001 [*] Import block is un-sorted or un-formatted + # UP035 [*] Import from {target} instead: {names} + # Q000 [*] Double quote found but single quotes preferred + # Q001 [*] Double quote multiline found but single quotes preferred + args: [ "--select", "I001,UP035,Q000,Q001", "--fix"] + + + - repo: https://github.com/JelleZijlstra/autotyping + rev: 24.9.0 + hooks: + - id: autotyping + types: [python] + args: [--safe] - repo: meta diff --git a/.ruff.toml b/.ruff.toml index 3e05d74..ef366af 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,66 +1,96 @@ - -line-length = 120 indent-width = 4 +line-length = 120 target-version = "py38" -src = ["src", "test"] - -[lint] -# https://docs.astral.sh/ruff/settings/#ignore-init-module-imports -ignore-init-module-imports = true - -select = [ - "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "I", # https://docs.astral.sh/ruff/rules/#isort-i - "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up - - "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a - "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async - "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em - "FIX", # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix - "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp - "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc - "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth - "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret - "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim - "SLOT", # https://docs.astral.sh/ruff/rules/#flake8-slots-slot - "T10", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 - "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch - "TD", # https://docs.astral.sh/ruff/rules/#flake8-todos-td - - "TRY", # https://docs.astral.sh/ruff/rules/#tryceratops-try - "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly - "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf - "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf - - # "PL", # https://docs.astral.sh/ruff/rules/#pylint-pl - # "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb +src = [ + "src", + "tests" ] + +[lint] +select = ["ALL"] + ignore = [ - "PT007" # Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple` + "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf + + "RET501", # https://docs.astral.sh/ruff/rules/unnecessary-return-none/#unnecessary-return-none-ret501 + "TRY400", # https://docs.astral.sh/ruff/rules/error-instead-of-exception/ + + # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "A003", # Python builtin is shadowed by class attribute {name} from {row} + + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F401", # {name} imported but unused; consider using importlib.util.find_spec to test for availability + + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP038", # Use X | Y in {} call instead of (X, Y) + + # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ANN101", # Missing type annotation for {name} in method + "ANN102", # Missing type annotation for {name} in classmethod + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + + # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "BLE001", # Do not catch blind exception: {name} + + # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RSE102", # Unnecessary parentheses on raised exception + + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing + "COM819", # Trailing comma prohibited + + # https://docs.astral.sh/ruff/rules/#warning-w_1 + "PLW0603", # Using the global statement to update {name} is discouraged + + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G004", # Logging statement uses f-string + + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR1711", # Useless return statement at end of function + + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF005", # Consider {expression} instead of concatenation ] [format] -# Use single quotes for non-triple-quoted strings. quote-style = "single" -# [lint.flake8-builtins] -# builtins-ignorelist = ["id", "input"] +# https://docs.astral.sh/ruff/settings/#lintflake8-quotes +[lint.flake8-quotes] +inline-quotes = "single" +multiline-quotes = "single" -[lint.per-file-ignores] -"src/smllib/errors.py" = ["A002"] # Argument `type` is shadowing a Python builtin -"src/smllib/sml/_field_info.py" = ["A002"] # Argument `type` is shadowing a Python builtin -"tests/*" = ["INP001"] # INP001 File `PATH` is part of an implicit namespace package. Add an `__init__.py`. +[lint.flake8-builtins] +builtins-ignorelist = ["id", "input"] +# https://docs.astral.sh/ruff/settings/#lintisort [lint.isort] -# https://docs.astral.sh/ruff/settings/#lint_isort_lines-after-imports -lines-after-imports = 2 +lines-after-imports = 2 # https://docs.astral.sh/ruff/settings/#lint_isort_lines-after-imports + + +[lint.per-file-ignores] +"setup.py" = ["PTH123"] + +"tests/*" = [ + "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + + # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "S101", # Use of assert detected + + # https://docs.astral.sh/ruff/rules/#refactor-r + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLR0913", # Too many arguments in function definition ({c_args} > {max_args}) +] diff --git a/readme.md b/readme.md index 7376cb2..55e9bff 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ [![Tests Status](https://github.com/spacemanspiff2007/SmlLib/workflows/Tests/badge.svg)](https://github.com/spacemanspiff2007/SmlLib/actions?query=workflow%3ATests) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/SmlLib)](https://pypi.org/project/smllib/) [![PyPI](https://img.shields.io/pypi/v/SmlLib)](https://pypi.org/project/smllib/) -[![Downloads](https://pepy.tech/badge/SmlLib)](https://pepy.tech/project/SmlLib) +[![Downloads](https://static.pepy.tech/badge/SmlLib/month)](https://pepy.tech/project/SmlLib) _A SML (Smart Message Language) library_ @@ -147,3 +147,19 @@ SmlMessage global_signature: None crc16 : 56696 ``` + + +## Different CRC functions + +Some meters e.g. the Holley DTZ541 via RS485 require a different crc function. +You can set the crc function when creating the StreamReader. + + +Example: +```python +from smllib import SmlStreamReader + +stream = SmlStreamReader(crc='kermit') # <-- use kermit for Holley DTZ541 +stream.add(b'BytesFromSerialPort') +sml_frame = stream.get_frame() +``` diff --git a/requirements.txt b/requirements.txt index cfed223..9a6554e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ # ----------------------------------------------------------------------------- # Packages for source formatting # ----------------------------------------------------------------------------- -ruff == 0.3.4 -pre-commit == 3.7.0 +ruff == 0.7.2 +pre-commit == 5.0.0 # ----------------------------------------------------------------------------- # Packages for other developement tasks # ----------------------------------------------------------------------------- -pur == 7.3.1 # Update requirements.txt +pur == 7.3.2 # Update requirements.txt diff --git a/requirements_tests.txt b/requirements_tests.txt index 2dd06ab..5bd4765 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1 +1 @@ -pytest == 8.1.1 +pytest == 8.3.3 diff --git a/setup.py b/setup.py index c49c2dd..04fc5bc 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ def load_version() -> str: version: typing.Dict[str, str] = {} - with (Path(__file__).parent / "src/smllib/__version__.py").open() as fp: + with (Path(__file__).parent / 'src/smllib/__version__.py').open() as fp: exec(fp.read(), version) assert version['__version__'], version return version['__version__'] @@ -21,15 +21,15 @@ def load_version() -> str: readme = Path(__file__).with_name('readme.md') long_description = '' if readme.is_file(): - with readme.open("r", encoding='utf-8') as fh: + with readme.open('r', encoding='utf-8') as fh: long_description = fh.read() setuptools.setup( - name="smllib", + name='smllib', version=__version__, - author="spaceman_spiff", + author='spaceman_spiff', # author_email="", - description="A library for the SML (Smart Message Language) protocol", + description='A library for the SML (Smart Message Language) protocol', keywords=[ 'sml', 'obis', @@ -37,24 +37,25 @@ def load_version() -> str: 'energy meter', ], long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/spacemanspiff2007/SmlLib", + long_description_content_type='text/markdown', + url='https://github.com/spacemanspiff2007/SmlLib', project_urls={ - 'GitHub': "https://github.com/spacemanspiff2007/SmlLib", + 'GitHub': 'https://github.com/spacemanspiff2007/SmlLib', }, packages=setuptools.find_packages(where='src', exclude=['tests*']), package_dir={'': 'src'}, package_data={'smllib': ['py.typed']}, classifiers=[ - "Development Status :: 4 - Beta", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Home Automation" + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Home Automation' ], ) diff --git a/src/smllib/__version__.py b/src/smllib/__version__.py index 0f66308..fcb6b5d 100644 --- a/src/smllib/__version__.py +++ b/src/smllib/__version__.py @@ -1 +1 @@ -__version__ = '1.4' +__version__ = '1.5' diff --git a/src/smllib/builder/_builder.py b/src/smllib/builder/_builder.py index 223f08c..547d050 100644 --- a/src/smllib/builder/_builder.py +++ b/src/smllib/builder/_builder.py @@ -8,7 +8,7 @@ class SmlObjBuilder(Generic[T_SML_OBJ]): BUILDS: Type[T_SML_OBJ] - def __init__(self): + def __init__(self) -> None: assert issubclass(self.BUILDS, SmlBaseObj), self.BUILDS self.fields: Dict[str, SmlObjFieldInfo] = inspect_obj(self.BUILDS) diff --git a/src/smllib/crc/__init__.py b/src/smllib/crc/__init__.py new file mode 100644 index 0000000..87fb809 --- /dev/null +++ b/src/smllib/crc/__init__.py @@ -0,0 +1 @@ +from . import kermit, x25 diff --git a/src/smllib/crc/kermit.py b/src/smllib/crc/kermit.py new file mode 100644 index 0000000..6995c78 --- /dev/null +++ b/src/smllib/crc/kermit.py @@ -0,0 +1,44 @@ +from typing import Union + + +CRC_16_KERMIT_TABLE = [ + 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, + 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, + 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, + 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, + 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, + 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, + 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, + 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, + 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, + 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, + 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, + 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, + 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, + 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, + 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, + 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, + 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, + 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, + 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, + 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, + 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, + 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, + 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, + 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, + 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, + 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, + 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, + 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, + 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, + 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, + 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, + 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78 +] + + +def get_crc(buf: Union[memoryview, bytes]) -> int: + crc = 0x0000 # Initial value for CRC16 KERMIT + for byte in buf: + crc = (crc >> 8) ^ CRC_16_KERMIT_TABLE[(crc ^ byte) & 0xFF] + return crc diff --git a/src/smllib/crc.py b/src/smllib/crc/x25.py similarity index 100% rename from src/smllib/crc.py rename to src/smllib/crc/x25.py diff --git a/src/smllib/errors.py b/src/smllib/errors.py index de5f095..2920898 100644 --- a/src/smllib/errors.py +++ b/src/smllib/errors.py @@ -6,12 +6,12 @@ class SmlLibException(Exception): class CrcError(SmlLibException): - def __init__(self, msg: bytes, crc_msg: int, crc_calc: int): + def __init__(self, msg: bytes, crc_msg: int, crc_calc: int) -> None: self.msg = msg self.crc_msg = crc_msg self.crc_calc = crc_calc - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__} msg: {self.crc_msg:04x} calc: {self.crc_calc:04x}>' diff --git a/src/smllib/reader.py b/src/smllib/reader.py index 5de3b60..d8d2401 100644 --- a/src/smllib/reader.py +++ b/src/smllib/reader.py @@ -1,7 +1,7 @@ -from typing import Optional +from typing import Callable, Literal, Optional, Union +import smllib.crc as crc_module from smllib.builder import CTX_HINT, create_context -from smllib.crc import get_crc from smllib.errors import CrcError from smllib.sml_frame import SmlFrame @@ -9,16 +9,23 @@ class SmlStreamReader: MAX_SIZE = 50 * 1024 - def __init__(self, build_ctx: Optional[CTX_HINT] = None): + def __init__(self, build_ctx: Optional[CTX_HINT] = None, crc: Literal['kermit', 'x25'] = 'x25') -> None: self.bytes: bytes = b'' self.build_ctx: CTX_HINT = build_ctx if build_ctx is not None else create_context() - def add(self, _bytes: bytes): + # This makes it easy to patch additional crc functions to the module + try: + self.crc_func: Callable[[Union[memoryview, bytes]], int] = getattr(crc_module, crc).get_crc + except AttributeError: + available = [f'"{n:s}"' for n in dir(crc_module) if not n.startswith('_')] + raise ValueError(f'Unsupported CRC "{crc}"! Available: {", ".join(available):s}') + + def add(self, _bytes: bytes) -> None: self.bytes += _bytes if len(self.bytes) > SmlStreamReader.MAX_SIZE: self.bytes = self.bytes[-1 * SmlStreamReader.MAX_SIZE:] - def clear(self): + def clear(self) -> None: self.bytes = b'' def get_frame(self) -> Optional[SmlFrame]: @@ -53,7 +60,7 @@ def get_frame(self) -> Optional[SmlFrame]: # check crc crc_msg = msg[-2] << 8 | msg[-1] - crc_calc = get_crc(msg[:-2]) + crc_calc = self.crc_func(msg[:-2]) if crc_msg != crc_calc: raise CrcError(msg, crc_msg, crc_calc) diff --git a/src/smllib/sml/_field_info.py b/src/smllib/sml/_field_info.py index 78e67a0..b42727b 100644 --- a/src/smllib/sml/_field_info.py +++ b/src/smllib/sml/_field_info.py @@ -5,7 +5,7 @@ class SmlObjFieldInfo: def __init__(self, func: Optional[Callable[[Any], Any]] = None, type=None, - choice: Optional[SmlChoice] = None, is_container=False): + choice: Optional[SmlChoice] = None, is_container=False) -> None: self.func: Final = func self.choice: Final = choice self.type = type @@ -18,7 +18,7 @@ def __eq__(self, other): def copy(self) -> 'SmlObjFieldInfo': return self.__class__(func=self.func, type=self.type, choice=self.choice, is_container=self.is_container) - def __repr__(self): + def __repr__(self) -> str: c = [] if self.func is not None: c.append(f'func={self.func.__name__}') diff --git a/src/smllib/sml/list_entry.py b/src/smllib/sml/list_entry.py index baa52bb..427fd56 100644 --- a/src/smllib/sml/list_entry.py +++ b/src/smllib/sml/list_entry.py @@ -22,7 +22,7 @@ class SmlListEntry(SmlBaseObj): value: Union[None, str, int, float] value_signature: Optional[str] - def __repr__(self): + def __repr__(self) -> str: r = [] for k, v in self.__dict__.items(): if v is not None: @@ -42,7 +42,7 @@ def format_msg(self, indent: int = 0): val = self.get_value() u = UNITS.get(self.unit) if u is None: - u = f" ?:{self.unit}" + u = f' ?:{self.unit}' summary += f'{val}{u}' desc = OBIS_NAMES.get(self.obis) diff --git a/src/smllib/sml/sml_choice.py b/src/smllib/sml/sml_choice.py index 87be9c1..68e2664 100644 --- a/src/smllib/sml/sml_choice.py +++ b/src/smllib/sml/sml_choice.py @@ -6,7 +6,7 @@ class SmlChoice(Generic[T_SML_OBJ]): - def __init__(self, choices: Mapping[int, Type[T_SML_OBJ]]): + def __init__(self, choices: Mapping[int, Type[T_SML_OBJ]]) -> None: self.choices: Final = choices def get(self, obj: List[SmlFrameSnippet]) -> Tuple[Type[T_SML_OBJ], SmlFrameSnippet]: diff --git a/src/smllib/sml/sml_eom.py b/src/smllib/sml/sml_eom.py index c0aa094..1df122f 100644 --- a/src/smllib/sml/sml_eom.py +++ b/src/smllib/sml/sml_eom.py @@ -1,5 +1,5 @@ class CEndOfSmlMsg: - def __repr__(self): + def __repr__(self) -> str: return '' diff --git a/src/smllib/sml/sml_obis.py b/src/smllib/sml/sml_obis.py index f832fcf..0e7063c 100644 --- a/src/smllib/sml/sml_obis.py +++ b/src/smllib/sml/sml_obis.py @@ -1,7 +1,7 @@ class ObisCode(str): __slots__ = ('obis_code', 'obis_short') - def __init__(self, obis_hex: str): + def __init__(self, obis_hex: str) -> None: _a = int(obis_hex[0:2], 16) _b = int(obis_hex[2:4], 16) _c = int(obis_hex[4:6], 16) diff --git a/src/smllib/sml_frame.py b/src/smllib/sml_frame.py index bdb820e..f05ff92 100644 --- a/src/smllib/sml_frame.py +++ b/src/smllib/sml_frame.py @@ -7,7 +7,7 @@ class SmlFrame: - def __init__(self, buffer: bytes, build_ctx: Optional[CTX_HINT] = None, msg_ctx: Optional[bytes] = None): + def __init__(self, buffer: bytes, build_ctx: Optional[CTX_HINT] = None, msg_ctx: Optional[bytes] = None) -> None: self.bytes = buffer self.buffer = memoryview(buffer) self.buf_len = len(buffer) diff --git a/src/smllib/sml_frame_snippet.py b/src/smllib/sml_frame_snippet.py index fdf9a8f..c59ce13 100644 --- a/src/smllib/sml_frame_snippet.py +++ b/src/smllib/sml_frame_snippet.py @@ -11,7 +11,7 @@ class SmlFrameSnippet: __slots__ = ('pos', 'value', 'msg') def __init__(self, value: Union[None, bool, int, str, float, list, CEndOfSmlMsg], start: int, - stop: Optional[int] = None, buf: Optional[memoryview] = None): + stop: Optional[int] = None, buf: Optional[memoryview] = None) -> None: msg = None if stop is not None: diff --git a/tests/builder/test_build.py b/tests/builder/test_build.py index 39ff9c5..9d45dd0 100644 --- a/tests/builder/test_build.py +++ b/tests/builder/test_build.py @@ -10,14 +10,14 @@ from smllib.sml import EndOfSmlMsg, SmlCloseResponse, SmlListEntry -def test_build_entry(): +def test_build_entry() -> None: builder = SmlListEntryBuilder() obj = builder.build(in_snip(['0100010800ff', None, None, None, None, '76616c', None]), {SmlListEntry: builder}) assert obj.obis == '0100010800ff' assert obj.value == 'val' -def test_build_entry_list(): +def test_build_entry_list() -> None: data = in_snip([ None, 'server', None, None, [['0100010800ff', None, None, None, None, '76616c31', None], @@ -50,7 +50,7 @@ def build(self, obj: list, classes): assert obj.val_list[1].value == 'val2' -def test_build_choice(): +def test_build_choice() -> None: data = in_snip(['t1', 1, 0, [0x0201, ['sig']], 1111, EndOfSmlMsg]) builder = SmlMessageBuilder() obj = builder.build(data, {SmlCloseResponse: SmlCloseResponseBuilder()}) diff --git a/tests/builder/test_field_obj.py b/tests/builder/test_field_obj.py index aabbf75..1b055b7 100644 --- a/tests/builder/test_field_obj.py +++ b/tests/builder/test_field_obj.py @@ -1,8 +1,8 @@ from smllib.sml import SmlObjFieldInfo -def test_copy(): - def func(a): +def test_copy() -> None: + def func(a) -> None: pass a = SmlObjFieldInfo(func) diff --git a/tests/builder/test_inspect.py b/tests/builder/test_inspect.py index 650e834..d134598 100644 --- a/tests/builder/test_inspect.py +++ b/tests/builder/test_inspect.py @@ -14,7 +14,7 @@ from smllib.sml.sml_time import build_time -def test_inspect_sml_message(): +def test_inspect_sml_message() -> None: fields = inspect_obj(SmlMessage) assert list(fields.keys()) == ['transaction_id', 'group_no', 'abort_on_error', 'message_body', 'crc16'] assert fields['transaction_id'] == SmlObjFieldInfo(type=str) @@ -25,7 +25,7 @@ def test_inspect_sml_message(): assert fields['crc16'] == SmlObjFieldInfo(type=int) -def test_inspect_sml_list_entry(): +def test_inspect_sml_list_entry() -> None: fields = inspect_obj(SmlListEntry) assert list(fields.keys()) == ['obis', 'status', 'val_time', 'unit', 'scaler', 'value', 'value_signature'] assert fields['obis'] == SmlObjFieldInfo(type=ObisCode, func=build_obis) diff --git a/tests/sml/test_format.py b/tests/sml/test_format.py index 480d33d..2fe5cc5 100644 --- a/tests/sml/test_format.py +++ b/tests/sml/test_format.py @@ -9,7 +9,7 @@ from smllib.sml import SmlListEntry -def test_open_response(): +def test_open_response() -> None: r = SmlOpenResponseBuilder().build(in_snip([None, None, 'ab', 'cd', None, 1]), {}) assert r.format_msg() == ( '\n' @@ -22,7 +22,7 @@ def test_open_response(): ) -def test_close_response(): +def test_close_response() -> None: r = SmlCloseResponseBuilder().build(in_snip(['my_sig']), {}) assert r.format_msg() == ( '\n' @@ -30,7 +30,7 @@ def test_close_response(): ) -def test_list_entry(): +def test_list_entry() -> None: data = in_snip([ None, 'server', None, None, [['0100010800ff', None, None, None, None, '76616c31', None], diff --git a/tests/sml/test_obis.py b/tests/sml/test_obis.py index b76a8d0..c08fbe2 100644 --- a/tests/sml/test_obis.py +++ b/tests/sml/test_obis.py @@ -3,13 +3,13 @@ from smllib.sml.sml_obis import build_obis -def test_obis(): +def test_obis() -> None: assert build_obis('0100010800ff') == '0100010800ff' assert build_obis('0100010800ff').obis_code == '1-0:1.8.0*255' assert build_obis('0100010800ff').obis_short == '1.8.0' -def test_obis_invalid(): +def test_obis_invalid() -> None: with pytest.raises(ValueError): # noqa: PT011 build_obis(None) with pytest.raises(ValueError): # noqa: PT011 diff --git a/tests/sml/test_time.py b/tests/sml/test_time.py index 65a5ebc..9de9004 100644 --- a/tests/sml/test_time.py +++ b/tests/sml/test_time.py @@ -7,7 +7,7 @@ from smllib.sml.sml_time import build_time -def test_sml_time(): +def test_sml_time() -> None: assert build_time(None) is None assert build_time(in_snip([1, 253], pack_top=False)) == 253 assert build_time(in_snip([2, 1609466461], pack_top=False)) == datetime(2021, 1, 1, 2, 1, 1) @@ -17,7 +17,7 @@ def test_sml_time(): assert build_time(in_snip([3, [1622509261, 120, 30]], pack_top=False)) == datetime(2021, 6, 1, 3, 31, 1) -def test_exception(): +def test_exception() -> None: with pytest.raises(UnsupportedChoiceValue) as e: build_time([in_snip(5), in_snip(55)]) assert e.value.type == 5 diff --git a/tests/test_crc.py b/tests/test_crc.py index d75cf18..3a2ea83 100644 --- a/tests/test_crc.py +++ b/tests/test_crc.py @@ -1,8 +1,13 @@ +import inspect +import typing from binascii import a2b_hex +from typing import Union import pytest -from smllib.crc import get_crc +from smllib import SmlStreamReader +from smllib import crc as crc_module +from smllib.crc.x25 import get_crc as x25_get_crc @pytest.mark.parametrize('msg', ( @@ -12,8 +17,51 @@ pytest.param('76051c414c02620062007263010176010102310b0a01445a47000282c0b07262016505471c2a620263f93800', id='crc4'), pytest.param('76040000016200620072650000010176010107000002dba23c0b0a01484c5902000424a0010163945b00', id='crc5'), )) -def test_crc(msg): +def test_crc_x25(msg) -> None: _msg = memoryview(a2b_hex(msg)) crc_msg = f'{_msg[-3]:02x}{_msg[-2]:02x}' - crc_calc = f'{get_crc(_msg[:-4]):04x}' + crc_calc = f'{x25_get_crc(_msg[:-4]):04x}' assert crc_msg == crc_calc + + +def _get_signature() -> inspect.Signature: + return inspect.Signature( + parameters=[ + inspect.Parameter('buf', kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Union[memoryview, bytes]) + ], + return_annotation=int + ) + + +@pytest.mark.parametrize('name', (n for n in dir(crc_module) if not n.startswith('_'))) +def test_signature_crc_funcs(name: str) -> None: + crc_impl = getattr(crc_module, name) + crc_sig = inspect.signature(crc_impl.get_crc) + assert crc_sig == _get_signature() + + +def test_type_hint_reader() -> None: + # Literal + available = [n for n in dir(crc_module) if not n.startswith('_')] + hint = typing.get_type_hints(SmlStreamReader.__init__) + literal = hint['crc'] + literal_values = typing.get_args(literal) + assert set(available) == set(literal_values) + + # crc_func variable + signature = _get_signature() + a = SmlStreamReader() + hint = typing.get_type_hints(a.crc_func) + assert hint.pop('return') is signature.return_annotation + + assert hint + for name in hint: + assert hint[name] == signature.parameters[name].annotation + + + +def test_invalid_crc_name() -> None: + + with pytest.raises(ValueError) as e: + SmlStreamReader(crc='asfd') + assert str(e.value) == 'Unsupported CRC "asfd"! Available: "kermit", "x25"' diff --git a/tests/test_frame_snipped.py b/tests/test_frame_snipped.py index 9cbb2f3..4c856f9 100644 --- a/tests/test_frame_snipped.py +++ b/tests/test_frame_snipped.py @@ -4,7 +4,7 @@ from smllib.sml_frame_snippet import SmlFrameSnippet -def test_snippet_type(): +def test_snippet_type() -> None: s = SmlFrameSnippet(8, 0) with pytest.raises(WrongValueType) as e: diff --git a/tests/test_frames.py b/tests/test_frames.py index 0267955..276d6d1 100644 --- a/tests/test_frames.py +++ b/tests/test_frames.py @@ -2,9 +2,32 @@ import pytest +from smllib.errors import InvalidBufferPos from smllib.reader import SmlFrame, SmlStreamReader +def process_frame(frame: SmlFrame, get_obis_fails: bool = True): + assert isinstance(frame, SmlFrame) + + # ensure that parsing always works + for _ in range(3): + sml_messages = frame.parse_frame() + assert len(sml_messages) >= 3, sml_messages + for msg in sml_messages: + msg.format_msg() + + try: + obis_values = frame.get_obis() + except InvalidBufferPos: + if not get_obis_fails: + raise + continue + else: + assert len(obis_values) >= 4, obis_values + for obis in obis_values: + obis.get_value() + + @pytest.mark.parametrize( 'frame', ( pytest.param( @@ -48,48 +71,61 @@ b'019EBD01010163F08A007605011FBC8562006200726500000201710163E38F000000001B1B1B1B1A0336DB', id='Frame Issue #8 (ERR)' ), - pytest.param( - b'1B1B1B1B010101017605011FBC8362006200726500000101760101070000000000000B00000000000000000000010163F8F6007' - b'605011FBC846200620072650000070177010B000000000000000000000172620165002FF64F7A77078181C78203FF0101010104' - b'45425A0177070100000009FF010101010B000000000000000000000177070100010800FF6401018001621E52FB690000000AC07' - b'048A70177070100010801FF0101621E52FB6900000000000000000177070100010802FF0101621E52FB6900000000060FD1A001' - b'77070100020800FF6401018001621E52FB69000000000D19E1C00177070100100700FF0101621B52FE550001C39701770701002' - b'40700FF0101621B52FE5500001AC60177070100380700FF0101621B52FE5500000A1401770701004C0700FF0101621B52FE5500' - b'019EBD01010163F08A007605011FBC8562006200726500000201710163E38F000000001B1B1B1B1A03509F', - id='Frame Issue #8 (FIXED)' - ), + ) +) +def test_frames(frame) -> None: + reader = SmlStreamReader() + reader.add(a2b_hex(frame)) + process_frame(reader.get_frame()) + + +@pytest.mark.parametrize( + 'frame', ( # # This is a frame where the shortcut fails # - # pytest.param( - # b'1b1b1b1b010101017605077707006200620072630101760107ffffffffffff05027d02560b0a01454d4800009f3846726201650' - # b'27d082b62016312980076050777070162006200726307017707ffffffffffff0b0a01454d4800009f3846070100620affff7262' - # b'0165027d082b7577070100603201010101010104454d480177070100600100ff010101010b0a01454d4800009f3846017707010' - # b'0010800ff641c010472620165027d082b621e52ff6501ddf5f40177070100020800ff0172620165027d082b621e52ff6501d4dc' - # b'0d0177070100100700ff0101621b520053039f01010163ec0100760507770702620062007263020171016344d5001b1b1b1b1a0' - # b'08aa9', - # id='Frame Issue #15 (FIXED)' - # ), + pytest.param( + b'1b1b1b1b010101017605077707006200620072630101760107ffffffffffff05027d02560b0a01454d4800009f3846726201650' + b'27d082b62016312980076050777070162006200726307017707ffffffffffff0b0a01454d4800009f3846070100620affff7262' + b'0165027d082b7577070100603201010101010104454d480177070100600100ff010101010b0a01454d4800009f3846017707010' + b'0010800ff641c010472620165027d082b621e52ff6501ddf5f40177070100020800ff0172620165027d082b621e52ff6501d4dc' + b'0d0177070100100700ff0101621b520053039f01010163ec0100760507770702620062007263020171016344d5001b1b1b1b1a0' + b'08aa9', + id='Frame Issue #15 (FIXED)' + ), ) ) -def test_frames(frame): - +def test_frames_get_obis_fails(frame) -> None: reader = SmlStreamReader() reader.add(a2b_hex(frame)) + process_frame(reader.get_frame(), get_obis_fails=True) - parsed_frame = reader.get_frame() - assert parsed_frame is not None - - # ensure that parsing always works - for _ in range(3): - sml_messages = parsed_frame.parse_frame() - assert len(sml_messages) >= 3, sml_messages - obis_values = parsed_frame.get_obis() - assert len(obis_values) >= 4, obis_values - - for obis in obis_values: - obis.get_value() +@pytest.mark.parametrize( + 'frame', ( + pytest.param( + b'1b1b1b1b010101017604000001620062007265000001017601010700000a5758520b0a01484c5902000159bb010163547d00760' + b'40000026200620072650000070177010b0a01484c5902000159bb0101f10e77070100603201010101010104484c590177070100' + b'600100ff010101010b0a01484c5902000159bb0177070100010800ff65001c0104650a575853621e52ff65066f04ab017707010' + b'0020800ff65001c0104650a575853621e52ff65081cb4540177070100100700ff0101621b52005300bc0177070100200700ff01' + b'01622352ff6309380177070100340700ff0101622352ff6309290177070100480700ff0101622352ff63092401770701001f070' + b'0ff0101622152fe62450177070100330700ff0101622152fe62150177070100470700ff0101622152fe621a0177070100510701' + b'ff01016208520062780177070100510702ff01016208520062f00177070100510704ff010162085200630146017707010051070' + b'fff010162085200630129017707010051071aff01016208520063012801770701000e0700ff0101622c52ff6301f40177070100' + b'010800600101621e520262290177070100010800610101621e52026301650177070100010800620101621e52026305050177070' + b'100010800630101621e520263600d0177070100010800640101621e5202650001a5a20177070100020800600101621e52026237' + b'0177070100020800610101621e52026301580177070100020800620101621e52026309090177070100020800630101621e52026' + b'34aec0177070100020800640101621e520265000213a20177070100000200000101010109312e30322e3030370177070100605a' + b'02010101010105413031410177070100600500ff0101010165001c0104010101633361007604000003620062007265000002017' + b'10163ebf400001b1b1b1b1a017502', + id='Frame Issue #8 (FIXED)' + ), + ) +) +def test_frames_kermit(frame) -> None: + reader = SmlStreamReader(crc='kermit') + reader.add(a2b_hex(frame)) + process_frame(reader.get_frame()) @pytest.mark.parametrize( @@ -134,20 +170,6 @@ def test_frames(frame): ), ) ) -def test_frame_only(data): +def test_frame_only(data) -> None: frame = SmlFrame(a2b_hex(data)) - - # ensure that parsing always works - for _ in range(3): - sml_messages = frame.parse_frame() - assert len(sml_messages) >= 3, sml_messages - for msg in sml_messages: - msg.format_msg() - - obis_values = frame.get_obis() - assert len(obis_values) >= 4, obis_values - - for obis in obis_values: - print(obis.format_msg()) - obis.get_value() - break + process_frame(frame) diff --git a/tests/test_sml_data_types.py b/tests/test_sml_data_types.py index a45aea3..5bd4784 100644 --- a/tests/test_sml_data_types.py +++ b/tests/test_sml_data_types.py @@ -3,7 +3,7 @@ from smllib.sml_frame import EndOfSmlMsg, SmlFrame, SmlFrameSnippet -def check(f: SmlFrameSnippet, value, msg: str): +def check(f: SmlFrameSnippet, value, msg: str) -> None: if value is True or value is False or value is None or value is EndOfSmlMsg: assert f.value is value else: @@ -15,7 +15,7 @@ def check(f: SmlFrameSnippet, value, msg: str): assert f.msg.hex() == msg -def test_get_int8(): +def test_get_int8() -> None: f = SmlFrame(b'\x52\xff') check(f.get_value(0), -1, '52ff') assert f.next_pos == 2 @@ -30,7 +30,7 @@ def test_get_int8(): assert f.next_pos == 3 -def test_get_uint8(): +def test_get_uint8() -> None: f = SmlFrame(b'\x62\xff') check(f.get_value(0), 255, '62ff') assert f.next_pos == 2 @@ -45,7 +45,7 @@ def test_get_uint8(): assert f.next_pos == 3 -def test_get_int16(): +def test_get_int16() -> None: f = SmlFrame(b'\x53\xff\x00') check(f.get_value(0), -256, '53ff00') assert f.next_pos == 3 @@ -60,7 +60,7 @@ def test_get_int16(): assert f.next_pos == 4 -def test_get_uint16(): +def test_get_uint16() -> None: f = SmlFrame(b'\x63\xff\x00') check(f.get_value(0), 65280, '63ff00') assert f.next_pos == 3 @@ -75,62 +75,32 @@ def test_get_uint16(): assert f.next_pos == 4 -def test_get_int32(): +def test_get_int32() -> None: f = SmlFrame(b'\x55\x00\x00\x0a\x8c') check(f.get_value(0), 2700, '5500000a8c') assert f.next_pos == 5 + f = SmlFrame(b'\x55\xFF\xFF\xFF\xFF') + check(f.get_value(0), -1, '55ffffffff') + assert f.next_pos == 5 + f = SmlFrame(b'\x01\x01\x55\x00\x00\x0a\x8c') f.next_pos = 9999 check(f.get_value(2), 2700, '5500000a8c') assert f.next_pos == 7 -def test_get_uint32(): - f = SmlFrame(b'\x55\x00\x00\x0a\x8c') - check(f.get_value(0), 2700, '5500000a8c') +def test_get_uint32() -> None: + f = SmlFrame(b'\x65\x00\x00\x0a\x8c') + check(f.get_value(0), 2700, '6500000a8c') assert f.next_pos == 5 + f = SmlFrame(b'\xab\xcd\x87\x44\x65\x0c\x6a\x50\xb5') + check(f.get_value(4), 208294069, '650c6a50b5') + assert f.next_pos == 9 + -# def test_int(): -# -# f = SmlFrame(b'\x65\x0c\x6a\x50\xb5') -# assert f.get_value(0) == 208294069 -# -# # too short stuff -# with pytest.raises(InvalidBufferPos): -# f = SmlFrame(b'\x56\x00\x04\xeb\x09') -# assert f.get_value(0) is None -# with pytest.raises(InvalidBufferPos): -# f = SmlFrame(b'\x65\x0c\x6a') -# assert f.get_value(0) is None -# -# # now with indexes > 0 -# f = SmlFrame(b'\xaa\xbb\x56\x00\x04\xeb\x09\x6c') -# assert f.get_value(2) == 82512236 -# f = SmlFrame(b'\x00\x52\xff') -# assert f.get_value(1) == -1 -# f = SmlFrame(b'\x01\xff\x33\x62\x1e') -# assert f.get_value(3) == 30 -# f = SmlFrame(b'\xab\xcd\x87\x44\x65\x0c\x6a\x50\xb5') -# assert f.get_value(4) == 208294069 -# -# # now with stuff appended -# f = SmlFrame(b'\x56\x00\x04\xeb\x09\x6c\x12\x34') -# assert f.get_value(0) == 82512236 -# f = SmlFrame(b'\xaa\xbb\x52\xff\xcc\xdd') -# assert f.get_value(2) == -1 -# f = SmlFrame(b'\x52\x62\x1e\x99') -# assert f.get_value(1) == 30 -# f = SmlFrame(b'\x65\x0c\x6a\x50\xb5\x77\x88') -# assert f.get_value(0) == 208294069 -# -# # first some values starting at index 0 -# f = SmlFrame(b'\x56\x00\x04\xeb\x09\x6c') -# assert check(f.get_value(0), 82512236, '5600') -# - -def test_none(): +def test_none() -> None: f = SmlFrame(b'\x01\x01\x01') assert f.get_value().value is None assert f.next_pos == 1 @@ -143,7 +113,7 @@ def test_none(): assert f.next_pos == 3 -def test_get_list(): +def test_get_list() -> None: f = SmlFrame(b'\x71\x01') f.next_pos = 9999 check(f._parse_msg(f.get_value(0)), [None], '7101') @@ -173,14 +143,14 @@ def test_get_list(): assert f.next_pos == 28 -def test_str(): +def test_str() -> None: f = SmlFrame(b'\x07\x01\x00\x01\x08\x00\xFF') f.next_pos = 9999 check(f.get_value(0), '0100010800ff', '070100010800ff') assert f.next_pos == 7 -def test_long_str(): +def test_long_str() -> None: f = SmlFrame(a2b_hex('8302010203040101010101010101010101010101010101010101010101010101010101010101010101010101010101010102')) # noqa: E501 check( f.get_value(0), @@ -197,7 +167,7 @@ def test_long_str(): assert f.buffer[f.next_pos] == 0xFF -def test_bool(): +def test_bool() -> None: f = SmlFrame(b'\x42\x01') f.next_pos = 9999 check(f.get_value(0), True, '4201') @@ -208,7 +178,7 @@ def test_bool(): assert f.next_pos == 2 -def test_eom(): +def test_eom() -> None: f = SmlFrame(b'\x00') check(f.get_value(0), EndOfSmlMsg, '00') assert f.next_pos == 1 diff --git a/tests/test_sml_fields.py b/tests/test_sml_fields.py index 0cb0e26..531a0d6 100644 --- a/tests/test_sml_fields.py +++ b/tests/test_sml_fields.py @@ -4,7 +4,7 @@ from smllib.sml_frame import SmlFrame -def test_sml_fields(): +def test_sml_fields() -> None: f = SmlFrame(a2b_hex('77078181c78203ff010101010449534b0177070100000009ff010101010b')) val_list = f._parse_msg(f.get_value(0)) o = SmlListEntryBuilder().build(val_list, create_context()) @@ -23,7 +23,7 @@ def test_sml_fields(): assert o.get_value() == 1796587.6 -def test_val_time(): +def test_val_time() -> None: # Frame where time is None f = SmlFrame(a2b_hex('77070100600100ff010101010b0a01484c5902000424a001')) val_list = f._parse_msg(f.get_value(0)) diff --git a/tests/test_sml_stream_reader.py b/tests/test_sml_stream_reader.py index 4d9a755..01d7e02 100644 --- a/tests/test_sml_stream_reader.py +++ b/tests/test_sml_stream_reader.py @@ -5,14 +5,14 @@ from smllib.reader import CrcError, SmlStreamReader -def test_strip_start(): +def test_strip_start() -> None: r = SmlStreamReader() r.add(b'asdfasdfasdf\x1B\x1B\x1B\x1B\x01\x01\x01\x01') assert r.get_frame() is None assert r.bytes == b'\x1B\x1B\x1B\x1B\x01\x01\x01\x01' -def test_skip_escape(): +def test_skip_escape() -> None: r = SmlStreamReader() msg = b'\x1B\x1B\x1B\x1B\x01\x01\x01\x01\x1B\x1B\x1B\x1B\x1B\x1B\x1B\x1B\x1A\x00\x00\x00' r.add(msg) @@ -20,7 +20,7 @@ def test_skip_escape(): assert r.bytes == msg -def test_exception(): +def test_exception() -> None: r = SmlStreamReader() msg = b'\x1B\x1B\x1B\x1B\x01\x01\x01\x01\x1B\x1B\x1B\x1B\x1A\x00\x00\x00' r.add(msg) @@ -29,7 +29,7 @@ def test_exception(): assert repr(e.value) == '' -def test_msg_long_list(): +def test_msg_long_list() -> None: msg2 = binascii.a2b_hex( '1b1b1b1b01010101' '76040000016200620072650000010176010107000002dba23c0b0a01484c5902000424a0010163945b00' diff --git a/tox.ini b/tox.ini index 8866d2c..045a081 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,9 @@ envlist = py310 py311 py312 - flake + py313 + slotscheck + [gh-actions] python = @@ -14,11 +16,20 @@ python = 3.9: py39 3.10: py310 3.11: py311 - 3.12: py312 + 3.12: py312, slotscheck + 3.13: py313 + [testenv] deps = -r{toxinidir}/requirements_tests.txt - commands = python -m pytest + + +[testenv:slotscheck] +deps = + slotscheck +change_dir = {toxinidir}/src +commands = + python -m slotscheck smllib --verbose