Skip to content

Commit 504046c

Browse files
committed
[lint] Fix type checking for config parser
1 parent ed6a725 commit 504046c

1 file changed

Lines changed: 45 additions & 27 deletions

File tree

scenedetect/_cli/config.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,14 @@ class CropValue(ValidatedValue):
152152
_IGNORE_CHARS = (",", "/", "(", ")")
153153
"""Characters to ignore."""
154154

155-
def __init__(self, value: str | tuple[int, int, int, int] | None = None):
156-
if isinstance(value, CropValue) or value is None:
157-
self._crop = value
155+
def __init__(self, value: "str | tuple[int, int, int, int] | CropValue | None" = None):
156+
self._crop: tuple[int, int, int, int] | None = None
157+
if isinstance(value, CropValue):
158+
self._crop = value._crop
159+
elif value is None:
160+
return
158161
else:
159-
crop = ()
162+
crop: tuple[int, ...] = ()
160163
if isinstance(value, str):
161164
translation_table = str.maketrans(
162165
{char: " " for char in ScoreWeightsValue._IGNORE_CHARS}
@@ -173,11 +176,13 @@ def __init__(self, value: str | tuple[int, int, int, int] | None = None):
173176
self._crop = (min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1))
174177

175178
@property
176-
def value(self) -> tuple[int, int, int, int]:
179+
def value(self) -> tuple[int, int, int, int] | None:
177180
return self._crop
178181

179182
def __str__(self) -> str:
180-
x0, y0, x1, y1 = self.value
183+
if self._crop is None:
184+
return "(none)"
185+
x0, y0, x1, y1 = self._crop
181186
return f"[{x0}, {y0}], [{x1}, {y1}]"
182187

183188
@staticmethod
@@ -228,25 +233,27 @@ class KernelSizeValue(ValidatedValue):
228233
"""Validator for kernel sizes (odd integer > 1, or -1 for auto size)."""
229234

230235
def __init__(self, value: int):
236+
self._value: int | None
231237
if value == -1:
232-
# Downscale factor of -1 maps to None internally for auto downscale.
233-
value = None
238+
# Kernel size of -1 maps to None internally for auto-sized kernel.
239+
self._value = None
234240
elif value < 0:
235241
# Disallow other negative values.
236242
raise ValueError()
237243
elif value % 2 == 0:
238244
# Disallow even values.
239245
raise ValueError()
240-
self._value = value
246+
else:
247+
self._value = value
241248

242249
@property
243-
def value(self) -> int:
250+
def value(self) -> int | None:
244251
return self._value
245252

246253
def __str__(self) -> str:
247-
if self.value is None:
254+
if self._value is None:
248255
return "auto"
249-
return str(self.value)
256+
return str(self._value)
250257

251258
@staticmethod
252259
def from_config(config_value: str, default: "KernelSizeValue") -> "KernelSizeValue":
@@ -291,7 +298,12 @@ def __init__(self, value: str):
291298

292299
@staticmethod
293300
def from_config(config_value: str, default: "EscapedString") -> "EscapedChar":
294-
return EscapedString.from_config(config_value, default, length_limit=1)
301+
try:
302+
return EscapedChar(config_value)
303+
except (UnicodeDecodeError, UnicodeEncodeError) as ex:
304+
raise OptionParseFailure(
305+
"Value must be valid UTF-8 string with escape characters."
306+
) from ex
295307

296308

297309
class TimecodeFormat(Enum):
@@ -323,7 +335,11 @@ class FcpFormat(Enum):
323335
"""Final Cut Pro 7 XML Format"""
324336

325337

326-
ConfigValue = bool | int | float | str
338+
# `ConfigValue` covers every concrete type that can appear as a default in
339+
# `CONFIG_MAP` or as a parsed value in `ConfigRegistry._config`. Custom
340+
# validators (`ValidatedValue` subclasses) and `Enum` defaults are included
341+
# because they appear directly in `CONFIG_MAP`.
342+
ConfigValue = bool | int | float | str | None | ValidatedValue | Enum
327343
ConfigDict = dict[str, dict[str, ConfigValue]]
328344

