Skip to content

Commit b552f1d

Browse files
authored
Refactor CLI filter parsing, add tests (#1805)
* refactor filter parsing, add tests * change chapter title to "Command Line Tools" * upper case
1 parent 2c90f9f commit b552f1d

File tree

7 files changed

+147
-84
lines changed

7 files changed

+147
-84
lines changed

can/logger.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@
33
import re
44
import sys
55
from datetime import datetime
6-
from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple, Union
6+
from typing import (
7+
TYPE_CHECKING,
8+
Any,
9+
Dict,
10+
List,
11+
Optional,
12+
Sequence,
13+
Tuple,
14+
Union,
15+
)
716

817
import can
18+
from can import Bus, BusState, Logger, SizedRotatingLogger
19+
from can.typechecking import TAdditionalCliArgs
920
from can.util import cast_from_string
1021

11-
from . import Bus, BusState, Logger, SizedRotatingLogger
12-
from .typechecking import CanFilter, CanFilters
13-
1422
if TYPE_CHECKING:
1523
from can.io import BaseRotatingLogger
1624
from can.io.generic import MessageWriter
25+
from can.typechecking import CanFilter
1726

1827

1928
def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None:
@@ -60,10 +69,7 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None:
6069

6170

6271
def _append_filter_argument(
63-
parser: Union[
64-
argparse.ArgumentParser,
65-
argparse._ArgumentGroup,
66-
],
72+
parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup],
6773
*args: str,
6874
**kwargs: Any,
6975
) -> None:
@@ -78,16 +84,17 @@ def _append_filter_argument(
7884
"\n <can_id>~<can_mask> (matches when <received_can_id> & mask !="
7985
" can_id & mask)"
8086
"\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:"
81-
"\n python -m can.viewer -f 100:7FC 200:7F0"
87+
"\n python -m can.viewer --filter 100:7FC 200:7F0"
8288
"\nNote that the ID and mask are always interpreted as hex values",
8389
metavar="{<can_id>:<can_mask>,<can_id>~<can_mask>}",
8490
nargs=argparse.ONE_OR_MORE,
85-
default="",
91+
action=_CanFilterAction,
92+
dest="can_filters",
8693
**kwargs,
8794
)
8895

8996

90-
def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC:
97+
def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC:
9198
logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"]
9299
can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)])
93100

@@ -100,16 +107,27 @@ def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC:
100107
config["fd"] = True
101108
if parsed_args.data_bitrate:
102109
config["data_bitrate"] = parsed_args.data_bitrate
110+
if getattr(parsed_args, "can_filters", None):
111+
config["can_filters"] = parsed_args.can_filters
103112

104113
return Bus(parsed_args.channel, **config)
105114

106115

107-
def _parse_filters(parsed_args: Any) -> CanFilters:
108-
can_filters: List[CanFilter] = []
116+
class _CanFilterAction(argparse.Action):
117+
def __call__(
118+
self,
119+
parser: argparse.ArgumentParser,
120+
namespace: argparse.Namespace,
121+
values: Union[str, Sequence[Any], None],
122+
option_string: Optional[str] = None,
123+
) -> None:
124+
if not isinstance(values, list):
125+
raise argparse.ArgumentError(None, "Invalid filter argument")
126+
127+
print(f"Adding filter(s): {values}")
128+
can_filters: List[CanFilter] = []
109129

110-
if parsed_args.filter:
111-
print(f"Adding filter(s): {parsed_args.filter}")
112-
for filt in parsed_args.filter:
130+
for filt in values:
113131
if ":" in filt:
114132
parts = filt.split(":")
115133
can_id = int(parts[0], base=16)
@@ -122,12 +140,10 @@ def _parse_filters(parsed_args: Any) -> CanFilters:
122140
raise argparse.ArgumentError(None, "Invalid filter argument")
123141
can_filters.append({"can_id": can_id, "can_mask": can_mask})
124142

125-
return can_filters
143+
setattr(namespace, self.dest, can_filters)
126144

127145

128-
def _parse_additional_config(
129-
unknown_args: Sequence[str],
130-
) -> Dict[str, Union[str, int, float, bool]]:
146+
def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs:
131147
for arg in unknown_args:
132148
if not re.match(r"^--[a-zA-Z\-]*?=\S*?$", arg):
133149
raise ValueError(f"Parsing argument {arg} failed")
@@ -142,12 +158,18 @@ def _split_arg(_arg: str) -> Tuple[str, str]:
142158
return args
143159

144160

145-
def main() -> None:
161+
def _parse_logger_args(
162+
args: List[str],
163+
) -> Tuple[argparse.Namespace, TAdditionalCliArgs]:
164+
"""Parse command line arguments for logger script."""
165+
146166
parser = argparse.ArgumentParser(
147167
description="Log CAN traffic, printing messages to stdout or to a "
148168
"given file.",
149169
)
150170

171+
# Generate the standard arguments:
172+
# Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support
151173
_create_base_argument_parser(parser)
152174

153175
parser.add_argument(
@@ -200,13 +222,18 @@ def main() -> None:
200222
)
201223

202224
# print help message when no arguments were given
203-
if len(sys.argv) < 2:
225+
if not args:
204226
parser.print_help(sys.stderr)
205227
raise SystemExit(errno.EINVAL)
206228

207-
results, unknown_args = parser.parse_known_args()
229+
results, unknown_args = parser.parse_known_args(args)
208230
additional_config = _parse_additional_config([*results.extra_args, *unknown_args])
209-
bus = _create_bus(results, can_filters=_parse_filters(results), **additional_config)
231+
return results, additional_config
232+
233+
234+
def main() -> None:
235+
results, additional_config = _parse_logger_args(sys.argv[1:])
236+
bus = _create_bus(results, **additional_config)
210237

