Skip to content

Commit 250232e

Browse files
committed
fix 'Console.get_args' is too complex and other linting
1 parent b4669b6 commit 250232e

3 files changed

Lines changed: 178 additions & 134 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* Changed the default syntax colors for `Data.to_str()` and therefore also `Data.print()` to console default colors.
2323
* Added a new attribute `is_positional` to `ArgResult`, which indicates whether the argument is a positional argument or not.
2424
* Added the option to add format specifiers to the `{current}`, `{total}` and `{percentage}` placeholders in the `bar_format` and `limited_bar_format` of `ProgressBar`.
25+
* Finally fixed the `C901 'Console.get_args' is too complex (39)` linting error by refactoring the method into its own helper class.
2526
* Made internal, global constants, which's values never change, into `Final` constants for better type checking.
2627
* The names of all internal classes and methods are all noi longer prefixed with a double underscore (`__`), but a single underscore (`_`) instead.
2728
* Changed all methods defined as `@staticmethod` to `@classmethod` where applicable, to improve inheritance capabilities.

src/xulbux/console.py

Lines changed: 169 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -257,137 +257,7 @@ def get_args(
257257
Normally if `allow_spaces` is false, it will take a space as the end of an args value.
258258
If it is true, it will take spaces as part of the value up until the next arg-flag is found.
259259
(Multiple spaces will become one space in the value.)"""
260-
results_positional: dict[str, ArgResultPositional] = {}
261-
results_regular: dict[str, ArgResultRegular] = {}
262-
positional_configs: dict[str, str] = {}
263-
arg_lookup: dict[str, str] = {}
264-
before_count, after_count = 0, 0
265-
args_len = len(args := _sys.argv[1:])
266-
267-
# PARSE 'find_args' CONFIGURATION
268-
for alias, config in find_args.items():
269-
flags: Optional[set[str]] = None
270-
default_value: Optional[str] = None
271-
272-
if isinstance(config, str):
273-
# HANDLE POSITIONAL ARGUMENT COLLECTION
274-
if config == "before":
275-
before_count += 1
276-
if before_count > 1:
277-
raise ValueError("Only one alias can have the value 'before' for positional argument collection.")
278-
elif config == "after":
279-
after_count += 1
280-
if after_count > 1:
281-
raise ValueError("Only one alias can have the value 'after' for positional argument collection.")
282-
else:
283-
raise ValueError(
284-
f"Invalid positional argument type '{config}' for alias '{alias}'.\n"
285-
"Must be either 'before' or 'after'."
286-
)
287-
positional_configs[alias] = config
288-
results_positional[alias] = {"exists": False, "values": []}
289-
elif isinstance(config, set):
290-
flags = config
291-
results_regular[alias] = {"exists": False, "value": default_value}
292-
elif isinstance(config, dict):
293-
flags, default_value = config.get("flags"), config.get("default")
294-
results_regular[alias] = {"exists": False, "value": default_value}
295-
else:
296-
raise TypeError(
297-
f"Invalid configuration type for alias '{alias}'.\n"
298-
"Must be a set, dict, literal 'before' or literal 'after'."
299-
)
300-
301-
# BUILD FLAG LOOKUP FOR NON-POSITIONAL ARGUMENTS
302-
if flags is not None:
303-
for flag in flags:
304-
if flag in arg_lookup:
305-
raise ValueError(
306-
f"Duplicate flag '{flag}' found. It's assigned to both '{arg_lookup[flag]}' and '{alias}'."
307-
)
308-
arg_lookup[flag] = alias
309-
310-
# FIND POSITIONS OF FIRST AND LAST FLAGS FOR POSITIONAL ARGUMENT COLLECTION
311-
first_flag_pos: Optional[int] = None
312-
last_flag_with_value_pos: Optional[int] = None
313-
314-
for i, arg in enumerate(args):
315-
if arg in arg_lookup:
316-
if first_flag_pos is None:
317-
first_flag_pos = i
318-
# CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
319-
if i + 1 < args_len and args[i + 1] not in arg_lookup:
320-
if not allow_spaces:
321-
last_flag_with_value_pos = i + 1
322-
else:
323-
# FIND THE END OF THE MULTI-WORD VALUE
324-
j = i + 1
325-
while j < args_len and args[j] not in arg_lookup:
326-
j += 1
327-
last_flag_with_value_pos = j - 1
328-
329-
# COLLECT "before" POSITIONAL ARGUMENTS
330-
for alias, pos_type in positional_configs.items():
331-
if pos_type == "before":
332-
before_args: list[str] = []
333-
end_pos: int = first_flag_pos if first_flag_pos is not None else args_len
334-
for i in range(end_pos):
335-
if args[i] not in arg_lookup:
336-
before_args.append(args[i])
337-
if before_args:
338-
results_positional[alias]["values"] = before_args
339-
results_positional[alias]["exists"] = len(before_args) > 0
340-
341-
# PROCESS FLAGGED ARGUMENTS
342-
i = 0
343-
while i < args_len:
344-
arg = args[i]
345-
if (opt_alias := arg_lookup.get(arg)) is not None:
346-
results_regular[opt_alias]["exists"] = True
347-
value_found_after_flag: bool = False
348-
if i + 1 < args_len and args[i + 1] not in arg_lookup:
349-
if not allow_spaces:
350-
results_regular[opt_alias]["value"] = args[i + 1]
351-
i += 1
352-
value_found_after_flag = True
353-
else:
354-
value_parts = []
355-
j = i + 1
356-
while j < args_len and args[j] not in arg_lookup:
357-
value_parts.append(args[j])
358-
j += 1
359-
if value_parts:
360-
results_regular[opt_alias]["value"] = " ".join(value_parts)
361-
i = j - 1
362-
value_found_after_flag = True
363-
if not value_found_after_flag:
364-
results_regular[opt_alias]["value"] = None
365-
i += 1
366-
367-
# COLLECT "after" POSITIONAL ARGUMENTS
368-
for alias, pos_type in positional_configs.items():
369-
if pos_type == "after":
370-
after_args: list[str] = []
371-
start_pos: int = (last_flag_with_value_pos + 1) if last_flag_with_value_pos is not None else 0
372-
# IF NO FLAGS WERE FOUND WITH VALUES, START AFTER THE LAST FLAG
373-
if last_flag_with_value_pos is None and first_flag_pos is not None:
374-
# FIND THE LAST FLAG POSITION
375-
last_flag_pos = None
376-
for i, arg in enumerate(args):
377-
if arg in arg_lookup:
378-
last_flag_pos = i
379-
if last_flag_pos is not None:
380-
start_pos = last_flag_pos + 1
381-
382-
for i in range(start_pos, args_len):
383-
if args[i] not in arg_lookup:
384-
after_args.append(args[i])
385-
386-
if after_args:
387-
results_positional[alias]["values"] = after_args
388-
results_positional[alias]["exists"] = len(after_args) > 0
389-
390-
return Args(**results_positional, **results_regular)
260+
return _ConsoleArgsParseHelper(allow_spaces=allow_spaces, find_args=find_args)()
391261

392262
@classmethod
393263
def pause_exit(
@@ -1099,6 +969,174 @@ def _multiline_input_submit(event: KeyPressEvent) -> None:
1099969
event.app.exit(result=event.app.current_buffer.document.text)
1100970

1101971

972+
class _ConsoleArgsParseHelper:
973+
"""Internal, callable helper class to parse command-line arguments."""
974+
975+
def __init__(
976+
self,
977+
allow_spaces: bool,
978+
find_args: dict[str, set[str] | ArgConfigWithDefault | Literal["before", "after"]],
979+
):
980+
self.allow_spaces = allow_spaces
981+
self.find_args = find_args
982+
983+
self.results_positional: dict[str, ArgResultPositional] = {}
984+
self.results_regular: dict[str, ArgResultRegular] = {}
985+
self.positional_configs: dict[str, str] = {}
986+
self.arg_lookup: dict[str, str] = {}
987+
988+
self.args = _sys.argv[1:]
989+
self.args_len = len(self.args)
990+
self.first_flag_pos: Optional[int] = None
991+
self.last_flag_with_value_pos: Optional[int] = None
992+
993+
def __call__(self) -> Args:
994+
self.parse_configuration()
995+
self.find_flag_positions()
996+
self.process_positional_args()
997+
self.process_flagged_args()
998+
return Args(**self.results_positional, **self.results_regular)
999+
1000+
def parse_configuration(self) -> None:
1001+
"""Parse the `find_args` configuration and build lookup structures."""
1002+
before_count, after_count = 0, 0
1003+
1004+
for alias, config in self.find_args.items():
1005+
flags: Optional[set[str]] = None
1006+
default_value: Optional[str] = None
1007+
1008+
if isinstance(config, str):
1009+
# HANDLE POSITIONAL ARGUMENT COLLECTION
1010+
if config == "before":
1011+
before_count += 1
1012+
if before_count > 1:
1013+
raise ValueError("Only one alias can have the value 'before' for positional argument collection.")
1014+
elif config == "after":
1015+
after_count += 1
1016+
if after_count > 1:
1017+
raise ValueError("Only one alias can have the value 'after' for positional argument collection.")
1018+
else:
1019+
raise ValueError(
1020+
f"Invalid positional argument type '{config}' for alias '{alias}'.\n"
1021+
"Must be either 'before' or 'after'."
1022+
)
1023+
self.positional_configs[alias] = config
1024+
self.results_positional[alias] = {"exists": False, "values": []}
1025+
elif isinstance(config, set):
1026+
flags = config
1027+
self.results_regular[alias] = {"exists": False, "value": default_value}
1028+
elif isinstance(config, dict):
1029+
flags, default_value = config.get("flags"), config.get("default")
1030+
self.results_regular[alias] = {"exists": False, "value": default_value}
1031+
else:
1032+
raise TypeError(
1033+
f"Invalid configuration type for alias '{alias}'.\n"
1034+
"Must be a set, dict, literal 'before' or literal 'after'."
1035+
)
1036+
1037+
# BUILD FLAG LOOKUP FOR NON-POSITIONAL ARGUMENTS
1038+
if flags is not None:
1039+
for flag in flags:
1040+
if flag in self.arg_lookup:
1041+
raise ValueError(
1042+
f"Duplicate flag '{flag}' found. It's assigned to both '{self.arg_lookup[flag]}' and '{alias}'."
1043+
)
1044+
self.arg_lookup[flag] = alias
1045+
1046+
def find_flag_positions(self) -> None:
1047+
"""Find positions of first and last flags for positional argument collection."""
1048+
for i, arg in enumerate(self.args):
1049+
if arg in self.arg_lookup:
1050+
if self.first_flag_pos is None:
1051+
self.first_flag_pos = i
1052+
1053+
# CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
1054+
if i + 1 < self.args_len and self.args[i + 1] not in self.arg_lookup:
1055+
if not self.allow_spaces:
1056+
self.last_flag_with_value_pos = i + 1
1057+
1058+
else:
1059+
# FIND THE END OF THE MULTI-WORD VALUE
1060+
j = i + 1
1061+
while j < self.args_len and self.args[j] not in self.arg_lookup:
1062+
j += 1
1063+
1064+
self.last_flag_with_value_pos = j - 1
1065+
1066+
def process_positional_args(self) -> None:
1067+
"""Collect positional `"before"/"after"` arguments."""
1068+
for alias, pos_type in self.positional_configs.items():
1069+
if pos_type == "before":
1070+
before_args: list[str] = []
1071+
end_pos: int = self.first_flag_pos if self.first_flag_pos is not None else self.args_len
1072+
1073+
for i in range(end_pos):
1074+
if self.args[i] not in self.arg_lookup:
1075+
before_args.append(self.args[i])
1076+
1077+
if before_args:
1078+
self.results_positional[alias]["values"] = before_args
1079+
self.results_positional[alias]["exists"] = len(before_args) > 0
1080+
1081+
if pos_type == "after":
1082+
after_args: list[str] = []
1083+
start_pos: int = (self.last_flag_with_value_pos + 1) if self.last_flag_with_value_pos is not None else 0
1084+
1085+
# IF NO FLAGS WERE FOUND WITH VALUES, START AFTER THE LAST FLAG
1086+
if self.last_flag_with_value_pos is None and self.first_flag_pos is not None:
1087+
# FIND THE LAST FLAG POSITION
1088+
last_flag_pos: Optional[int] = None
1089+
for i, arg in enumerate(self.args):
1090+
if arg in self.arg_lookup:
1091+
last_flag_pos = i
1092+
1093+
if last_flag_pos is not None:
1094+
start_pos = last_flag_pos + 1
1095+
1096+
for i in range(start_pos, self.args_len):
1097+
if self.args[i] not in self.arg_lookup:
1098+
after_args.append(self.args[i])
1099+
1100+
if after_args:
1101+
self.results_positional[alias]["values"] = after_args
1102+
self.results_positional[alias]["exists"] = len(after_args) > 0
1103+
1104+
def process_flagged_args(self) -> None:
1105+
"""Process normal flagged arguments."""
1106+
i = 0
1107+
1108+
while i < self.args_len:
1109+
arg = self.args[i]
1110+
1111+
if (opt_alias := self.arg_lookup.get(arg)) is not None:
1112+
self.results_regular[opt_alias]["exists"] = True
1113+
value_found_after_flag: bool = False
1114+
1115+
if i + 1 < self.args_len and self.args[i + 1] not in self.arg_lookup:
1116+
if not self.allow_spaces:
1117+
self.results_regular[opt_alias]["value"] = self.args[i + 1]
1118+
i += 1
1119+
value_found_after_flag = True
1120+
1121+
else:
1122+
value_parts = []
1123+
1124+
j = i + 1
1125+
while j < self.args_len and self.args[j] not in self.arg_lookup:
1126+
value_parts.append(self.args[j])
1127+
j += 1
1128+
1129+
if value_parts:
1130+
self.results_regular[opt_alias]["value"] = " ".join(value_parts)
1131+
i = j - 1
1132+
value_found_after_flag = True
1133+
1134+
if not value_found_after_flag:
1135+
self.results_regular[opt_alias]["value"] = None
1136+
1137+
i += 1
1138+
1139+
11021140
class _ConsoleLogBoxBgReplacer:
11031141
"""Internal, callable class to replace matched text with background-colored text for log boxes."""
11041142

tests/test_console.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from xulbux.console import Console
44
from xulbux import console
55

6-
from unittest.mock import MagicMock, patch, call
6+
from unittest.mock import MagicMock, patch
77
from collections import namedtuple
88
import builtins
99
import pytest
@@ -1111,8 +1111,13 @@ def test_progressbar_emergency_cleanup():
11111111

11121112
def test_progressbar_get_formatted_info_and_bar_width(mock_terminal_size):
11131113
pb = ProgressBar()
1114-
formatted, bar_width = pb._get_formatted_info_and_bar_width(["{l}", "|{b}|", "{c}/{t}", "({p}%)"], 50, 100, 50.0,
1115-
"Loading")
1114+
formatted, bar_width = pb._get_formatted_info_and_bar_width(
1115+
["{l}", "|{b}|", "{c}/{t}", "({p}%)"],
1116+
50,
1117+
100,
1118+
50.0,
1119+
"Loading",
1120+
)
11161121
assert "Loading" in formatted
11171122
assert "50" in formatted
11181123
assert "100" in formatted

0 commit comments

Comments
 (0)