From 3916009dbd0bba603af8dabdfcc66e98a7218eed Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Sun, 31 May 2026 10:01:39 -0700 Subject: [PATCH] pylint cleanups --- bin/inject_nanopb_options.py | 9 ++-- .../tests/test_inject_nanopb_options.py | 42 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/bin/inject_nanopb_options.py b/bin/inject_nanopb_options.py index bfd1b19..43fd337 100644 --- a/bin/inject_nanopb_options.py +++ b/bin/inject_nanopb_options.py @@ -21,7 +21,7 @@ generated by regen-protobufs.sh, not the source .proto files. import re import sys from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Tuple # IntSize enum values from nanopb.proto INT_SIZE_ENUM = {8: "IS_8", 16: "IS_16", 32: "IS_32", 64: "IS_64"} @@ -72,7 +72,7 @@ def parse_options_file( specific: Dict[Tuple[str, ...], Dict[str, Any]] = {} wildcard: Dict[str, Dict[str, Any]] = {} - with open(path) as f: + with open(path, encoding="utf-8") as f: for line in f: # Strip inline comments comment_pos = line.find("#") @@ -287,6 +287,7 @@ def inject_into_proto( def main() -> int: + """Parse an .options file and inject its constraints into a .proto file in-place.""" if len(sys.argv) != 3: print( f"Usage: {sys.argv[0]} ", @@ -312,13 +313,13 @@ def main() -> int: print(f" [{opts_path.name}] No injectable options found, skipping.") return 0 - content = proto_path.read_text() + content = proto_path.read_text(encoding="utf-8") # After regen-protobufs.sh's sed fixup, the nanopb import path is: nanopb_import_path = "meshtastic/protobuf/nanopb.proto" modified = inject_into_proto(content, specific, wildcard, nanopb_import_path) - proto_path.write_text(modified) + proto_path.write_text(modified, encoding="utf-8") print( f" [{opts_path.name}] Injected {len(specific)} specific + " diff --git a/meshtastic/tests/test_inject_nanopb_options.py b/meshtastic/tests/test_inject_nanopb_options.py index 2d89188..9239a8a 100644 --- a/meshtastic/tests/test_inject_nanopb_options.py +++ b/meshtastic/tests/test_inject_nanopb_options.py @@ -12,11 +12,18 @@ import importlib.util import sys import textwrap from pathlib import Path -from typing import Any, Dict, Tuple from unittest.mock import patch import pytest +from meshtastic.protobuf import ( + atak_pb2, + config_pb2, + mesh_pb2, + nanopb_pb2, + telemetry_pb2, +) + # --------------------------------------------------------------------------- # Load bin/inject_nanopb_options.py as a module without adding it to the # package. __main__ guard means no side-effects on import. @@ -55,26 +62,31 @@ NANOPB_IMPORT = 'import "meshtastic/protobuf/nanopb.proto";' @pytest.mark.unit def test_parse_value_integer(): + """parse_value converts a decimal string to int.""" assert parse_value("40") == 40 @pytest.mark.unit def test_parse_value_negative_integer(): + """parse_value handles negative integer strings.""" assert parse_value("-1") == -1 @pytest.mark.unit def test_parse_value_true(): + """parse_value converts 'true' to Python True.""" assert parse_value("true") is True @pytest.mark.unit def test_parse_value_false(): + """parse_value converts 'false' to Python False.""" assert parse_value("false") is False @pytest.mark.unit def test_parse_value_string(): + """parse_value returns non-numeric, non-boolean strings as-is.""" assert parse_value("IS_8") == "IS_8" @@ -113,7 +125,7 @@ def test_parse_specific(tmp_path): def test_parse_multilevel(tmp_path): """Three-part pattern (Route.Link.uid) produces a 3-tuple key.""" f = _write_options(tmp_path, "*Route.Link.uid max_size:48\n") - specific, wildcard = parse_options_file(f) + specific, _ = parse_options_file(f) assert ("Route", "Link", "uid") in specific assert specific[("Route", "Link", "uid")] == {"max_size": 48} @@ -181,12 +193,14 @@ def test_parse_int_and_bool_values(tmp_path): @pytest.mark.unit def test_message_path_matches_simple(): + """A single-element path matches the current message on the stack.""" stack = [("message", "User")] assert message_path_matches(stack, ("User",)) @pytest.mark.unit def test_message_path_matches_nested(): + """Both a 1-element and 2-element path match correctly against a nested stack.""" stack = [("message", "Config"), ("message", "DeviceConfig")] assert message_path_matches(stack, ("DeviceConfig",)) assert message_path_matches(stack, ("Config", "DeviceConfig")) @@ -201,6 +215,7 @@ def test_message_path_matches_with_oneof_in_stack(): @pytest.mark.unit def test_message_path_no_match(): + """A path with the wrong message name does not match.""" stack = [("message", "User")] assert not message_path_matches(stack, ("Route",)) @@ -220,11 +235,13 @@ def test_message_path_multilevel_partial_match(): @pytest.mark.unit def test_format_max_size(): + """max_size is rendered as an integer literal.""" assert format_nanopb_opts({"max_size": 40}) == "(nanopb).max_size = 40" @pytest.mark.unit def test_format_int_size_as_enum(): + """int_size numeric values are rendered as IS_8/IS_16/IS_32/IS_64 enum names.""" assert format_nanopb_opts({"int_size": 8}) == "(nanopb).int_size = IS_8" assert format_nanopb_opts({"int_size": 16}) == "(nanopb).int_size = IS_16" assert format_nanopb_opts({"int_size": 32}) == "(nanopb).int_size = IS_32" @@ -233,11 +250,13 @@ def test_format_int_size_as_enum(): @pytest.mark.unit def test_format_bool_true(): + """True is rendered as the proto literal 'true'.""" assert format_nanopb_opts({"fixed_length": True}) == "(nanopb).fixed_length = true" @pytest.mark.unit def test_format_bool_false(): + """False is rendered as the proto literal 'false'.""" assert format_nanopb_opts({"fixed_length": False}) == "(nanopb).fixed_length = false" @@ -293,6 +312,7 @@ def test_inject_merges_with_existing_options(): @pytest.mark.unit def test_inject_int_size_uses_enum_name(): + """int_size values are written as IS_N enum names, not raw integers.""" proto = """\ syntax = "proto3"; import "meshtastic/protobuf/mesh.proto"; @@ -335,8 +355,6 @@ def test_inject_specific_not_leaking_to_other_messages(): } """ result = _inject(proto, specific={("User", "id"): {"max_size": 16}}) - lines = result.splitlines() - user_line = next(l for l in lines if "User" not in l and "id = 1" in l and "Other" not in l.split("message")[0] if "message" not in l) # Easier: count annotations — should be exactly one assert result.count("(nanopb).max_size = 16") == 1 @@ -400,6 +418,7 @@ def test_inject_optional_qualifier_preserved(): @pytest.mark.unit def test_inject_repeated_qualifier_preserved(): + """The 'repeated' qualifier is kept when a field gets an annotation.""" proto = """\ syntax = "proto3"; import "meshtastic/protobuf/mesh.proto"; @@ -413,6 +432,7 @@ def test_inject_repeated_qualifier_preserved(): @pytest.mark.unit def test_inject_multiple_options_on_one_field(): + """Multiple options from the same pattern are all injected on one field.""" proto = """\ syntax = "proto3"; import "meshtastic/protobuf/mesh.proto"; @@ -510,14 +530,6 @@ def test_inject_noop_when_no_options(): # embedded in the serialized descriptors. # =========================================================================== -from meshtastic.protobuf import ( # noqa: E402 (after local helpers) - atak_pb2, - config_pb2, - mesh_pb2, - nanopb_pb2, - telemetry_pb2, -) - def _field_opts(descriptor, *path): """Walk a descriptor by field/nested-type path and return its nanopb opts. @@ -534,12 +546,14 @@ def _field_opts(descriptor, *path): @pytest.mark.unit def test_descriptor_user_long_name(): + """User.long_name has max_size = 40 from mesh.options.""" opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "long_name") assert opts.max_size == 40 @pytest.mark.unit def test_descriptor_user_short_name(): + """User.short_name has max_size = 5 from mesh.options.""" opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "short_name") assert opts.max_size == 5 @@ -554,12 +568,14 @@ def test_descriptor_wildcard_macaddr(): @pytest.mark.unit def test_descriptor_meshpacket_hop_limit(): + """MeshPacket.hop_limit has int_size = IS_8 from mesh.options.""" opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["MeshPacket"], "hop_limit") assert opts.int_size == nanopb_pb2.IS_8 @pytest.mark.unit def test_descriptor_routediscovery_snr_towards(): + """RouteDiscovery.snr_towards has max_count = 8 and int_size = IS_8 from mesh.options.""" opts = _field_opts( mesh_pb2.DESCRIPTOR.message_types_by_name["RouteDiscovery"], "snr_towards" ) @@ -569,6 +585,7 @@ def test_descriptor_routediscovery_snr_towards(): @pytest.mark.unit def test_descriptor_data_payload(): + """Data.payload has max_size = 233 from mesh.options.""" opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["Data"], "payload") assert opts.max_size == 233 @@ -600,6 +617,7 @@ def test_descriptor_multilevel_nested_route_link_uid(): @pytest.mark.unit def test_descriptor_telemetry_environment_one_wire_temperature(): + """EnvironmentMetrics.one_wire_temperature has max_count = 8 from telemetry.options.""" env = telemetry_pb2.DESCRIPTOR.message_types_by_name["EnvironmentMetrics"] opts = _field_opts(env, "one_wire_temperature") assert opts.max_count == 8