From 7642acec8be8647bfd6a30291710d0e71b3cf109 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 16:54:49 +0200 Subject: [PATCH 01/10] Tag/improve quality --- CHANGELOG.md | 6 + airos/airos8.py | 5 +- airos/data.py | 8 +- airos/discovery.py | 16 +- mypy.ini | 44 ++ pyproject.toml | 1025 ++++++++++++++++---------- requirements-test.txt | 1 + script/__init__.py | 1 + script/generate_discovery_fixture.py | 8 +- script/generate_ha_fixture.py | 20 +- script/mashumaro-step-debug.py | 14 +- tests/conftest.py | 6 +- tests/test_airos8.py | 8 +- tests/test_airos_request.py | 16 +- tests/test_data.py | 3 +- tests/test_discovery.py | 31 +- tests/test_stations.py | 8 +- 17 files changed, 744 insertions(+), 476 deletions(-) create mode 100644 mypy.ini create mode 100644 script/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7185045..9f02da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.4.2] - 2025-08-17 + +### Changed + +- Aligned quality targets either improved or tagged + ## [0.4.1] - 2025-08-17 ### Changed diff --git a/airos/airos8.py b/airos/airos8.py index 184fac1..47ca7a5 100644 --- a/airos/airos8.py +++ b/airos/airos8.py @@ -217,7 +217,7 @@ async def _request_json( raise AirOSConnectionAuthenticationError from err raise AirOSConnectionSetupError from err except (TimeoutError, aiohttp.ClientError) as err: - _LOGGER.exception("Error during API call to %s: %s", url, err) + _LOGGER.exception("Error during API call to %s", url) raise AirOSDeviceConnectionError from err except json.JSONDecodeError as err: _LOGGER.error("Failed to decode JSON from %s", url) @@ -242,8 +242,7 @@ async def status(self) -> AirOSData: try: adjusted_json = self.derived_data(response) - airos_data = AirOSData.from_dict(adjusted_json) - return airos_data + return AirOSData.from_dict(adjusted_json) except InvalidFieldValue as err: # Log with .error() as this is a specific, known type of issue redacted_data = redact_data_smart(response) diff --git a/airos/data.py b/airos/data.py index e840a25..b3d092a 100644 --- a/airos/data.py +++ b/airos/data.py @@ -34,9 +34,9 @@ def is_ip_address(value: str) -> bool: """Check if a string is a valid IPv4 or IPv6 address.""" try: ipaddress.ip_address(value) - return True except ValueError: return False + return True def redact_data_smart(data: dict[str, Any]) -> dict[str, Any]: @@ -107,8 +107,6 @@ def _redact(d: dict[str, Any]) -> dict[str, Any]: class AirOSDataClass(DataClassDictMixin): """A base class for all mashumaro dataclasses.""" - pass - def _check_and_log_unknown_enum_value( data_dict: dict[str, Any], @@ -534,15 +532,11 @@ class Interface(AirOSDataClass): class ProvisioningMode(AirOSDataClass): """Leaf definition.""" - pass - @dataclass class NtpClient(AirOSDataClass): """Leaf definition.""" - pass - @dataclass class GPSMain(AirOSDataClass): diff --git a/airos/discovery.py b/airos/discovery.py index d473d6d..dd0a6ad 100644 --- a/airos/discovery.py +++ b/airos/discovery.py @@ -151,7 +151,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None log = f"Truncated MAC address TLV (Type 0x06). Expected {expected_length}, got {len(data) - offset} bytes. Remaining: {data[offset:].hex()}" _LOGGER.warning(log) log = f"Malformed packet: {log}" - raise AirOSEndpointError(log) + raise AirOSEndpointError(log) from None # noqa: TRY301 elif tlv_type in [ 0x02, @@ -169,7 +169,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None log = f"Truncated TLV (Type {tlv_type:#x}), no 2-byte length field. Remaining: {data[offset:].hex()}" _LOGGER.warning(log) log = f"Malformed packet: {log}" - raise AirOSEndpointError(log) + raise AirOSEndpointError(log) from None # noqa: TRY301 tlv_length: int = struct.unpack_from(">H", data, offset)[0] offset += 2 @@ -181,7 +181,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None f"Data from TLV start: {data[offset - 3 :].hex()}" ) _LOGGER.warning(log) - raise AirOSEndpointError(f"Malformed packet: {log}") + raise AirOSEndpointError(f"Malformed packet: {log}") from None # noqa: TRY301 tlv_value: bytes = data[offset : offset + tlv_length] @@ -194,7 +194,9 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None else: log = f"Unexpected length for 0x02 TLV (MAC+IP). Expected 10, got {tlv_length}. Value: {tlv_value.hex()}" _LOGGER.warning(log) - raise AirOSEndpointError(f"Malformed packet: {log}") + raise AirOSEndpointError( # noqa: TRY301 + f"Malformed packet: {log}" + ) from None elif tlv_type == 0x03: parsed_info["firmware_version"] = tlv_value.decode( @@ -213,7 +215,9 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None else: log = f"Unexpected length for Uptime (Type 0x0A): {tlv_length}. Value: {tlv_value.hex()}" _LOGGER.warning(log) - raise AirOSEndpointError(f"Malformed packet: {log}") + raise AirOSEndpointError( # noqa: TRY301 + f"Malformed packet: {log}" + ) from None elif tlv_type == 0x0B: parsed_info["hostname"] = tlv_value.decode( @@ -260,7 +264,7 @@ def parse_airos_packet(self, data: bytes, host_ip: str) -> dict[str, Any] | None log += f"Cannot determine length, stopping parsing. Remaining: {data[offset - 1 :].hex()}" _LOGGER.warning(log) log = f"Malformed packet: {log}" - raise AirOSEndpointError(log) + raise AirOSEndpointError(log) from None # noqa: TRY301 except (struct.error, IndexError) as err: log = f"Parsing error (struct/index) in AirOSDiscoveryProtocol: {err} at offset {offset}. Remaining data: {data[offset:].hex()}" diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..67755f1 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,44 @@ +[mypy] +python_version = 3.13 +platform = linux +plugins = pydantic.mypy, pydantic.v1.mypy +show_error_codes = true +follow_imports = normal +local_partial_types = true +strict_equality = true +strict_bytes = true +no_implicit_optional = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +enable_error_code = deprecated, ignore-without-code, redundant-self, truthy-iterable +disable_error_code = annotation-unchecked, import-not-found, import-untyped +extra_checks = false +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + +[pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true +warn_untyped_fields = true + +[mypy-airos.*] +no_implicit_reexport = true + +[mypy-tests.*] +check_untyped_defs = false +disallow_incomplete_defs = false +disallow_subclassing_any = false +disallow_untyped_calls = false +disallow_untyped_decorators = false +disallow_untyped_defs = false +warn_return_any = false +warn_unreachable = false \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bc8f9d1..df5624c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.4.1" +version = "0.4.2a0" license = "MIT" description = "Ubiquiti airOS module(s) for Python 3." readme = "README.md" @@ -54,8 +54,8 @@ init-hook = """\ ) \ """ load-plugins = [ - "pylint.extensions.code_style", - "pylint.extensions.typing", + "pylint.extensions.code_style", + "pylint.extensions.typing", ] persistent = false extension-pkg-allow-list = [ @@ -93,258 +93,258 @@ class-const-naming-style = "any" # consider-using-namedtuple-or-dataclass - too opinionated # consider-using-assignment-expr - decision to use := better left to devs disable = [ - - # Plugwise - "attribute-defined-outside-init", # W0201 - "too-many-nested-blocks", #R1702 - "consider-using-tuple", #R6102 - - # Generic - "format", - "abstract-method", - "cyclic-import", - "duplicate-code", - "inconsistent-return-statements", - "locally-disabled", - "not-context-manager", - "too-few-public-methods", - "too-many-ancestors", - "too-many-arguments", - "too-many-instance-attributes", - "too-many-lines", - "too-many-locals", - "too-many-public-methods", - "too-many-boolean-expressions", - "too-many-positional-arguments", - "wrong-import-order", - "consider-using-namedtuple-or-dataclass", - "consider-using-assignment-expr", - "possibly-used-before-assignment", - - # Handled by ruff - # Ref: - "await-outside-async", # PLE1142 - "bad-str-strip-call", # PLE1310 - "bad-string-format-type", # PLE1307 - "bidirectional-unicode", # PLE2502 - "continue-in-finally", # PLE0116 - "duplicate-bases", # PLE0241 - "misplaced-bare-raise", # PLE0704 - "format-needs-mapping", # F502 - "function-redefined", # F811 - # Needed because ruff does not understand type of __all__ generated by a function - # "invalid-all-format", # PLE0605 - "invalid-all-object", # PLE0604 - "invalid-character-backspace", # PLE2510 - "invalid-character-esc", # PLE2513 - "invalid-character-nul", # PLE2514 - "invalid-character-sub", # PLE2512 - "invalid-character-zero-width-space", # PLE2515 - "logging-too-few-args", # PLE1206 - "logging-too-many-args", # PLE1205 - "missing-format-string-key", # F524 - "mixed-format-string", # F506 - "no-method-argument", # N805 - "no-self-argument", # N805 - "nonexistent-operator", # B002 - "nonlocal-without-binding", # PLE0117 - "not-in-loop", # F701, F702 - "notimplemented-raised", # F901 - "return-in-init", # PLE0101 - "return-outside-function", # F706 - "syntax-error", # E999 - "too-few-format-args", # F524 - "too-many-format-args", # F522 - "too-many-star-expressions", # F622 - "truncated-format-string", # F501 - "undefined-all-variable", # F822 - "undefined-variable", # F821 - "used-prior-global-declaration", # PLE0118 - "yield-inside-async-function", # PLE1700 - "yield-outside-function", # F704 - "anomalous-backslash-in-string", # W605 - "assert-on-string-literal", # PLW0129 - "assert-on-tuple", # F631 - "bad-format-string", # W1302, F - "bad-format-string-key", # W1300, F - "bare-except", # E722 - "binary-op-exception", # PLW0711 - "cell-var-from-loop", # B023 - # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work - "duplicate-except", # B014 - "duplicate-key", # F601 - "duplicate-string-formatting-argument", # F - "duplicate-value", # F - "eval-used", # S307 - "exec-used", # S102 - "expression-not-assigned", # B018 - "f-string-without-interpolation", # F541 - "forgotten-debug-statement", # T100 - "format-string-without-interpolation", # F - # "global-statement", # PLW0603, ruff catches new occurrences, needs more work - "global-variable-not-assigned", # PLW0602 - # "implicit-str-concat", # ISC001 - "import-self", # PLW0406 - "inconsistent-quotes", # Q000 - "invalid-envvar-default", # PLW1508 - "keyword-arg-before-vararg", # B026 - "logging-format-interpolation", # G - "logging-fstring-interpolation", # G - "logging-not-lazy", # G - "misplaced-future", # F404 - "named-expr-without-context", # PLW0131 - "nested-min-max", # PLW3301 - "pointless-statement", # B018 - "raise-missing-from", # B904 - "redefined-builtin", # A001 - "try-except-raise", # TRY302 - "unused-argument", # ARG001, we don't use it - "unused-format-string-argument", #F507 - "unused-format-string-key", # F504 - "unused-import", # F401 - "unused-variable", # F841 - "useless-else-on-loop", # PLW0120 - "wildcard-import", # F403 - "bad-classmethod-argument", # N804 - "consider-iterating-dictionary", # SIM118 - "empty-docstring", # D419 - "invalid-name", # N815 - "line-too-long", # E501, disabled globally - "missing-class-docstring", # D101 - "missing-final-newline", # W292 - "missing-function-docstring", # D103 - "missing-module-docstring", # D100 - "multiple-imports", #E401 - "singleton-comparison", # E711, E712 - "subprocess-run-check", # PLW1510 - "superfluous-parens", # UP034 - "ungrouped-imports", # I001 - "unidiomatic-typecheck", # E721 - "unnecessary-direct-lambda-call", # PLC3002 - "unnecessary-lambda-assignment", # PLC3001 - "unnecessary-pass", # PIE790 - "unneeded-not", # SIM208 - "useless-import-alias", # PLC0414 - "wrong-import-order", # I001 - "wrong-import-position", # E402 - "comparison-of-constants", # PLR0133 - "comparison-with-itself", # PLR0124 - "consider-alternative-union-syntax", # UP007 - "consider-merging-isinstance", # PLR1701 - "consider-using-alias", # UP006 - "consider-using-dict-comprehension", # C402 - "consider-using-generator", # C417 - "consider-using-get", # SIM401 - "consider-using-set-comprehension", # C401 - "consider-using-sys-exit", # PLR1722 - "consider-using-ternary", # SIM108 - "literal-comparison", # F632 - "property-with-parameters", # PLR0206 - "super-with-arguments", # UP008 - "too-many-branches", # PLR0912 - "too-many-return-statements", # PLR0911 - "too-many-statements", # PLR0915 - "trailing-comma-tuple", # COM818 - "unnecessary-comprehension", # C416 - "use-a-generator", # C417 - "use-dict-literal", # C406 - "use-list-literal", # C405 - "useless-object-inheritance", # UP004 - "useless-return", # PLR1711 - "no-else-break", # RET508 - "no-else-continue", # RET507 - "no-else-raise", # RET506 - "no-else-return", # RET505 - "broad-except", # BLE001 - "protected-access", # SLF001 - "broad-exception-raised", # TRY002 - "consider-using-f-string", # PLC0209 - # "no-self-use", # PLR6301 # Optional plugin, not enabled - - # Handled by mypy - # Ref: - "abstract-class-instantiated", - "arguments-differ", - "assigning-non-slot", - "assignment-from-no-return", - "assignment-from-none", - "bad-exception-cause", - "bad-format-character", - "bad-reversed-sequence", - "bad-super-call", - "bad-thread-instantiation", - "catching-non-exception", - "comparison-with-callable", - "deprecated-class", - "dict-iter-missing-items", - "format-combined-specification", - "global-variable-undefined", - "import-error", - "inconsistent-mro", - "inherit-non-class", - "init-is-generator", - "invalid-class-object", - "invalid-enum-extension", - "invalid-envvar-value", - "invalid-format-returned", - "invalid-hash-returned", - "invalid-metaclass", - "invalid-overridden-method", - "invalid-repr-returned", - "invalid-sequence-index", - "invalid-slice-index", - "invalid-slots-object", - "invalid-slots", - "invalid-star-assignment-target", - "invalid-str-returned", - "invalid-unary-operand-type", - "invalid-unicode-codec", - "isinstance-second-argument-not-valid-type", - "method-hidden", - "misplaced-format-function", - "missing-format-argument-key", - "missing-format-attribute", - "missing-kwoa", - "no-member", - "no-value-for-parameter", - "non-iterator-returned", - "non-str-assignment-to-dunder-name", - "nonlocal-and-global", - "not-a-mapping", - "not-an-iterable", - "not-async-context-manager", - "not-callable", - "not-context-manager", - "overridden-final-method", - "raising-bad-type", - "raising-non-exception", - "redundant-keyword-arg", - "relative-beyond-top-level", - "self-cls-assignment", - "signature-differs", - "star-needs-assignment-target", - "subclassed-final-class", - "super-without-brackets", - "too-many-function-args", - "typevar-double-variance", - "typevar-name-mismatch", - "unbalanced-dict-unpacking", - "unbalanced-tuple-unpacking", - "unexpected-keyword-arg", - "unhashable-member", - "unpacking-non-sequence", - "unsubscriptable-object", - "unsupported-assignment-operation", - "unsupported-binary-operation", - "unsupported-delete-operation", - "unsupported-membership-test", - "used-before-assignment", - "using-final-decorator-in-unsupported-version", - "wrong-exception-operation", + "format", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-boolean-expressions", + "too-many-positional-arguments", + "wrong-import-order", + "consider-using-namedtuple-or-dataclass", + "consider-using-assignment-expr", + "possibly-used-before-assignment", + + # Handled by ruff + # Ref: + "await-outside-async", # PLE1142 + "bad-str-strip-call", # PLE1310 + "bad-string-format-type", # PLE1307 + "bidirectional-unicode", # PLE2502 + "continue-in-finally", # PLE0116 + "duplicate-bases", # PLE0241 + "misplaced-bare-raise", # PLE0704 + "format-needs-mapping", # F502 + "function-redefined", # F811 + # Needed because ruff does not understand type of __all__ generated by a function + # "invalid-all-format", # PLE0605 + "invalid-all-object", # PLE0604 + "invalid-character-backspace", # PLE2510 + "invalid-character-esc", # PLE2513 + "invalid-character-nul", # PLE2514 + "invalid-character-sub", # PLE2512 + "invalid-character-zero-width-space", # PLE2515 + "logging-too-few-args", # PLE1206 + "logging-too-many-args", # PLE1205 + "missing-format-string-key", # F524 + "mixed-format-string", # F506 + "no-method-argument", # N805 + "no-self-argument", # N805 + "nonexistent-operator", # B002 + "nonlocal-without-binding", # PLE0117 + "not-in-loop", # F701, F702 + "notimplemented-raised", # F901 + "return-in-init", # PLE0101 + "return-outside-function", # F706 + "syntax-error", # E999 + "too-few-format-args", # F524 + "too-many-format-args", # F522 + "too-many-star-expressions", # F622 + "truncated-format-string", # F501 + "undefined-all-variable", # F822 + "undefined-variable", # F821 + "used-prior-global-declaration", # PLE0118 + "yield-inside-async-function", # PLE1700 + "yield-outside-function", # F704 + "anomalous-backslash-in-string", # W605 + "assert-on-string-literal", # PLW0129 + "assert-on-tuple", # F631 + "bad-format-string", # W1302, F + "bad-format-string-key", # W1300, F + "bare-except", # E722 + "binary-op-exception", # PLW0711 + "cell-var-from-loop", # B023 + # "dangerous-default-value", # B006, ruff catches new occurrences, needs more work + "duplicate-except", # B014 + "duplicate-key", # F601 + "duplicate-string-formatting-argument", # F + "duplicate-value", # F + "eval-used", # S307 + "exec-used", # S102 + "expression-not-assigned", # B018 + "f-string-without-interpolation", # F541 + "forgotten-debug-statement", # T100 + "format-string-without-interpolation", # F + # "global-statement", # PLW0603, ruff catches new occurrences, needs more work + "global-variable-not-assigned", # PLW0602 + "implicit-str-concat", # ISC001 + "import-outside-toplevel", # PLC0415 + "import-self", # PLW0406 + "inconsistent-quotes", # Q000 + "invalid-envvar-default", # PLW1508 + "keyword-arg-before-vararg", # B026 + "logging-format-interpolation", # G + "logging-fstring-interpolation", # G + "logging-not-lazy", # G + "misplaced-future", # F404 + "named-expr-without-context", # PLW0131 + "nested-min-max", # PLW3301 + "pointless-statement", # B018 + "raise-missing-from", # B904 + "redefined-builtin", # A001 + "try-except-raise", # TRY302 + "unused-argument", # ARG001, we don't use it + "unused-format-string-argument", #F507 + "unused-format-string-key", # F504 + "unused-import", # F401 + "unused-variable", # F841 + "useless-else-on-loop", # PLW0120 + "wildcard-import", # F403 + "bad-classmethod-argument", # N804 + "consider-iterating-dictionary", # SIM118 + "empty-docstring", # D419 + "invalid-name", # N815 + "line-too-long", # E501, disabled globally + "missing-class-docstring", # D101 + "missing-final-newline", # W292 + "missing-function-docstring", # D103 + "missing-module-docstring", # D100 + "multiple-imports", #E401 + "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 + "superfluous-parens", # UP034 + "ungrouped-imports", # I001 + "unidiomatic-typecheck", # E721 + "unnecessary-direct-lambda-call", # PLC3002 + "unnecessary-lambda-assignment", # PLC3001 + "unnecessary-pass", # PIE790 + "unneeded-not", # SIM208 + "useless-import-alias", # PLC0414 + "wrong-import-order", # I001 + "wrong-import-position", # E402 + "comparison-of-constants", # PLR0133 + "comparison-with-itself", # PLR0124 + "consider-alternative-union-syntax", # UP007 + "consider-merging-isinstance", # PLR1701 + "consider-using-alias", # UP006 + "consider-using-dict-comprehension", # C402 + "consider-using-generator", # C417 + "consider-using-get", # SIM401 + "consider-using-set-comprehension", # C401 + "consider-using-sys-exit", # PLR1722 + "consider-using-ternary", # SIM108 + "literal-comparison", # F632 + "property-with-parameters", # PLR0206 + "super-with-arguments", # UP008 + "too-many-branches", # PLR0912 + "too-many-return-statements", # PLR0911 + "too-many-statements", # PLR0915 + "trailing-comma-tuple", # COM818 + "unnecessary-comprehension", # C416 + "use-a-generator", # C417 + "use-dict-literal", # C406 + "use-list-literal", # C405 + "useless-object-inheritance", # UP004 + "useless-return", # PLR1711 + "no-else-break", # RET508 + "no-else-continue", # RET507 + "no-else-raise", # RET506 + "no-else-return", # RET505 + "broad-except", # BLE001 + "protected-access", # SLF001 + "broad-exception-raised", # TRY002 + "consider-using-f-string", # PLC0209 + # "no-self-use", # PLR6301 # Optional plugin, not enabled + + # Handled by mypy + # Ref: + "abstract-class-instantiated", + "arguments-differ", + "assigning-non-slot", + "assignment-from-no-return", + "assignment-from-none", + "bad-exception-cause", + "bad-format-character", + "bad-reversed-sequence", + "bad-super-call", + "bad-thread-instantiation", + "catching-non-exception", + "comparison-with-callable", + "deprecated-class", + "dict-iter-missing-items", + "format-combined-specification", + "global-variable-undefined", + "import-error", + "inconsistent-mro", + "inherit-non-class", + "init-is-generator", + "invalid-class-object", + "invalid-enum-extension", + "invalid-envvar-value", + "invalid-format-returned", + "invalid-hash-returned", + "invalid-metaclass", + "invalid-overridden-method", + "invalid-repr-returned", + "invalid-sequence-index", + "invalid-slice-index", + "invalid-slots-object", + "invalid-slots", + "invalid-star-assignment-target", + "invalid-str-returned", + "invalid-unary-operand-type", + "invalid-unicode-codec", + "isinstance-second-argument-not-valid-type", + "method-hidden", + "misplaced-format-function", + "missing-format-argument-key", + "missing-format-attribute", + "missing-kwoa", + "no-member", + "no-value-for-parameter", + "non-iterator-returned", + "non-str-assignment-to-dunder-name", + "nonlocal-and-global", + "not-a-mapping", + "not-an-iterable", + "not-async-context-manager", + "not-callable", + "not-context-manager", + "overridden-final-method", + "raising-bad-type", + "raising-non-exception", + "redundant-keyword-arg", + "relative-beyond-top-level", + "self-cls-assignment", + "signature-differs", + "star-needs-assignment-target", + "subclassed-final-class", + "super-without-brackets", + "too-many-function-args", + "typevar-double-variance", + "typevar-name-mismatch", + "unbalanced-dict-unpacking", + "unbalanced-tuple-unpacking", + "unexpected-keyword-arg", + "unhashable-member", + "unpacking-non-sequence", + "unsubscriptable-object", + "unsupported-assignment-operation", + "unsupported-binary-operation", + "unsupported-delete-operation", + "unsupported-membership-test", + "used-before-assignment", + "using-final-decorator-in-unsupported-version", + "wrong-exception-operation", ] enable = [ - #"useless-suppression", # temporarily every now and then to clean them up - "use-symbolic-message-instead", + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] +per-file-ignores = [ + # redefined-outer-name: Tests reference fixtures in the test function + # use-implicit-booleaness-not-comparison: Tests need to validate that a list + # or a dict is returned + "/tests/:redefined-outer-name,use-implicit-booleaness-not-comparison", ] [tool.pylint.REPORTS] @@ -352,7 +352,7 @@ score = false [tool.pylint.TYPECHECK] ignored-classes = [ - "_CountingAttr", # for attrs + "_CountingAttr", # for attrs ] mixin-class-rgx = ".*[Mm]ix[Ii]n" @@ -363,7 +363,6 @@ expected-line-ending-format = "LF" overgeneral-exceptions = [ "builtins.BaseException", "builtins.Exception", - # "homeassistant.exceptions.HomeAssistantError", # too many issues ] [tool.pylint.TYPING] @@ -372,173 +371,395 @@ runtime-typing = false [tool.pylint.CODE_STYLE] max-line-length-suggestions = 72 - -[tool.mypy] -python_version = "3.13" -show_error_codes = true -follow_imports = "silent" -ignore_missing_imports = true -strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_unused_configs = true -warn_unused_ignores = true -enable_error_code = "ignore-without-code" -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -no_implicit_optional = true -strict = true -warn_return_any = true -warn_unreachable = true -exclude = [] - -[[tool.mypy.overrides]] -module = "tests.*" -ignore_missing_imports = true # You'll likely need this for test-only dependencies -disallow_untyped_decorators = false # The fix for your current errors -check_untyped_defs = false +[tool.pytest.ini_options] +testpaths = ["tests"] +norecursedirs = [".git", "testing_config"] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "error:usefixtures\\(\\) in .* without arguments has no effect:UserWarning", # pytest + + # -- HomeAssistant - aiohttp + # Overwrite web.Application to pass a custom default argument to _make_request + "ignore:Inheritance class HomeAssistantApplication from web.Application is discouraged:DeprecationWarning", + # Hass wraps `ClientSession.close` to emit a warning if the session is closed accidentally + "ignore:Setting custom ClientSession.close attribute is discouraged:DeprecationWarning:homeassistant.helpers.aiohttp_client", + # Modify app state for testing + "ignore:Changing state of started or joined application is deprecated:DeprecationWarning:tests.components.http.test_ban", + + # -- Tests + # Ignore custom pytest marks + "ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met", + "ignore:Unknown pytest.mark.dataset:pytest.PytestUnknownMarkWarning:tests.components.screenlogic", + + # -- DeprecationWarning already fixed in our codebase + # https://github.com/kurtmckee/feedparser/pull/389 - 6.0.11 + "ignore:.*a temporary mapping .* from `updated_parsed` to `published_parsed` if `updated_parsed` doesn't exist:DeprecationWarning:feedparser.util", + + # -- design choice 3rd party + # https://github.com/gwww/elkm1/blob/2.2.11/elkm1_lib/util.py#L8-L19 + "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", + # https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52 + "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", + + # -- Setuptools DeprecationWarnings + # https://github.com/googleapis/google-cloud-python/issues/11184 + # https://github.com/zopefoundation/meta/issues/194 + # https://github.com/Azure/azure-sdk-for-python + "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", + + # -- tracked upstream / open PRs + # https://github.com/hacf-fr/meteofrance-api/pull/688 - v1.4.0 - 2025-03-26 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast", + + # -- fixed, waiting for release / update + # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", + # https://github.com/ReactiveX/RxPY/pull/716 - >4.0.4 + "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:reactivex.internal.constants", + # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", + # https://github.com/rytilahti/python-miio/pull/1993 - >0.6.0.dev0 + "ignore:functools.partial will be a method descriptor in future Python versions; wrap it in enum.member\\(\\) if you want to preserve the old behavior:FutureWarning:miio.miot_device", + # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 + "ignore:.*invalid escape sequence:SyntaxWarning:.*stringcase", + # https://github.com/xchwarze/samsung-tv-ws-api/pull/151 - >2.7.2 - 2024-12-06 # wrong stacklevel in aiohttp + "ignore:verify_ssl is deprecated, use ssl=False instead:DeprecationWarning:aiohttp.client", + + # -- other + # Locale changes might take some time to resolve upstream + # https://github.com/Squachen/micloud/blob/v_0.6/micloud/micloud.py#L35 - v0.6 - 2022-12-08 + "ignore:'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15:DeprecationWarning:micloud.micloud", + # https://pypi.org/project/agent-py/ - v0.0.24 - 2024-11-07 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a", + # https://github.com/MatsNl/pyatag/issues/11 - v0.3.7.1 - 2023-10-09 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pyatag.gateway", + # https://github.com/lidatong/dataclasses-json/issues/328 + # https://github.com/lidatong/dataclasses-json/pull/351 + "ignore:The 'default' argument to fields is deprecated. Use 'dump_default' instead:DeprecationWarning:dataclasses_json.mm", + # https://pypi.org/project/emulated-roku/ - v0.3.0 - 2023-12-19 + # https://github.com/martonperei/emulated_roku + "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", + # https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12 + # https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390 + "ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device", + # https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15 + # https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api", + # https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15 + "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", + # https://pypi.org/project/PyMetEireann/ - v2024.11.0 - 2024-11-23 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", + # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 - 2024-02-24 + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils", + # https://github.com/lextudio/pysnmp/blob/v7.1.21/pysnmp/smi/compiler.py#L23-L31 - v7.1.21 - 2025-06-19 + "ignore:smiV1Relaxed is deprecated. Please use smi_v1_relaxed instead:DeprecationWarning:pysnmp.smi.compiler", + "ignore:getReadersFromUrls is deprecated. Please use get_readers_from_urls instead:DeprecationWarning:pysnmp.smi.compiler", + # https://github.com/Python-roborock/python-roborock/issues/305 - 2.19.0 - 2025-05-13 + "ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api", + # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 + "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", + # - SyntaxWarnings + # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 + "ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common", + # https://pypi.org/project/panasonic-viera/ - v0.4.2 - 2024-04-24 + # https://github.com/florianholzapfel/panasonic-viera/blob/0.4.2/panasonic_viera/__init__.py#L789 + "ignore:.*invalid escape sequence:SyntaxWarning:.*panasonic_viera", + # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 + # https://github.com/koolsb/pyblackbird/pull/9 -> closed + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyblackbird", + # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyws66i", + # https://pypi.org/project/sanix/ - v1.0.6 - 2024-05-01 + # https://github.com/tomaszsluszniak/sanix_py/blob/v1.0.6/sanix/__init__.py#L42 + "ignore:.*invalid escape sequence:SyntaxWarning:.*sanix", + # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 + "ignore:.*invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty + # - pkg_resources + # https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20 + "ignore:pkg_resources is deprecated as an API:UserWarning:aiomusiccast", + # https://github.com/eavanvalkenburg/pysiaalarm/blob/v3.1.1/src/pysiaalarm/data/data.py#L7 - v3.1.1 - 2023-04-17 + "ignore:pkg_resources is deprecated as an API:UserWarning:pysiaalarm.data.data", + # https://pypi.org/project/pybotvac/ - v0.0.28 - 2025-06-11 + "ignore:pkg_resources is deprecated as an API:UserWarning:pybotvac.version", + # - SyntaxWarning - is with literal + # https://github.com/majuss/lupupy/pull/15 - >0.3.2 + # https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16 + # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 + # https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19 + "ignore:\"is.*\" with '.*' literal:SyntaxWarning:importlib._bootstrap", + + # -- New in Python 3.13 + # https://github.com/kurtmckee/feedparser/pull/389 - >6.0.11 + # https://github.com/kurtmckee/feedparser/issues/481 + "ignore:'count' is passed as positional argument:DeprecationWarning:feedparser.html", + # https://github.com/youknowone/python-deadlib - Backports for aifc, telnetlib + "ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:plumlightpad.lightpad", + "ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i", + + # -- Websockets 14.1 + # https://websockets.readthedocs.io/en/stable/howto/upgrade.html + "ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", + # https://github.com/graphql-python/gql/pull/543 - >=4.0.0b0 + "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base", + + # -- unmaintained projects, last release about 2+ years + # https://pypi.org/project/aiomodernforms/ - v0.1.8 - 2021-06-27 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:aiomodernforms.modernforms", + # https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv", + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models", + # https://pypi.org/project/enocean/ - v0.50.1 (installed) -> v0.60.1 - 2021-06-18 + "ignore:It looks like you're using an HTML parser to parse an XML document:UserWarning:enocean.protocol.eep", + # https://pypi.org/project/influxdb/ - v5.3.2 - 2024-04-18 (archived) + "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb.line_protocol", + # https://pypi.org/project/lark-parser/ - v0.12.0 - 2021-08-30 -> moved to `lark` + # https://pypi.org/project/commentjson/ - v0.9.0 - 2020-10-05 + # https://github.com/vaidik/commentjson/issues/51 + # https://github.com/vaidik/commentjson/pull/52 + # Fixed upstream, commentjson depends on old version and seems to be unmaintained + "ignore:module '(sre_parse|sre_constants)' is deprecate:DeprecationWarning:lark.utils", + # https://pypi.org/project/lomond/ - v0.3.3 - 2018-09-21 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:lomond.session", + # https://pypi.org/project/oauth2client/ - v4.1.3 - 2018-09-07 (archived) + "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client", + # https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19 + "ignore:pkg_resources is deprecated as an API:UserWarning:pilight", + # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 + "ignore:.*invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery", + # https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05 + "ignore:.*invalid escape sequence:SyntaxWarning:.*ppadb", + # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pydub.utils", + # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 + "ignore:.*invalid escape sequence:SyntaxWarning:.*pypasser.utils", + # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 + "ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_", + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_", + # https://pypi.org/project/rxv/ - v0.7.0 - 2021-10-10 + "ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp", +] [tool.coverage.run] -source = [ "airos" ] -omit= [ - "*/venv/*", - "setup.py", +source = ["homeassistant"] + +[tool.coverage.report] +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about missing debug-only code: + "def __repr__", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # TYPE_CHECKING and @overload blocks are never executed during pytest run + "if TYPE_CHECKING:", + "@overload", ] [tool.ruff] -target-version = "py313" - -lint.select = [ - "B002", # Python does not support the unary prefix increment - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "B023", # Function definition does not bind loop variable {name} - "B026", # Star-arg unpacking after a keyword argument is strongly discouraged - "B904", # Use raise from err or None to specify exception cause - "C", # complexity - "COM818", # Trailing comma on bare tuple prohibited - "D", # docstrings - "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() - "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) - "E", # pycodestyle - "F", # pyflakes/autoflake - "G", # flake8-logging-format - "I", # isort - "ICN001", # import concentions; {name} should be imported as {asname} - # "ISC001", # Implicitly concatenated string literals on one line - "N804", # First argument of a class method should be named cls - "N805", # First argument of a method should be named self - "N815", # Variable {name} in class scope should not be mixedCase - # "PGH001", # PGH001 has been remapped to S307 - "PGH004", # Use specific rule codes when using noqa - "PL", # https://github.com/astral-sh/ruff/issues/7491#issuecomment-1730008111 - "PLC0414", # Useless import alias. Import alias does not rename original package. - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint - "Q000", # Double quotes found but single quotes preferred - "RUF006", # Store a reference to the return value of asyncio.create_task - "S102", # Use of exec detected - "S103", # bad-file-permissions - "S108", # hardcoded-temp-file - "S306", # suspicious-mktemp-usage - "S307", # suspicious-eval-usage - "S313", # suspicious-xmlc-element-tree-usage - "S314", # suspicious-xml-element-tree-usage - "S315", # suspicious-xml-expat-reader-usage - "S316", # suspicious-xml-expat-builder-usage - "S317", # suspicious-xml-sax-usage - "S318", # suspicious-xml-mini-dom-usage - "S319", # suspicious-xml-pull-dom-usage - "S601", # paramiko-call - "S602", # subprocess-popen-with-shell-equals-true - "S604", # call-with-shell-equals-true - "S608", # hardcoded-sql-expression - "S609", # unix-command-wildcard-injection - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM208", # Use {expr} instead of not (not {expr}) - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T100", # Trace found: {name} used - "T20", # flake8-print - "TID251", # Banned imports - "TRY004", # Prefer TypeError exception for invalid type - # "TRY200", # TRY200 has been remapped to B904 - "TRY203", # Remove exception handler; error is immediately re-raised - "UP", # pyupgrade - "W", # pycodestyle +required-version = ">=0.12.1" + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC", # flake8-async + "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading + "B007", # Loop control variable {name} not used within loop body + "B009", # Do not call getattr with a constant attribute value. It is not any safer than normal property access. + "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B017", # pytest.raises(BaseException) should be considered evil + "B018", # Found useless attribute access. Either assign it to a variable or remove it. + "B023", # Function definition does not bind loop variable {name} + "B024", # `{name}` is an abstract base class, but it has no abstract methods or properties + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? + "B035", # Dictionary comprehension uses static key + "B904", # Use raise from to specify exception cause + "B905", # zip() without an explicit strict= parameter + "BLE", + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "F541", # f-string without any placeholders + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF007", # Prefer itertools.pairwise() over zip() when iterating over successive pairs + "RUF008", # Do not use mutable default values for dataclass attributes + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF016", # Slice in indexed access to type {value_type} uses type {index_type} instead of an integer + "RUF017", # Avoid quadratic list summation + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + "RUF020", # {never_like} | T is equivalent to T + "RUF021", # Parenthesize a and b expressions when chaining and and or together, to make the precedence clear + "RUF022", # Sort __all__ + "RUF023", # Sort __slots__ + "RUF024", # Do not pass mutable objects as values to dict.fromkeys + "RUF026", # default_factory is a positional-only argument to defaultdict + "RUF030", # print() call in assert statement is likely unintentional + "RUF032", # Decimal() called with float literal argument + "RUF033", # __post_init__ method with argument defaults + "RUF034", # Useless if-else condition + "RUF100", # Unused `noqa` directive + "RUF101", # noqa directives that use redirected rule codes + "RUF200", # Failed to parse pyproject.toml: {message} + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "T20", # flake8-print + "TC", # flake8-type-checking + "TID", # Tidy imports + "TRY", # tryceratops + "UP", # pyupgrade + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call + "W", # pycodestyle ] -lint.ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D406", # Section name should end with a newline - "D407", # Section name underlining - "E501", # line too long - "E731", # do not assign a lambda expression, use a def - # False positives https://github.com/astral-sh/ruff/issues/5386 - "PLC0208", # Use a sequence type instead of a `set` when iterating over values - "PLR0911", # Too many return statements ({returns} > {max_returns}) - "PLR0912", # Too many branches ({branches} > {max_branches}) - "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) - "PLR0915", # Too many statements ({statements} > {max_statements}) - "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable - "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target - "UP006", # keep type annotation style as is - "UP007", # keep type annotation style as is - # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 - #"UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` +ignore = [ + "ASYNC109", # Async function definition with a `timeout` parameter Use `asyncio.timeout` instead + "ASYNC110", # Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW1641", # __eq__ without __hash__ + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "RUF001", # String contains ambiguous unicode character. + "RUF002", # Docstring contains ambiguous unicode character. + "RUF003", # Comment contains ambiguous unicode character. + "RUF015", # Prefer next(...) over single element slice + "SIM102", # Use a single if statement instead of nested if statements + "SIM103", # Return the condition {condition} directly + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files + + # Moving imports into type-checking blocks can mess with pytest.patch() + "TC001", # Move application import {} into a type-checking block + "TC002", # Move third-party import {} into a type-checking block + "TC003", # Move standard library import {} into a type-checking block + # Quotes for typing.cast generally not necessary, only for performance critical paths + "TC006", # Add quotes to type expression in typing.cast() + + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use `logging.exception` instead of `logging.error` + # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + "UP046", # Non PEP 695 generic class + "UP047", # Non PEP 696 generic function + "UP049", # Avoid private type parameter names + + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605", ] -exclude = [] - [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" -"homeassistant.helpers.area_registry" = "ar" -"homeassistant.helpers.config_validation" = "cv" -"homeassistant.helpers.device_registry" = "dr" -"homeassistant.helpers.entity_registry" = "er" -"homeassistant.helpers.issue_registry" = "ir" [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false - -[tool.ruff.lint.mccabe] -max-complexity = 25 +mark-parentheses = false [tool.ruff.lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" "pytz".msg = "use zoneinfo instead" +"tests".msg = "You should not import tests" [tool.ruff.lint.isort] force-sort-within-sections = true -section-order = ["future", "standard-library", "first-party", "third-party", "local-folder"] -known-third-party = [ - "tests", -] -known-first-party = [ - "airos", - "voluptuous", - "pytest", -] -forced-separate = [ - "tests", -] +known-first-party = ["airos"] combine-as-imports = true split-on-trailing-comma = false -[tool.pytest.ini_options] -testpaths = [ - "tests", -] -asyncio_default_fixture_loop_scope = "session" -asyncio_mode = "auto" +[tool.ruff.lint.per-file-ignores] + +# Allow for main entry & scripts to write to stdout +"script/*" = ["T20"] + +# Temporary +"airos/**" = ["PTH"] +"tests/**" = ["PTH"] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +convention = "google" +property-decorators = ["propcache.api.cached_property"] diff --git a/requirements-test.txt b/requirements-test.txt index 086e7d5..065b40f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -11,3 +11,4 @@ types-aiofiles==24.1.0.20250809 mypy==1.17.1 pylint==3.3.7 aiofiles==24.1.0 +pydantic==2.11.7 diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 0000000..bde9709 --- /dev/null +++ b/script/__init__.py @@ -0,0 +1 @@ +"""airOS Helper Scripts.""" diff --git a/script/generate_discovery_fixture.py b/script/generate_discovery_fixture.py index 4ca90d0..57d8d0d 100644 --- a/script/generate_discovery_fixture.py +++ b/script/generate_discovery_fixture.py @@ -8,9 +8,9 @@ _LOGGER = logging.getLogger(__name__) # Define the path to save the fixture -fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures") -os.makedirs(fixture_dir, exist_ok=True) # Ensure the directory exists -fixture_path = os.path.join(fixture_dir, "airos_sta_discovery_packet.bin") +fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures") # noqa: PTH118, PTH120 +os.makedirs(fixture_dir, exist_ok=True) # Ensure the directory exists # noqa: PTH103 +fixture_path = os.path.join(fixture_dir, "airos_sta_discovery_packet.bin") # noqa: PTH118 # Header: 0x01 0x06 (2 bytes) + 4 reserved bytes = 6 bytes HEADER = b"\x01\x06\x00\x00\x00\x00" @@ -94,7 +94,7 @@ ) # Write the actual binary file -with open(fixture_path, "wb") as f: +with open(fixture_path, "wb") as f: # noqa: PTH123 f.write(FULL_PACKET) log = f"Generated discovery packet fixture at: {fixture_path}" diff --git a/script/generate_ha_fixture.py b/script/generate_ha_fixture.py index 315cb80..445277f 100644 --- a/script/generate_ha_fixture.py +++ b/script/generate_ha_fixture.py @@ -7,8 +7,8 @@ _LOGGER = logging.getLogger(__name__) -current_script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root_dir = os.path.abspath(os.path.join(current_script_dir, os.pardir)) +current_script_dir = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 +project_root_dir = os.path.abspath(os.path.join(current_script_dir, os.pardir)) # noqa: PTH100, PTH118 if project_root_dir not in sys.path: sys.path.append(project_root_dir) @@ -23,32 +23,32 @@ def generate_airos_fixtures() -> None: """Process all (intended) JSON files from the userdata directory to potential fixtures.""" # Define the paths to the directories - fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures") - userdata_dir = os.path.join(os.path.dirname(__file__), "../fixtures/userdata") + fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures") # noqa: PTH118, PTH120 + userdata_dir = os.path.join(os.path.dirname(__file__), "../fixtures/userdata") # noqa: PTH118, PTH120 # Ensure the fixture directory exists - os.makedirs(fixture_dir, exist_ok=True) + os.makedirs(fixture_dir, exist_ok=True) # noqa: PTH103 # Iterate over all files in the userdata_dir - for filename in os.listdir(userdata_dir): + for filename in os.listdir(userdata_dir): # noqa: PTH208 if "mocked" in filename: continue if filename.endswith(".json"): # Construct the full paths for the base and new fixtures - base_fixture_path = os.path.join(userdata_dir, filename) + base_fixture_path = os.path.join(userdata_dir, filename) # noqa: PTH118 new_filename = f"airos_{filename}" - new_fixture_path = os.path.join(fixture_dir, new_filename) + new_fixture_path = os.path.join(fixture_dir, new_filename) # noqa: PTH118 _LOGGER.info("Processing '%s'...", filename) try: - with open(base_fixture_path, encoding="utf-8") as source: + with open(base_fixture_path, encoding="utf-8") as source: # noqa: PTH123 source_data = json.loads(source.read()) derived_data = AirOS.derived_data(source_data) new_data = AirOSData.from_dict(derived_data) - with open(new_fixture_path, "w", encoding="utf-8") as new: + with open(new_fixture_path, "w", encoding="utf-8") as new: # noqa: PTH123 json.dump(new_data.to_dict(), new, indent=2, sort_keys=True) _LOGGER.info("Successfully created '%s'", new_filename) diff --git a/script/mashumaro-step-debug.py b/script/mashumaro-step-debug.py index 8f201be..e592bf3 100644 --- a/script/mashumaro-step-debug.py +++ b/script/mashumaro-step-debug.py @@ -6,8 +6,8 @@ import sys from typing import Any -_current_script_dir = os.path.dirname(os.path.abspath(__file__)) -_project_root_dir = os.path.abspath(os.path.join(_current_script_dir, os.pardir)) +_current_script_dir = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 +_project_root_dir = os.path.abspath(os.path.join(_current_script_dir, os.pardir)) # noqa: PTH100, PTH118 if _project_root_dir not in sys.path: sys.path.append(_project_root_dir) @@ -23,16 +23,16 @@ def main() -> None: """Debug data.""" if len(sys.argv) <= 1: _LOGGER.info("Use with file to check") - raise Exception("File to check not provided.") + raise Exception("File to check not provided.") # noqa: TRY002 - current_script_dir = os.path.dirname(os.path.abspath(__file__)) - project_root_dir = os.path.abspath(os.path.join(current_script_dir, os.pardir)) + current_script_dir = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 + project_root_dir = os.path.abspath(os.path.join(current_script_dir, os.pardir)) # noqa: PTH100, PTH118 if project_root_dir not in sys.path: sys.path.append(project_root_dir) # Load the JSON data - with open(sys.argv[1], encoding="utf-8") as f: + with open(sys.argv[1], encoding="utf-8") as f: # noqa: PTH123 data = json.loads(f.read()) try: @@ -57,7 +57,7 @@ def main() -> None: _LOGGER.info(" Success! Remote is valid.") station_obj = Station.from_dict(station_data) - station_obj_list.append(station_obj) # noqa: F841 + station_obj_list.append(station_obj) _LOGGER.info(" Success! Station at index %s is valid.", i) _LOGGER.info(" -> Checking top-level Wireless object...") diff --git a/tests/conftest.py b/tests/conftest.py index b00eed2..bf63481 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,11 +4,11 @@ import asyncio from unittest.mock import AsyncMock, MagicMock, patch -from airos.airos8 import AirOS -from airos.discovery import AirOSDiscoveryProtocol +import aiohttp import pytest -import aiohttp +from airos.airos8 import AirOS +from airos.discovery import AirOSDiscoveryProtocol # pylint: disable=redefined-outer-name, unnecessary-default-type-args diff --git a/tests/test_airos8.py b/tests/test_airos8.py index ddba6e3..ee06c8c 100644 --- a/tests/test_airos8.py +++ b/tests/test_airos8.py @@ -4,13 +4,13 @@ import json from unittest.mock import AsyncMock, MagicMock, patch -from airos.airos8 import AirOS -import airos.exceptions -import pytest - import aiofiles import aiohttp from mashumaro.exceptions import MissingField +import pytest + +from airos.airos8 import AirOS +import airos.exceptions @pytest.mark.skip(reason="broken, needs investigation") diff --git a/tests/test_airos_request.py b/tests/test_airos_request.py index 2a8089f..f1cb222 100644 --- a/tests/test_airos_request.py +++ b/tests/test_airos_request.py @@ -3,15 +3,15 @@ import logging from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp +import pytest + from airos.airos8 import AirOS from airos.exceptions import ( AirOSConnectionAuthenticationError, AirOSDataMissingError, AirOSDeviceConnectionError, ) -import pytest - -import aiohttp # pylint: disable=redefined-outer-name @@ -48,7 +48,7 @@ async def test_request_json_success( mock_session.request.return_value.__aenter__.return_value = mock_response with patch.object(mock_airos_device, "connected", True): - response_data = await mock_airos_device._request_json("GET", "/test/path") + response_data = await mock_airos_device._request_json("GET", "/test/path") # noqa: SLF001 assert response_data == expected_response_data mock_session.request.assert_called_once() @@ -75,7 +75,7 @@ async def test_request_json_connection_error( patch.object(mock_airos_device, "connected", True), pytest.raises(AirOSDeviceConnectionError), ): - await mock_airos_device._request_json("GET", "/test/path") + await mock_airos_device._request_json("GET", "/test/path") # noqa: SLF001 @pytest.mark.asyncio @@ -99,7 +99,7 @@ async def test_request_json_http_error( patch.object(mock_airos_device, "connected", True), pytest.raises(AirOSConnectionAuthenticationError), ): - await mock_airos_device._request_json("GET", "/test/path") + await mock_airos_device._request_json("GET", "/test/path") # noqa: SLF001 mock_response.raise_for_status.assert_called_once() @@ -122,7 +122,7 @@ async def test_request_json_non_json_response( pytest.raises(AirOSDataMissingError), caplog.at_level(logging.DEBUG), ): - await mock_airos_device._request_json("GET", "/test/path") + await mock_airos_device._request_json("GET", "/test/path") # noqa: SLF001 assert "Failed to decode JSON from /test/path" in caplog.text @@ -144,7 +144,7 @@ async def test_request_json_with_params_and_data( data = {"key": "value"} with patch.object(mock_airos_device, "connected", True): - await mock_airos_device._request_json( + await mock_airos_device._request_json( # noqa: SLF001 "POST", "/test/path", json_data=params, form_data=data ) diff --git a/tests/test_data.py b/tests/test_data.py index dc319d8..7120d47 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -2,9 +2,10 @@ from unittest.mock import patch -from airos.data import Host, Wireless import pytest +from airos.data import Host, Wireless + @pytest.mark.asyncio async def test_unknown_enum_values() -> None: diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 9998b4e..bbc25ed 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -7,13 +7,14 @@ from typing import Any, cast from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from airos.discovery import ( DISCOVERY_PORT, AirOSDiscoveryProtocol, airos_discover_devices, ) from airos.exceptions import AirOSDiscoveryError, AirOSEndpointError, AirOSListenerError -import pytest # pylint: disable=redefined-outer-name @@ -33,7 +34,7 @@ def _read_file() -> bytes: return await asyncio.to_thread(_read_file) except FileNotFoundError: pytest.fail(f"Fixture file not found: {path}") - except Exception as e: + except OSError as e: pytest.fail(f"Error reading fixture file {path}: {e}") @@ -110,7 +111,7 @@ async def test_parse_airos_packet_truncated_tlv() -> None: # Header + MAC TLV (valid) + then a truncated TLV_IP truncated_data = ( b"\x01\x06\x00\x00\x00\x00" # Header - + b"\x06" + b"\x06" + bytes.fromhex("0123456789CD") # Valid MAC (scrubbed) + b"\x02\x00" # TLV type 0x02, followed by only 1 byte for length (should be 2) ) @@ -289,16 +290,14 @@ async def test_async_discover_devices_oserror( """Test discovery handles OSError during endpoint creation.""" mock_transport, _ = mock_datagram_endpoint - with ( - patch("asyncio.get_running_loop") as mock_get_loop, - pytest.raises(AirOSEndpointError) as excinfo, - ): + with patch("asyncio.get_running_loop") as mock_get_loop: mock_loop = mock_get_loop.return_value mock_loop.create_datagram_endpoint = AsyncMock( side_effect=OSError(98, "Address in use") ) - await airos_discover_devices(timeout=1) + with pytest.raises(AirOSEndpointError) as excinfo: + await airos_discover_devices(timeout=1) assert "address_in_use" in str(excinfo.value) close_mock = cast(MagicMock, mock_transport.close) @@ -352,7 +351,7 @@ async def test_datagram_received_handles_general_exception() -> None: @pytest.mark.parametrize( - "packet_fragment, error_message", + ("packet_fragment", "error_message"), [ # Case 1: TLV type 0x0A (Uptime) with wrong length (b"\x0a\x00\x02\x01\x02", "Unexpected length for Uptime (Type 0x0A)"), @@ -386,16 +385,14 @@ async def test_async_discover_devices_generic_oserror( """Test discovery handles a generic OSError during endpoint creation.""" mock_transport, _ = mock_datagram_endpoint - with ( - patch("asyncio.get_running_loop") as mock_get_loop, - pytest.raises(AirOSEndpointError) as excinfo, - ): + with patch("asyncio.get_running_loop") as mock_get_loop: mock_loop = mock_get_loop.return_value # Simulate an OSError that is NOT 'address in use' mock_loop.create_datagram_endpoint = AsyncMock( side_effect=OSError(13, "Permission denied") ) - await airos_discover_devices(timeout=1) + with pytest.raises(AirOSEndpointError) as excinfo: + await airos_discover_devices(timeout=1) assert "cannot_connect" in str(excinfo.value) close_mock = cast(MagicMock, mock_transport.close) @@ -430,7 +427,7 @@ async def test_parse_airos_packet_truncated_two_byte_tlv() -> None: # Header + valid MAC TLV, then a valid type (0x0a) but a truncated length field data_with_fragment = ( b"\x01\x06\x00\x00\x00\x00" - + b"\x06" + b"\x06" + bytes.fromhex("0123456789CD") + b"\x0a\x00" # TLV type 0x0a, followed by only 1 byte for length (should be 2) ) @@ -451,7 +448,7 @@ async def test_parse_airos_packet_malformed_tlv_length() -> None: # Header + valid MAC TLV, then a valid type (0x02) but a truncated length field data_with_fragment = ( b"\x01\x06\x00\x00\x00\x00" - + b"\x06" + b"\x06" + bytes.fromhex("0123456789CD") + b"\x02\x00" # TLV type 0x02, followed by only 1 byte for length (should be 2) ) @@ -466,7 +463,7 @@ async def test_parse_airos_packet_malformed_tlv_length() -> None: @pytest.mark.parametrize( - "packet_fragment, unhandled_type", + ("packet_fragment", "unhandled_type"), [ (b"\x0e\x00\x02\x01\x02", "0xe"), # Unhandled 2-byte length TLV (b"\x10\x00\x02\x01\x02", "0x10"), # Unhandled 2-byte length TLV diff --git a/tests/test_stations.py b/tests/test_stations.py index 37a5c6f..0611b28 100644 --- a/tests/test_stations.py +++ b/tests/test_stations.py @@ -6,13 +6,13 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch +import aiofiles +from mashumaro.exceptions import MissingField +import pytest + from airos.airos8 import AirOS from airos.data import AirOS8Data as AirOSData, Wireless from airos.exceptions import AirOSDeviceConnectionError, AirOSKeyDataMissingError -import pytest - -import aiofiles -from mashumaro.exceptions import MissingField async def _read_fixture(fixture: str = "loco5ac_ap-ptp") -> Any: From d9f657a3c4c751c704a529b8d17e46957d43e126 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 16:59:45 +0200 Subject: [PATCH 02/10] Skip covering non-parts --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df5624c..9eb7aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -550,7 +550,7 @@ filterwarnings = [ ] [tool.coverage.run] -source = ["homeassistant"] +source = ["airos"] [tool.coverage.report] exclude_lines = [ From 8892e37cdee89b23fe26efe6294a9d5e88f11d78 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:09:12 +0200 Subject: [PATCH 03/10] Align coverage --- .github/workflows/verify.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 59983bc..cb7f985 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -96,7 +96,7 @@ jobs: - name: Run all tests run: | . venv/bin/activate - pytest --log-level info tests/*.py --cov='.' + pytest --log-level info tests/*.py - name: Upload coverage artifact uses: actions/upload-artifact@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b632e4..d4c0796 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,7 +92,7 @@ repos: pass_filenames: false - id: pytest name: "pytest" - entry: script/run-in-env.sh pytest + entry: script/run-in-env.sh pytest --log-level info tests/*.py language: script types: [python] pass_filenames: false From fb29c207db9e648123b26d9189c780c17022773e Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:12:14 +0200 Subject: [PATCH 04/10] Align coverage --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4c0796..c2a99e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,7 +92,7 @@ repos: pass_filenames: false - id: pytest name: "pytest" - entry: script/run-in-env.sh pytest --log-level info tests/*.py + entry: script/run-in-env.sh pytest --log-level info language: script types: [python] pass_filenames: false From 7f82f67236b49eab5389f2df72f4d7050e37b22d Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:17:48 +0200 Subject: [PATCH 05/10] Align coverage --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 9eb7aed..14c261b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -378,6 +378,7 @@ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%( log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +adopts = "--cov=airos" filterwarnings = [ "error:usefixtures\\(\\) in .* without arguments has no effect:UserWarning", # pytest From e0673cba97a4f5204b05685929ff3decbc1a738a Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:21:02 +0200 Subject: [PATCH 06/10] Align coverage --- .github/workflows/verify.yml | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index cb7f985..ccbe2b3 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -96,7 +96,7 @@ jobs: - name: Run all tests run: | . venv/bin/activate - pytest --log-level info tests/*.py + pytest --log-level info tests/*.py --cov=airos - name: Upload coverage artifact uses: actions/upload-artifact@v4 with: diff --git a/pyproject.toml b/pyproject.toml index 14c261b..9eb7aed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -378,7 +378,6 @@ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%( log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" -adopts = "--cov=airos" filterwarnings = [ "error:usefixtures\\(\\) in .* without arguments has no effect:UserWarning", # pytest From 3c795ffb83ed130277cd6116cdd4ebd177b01a62 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:28:02 +0200 Subject: [PATCH 07/10] Align coverage --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ccbe2b3..3fb372e 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -96,7 +96,7 @@ jobs: - name: Run all tests run: | . venv/bin/activate - pytest --log-level info tests/*.py --cov=airos + pytest --log-level info tests/*.py --cov='airos/' - name: Upload coverage artifact uses: actions/upload-artifact@v4 with: From 6e274d6a198473374024795ead92390e554e3960 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:35:31 +0200 Subject: [PATCH 08/10] Align coverage --- .github/workflows/verify.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3fb372e..52a7b4a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -96,7 +96,7 @@ jobs: - name: Run all tests run: | . venv/bin/activate - pytest --log-level info tests/*.py --cov='airos/' + pytest --log-level info tests/ --cov='airos/' - name: Upload coverage artifact uses: actions/upload-artifact@v4 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2a99e1..fee3f22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,7 +92,7 @@ repos: pass_filenames: false - id: pytest name: "pytest" - entry: script/run-in-env.sh pytest --log-level info + entry: script/run-in-env.sh pytest --log-level info tests/ --cov='airos/' language: script types: [python] pass_filenames: false From 20b6a15e6debafa3331ff4ac04773938a2eba96c Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:43:38 +0200 Subject: [PATCH 09/10] Align coverage --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9eb7aed..5ad7338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -374,8 +374,6 @@ max-line-length-suggestions = 72 [tool.pytest.ini_options] testpaths = ["tests"] norecursedirs = [".git", "testing_config"] -log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" -log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ From 604fa3f483349aa7e2d476c3b41ff7965511b2a0 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sun, 17 Aug 2025 17:50:22 +0200 Subject: [PATCH 10/10] Bump release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5ad7338..ebcf0b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "airos" -version = "0.4.2a0" +version = "0.4.2" license = "MIT" description = "Ubiquiti airOS module(s) for Python 3." readme = "README.md"