@@ -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
297309class 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
327343ConfigDict = 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
677694class 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