211238
if results.active:
212239
bus.state = BusState.ACTIVE

can/typechecking.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
"""
33

44
import gzip
5+
import struct
6+
import sys
57
import typing
68

9+
if sys.version_info >= (3, 10):
10+
from typing import TypeAlias
11+
else:
12+
from typing_extensions import TypeAlias
13+
14+
715
if typing.TYPE_CHECKING:
816
import os
917

@@ -40,6 +48,13 @@ class CanFilterExtended(typing.TypedDict):
4048

4149
BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any])
4250

51+
# Used by CLI scripts
52+
TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]]
53+
TDataStructs: TypeAlias = typing.Dict[
54+
typing.Union[int, typing.Tuple[int, ...]],
55+
typing.Union[struct.Struct, typing.Tuple, None],
56+
]
57+
4358

4459
class AutoDetectedConfig(typing.TypedDict):
4560
interface: str

can/viewer.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,16 @@
2727
import struct
2828
import sys
2929
import time
30-
from typing import Dict, List, Tuple, Union
30+
from typing import Dict, List, Tuple
3131

3232
from can import __version__
33-
34-
from .logger import (
33+
from can.logger import (
3534
_append_filter_argument,
3635
_create_base_argument_parser,
3736
_create_bus,
3837
_parse_additional_config,
39-
_parse_filters,
4038
)
39+
from can.typechecking import TAdditionalCliArgs, TDataStructs
4140

4241
logger = logging.getLogger("can.viewer")
4342

@@ -391,7 +390,9 @@ def _fill_text(self, text, width, indent):
391390
return super()._fill_text(text, width, indent)
392391

393392

394-
def parse_args(args: List[str]) -> Tuple:
393+
def _parse_viewer_args(
394+
args: List[str],
395+
) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]:
395396
# Parse command line arguments
396397
parser = argparse.ArgumentParser(
397398
"python -m can.viewer",
@@ -489,8 +490,6 @@ def parse_args(args: List[str]) -> Tuple:
489490

490491
parsed_args, unknown_args = parser.parse_known_args(args)
491492

492-
can_filters = _parse_filters(parsed_args)
493-
494493
# Dictionary used to convert between Python values and C structs represented as Python strings.
495494
# If the value is 'None' then the message does not contain any data package.
496495
#
@@ -511,9 +510,7 @@ def parse_args(args: List[str]) -> Tuple:
511510
# similarly the values
512511
# are divided by the value in order to convert from real units to raw integer values.
513512

514-
data_structs: Dict[
515-
Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]
516-
] = {}
513+
data_structs: TDataStructs = {}
517514
if parsed_args.decode:
518515
if os.path.isfile(parsed_args.decode[0]):
519516
with open(parsed_args.decode[0], encoding="utf-8") as f:
@@ -544,16 +541,12 @@ def parse_args(args: List[str]) -> Tuple:
544541
additional_config = _parse_additional_config(
545542
[*parsed_args.extra_args, *unknown_args]
546543
)
547-
return parsed_args, can_filters, data_structs, additional_config
544+
return parsed_args, data_structs, additional_config
548545

549546

550547
def main() -> None:
551-
parsed_args, can_filters, data_structs, additional_config = parse_args(sys.argv[1:])
552-
553-
if can_filters:
554-
additional_config.update({"can_filters": can_filters})
548+
parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:])
555549
bus = _create_bus(parsed_args, **additional_config)
556-
557550
curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore]
558551

559552

doc/other-tools.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Other CAN bus tools
1+
Other CAN Bus Tools
22
===================
33

44
In order to keep the project maintainable, the scope of the package is limited to providing common

doc/scripts.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Scripts
2-
=======
1+
Command Line Tools
2+
==================
33

44
The following modules are callable from ``python-can``.
55

test/test_logger.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import can
1717
import can.logger
1818

19-
from .config import *
20-
2119

2220
class TestLoggerScriptModule(unittest.TestCase):
2321
def setUp(self) -> None:
@@ -108,6 +106,39 @@ def test_log_virtual_sizedlogger(self):
108106
self.assertSuccessfullCleanup()
109107
self.mock_logger_sized.assert_called_once()
110108

109+
def test_parse_logger_args(self):
110+
args = self.baseargs + [
111+
"--bitrate",
112+
"250000",
113+
"--fd",
114+
"--data_bitrate",
115+
"2000000",
116+
"--receive-own-messages=True",
117+
]
118+
results, additional_config = can.logger._parse_logger_args(args[1:])
119+
assert results.interface == "virtual"
120+
assert results.bitrate == 250_000
121+
assert results.fd is True
122+
assert results.data_bitrate == 2_000_000
123+
assert additional_config["receive_own_messages"] is True
124+
125+
def test_parse_can_filters(self):
126+
expected_can_filters = [{"can_id": 0x100, "can_mask": 0x7FC}]
127+
results, additional_config = can.logger._parse_logger_args(
128+
["--filter", "100:7FC", "--bitrate", "250000"]
129+
)
130+
assert results.can_filters == expected_can_filters
131+
132+
def test_parse_can_filters_list(self):
133+
expected_can_filters = [
134+
{"can_id": 0x100, "can_mask": 0x7FC},
135+
{"can_id": 0x200, "can_mask": 0x7F0},
136+
]
137+
results, additional_config = can.logger._parse_logger_args(
138+
["--filter", "100:7FC", "200:7F0", "--bitrate", "250000"]
139+
)
140+
assert results.can_filters == expected_can_filters
141+
111142
def test_parse_additional_config(self):
112143
unknown_args = [
113144
"--app-name=CANalyzer",

0 commit comments

Comments
 (0)