diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index ffb0e5ad..2467fee1 100644 --- a/src/ytdl_sub/config/overrides.py +++ b/src/ytdl_sub/config/overrides.py @@ -118,17 +118,13 @@ class Overrides(DictFormatterValidator, Scriptable): The format_string after .format has been called """ if entry: - script = copy.deepcopy(entry.script) + script = entry.script unresolvable = entry.unresolvable else: - script = copy.deepcopy(self.script) + script = self.script unresolvable = self.unresolvable if function_overrides: - script.add(function_overrides) + script = copy.deepcopy(script).add(function_overrides) - return ( - script.add({"tmp_var": formatter.format_string}) - .resolve(unresolvable=unresolvable) - .get_str("tmp_var") - ) + return str(script.is_resolvable(formatter.format_string, unresolvable=unresolvable)) diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index f3195bbe..64ea41ed 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -190,6 +190,8 @@ class Preset(_PresetShell): {source_var: "dummy_string" for source_var in self._source_variables} ) ) + # updates any resolved variables + _ = script.partial_build() return script @property @@ -209,7 +211,7 @@ class Preset(_PresetShell): """ Contains actualized script which should hold all Override variables """ - return self._script_builder_with_added_variables.partial_build(update=True) + return self._script_builder_with_added_variables.partial_build() @property def _script(self) -> Script: @@ -252,19 +254,27 @@ class Preset(_PresetShell): ) ) - def __validate_override_string_formatter_validator( + @functools.cache + def _get_unresolvable_variables( self, formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], - ) -> None: + ) -> Optional[Set[str]]: unresolvable = ( set([VARIABLES.entry_metadata.variable_name] + list(self._added_variables.keys())) if isinstance(formatter_validator, OverridesStringFormatterValidator) else None ) + return unresolvable + + def __validate_override_string_formatter_validator( + self, + formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], + ) -> None: try: - self._script.add({"tmp_var": formatter_validator.format_string}).resolve( - unresolvable=unresolvable - ).get("tmp_var") + self._script.is_resolvable( + formatter_validator.format_string, + unresolvable=self._get_unresolvable_variables(formatter_validator), + ) except VariableDoesNotExist as exc: raise StringFormattingVariableNotFoundException(exc) from exc diff --git a/src/ytdl_sub/downloaders/url/validators.py b/src/ytdl_sub/downloaders/url/validators.py index 05fbac7c..9d8dde3f 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -261,7 +261,7 @@ class MultiUrlValidator(OptionsValidator): for collection_url in self.urls.list: script.add(collection_url.variables.dict_with_format_strings) - resolved_script = script.partial_build(update=True) + resolved_script = script.partial_build() # Ensure at least URL is non-empty has_non_empty_url = False diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index b8e63914..57fb3113 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -246,11 +246,12 @@ class Script: for variable_name, resolved in resolved_variables.items(): self._variables[variable_name] = SyntaxTree(ast=[resolved]) - def resolve( + def _resolve( self, resolved: Optional[Dict[str, Resolvable]] = None, unresolvable: Optional[Set[str]] = None, update: bool = False, + output_filter: Optional[Set[str]] = None, ) -> ScriptOutput: """ Parameters @@ -268,9 +269,17 @@ class Script: ------- Dict of resolved values """ - resolved: Dict[Variable, Resolvable] = { - Variable(name): value for name, value in (resolved or {}).items() - } + resolved: Dict[Variable, Resolvable] = dict( + # include all current variables that are resolvable + { + Variable(name): ast.resolvable + for name, ast in self._variables.items() + if ast.resolvable is not None + }, + # add explicit defined resolved variables + **{Variable(name): value for name, value in (resolved or {}).items()}, + ) + unresolvable: Set[Variable] = {Variable(name) for name in (unresolvable or {})} unresolved: Dict[Variable, SyntaxTree] = { Variable(name): ast @@ -278,6 +287,8 @@ class Script: if Variable(name) not in set(resolved.keys()).union(unresolvable) } + tmp = 0 + while unresolved: unresolved_count: int = len(unresolved) @@ -308,8 +319,27 @@ class Script: if update: self._update_internally(resolved_variables=resolved_variables) + if output_filter: + return ScriptOutput( + { + name: resolvable + for name, resolvable in resolved_variables.items() + if name in output_filter + } + ) + return ScriptOutput(resolved_variables) + def resolve( + self, + resolved: Optional[Dict[str, Resolvable]] = None, + unresolvable: Optional[Set[str]] = None, + update: bool = False, + ) -> ScriptOutput: + return self._resolve( + resolved=resolved, unresolvable=unresolvable, update=update, output_filter=None + ) + def add(self, variables: Dict[str, str]) -> "Script": for variable_name, variable_definition in variables.items(): self._variables[variable_name] = parse( @@ -321,6 +351,21 @@ class Script: self._validate() return self + def is_resolvable( + self, + variable_definition: str, + resolved: Optional[Dict[str, Resolvable]] = None, + unresolvable: Optional[Set[str]] = None, + ) -> Resolvable: + try: + self.add({"tmp_var": variable_definition}) + return self._resolve( + resolved=resolved, unresolvable=unresolvable, output_filter={"tmp_var"} + ).get("tmp_var") + finally: + if "tmp_var" in self._variables: + del self._variables["tmp_var"] + def get(self, variable_name: str) -> Resolvable: if variable_name not in self._variables: raise RuntimeException( @@ -419,7 +464,7 @@ class ScriptBuilder: script._validate() return script - def partial_build(self, update: bool = False) -> Script: + def partial_build(self) -> Script: missing_variables, missing_functions = self._missing_metadata maybe_resolvable_variables: Dict[str, SyntaxTree] = { name: variable @@ -435,8 +480,9 @@ class ScriptBuilder: script = self._build( variables=maybe_resolvable_variables, functions=maybe_resolvable_functions ) - if update: - script.resolve(update=True) + # Update internal variables with anything that is resolved + for variable_name, variable_output in script.resolve(update=True).output.items(): + self._variables[variable_name] = SyntaxTree([variable_output]) return script def build(self) -> Script: diff --git a/tests/unit/prebuilt_presets/test_prebuilt_presets.py b/tests/unit/prebuilt_presets/test_prebuilt_presets.py index 90b0b993..20f5b981 100644 --- a/tests/unit/prebuilt_presets/test_prebuilt_presets.py +++ b/tests/unit/prebuilt_presets/test_prebuilt_presets.py @@ -5,6 +5,7 @@ from typing import Optional from typing import Tuple import pytest +import yappi from expected_download import assert_expected_downloads from expected_transaction_log import assert_transaction_log_matches @@ -149,6 +150,9 @@ class TestPrebuiltTVShowPresets: is_youtube_channel: bool, is_many_urls: bool, ): + # yappi.set_clock_type("wall") # Use set_clock_type("wall") for wall time + # yappi.start() + expected_summary_name = "unit/{}/{}/is_yt_{}{}".format( media_player_preset, tv_show_structure_preset, @@ -218,6 +222,8 @@ class TestPrebuiltTVShowPresets: ) reformatted_transaction_log = reformatted_subscription.update_with_info_json(dry_run=False) + # yappi.get_func_stats().print_all() + assert_transaction_log_matches( output_directory=output_directory, transaction_log=reformatted_transaction_log,