329345
_CONFIG_FILE_NAME: str = "scenedetect.cfg"
@@ -577,26 +593,28 @@ def _parse_config(parser: ConfigParser) -> tuple[ConfigDict | None, list[LogMess
577593
config[command] = {}
578594
for option in CONFIG_MAP[command]:
579595
if command in parser and option in parser[command]:
596+
# Bind to a local so pyright can narrow inside the isinstance branches.
597+
default_value = CONFIG_MAP[command][option]
580598
try:
581599
value_type = None
582-
if isinstance(CONFIG_MAP[command][option], bool):
600+
if isinstance(default_value, bool):
583601
value_type = "yes/no value"
584602
config[command][option] = parser.getboolean(command, option)
585603
continue
586-
elif isinstance(CONFIG_MAP[command][option], int):
604+
elif isinstance(default_value, int):
587605
value_type = "integer"
588606
config[command][option] = parser.getint(command, option)
589607
continue
590-
elif isinstance(CONFIG_MAP[command][option], float):
608+
elif isinstance(default_value, float):
591609
value_type = "number"
592610
config[command][option] = parser.getfloat(command, option)
593611
continue
594-
elif isinstance(CONFIG_MAP[command][option], Enum):
612+
elif isinstance(default_value, Enum):
595613
config_value = (
596614
parser.get(command, option).replace("\n", " ").strip().upper()
597615
)
598616
try:
599-
parsed = CONFIG_MAP[command][option].__class__[config_value]
617+
parsed = default_value.__class__[config_value]
600618
config[command][option] = parsed
601619
except TypeError:
602620
success = False
@@ -627,12 +645,11 @@ def _parse_config(parser: ConfigParser) -> tuple[ConfigDict | None, list[LogMess
627645

628646
# Handle custom validation types.
629647
config_value = parser.get(command, option)
630-
default = CONFIG_MAP[command][option]
631-
option_type = type(default)
632-
if issubclass(option_type, ValidatedValue):
648+
if isinstance(default_value, ValidatedValue):
649+
option_type = type(default_value)
633650
try:
634651
config[command][option] = option_type.from_config(
635-
config_value=config_value, default=default
652+
config_value=config_value, default=default_value
636653
)
637654
except OptionParseFailure as ex:
638655
success = False
@@ -677,7 +694,7 @@ def _parse_config(parser: ConfigParser) -> tuple[ConfigDict | None, list[LogMess
677694
class ConfigLoadFailure(Exception):
678695
"""Raised when a user-specified configuration file fails to be loaded or validated."""
679696

680-
def __init__(self, init_log: tuple[int, str], reason: Exception | None = None):
697+
def __init__(self, init_log: list[LogMessage], reason: Exception | None = None):
681698
super().__init__()
682699
self.init_log = init_log
683700
self.reason = reason
@@ -774,16 +791,17 @@ def get_value(
774791
annotation. Callers should know the expected type for the option they are reading.
775792
"""
776793
assert command in CONFIG_MAP and option in CONFIG_MAP[command]
794+
default_value = CONFIG_MAP[command][option]
777795
if override is not None:
778796
value = override
779797
elif command in self._config and option in self._config[command]:
780798
value = self._config[command][option]
781799
else:
782-
value = CONFIG_MAP[command][option]
800+
value = default_value
783801
if isinstance(value, ValidatedValue):
784802
return value.value
785-
if isinstance(CONFIG_MAP[command][option], Enum) and isinstance(override, str):
786-
return CONFIG_MAP[command][option].__class__[value.upper().strip()]
803+
if isinstance(default_value, Enum) and isinstance(override, str):
804+
return default_value.__class__[override.upper().strip()]
787805
return value
788806

789807
def get_help_string(self, command: str, option: str, show_default: bool | None = None) -> str:

0 commit comments

Comments
 (0)