[BACKEND] Refactor validation, preparation for subscription dissect (#1398)
Completely rewrite how subscription validation is performed. Optimizes it quite a bit while also adding backend support for the upcoming `dissect` sub-command, where you can resolve any subscription into its 'raw' form for easier debugging when making scripting changes.
This commit is contained in:
parent
344753cc63
commit
1abe2a44f5
17 changed files with 575 additions and 391 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -146,8 +146,11 @@ docker/testing/volumes
|
||||||
.local/
|
.local/
|
||||||
|
|
||||||
.ytdl-sub-working-directory
|
.ytdl-sub-working-directory
|
||||||
|
.ytdl-sub-lock
|
||||||
|
|
||||||
ffmpeg.exe
|
ffmpeg.exe
|
||||||
ffprobe.exe
|
ffprobe.exe
|
||||||
|
|
||||||
tools/docgen/out
|
tools/docgen/out
|
||||||
|
|
||||||
|
prof/
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ from typing import Iterable
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
import mergedeep
|
|
||||||
|
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||||
from ytdl_sub.entries.variables.override_variables import OverrideHelpers
|
from ytdl_sub.entries.variables.override_variables import OverrideHelpers
|
||||||
from ytdl_sub.script.parser import parse
|
from ytdl_sub.script.parser import parse
|
||||||
from ytdl_sub.script.script import Script
|
from ytdl_sub.script.script import Script
|
||||||
|
from ytdl_sub.script.types.function import BuiltInFunction
|
||||||
from ytdl_sub.script.types.resolvable import Resolvable
|
from ytdl_sub.script.types.resolvable import Resolvable
|
||||||
|
from ytdl_sub.script.types.resolvable import String
|
||||||
|
from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
||||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||||
from ytdl_sub.utils.exceptions import InvalidVariableNameException
|
from ytdl_sub.utils.exceptions import InvalidVariableNameException
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||||
|
|
@ -134,29 +135,35 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
||||||
)
|
)
|
||||||
|
|
||||||
def initial_variables(
|
def initial_variables(
|
||||||
self, unresolved_variables: Optional[Dict[str, str]] = None
|
self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, SyntaxTree]:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Variables and format strings for all Override variables + additional variables (Optional)
|
Variables and format strings for all Override variables + additional variables (Optional)
|
||||||
"""
|
"""
|
||||||
initial_variables: Dict[str, str] = {}
|
initial_variables: Dict[str, SyntaxTree] = self.dict_with_parsed_format_strings
|
||||||
mergedeep.merge(
|
if unresolved_variables:
|
||||||
initial_variables,
|
initial_variables |= unresolved_variables
|
||||||
self.dict_with_format_strings,
|
return ScriptUtils.add_sanitized_parsed_variables(initial_variables)
|
||||||
unresolved_variables if unresolved_variables else {},
|
|
||||||
)
|
|
||||||
return ScriptUtils.add_sanitized_variables(initial_variables)
|
|
||||||
|
|
||||||
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
|
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
|
||||||
"""
|
"""
|
||||||
Initialize the override script with any unresolved variables
|
Initialize the override script with any unresolved variables
|
||||||
"""
|
"""
|
||||||
self.script.add(
|
self.script.add_parsed(
|
||||||
self.initial_variables(
|
self.initial_variables(
|
||||||
unresolved_variables={
|
unresolved_variables={
|
||||||
var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}"
|
var_name: SyntaxTree(
|
||||||
|
ast=[
|
||||||
|
BuiltInFunction(
|
||||||
|
name="throw",
|
||||||
|
args=[
|
||||||
|
String(f"Plugin variable {var_name} has not been created yet")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
for var_name in unresolved_variables
|
for var_name in unresolved_variables
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Set
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
|
@ -44,3 +46,34 @@ class PresetPlugins:
|
||||||
if plugin_type in plugin_option_types:
|
if plugin_type in plugin_option_types:
|
||||||
return self.plugin_options[plugin_option_types.index(plugin_type)]
|
return self.plugin_options[plugin_option_types.index(plugin_type)]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_added_and_modified_variables(
|
||||||
|
self, additional_options: List[OptionsValidator]
|
||||||
|
) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
|
||||||
|
"""
|
||||||
|
Iterates and returns the plugin options, added variables, modified variables
|
||||||
|
"""
|
||||||
|
for plugin_options in self.plugin_options + additional_options:
|
||||||
|
added_variables: Set[str] = set()
|
||||||
|
modified_variables: Set[str] = set()
|
||||||
|
|
||||||
|
for plugin_added_variables in plugin_options.added_variables(
|
||||||
|
unresolved_variables=set(),
|
||||||
|
).values():
|
||||||
|
added_variables |= set(plugin_added_variables)
|
||||||
|
|
||||||
|
for plugin_modified_variables in plugin_options.modified_variables().values():
|
||||||
|
modified_variables = plugin_modified_variables
|
||||||
|
|
||||||
|
yield plugin_options, added_variables, modified_variables
|
||||||
|
|
||||||
|
def get_all_variables(self, additional_options: List[OptionsValidator]) -> Set[str]:
|
||||||
|
"""
|
||||||
|
Returns set of all added and modified variables' names.
|
||||||
|
"""
|
||||||
|
all_variables: Set[str] = set()
|
||||||
|
for _, added, modified in self.get_added_and_modified_variables(additional_options):
|
||||||
|
all_variables.update(added)
|
||||||
|
all_variables.update(modified)
|
||||||
|
|
||||||
|
return all_variables
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import copy
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
from mergedeep import mergedeep
|
from mergedeep import mergedeep
|
||||||
|
|
||||||
|
|
@ -11,7 +12,6 @@ from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||||
from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
||||||
from ytdl_sub.config.preset_options import OutputOptions
|
from ytdl_sub.config.preset_options import OutputOptions
|
||||||
from ytdl_sub.config.preset_options import YTDLOptions
|
from ytdl_sub.config.preset_options import YTDLOptions
|
||||||
from ytdl_sub.config.validators.variable_validation import VariableValidation
|
|
||||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
|
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
|
||||||
from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
|
from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
|
||||||
|
|
@ -172,6 +172,37 @@ class Preset(_PresetShell):
|
||||||
mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE)
|
mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _initialize_overrides_script(self, overrides: Overrides) -> Overrides:
|
||||||
|
"""
|
||||||
|
Do some gymnastics to initialize the Overrides script.
|
||||||
|
"""
|
||||||
|
unresolved_variables: Set[str] = set()
|
||||||
|
|
||||||
|
for (
|
||||||
|
plugin_options,
|
||||||
|
added_variables,
|
||||||
|
modified_variables,
|
||||||
|
) in self.plugins.get_added_and_modified_variables(
|
||||||
|
additional_options=[self.downloader_options, self.output_options]
|
||||||
|
):
|
||||||
|
for added_variable in added_variables:
|
||||||
|
if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
raise plugin_options._validation_exception(
|
||||||
|
f"Cannot use the variable name {added_variable} because it exists as a"
|
||||||
|
" built-in ytdl-sub variable name."
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
# Set unresolved as variables that are added but do not exist as
|
||||||
|
# entry/override variables since they are created at run-time
|
||||||
|
unresolved_variables |= added_variables | modified_variables
|
||||||
|
|
||||||
|
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
||||||
|
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
||||||
|
# dependencies until it has been explicitly added
|
||||||
|
return overrides.initialize_script(unresolved_variables=unresolved_variables)
|
||||||
|
|
||||||
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
||||||
super().__init__(name=name, value=value)
|
super().__init__(name=name, value=value)
|
||||||
|
|
||||||
|
|
@ -192,16 +223,11 @@ class Preset(_PresetShell):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.plugins: PresetPlugins = self._validate_and_get_plugins()
|
self.plugins: PresetPlugins = self._validate_and_get_plugins()
|
||||||
self.overrides = self._validate_key(key="overrides", validator=Overrides, default={})
|
self.overrides = self._initialize_overrides_script(
|
||||||
|
overrides=self._validate_key(key="overrides", validator=Overrides, default={})
|
||||||
|
)
|
||||||
self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
|
self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
|
||||||
|
|
||||||
VariableValidation(
|
|
||||||
downloader_options=self.downloader_options,
|
|
||||||
output_options=self.output_options,
|
|
||||||
plugins=self.plugins,
|
|
||||||
).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import copy
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Iterable
|
|
||||||
from typing import List
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Set
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from ytdl_sub.config.overrides import Overrides
|
from ytdl_sub.config.overrides import Overrides
|
||||||
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||||
|
|
@ -13,154 +7,26 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
||||||
from ytdl_sub.config.preset_options import OutputOptions
|
from ytdl_sub.config.preset_options import OutputOptions
|
||||||
from ytdl_sub.config.validators.options import OptionsValidator
|
from ytdl_sub.config.validators.options import OptionsValidator
|
||||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
|
||||||
from ytdl_sub.script.script import Script
|
|
||||||
from ytdl_sub.script.script import _is_function
|
|
||||||
from ytdl_sub.utils.scriptable import BASE_SCRIPT
|
|
||||||
from ytdl_sub.validators.string_formatter_validators import to_variable_dependency_format_string
|
|
||||||
from ytdl_sub.validators.string_formatter_validators import validate_formatters
|
from ytdl_sub.validators.string_formatter_validators import validate_formatters
|
||||||
|
|
||||||
# Entry variables to mock during validation
|
|
||||||
_DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
|
|
||||||
name: to_variable_dependency_format_string(
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
script=BASE_SCRIPT,
|
|
||||||
parsed_format_string=BASE_SCRIPT._variables[name],
|
|
||||||
# pylint: enable=protected-access
|
|
||||||
)
|
|
||||||
for name in BASE_SCRIPT.variable_names
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _add_dummy_variables(variables: Iterable[str]) -> Dict[str, str]:
|
|
||||||
dummy_variables: Dict[str, str] = {}
|
|
||||||
for var in variables:
|
|
||||||
dummy_variables[var] = ""
|
|
||||||
dummy_variables[f"{var}_sanitized"] = ""
|
|
||||||
|
|
||||||
return dummy_variables
|
|
||||||
|
|
||||||
|
|
||||||
def _add_dummy_overrides(overrides: Overrides) -> Dict[str, str]:
|
|
||||||
# Have the dummy override variable contain all variable deps that it uses in the string
|
|
||||||
dummy_overrides: Dict[str, str] = {}
|
|
||||||
for override_name in _override_variables(overrides):
|
|
||||||
if _is_function(override_name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
dummy_overrides[override_name] = to_variable_dependency_format_string(
|
|
||||||
script=overrides.script, parsed_format_string=overrides.script._variables[override_name]
|
|
||||||
)
|
|
||||||
# pylint: enable=protected-access
|
|
||||||
return dummy_overrides
|
|
||||||
|
|
||||||
|
|
||||||
def _get_added_and_modified_variables(
|
|
||||||
plugins: PresetPlugins, downloader_options: MultiUrlValidator, output_options: OutputOptions
|
|
||||||
) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
|
|
||||||
"""
|
|
||||||
Iterates and returns the plugin options, added variables, modified variables
|
|
||||||
"""
|
|
||||||
options: List[OptionsValidator] = plugins.plugin_options
|
|
||||||
options.append(downloader_options)
|
|
||||||
options.append(output_options)
|
|
||||||
|
|
||||||
for plugin_options in options:
|
|
||||||
added_variables: Set[str] = set()
|
|
||||||
modified_variables: Set[str] = set()
|
|
||||||
|
|
||||||
for plugin_added_variables in plugin_options.added_variables(
|
|
||||||
unresolved_variables=set(),
|
|
||||||
).values():
|
|
||||||
added_variables |= set(plugin_added_variables)
|
|
||||||
|
|
||||||
for plugin_modified_variables in plugin_options.modified_variables().values():
|
|
||||||
modified_variables = plugin_modified_variables
|
|
||||||
|
|
||||||
yield plugin_options, added_variables, modified_variables
|
|
||||||
|
|
||||||
|
|
||||||
def _override_variables(overrides: Overrides) -> Set[str]:
|
|
||||||
return set(list(overrides.initial_variables().keys()))
|
|
||||||
|
|
||||||
|
|
||||||
class VariableValidation:
|
class VariableValidation:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
overrides: Overrides,
|
||||||
downloader_options: MultiUrlValidator,
|
downloader_options: MultiUrlValidator,
|
||||||
output_options: OutputOptions,
|
output_options: OutputOptions,
|
||||||
plugins: PresetPlugins,
|
plugins: PresetPlugins,
|
||||||
):
|
):
|
||||||
|
self.overrides = overrides
|
||||||
self.downloader_options = downloader_options
|
self.downloader_options = downloader_options
|
||||||
self.output_options = output_options
|
self.output_options = output_options
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
|
|
||||||
self.script: Optional[Script] = None
|
self.script = self.overrides.script
|
||||||
self.resolved_variables: Set[str] = set()
|
self.unresolved_variables = self.plugins.get_all_variables(
|
||||||
self.unresolved_variables: Set[str] = set()
|
additional_options=[self.output_options, self.downloader_options]
|
||||||
|
|
||||||
def initialize_preset_overrides(self, overrides: Overrides) -> "VariableValidation":
|
|
||||||
"""
|
|
||||||
Do some gymnastics to initialize the Overrides script.
|
|
||||||
"""
|
|
||||||
override_variables = set(list(overrides.initial_variables().keys()))
|
|
||||||
|
|
||||||
# Set resolved variables as all entry + override variables
|
|
||||||
# at this point to generate every possible added/modified variable
|
|
||||||
self.resolved_variables = set(_DUMMY_ENTRY_VARIABLES.keys()) | override_variables
|
|
||||||
plugin_variables: Set[str] = set()
|
|
||||||
|
|
||||||
for (
|
|
||||||
plugin_options,
|
|
||||||
added_variables,
|
|
||||||
modified_variables,
|
|
||||||
) in _get_added_and_modified_variables(
|
|
||||||
plugins=self.plugins,
|
|
||||||
downloader_options=self.downloader_options,
|
|
||||||
output_options=self.output_options,
|
|
||||||
):
|
|
||||||
|
|
||||||
for added_variable in added_variables:
|
|
||||||
if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
raise plugin_options._validation_exception(
|
|
||||||
f"Cannot use the variable name {added_variable} because it exists as a"
|
|
||||||
" built-in ytdl-sub variable name."
|
|
||||||
)
|
)
|
||||||
# pylint: enable=protected-access
|
|
||||||
|
|
||||||
# Set unresolved as variables that are added but do not exist as
|
|
||||||
# entry/override variables since they are created at run-time
|
|
||||||
self.unresolved_variables |= added_variables | modified_variables
|
|
||||||
plugin_variables |= added_variables | modified_variables
|
|
||||||
|
|
||||||
# Then update resolved variables to reflect that
|
|
||||||
self.resolved_variables -= self.unresolved_variables
|
|
||||||
|
|
||||||
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
|
||||||
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
|
||||||
# dependencies until it has been explicitly added
|
|
||||||
overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables)
|
|
||||||
|
|
||||||
# copy the script and mock entry variables
|
|
||||||
self.script = copy.deepcopy(overrides.script)
|
|
||||||
self.script.add(
|
|
||||||
variables=_add_dummy_overrides(overrides=overrides)
|
|
||||||
| _add_dummy_variables(variables=plugin_variables)
|
|
||||||
| _DUMMY_ENTRY_VARIABLES
|
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _update_script(self) -> None:
|
|
||||||
_ = self.script.resolve(unresolvable=self.unresolved_variables, update=True)
|
|
||||||
|
|
||||||
def _add_subscription_override_variables(self) -> None:
|
|
||||||
"""
|
|
||||||
Add dummy subscription variables for script validation
|
|
||||||
"""
|
|
||||||
self.resolved_variables |= REQUIRED_OVERRIDE_VARIABLE_NAMES
|
|
||||||
|
|
||||||
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -171,19 +37,17 @@ class VariableValidation:
|
||||||
).get(plugin_op, set())
|
).get(plugin_op, set())
|
||||||
modified_variables = options.modified_variables().get(plugin_op, set())
|
modified_variables = options.modified_variables().get(plugin_op, set())
|
||||||
|
|
||||||
resolved_variables = added_variables | modified_variables
|
self.unresolved_variables -= added_variables | modified_variables
|
||||||
|
|
||||||
self.resolved_variables |= resolved_variables
|
def ensure_proper_usage(self) -> Dict:
|
||||||
self.unresolved_variables -= resolved_variables
|
|
||||||
|
|
||||||
def ensure_proper_usage(self) -> None:
|
|
||||||
"""
|
"""
|
||||||
Validate variables resolve as plugins are executed, and return
|
Validate variables resolve as plugins are executed, and return
|
||||||
a mock script which contains actualized added variables from the plugins
|
a mock script which contains actualized added variables from the plugins
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
resolved_subscription: Dict = {}
|
||||||
|
|
||||||
self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
||||||
self._add_subscription_override_variables()
|
|
||||||
|
|
||||||
# Always add output options first
|
# Always add output options first
|
||||||
self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options)
|
self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options)
|
||||||
|
|
@ -200,16 +64,28 @@ class VariableValidation:
|
||||||
self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
||||||
|
|
||||||
# Validate that any formatter in the plugin options can resolve
|
# Validate that any formatter in the plugin options can resolve
|
||||||
validate_formatters(
|
resolved_subscription |= validate_formatters(
|
||||||
script=self.script,
|
script=self.script,
|
||||||
unresolved_variables=self.unresolved_variables,
|
unresolved_variables=self.unresolved_variables,
|
||||||
validator=plugin_options,
|
validator=plugin_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
validate_formatters(
|
resolved_subscription |= validate_formatters(
|
||||||
script=self.script,
|
script=self.script,
|
||||||
unresolved_variables=self.unresolved_variables,
|
unresolved_variables=self.unresolved_variables,
|
||||||
validator=self.output_options,
|
validator=self.output_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: make this a function
|
||||||
|
raw_download_output = validate_formatters(
|
||||||
|
script=self.script,
|
||||||
|
unresolved_variables=self.unresolved_variables,
|
||||||
|
validator=self.downloader_options.urls,
|
||||||
|
)
|
||||||
|
resolved_subscription["download"] = []
|
||||||
|
for url_output in raw_download_output["download"]:
|
||||||
|
if url_output["url"]:
|
||||||
|
resolved_subscription["download"].append(url_output)
|
||||||
|
|
||||||
assert not self.unresolved_variables
|
assert not self.unresolved_variables
|
||||||
|
return resolved_subscription
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
|
|
||||||
self._name = self._validate_key(key="name", validator=StringFormatterValidator)
|
self._thumb_name = self._validate_key(key="name", validator=StringFormatterValidator)
|
||||||
self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator)
|
self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -29,7 +29,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
||||||
"""
|
"""
|
||||||
File name for the thumbnail
|
File name for the thumbnail
|
||||||
"""
|
"""
|
||||||
return self._name
|
return self._thumb_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uid(self) -> OverridesStringFormatterValidator:
|
def uid(self) -> OverridesStringFormatterValidator:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException
|
||||||
from ytdl_sub.script.utils.exceptions import InvalidVariableName
|
from ytdl_sub.script.utils.exceptions import InvalidVariableName
|
||||||
from ytdl_sub.script.utils.exceptions import UserException
|
from ytdl_sub.script.utils.exceptions import UserException
|
||||||
from ytdl_sub.script.utils.exceptions import VariableDoesNotExist
|
from ytdl_sub.script.utils.exceptions import VariableDoesNotExist
|
||||||
|
from ytdl_sub.script.utils.name_validation import is_function
|
||||||
|
from ytdl_sub.script.utils.name_validation import to_function_name
|
||||||
from ytdl_sub.script.utils.name_validation import validate_variable_name
|
from ytdl_sub.script.utils.name_validation import validate_variable_name
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
@ -144,6 +146,9 @@ class _Parser:
|
||||||
):
|
):
|
||||||
self._text = text
|
self._text = text
|
||||||
self._name = name
|
self._name = name
|
||||||
|
if name and is_function(name):
|
||||||
|
self._name = to_function_name(name)
|
||||||
|
|
||||||
self._custom_function_names = custom_function_names
|
self._custom_function_names = custom_function_names
|
||||||
self._variable_names = variable_names
|
self._variable_names = variable_names
|
||||||
self._pos = 0
|
self._pos = 0
|
||||||
|
|
|
||||||
|
|
@ -20,28 +20,13 @@ from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
|
||||||
from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments
|
from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments
|
||||||
from ytdl_sub.script.utils.exceptions import RuntimeException
|
from ytdl_sub.script.utils.exceptions import RuntimeException
|
||||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||||
|
from ytdl_sub.script.utils.name_validation import is_function
|
||||||
|
from ytdl_sub.script.utils.name_validation import to_function_definition_name
|
||||||
|
from ytdl_sub.script.utils.name_validation import to_function_name
|
||||||
from ytdl_sub.script.utils.name_validation import validate_variable_name
|
from ytdl_sub.script.utils.name_validation import validate_variable_name
|
||||||
from ytdl_sub.script.utils.type_checking import FunctionSpec
|
from ytdl_sub.script.utils.type_checking import FunctionSpec
|
||||||
|
|
||||||
|
|
||||||
def _is_function(override_name: str):
|
|
||||||
return override_name.startswith("%")
|
|
||||||
|
|
||||||
|
|
||||||
def _function_name(function_key: str) -> str:
|
|
||||||
"""
|
|
||||||
Drop the % in %custom_function
|
|
||||||
"""
|
|
||||||
return function_key[1:]
|
|
||||||
|
|
||||||
|
|
||||||
def _to_function_definition_name(function_key: str) -> str:
|
|
||||||
"""
|
|
||||||
Add % in %custom_function
|
|
||||||
"""
|
|
||||||
return f"%{function_key}"
|
|
||||||
|
|
||||||
|
|
||||||
class Script:
|
class Script:
|
||||||
"""
|
"""
|
||||||
Takes a dictionary of both
|
Takes a dictionary of both
|
||||||
|
|
@ -241,23 +226,23 @@ class Script:
|
||||||
|
|
||||||
def __init__(self, script: Dict[str, str]):
|
def __init__(self, script: Dict[str, str]):
|
||||||
function_names: Set[str] = {
|
function_names: Set[str] = {
|
||||||
_function_name(name) for name in script.keys() if _is_function(name)
|
to_function_name(name) for name in script.keys() if is_function(name)
|
||||||
}
|
}
|
||||||
variable_names: Set[str] = {
|
variable_names: Set[str] = {
|
||||||
validate_variable_name(name) for name in script.keys() if not _is_function(name)
|
validate_variable_name(name) for name in script.keys() if not is_function(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
self._functions: Dict[str, SyntaxTree] = {
|
self._functions: Dict[str, SyntaxTree] = {
|
||||||
# custom_function_name must be passed to properly type custom function
|
# custom_function_name must be passed to properly type custom function
|
||||||
# arguments uniquely if they're nested (i.e. $0 to $custom_func___0)
|
# arguments uniquely if they're nested (i.e. $0 to $custom_func___0)
|
||||||
_function_name(function_key): parse(
|
to_function_name(function_key): parse(
|
||||||
text=function_value,
|
text=function_value,
|
||||||
name=_function_name(function_key),
|
name=to_function_name(function_key),
|
||||||
custom_function_names=function_names,
|
custom_function_names=function_names,
|
||||||
variable_names=variable_names,
|
variable_names=variable_names,
|
||||||
)
|
)
|
||||||
for function_key, function_value in script.items()
|
for function_key, function_value in script.items()
|
||||||
if _is_function(function_key)
|
if is_function(function_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
self._variables: Dict[str, SyntaxTree] = {
|
self._variables: Dict[str, SyntaxTree] = {
|
||||||
|
|
@ -268,7 +253,7 @@ class Script:
|
||||||
variable_names=variable_names,
|
variable_names=variable_names,
|
||||||
)
|
)
|
||||||
for variable_key, variable_value in script.items()
|
for variable_key, variable_value in script.items()
|
||||||
if not _is_function(variable_key)
|
if not is_function(variable_key)
|
||||||
}
|
}
|
||||||
self._validate()
|
self._validate()
|
||||||
|
|
||||||
|
|
@ -485,12 +470,12 @@ class Script:
|
||||||
added_variables_to_validate: Set[str] = set()
|
added_variables_to_validate: Set[str] = set()
|
||||||
|
|
||||||
functions_to_add = {
|
functions_to_add = {
|
||||||
_function_name(name): definition
|
to_function_name(name): definition
|
||||||
for name, definition in variables.items()
|
for name, definition in variables.items()
|
||||||
if _is_function(name)
|
if is_function(name)
|
||||||
}
|
}
|
||||||
variables_to_add = {
|
variables_to_add = {
|
||||||
name: definition for name, definition in variables.items() if not _is_function(name)
|
name: definition for name, definition in variables.items() if not is_function(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
custom_function_names = set(self._functions.keys()) | functions_to_add.keys()
|
custom_function_names = set(self._functions.keys()) | functions_to_add.keys()
|
||||||
|
|
@ -520,6 +505,46 @@ class Script:
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def add_parsed(self, variables: Dict[str, SyntaxTree]) -> "Script":
|
||||||
|
"""
|
||||||
|
Adds already parsed, new variables to the script.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
variables
|
||||||
|
Mapping containing variable name to definition.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Script
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
added_variables_to_validate: Set[str] = set()
|
||||||
|
|
||||||
|
functions_to_add = {
|
||||||
|
to_function_name(name): definition
|
||||||
|
for name, definition in variables.items()
|
||||||
|
if is_function(name)
|
||||||
|
}
|
||||||
|
variables_to_add = {
|
||||||
|
name: definition for name, definition in variables.items() if not is_function(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for definitions in [functions_to_add, variables_to_add]:
|
||||||
|
for name, parsed in definitions.items():
|
||||||
|
if parsed.maybe_resolvable is None:
|
||||||
|
added_variables_to_validate.add(name)
|
||||||
|
|
||||||
|
if name in functions_to_add:
|
||||||
|
self._functions[name] = parsed
|
||||||
|
else:
|
||||||
|
self._variables[name] = parsed
|
||||||
|
|
||||||
|
if added_variables_to_validate:
|
||||||
|
self._validate(added_variables=added_variables_to_validate)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def resolve_once(
|
def resolve_once(
|
||||||
self,
|
self,
|
||||||
variable_definitions: Dict[str, str],
|
variable_definitions: Dict[str, str],
|
||||||
|
|
@ -560,6 +585,46 @@ class Script:
|
||||||
for name in variable_definitions.keys():
|
for name in variable_definitions.keys():
|
||||||
self._variables.pop(name, None)
|
self._variables.pop(name, None)
|
||||||
|
|
||||||
|
def resolve_once_parsed(
|
||||||
|
self,
|
||||||
|
variable_definitions: Dict[str, SyntaxTree],
|
||||||
|
resolved: Optional[Dict[str, Resolvable]] = None,
|
||||||
|
unresolvable: Optional[Set[str]] = None,
|
||||||
|
update: bool = False,
|
||||||
|
) -> Dict[str, Resolvable]:
|
||||||
|
"""
|
||||||
|
Given a new set of variable definitions, resolve them using the Script, but do not
|
||||||
|
add them to the Script itself.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
variable_definitions
|
||||||
|
Variables to resolve, but not store in the Script
|
||||||
|
resolved
|
||||||
|
Optional. Pre-resolved variables that should be used instead of what is in the script.
|
||||||
|
unresolvable
|
||||||
|
Optional. Unresolvable variables that will be ignored in resolution, including all
|
||||||
|
variables with a dependency to them.
|
||||||
|
update
|
||||||
|
Whether to update the script's state with resolved variables. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Dict[str, Resolvable]
|
||||||
|
Dict containing the variable names to their resolved values.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.add_parsed(variable_definitions)
|
||||||
|
return self._resolve(
|
||||||
|
pre_resolved=resolved,
|
||||||
|
unresolvable=unresolvable,
|
||||||
|
output_filter=set(list(variable_definitions.keys())),
|
||||||
|
update=update,
|
||||||
|
).output
|
||||||
|
finally:
|
||||||
|
for name in variable_definitions.keys():
|
||||||
|
self._variables.pop(name, None)
|
||||||
|
|
||||||
def get(self, variable_name: str) -> Resolvable:
|
def get(self, variable_name: str) -> Resolvable:
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
|
|
@ -605,4 +670,4 @@ class Script:
|
||||||
Set[str]
|
Set[str]
|
||||||
Names of all functions within the Script.
|
Names of all functions within the Script.
|
||||||
"""
|
"""
|
||||||
return set(_to_function_definition_name(name) for name in self._functions.keys())
|
return set(to_function_definition_name(name) for name in self._functions.keys())
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,24 @@ def validate_custom_function_name(custom_function_name: str) -> None:
|
||||||
f"Custom function name '%{custom_function_name}' is invalid:"
|
f"Custom function name '%{custom_function_name}' is invalid:"
|
||||||
" The name is used by a built-in function and cannot be overwritten."
|
" The name is used by a built-in function and cannot be overwritten."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_function(override_name: str):
|
||||||
|
"""
|
||||||
|
Whether the definition is a function or not.
|
||||||
|
"""
|
||||||
|
return override_name.startswith("%")
|
||||||
|
|
||||||
|
|
||||||
|
def to_function_name(function_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Drop the % in %custom_function
|
||||||
|
"""
|
||||||
|
return function_key[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def to_function_definition_name(function_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Add % in %custom_function
|
||||||
|
"""
|
||||||
|
return f"%{function_key}"
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
||||||
from ytdl_sub.config.preset import Preset
|
from ytdl_sub.config.preset import Preset
|
||||||
from ytdl_sub.config.preset_options import OutputOptions
|
from ytdl_sub.config.preset_options import OutputOptions
|
||||||
from ytdl_sub.config.preset_options import YTDLOptions
|
from ytdl_sub.config.preset_options import YTDLOptions
|
||||||
|
from ytdl_sub.config.validators.variable_validation import VariableValidation
|
||||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||||
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
|
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
|
||||||
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
|
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
|
||||||
from ytdl_sub.utils.file_handler import FileHandler
|
from ytdl_sub.utils.file_handler import FileHandler
|
||||||
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
|
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
|
from ytdl_sub.utils.yaml import dump_yaml
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
logger = Logger.get("subscription")
|
logger = Logger.get("subscription")
|
||||||
|
|
@ -76,6 +78,14 @@ class BaseSubscription(ABC):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate after adding the subscription name
|
||||||
|
self._validated_dict = VariableValidation(
|
||||||
|
overrides=self.overrides,
|
||||||
|
downloader_options=self.downloader_options,
|
||||||
|
output_options=self.output_options,
|
||||||
|
plugins=self.plugins,
|
||||||
|
).ensure_proper_usage()
|
||||||
|
|
||||||
self._enhanced_download_archive: Optional[EnhancedDownloadArchive] = (
|
self._enhanced_download_archive: Optional[EnhancedDownloadArchive] = (
|
||||||
_initialize_download_archive(
|
_initialize_download_archive(
|
||||||
output_options=self.output_options,
|
output_options=self.output_options,
|
||||||
|
|
@ -88,9 +98,9 @@ class BaseSubscription(ABC):
|
||||||
# Add post-archive variables
|
# Add post-archive variables
|
||||||
self.overrides.add(
|
self.overrides.add(
|
||||||
{
|
{
|
||||||
SubscriptionVariables.subscription_has_download_archive(): f"""{{
|
SubscriptionVariables.subscription_has_download_archive(): (
|
||||||
%bool({self.download_archive.num_entries > 0})
|
f"{{%bool({self.download_archive.num_entries > 0})}}"
|
||||||
}}""",
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -245,3 +255,11 @@ class BaseSubscription(ABC):
|
||||||
Subscription in yaml format
|
Subscription in yaml format
|
||||||
"""
|
"""
|
||||||
return self._preset_options.yaml
|
return self._preset_options.yaml
|
||||||
|
|
||||||
|
def resolved_yaml(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Human-readable, condensed YAML definition of the subscription.
|
||||||
|
"""
|
||||||
|
return dump_yaml(self._validated_dict)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from ytdl_sub.script.parser import parse
|
from ytdl_sub.script.parser import parse
|
||||||
from ytdl_sub.script.script import _is_function
|
|
||||||
from ytdl_sub.script.types.array import UnresolvedArray
|
from ytdl_sub.script.types.array import UnresolvedArray
|
||||||
from ytdl_sub.script.types.function import BuiltInFunction
|
from ytdl_sub.script.types.function import BuiltInFunction
|
||||||
from ytdl_sub.script.types.function import Function
|
from ytdl_sub.script.types.function import Function
|
||||||
|
|
@ -15,8 +14,10 @@ from ytdl_sub.script.types.resolvable import Float
|
||||||
from ytdl_sub.script.types.resolvable import Integer
|
from ytdl_sub.script.types.resolvable import Integer
|
||||||
from ytdl_sub.script.types.resolvable import Lambda
|
from ytdl_sub.script.types.resolvable import Lambda
|
||||||
from ytdl_sub.script.types.resolvable import String
|
from ytdl_sub.script.types.resolvable import String
|
||||||
|
from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
||||||
from ytdl_sub.script.types.variable import Variable
|
from ytdl_sub.script.types.variable import Variable
|
||||||
from ytdl_sub.script.utils.exceptions import UNREACHABLE
|
from ytdl_sub.script.utils.exceptions import UNREACHABLE
|
||||||
|
from ytdl_sub.script.utils.name_validation import is_function
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
|
|
||||||
|
|
@ -30,10 +31,26 @@ class ScriptUtils:
|
||||||
sanitized_variables = {
|
sanitized_variables = {
|
||||||
f"{name}_sanitized": f"{{%sanitize({name})}}"
|
f"{name}_sanitized": f"{{%sanitize({name})}}"
|
||||||
for name in variables.keys()
|
for name in variables.keys()
|
||||||
if not _is_function(name)
|
if not is_function(name)
|
||||||
}
|
}
|
||||||
return dict(variables, **sanitized_variables)
|
return dict(variables, **sanitized_variables)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_sanitized_parsed_variables(
|
||||||
|
cls, variables: Dict[str, SyntaxTree]
|
||||||
|
) -> Dict[str, SyntaxTree]:
|
||||||
|
"""
|
||||||
|
Helper to add sanitized variables to a Script
|
||||||
|
"""
|
||||||
|
sanitized_variables = {
|
||||||
|
f"{name}_sanitized": SyntaxTree(
|
||||||
|
ast=[BuiltInFunction(name="sanitize", args=[Variable(name)])]
|
||||||
|
)
|
||||||
|
for name in variables.keys()
|
||||||
|
if not is_function(name)
|
||||||
|
}
|
||||||
|
return variables | sanitized_variables
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def to_script(cls, value: Any, sort_keys: bool = True) -> str:
|
def to_script(cls, value: Any, sort_keys: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
||||||
from ytdl_sub.script.utils.exceptions import RuntimeException
|
from ytdl_sub.script.utils.exceptions import RuntimeException
|
||||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||||
from ytdl_sub.script.utils.exceptions import UserException
|
from ytdl_sub.script.utils.exceptions import UserException
|
||||||
|
from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
|
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
|
||||||
from ytdl_sub.utils.script import ScriptUtils
|
from ytdl_sub.utils.script import ScriptUtils
|
||||||
from ytdl_sub.validators.validators import DictValidator
|
from ytdl_sub.validators.validators import DictValidator
|
||||||
|
|
@ -19,6 +20,8 @@ from ytdl_sub.validators.validators import LiteralDictValidator
|
||||||
from ytdl_sub.validators.validators import StringValidator
|
from ytdl_sub.validators.validators import StringValidator
|
||||||
from ytdl_sub.validators.validators import Validator
|
from ytdl_sub.validators.validators import Validator
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
|
|
||||||
class StringFormatterValidator(StringValidator):
|
class StringFormatterValidator(StringValidator):
|
||||||
"""
|
"""
|
||||||
|
|
@ -51,7 +54,10 @@ class StringFormatterValidator(StringValidator):
|
||||||
def __init__(self, name, value: str):
|
def __init__(self, name, value: str):
|
||||||
super().__init__(name=name, value=value)
|
super().__init__(name=name, value=value)
|
||||||
try:
|
try:
|
||||||
_ = parse(str(value))
|
self._parsed = parse(
|
||||||
|
text=str(value),
|
||||||
|
name=self.leaf_name,
|
||||||
|
)
|
||||||
except UserException as exc:
|
except UserException as exc:
|
||||||
raise self._validation_exception(exc) from exc
|
raise self._validation_exception(exc) from exc
|
||||||
|
|
||||||
|
|
@ -65,6 +71,16 @@ class StringFormatterValidator(StringValidator):
|
||||||
"""
|
"""
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
|
@property
|
||||||
|
@final
|
||||||
|
def parsed(self) -> SyntaxTree:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The parsed format string.
|
||||||
|
"""
|
||||||
|
return self._parsed
|
||||||
|
|
||||||
def post_process(self, resolved: str) -> str:
|
def post_process(self, resolved: str) -> str:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
|
|
@ -167,9 +183,14 @@ class DictFormatterValidator(LiteralDictValidator):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dict_with_format_strings(self) -> Dict[str, str]:
|
def dict_with_format_strings(self) -> Dict[str, str]:
|
||||||
"""Returns dict with the format strings themselves"""
|
"""Returns dict with the format strings themselves."""
|
||||||
return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
|
return {key: string_formatter.format_string for key, string_formatter in self.dict.items()}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dict_with_parsed_format_strings(self) -> Dict[str, SyntaxTree]:
|
||||||
|
"""Returns dict with the parsed format strings."""
|
||||||
|
return {key: string_formatter.parsed for key, string_formatter in self.dict.items()}
|
||||||
|
|
||||||
|
|
||||||
class OverridesDictFormatterValidator(DictFormatterValidator):
|
class OverridesDictFormatterValidator(DictFormatterValidator):
|
||||||
"""
|
"""
|
||||||
|
|
@ -199,10 +220,8 @@ def to_variable_dependency_format_string(script: Script, parsed_format_string: S
|
||||||
dummy_format_string = ""
|
dummy_format_string = ""
|
||||||
for var in parsed_format_string.variables:
|
for var in parsed_format_string.variables:
|
||||||
dummy_format_string += f"{{ {var.name} }}"
|
dummy_format_string += f"{{ {var.name} }}"
|
||||||
# pylint: disable=protected-access
|
|
||||||
for variable_dependency in script._variables[var.name].variables:
|
for variable_dependency in script._variables[var.name].variables:
|
||||||
dummy_format_string += f"{{ {variable_dependency.name} }}"
|
dummy_format_string += f"{{ {variable_dependency.name} }}"
|
||||||
# pylint: enable=protected-access
|
|
||||||
return dummy_format_string
|
return dummy_format_string
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -210,16 +229,15 @@ def _validate_formatter(
|
||||||
mock_script: Script,
|
mock_script: Script,
|
||||||
unresolved_variables: Set[str],
|
unresolved_variables: Set[str],
|
||||||
formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
|
formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator],
|
||||||
) -> None:
|
) -> str:
|
||||||
is_static_formatter = False
|
parsed = formatter_validator.parsed
|
||||||
unresolvable = unresolved_variables
|
if resolved := parsed.maybe_resolvable:
|
||||||
if isinstance(formatter_validator, OverridesStringFormatterValidator):
|
return resolved.native
|
||||||
is_static_formatter = True
|
|
||||||
unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
|
is_static_formatter = isinstance(formatter_validator, OverridesStringFormatterValidator)
|
||||||
|
if is_static_formatter:
|
||||||
|
unresolved_variables = unresolved_variables.union({VARIABLES.entry_metadata.variable_name})
|
||||||
|
|
||||||
parsed = parse(
|
|
||||||
text=formatter_validator.format_string,
|
|
||||||
)
|
|
||||||
variable_names = {var.name for var in parsed.variables}
|
variable_names = {var.name for var in parsed.variables}
|
||||||
custom_function_names = {f"%{func.name}" for func in parsed.custom_functions}
|
custom_function_names = {f"%{func.name}" for func in parsed.custom_functions}
|
||||||
|
|
||||||
|
|
@ -233,21 +251,20 @@ def _validate_formatter(
|
||||||
"contains the following custom functions that do not exist: "
|
"contains the following custom functions that do not exist: "
|
||||||
f"{', '.join(sorted(custom_function_names - mock_script.function_names))}"
|
f"{', '.join(sorted(custom_function_names - mock_script.function_names))}"
|
||||||
)
|
)
|
||||||
if unresolved := variable_names.intersection(unresolvable):
|
if unresolved := variable_names.intersection(unresolved_variables):
|
||||||
raise StringFormattingVariableNotFoundException(
|
raise StringFormattingVariableNotFoundException(
|
||||||
"contains the following variables that are unresolved when executing this "
|
"contains the following variables that are unresolved when executing this "
|
||||||
f"formatter: {', '.join(sorted(unresolved))}"
|
f"formatter: {', '.join(sorted(unresolved))}"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
mock_script.resolve_once(
|
if is_static_formatter:
|
||||||
{
|
return mock_script.resolve_once_parsed(
|
||||||
"tmp_var": to_variable_dependency_format_string(
|
{"tmp_var": formatter_validator.parsed},
|
||||||
script=mock_script, parsed_format_string=parsed
|
unresolvable=unresolved_variables,
|
||||||
)
|
|
||||||
},
|
|
||||||
unresolvable=unresolvable,
|
|
||||||
update=True,
|
update=True,
|
||||||
)
|
)["tmp_var"].native
|
||||||
|
|
||||||
|
return formatter_validator.format_string
|
||||||
except RuntimeException as exc:
|
except RuntimeException as exc:
|
||||||
if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter:
|
if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter:
|
||||||
raise StringFormattingVariableNotFoundException(
|
raise StringFormattingVariableNotFoundException(
|
||||||
|
|
@ -255,45 +272,60 @@ def _validate_formatter(
|
||||||
"entry variables"
|
"entry variables"
|
||||||
) from exc
|
) from exc
|
||||||
raise StringFormattingVariableNotFoundException(exc) from exc
|
raise StringFormattingVariableNotFoundException(exc) from exc
|
||||||
|
except UserThrownRuntimeError as exc:
|
||||||
|
# Errors are expected for non-static formatters due to missing entry
|
||||||
|
# data. Raise otherwise.
|
||||||
|
if not is_static_formatter:
|
||||||
|
return formatter_validator.format_string
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
def validate_formatters(
|
def validate_formatters(
|
||||||
script: Script,
|
script: Script,
|
||||||
unresolved_variables: Set[str],
|
unresolved_variables: Set[str],
|
||||||
validator: Validator,
|
validator: Validator,
|
||||||
) -> None:
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
|
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
|
||||||
and resolve.
|
and resolve.
|
||||||
"""
|
"""
|
||||||
|
resolved_dict: Dict = {}
|
||||||
|
|
||||||
if isinstance(validator, DictValidator):
|
if isinstance(validator, DictValidator):
|
||||||
# pylint: disable=protected-access
|
resolved_dict[validator.leaf_name] = {}
|
||||||
# Usage of protected variables in other validators is fine. The reason to keep
|
# Usage of protected variables in other validators is fine. The reason to keep
|
||||||
# them protected is for readability when using them in subscriptions.
|
# them protected is for readability when using them in subscriptions.
|
||||||
for validator_value in validator._validator_dict.values():
|
for validator_value in validator._validator_dict.values():
|
||||||
validate_formatters(
|
resolved_dict[validator.leaf_name] |= validate_formatters(
|
||||||
script=script,
|
script=script,
|
||||||
unresolved_variables=unresolved_variables,
|
unresolved_variables=unresolved_variables,
|
||||||
validator=validator_value,
|
validator=validator_value,
|
||||||
)
|
)
|
||||||
# pylint: enable=protected-access
|
|
||||||
elif isinstance(validator, ListValidator):
|
elif isinstance(validator, ListValidator):
|
||||||
|
resolved_dict[validator.leaf_name] = []
|
||||||
for list_value in validator.list:
|
for list_value in validator.list:
|
||||||
validate_formatters(
|
list_output = validate_formatters(
|
||||||
script=script,
|
script=script,
|
||||||
unresolved_variables=unresolved_variables,
|
unresolved_variables=unresolved_variables,
|
||||||
validator=list_value,
|
validator=list_value,
|
||||||
)
|
)
|
||||||
|
assert len(list_output) == 1
|
||||||
|
resolved_dict[validator.leaf_name].append(list(list_output.values())[0])
|
||||||
elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
|
elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
|
||||||
_validate_formatter(
|
resolved_dict[validator.leaf_name] = _validate_formatter(
|
||||||
mock_script=script,
|
mock_script=script,
|
||||||
unresolved_variables=unresolved_variables,
|
unresolved_variables=unresolved_variables,
|
||||||
formatter_validator=validator,
|
formatter_validator=validator,
|
||||||
)
|
)
|
||||||
elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
|
elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
|
||||||
|
resolved_dict[validator.leaf_name] = {}
|
||||||
for validator_value in validator.dict.values():
|
for validator_value in validator.dict.values():
|
||||||
_validate_formatter(
|
resolved_dict[validator.leaf_name] |= _validate_formatter(
|
||||||
mock_script=script,
|
mock_script=script,
|
||||||
unresolved_variables=unresolved_variables,
|
unresolved_variables=unresolved_variables,
|
||||||
formatter_validator=validator_value,
|
formatter_validator=validator_value,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
resolved_dict[validator.leaf_name] = validator._value
|
||||||
|
|
||||||
|
return resolved_dict
|
||||||
|
|
|
||||||
|
|
@ -95,21 +95,11 @@ class Validator(ABC):
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@property
|
@property
|
||||||
def _root_name(self) -> str:
|
def leaf_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
"first" from the first.element.of.the.name
|
"name" from the first.element.of.the.name
|
||||||
"""
|
|
||||||
return self._name.split(".")[0]
|
|
||||||
|
|
||||||
@final
|
|
||||||
@property
|
|
||||||
def _leaf_name(self) -> str:
|
|
||||||
"""
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
"first" from the first.element.of.the.name
|
|
||||||
"""
|
"""
|
||||||
return self._name.split(".")[-1]
|
return self._name.split(".")[-1]
|
||||||
|
|
||||||
|
|
@ -274,7 +264,7 @@ class DictValidator(Validator):
|
||||||
value=self._dict.get(key, default),
|
value=self._dict.get(key, default),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__validator_dict[validator_name] = validator_instance
|
self.__validator_dict[key] = validator_instance
|
||||||
return validator_instance
|
return validator_instance
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import pytest
|
||||||
|
|
||||||
from ytdl_sub.config.preset import Preset
|
from ytdl_sub.config.preset import Preset
|
||||||
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
|
|
||||||
from ytdl_sub.utils.exceptions import ValidationException
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,100 +156,6 @@ class TestPreset:
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_preset_error__source_variable_does_not_exist(
|
|
||||||
self, config_file, output_options, youtube_video
|
|
||||||
):
|
|
||||||
with pytest.raises(
|
|
||||||
StringFormattingVariableNotFoundException,
|
|
||||||
match="contains the following variables that do not exist: dne_var",
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": youtube_video,
|
|
||||||
"output_options": {"output_directory": "dir", "file_name": "{dne_var}"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_error__override_variable_does_not_exist(
|
|
||||||
self, config_file, output_options, youtube_video
|
|
||||||
):
|
|
||||||
with pytest.raises(
|
|
||||||
StringFormattingVariableNotFoundException,
|
|
||||||
match="contains the following variables that do not exist: dne_var",
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": youtube_video,
|
|
||||||
"output_options": {"output_directory": "{dne_var}", "file_name": "file"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_error__dict_source_variable_does_not_exist(
|
|
||||||
self, config_file, output_options, youtube_video
|
|
||||||
):
|
|
||||||
with pytest.raises(
|
|
||||||
StringFormattingVariableNotFoundException,
|
|
||||||
match="contains the following variables that do not exist: dne_var",
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": youtube_video,
|
|
||||||
"output_options": {"output_directory": "dir", "file_name": "file"},
|
|
||||||
"nfo_tags": {
|
|
||||||
"nfo_name": "the nfo name",
|
|
||||||
"nfo_root": "the root",
|
|
||||||
"tags": {"tag_a": "{dne_var}"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_error__dict_override_variable_does_not_exist(
|
|
||||||
self, config_file, output_options, youtube_video
|
|
||||||
):
|
|
||||||
with pytest.raises(
|
|
||||||
StringFormattingVariableNotFoundException,
|
|
||||||
match="contains the following variables that do not exist: dne_var",
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": youtube_video,
|
|
||||||
"output_options": output_options,
|
|
||||||
"output_directory_nfo_tags": {
|
|
||||||
"nfo_name": "the nfo name",
|
|
||||||
"nfo_root": "the root",
|
|
||||||
"tags": {"tag_a": "{dne_var}"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_error__dict_override_variable_not_static(
|
|
||||||
self, config_file, output_options, youtube_video
|
|
||||||
):
|
|
||||||
with pytest.raises(
|
|
||||||
StringFormattingVariableNotFoundException,
|
|
||||||
match="static formatters must contain variables that "
|
|
||||||
"have no dependency to entry variables",
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": youtube_video,
|
|
||||||
"output_options": {
|
|
||||||
"output_directory": "{title}",
|
|
||||||
"file_name": "{uid}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_with_multi_url__contains_empty_url(self, config_file, output_options):
|
def test_preset_with_multi_url__contains_empty_url(self, config_file, output_options):
|
||||||
_ = Preset(
|
_ = Preset(
|
||||||
config=config_file,
|
config=config_file,
|
||||||
|
|
@ -412,50 +317,3 @@ class TestPreset:
|
||||||
"overrides": {name: "ack"},
|
"overrides": {name: "ack"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_preset_error_added_url_variable_cannot_resolve(self, config_file, output_options):
|
|
||||||
with pytest.raises(
|
|
||||||
ValidationException,
|
|
||||||
match=re.escape(
|
|
||||||
"variable the_bad_one cannot use the variables subtitles_ext because it "
|
|
||||||
"depends on other variables that are computed later in execution"
|
|
||||||
),
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": {
|
|
||||||
"url": "youtube.com/watch?v=123abc",
|
|
||||||
"variables": {"the_bad_one": "{subtitles_ext}"},
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"embed_subtitles": True,
|
|
||||||
},
|
|
||||||
"output_options": {"output_directory": "dir", "file_name": "acjk"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_preset_error_override_name_conflicts_with_plugin(self, config_file, output_options):
|
|
||||||
with pytest.raises(
|
|
||||||
ValidationException,
|
|
||||||
match=re.escape(
|
|
||||||
"Override variable with name throttle_protection cannot be used since it is the "
|
|
||||||
"name of a plugin. Perhaps you meant to define it as a plugin? If so, indent it "
|
|
||||||
"left to make it at the same level as overrides."
|
|
||||||
),
|
|
||||||
):
|
|
||||||
_ = Preset(
|
|
||||||
config=config_file,
|
|
||||||
name="test",
|
|
||||||
value={
|
|
||||||
"download": {
|
|
||||||
"url": "youtube.com/watch?v=123abc",
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"embed_subtitles": True,
|
|
||||||
},
|
|
||||||
"output_options": {"output_directory": "dir", "file_name": "acjk"},
|
|
||||||
"overrides": {"throttle_protection": "nope"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from typing import Dict
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
from ytdl_sub.config.config_file import ConfigFile
|
from ytdl_sub.config.config_file import ConfigFile
|
||||||
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
from ytdl_sub.plugins.nfo_tags import NfoTagsOptions
|
||||||
|
|
@ -540,9 +541,67 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc
|
||||||
assert gnr.get("url2").native == "https://www.youtube.com/watch?v=OldpIhHPsbs"
|
assert gnr.get("url2").native == "https://www.youtube.com/watch?v=OldpIhHPsbs"
|
||||||
|
|
||||||
|
|
||||||
def test_default_docker_config_and_subscriptions(docker_default_subscription_path: Path):
|
def test_default_docker_config_and_subscriptions(
|
||||||
|
docker_default_subscription_path: Path, output_directory: str
|
||||||
|
):
|
||||||
default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml")
|
default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml")
|
||||||
default_subs = Subscription.from_file_path(
|
default_subs = Subscription.from_file_path(
|
||||||
config=default_config, subscription_path=docker_default_subscription_path
|
config=default_config, subscription_path=docker_default_subscription_path
|
||||||
)
|
)
|
||||||
assert len(default_subs) == 1
|
assert len(default_subs) == 1
|
||||||
|
|
||||||
|
resolved_yaml_as_json = yaml.safe_load(default_subs[0].resolved_yaml())
|
||||||
|
|
||||||
|
# Since this creates random values, ignore it for this test
|
||||||
|
assert "throttle_protection" in resolved_yaml_as_json
|
||||||
|
del resolved_yaml_as_json["throttle_protection"]
|
||||||
|
|
||||||
|
assert resolved_yaml_as_json == {
|
||||||
|
"chapters": {
|
||||||
|
"allow_chapters_from_comments": False,
|
||||||
|
"embed_chapters": True,
|
||||||
|
"enable": "True",
|
||||||
|
"force_key_frames": False,
|
||||||
|
},
|
||||||
|
"date_range": {"breaks": "True", "enable": "True", "type": "upload_date"},
|
||||||
|
"download": [
|
||||||
|
{
|
||||||
|
"download_reverse": "True",
|
||||||
|
"include_sibling_metadata": False,
|
||||||
|
"playlist_thumbnails": [
|
||||||
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
||||||
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
||||||
|
],
|
||||||
|
"source_thumbnails": [
|
||||||
|
{"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"},
|
||||||
|
{"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"},
|
||||||
|
],
|
||||||
|
"url": "https://www.youtube.com/@novapbs",
|
||||||
|
"variables": {},
|
||||||
|
"webpage_url": "{modified_webpage_url}",
|
||||||
|
"ytdl_options": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"file_convert": {"convert_to": "mp4", "convert_with": "yt-dlp", "enable": "True"},
|
||||||
|
"format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)",
|
||||||
|
"output_options": {
|
||||||
|
"download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json",
|
||||||
|
"file_name": "{episode_file_path}.{ext}",
|
||||||
|
"info_json_name": "{episode_file_path}.{info_json_ext}",
|
||||||
|
"keep_files_date_eval": "{episode_date_standardized}",
|
||||||
|
"maintain_download_archive": True,
|
||||||
|
"output_directory": f"{output_directory}/NOVA PBS",
|
||||||
|
"preserve_mtime": False,
|
||||||
|
"thumbnail_name": "{thumbnail_file_name}",
|
||||||
|
},
|
||||||
|
"video_tags": {
|
||||||
|
"contentRating": "{episode_content_rating}",
|
||||||
|
"date": "{episode_date_standardized}",
|
||||||
|
"episode_id": "{episode_number}",
|
||||||
|
"genre": "{tv_show_genre}",
|
||||||
|
"show": "{tv_show_name}",
|
||||||
|
"synopsis": "{episode_plot}",
|
||||||
|
"title": "{episode_title}",
|
||||||
|
"year": "{episode_year}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
172
tests/unit/config/test_subscription_validation.py
Normal file
172
tests/unit/config/test_subscription_validation.py
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ytdl_sub.config.preset import Preset
|
||||||
|
from ytdl_sub.subscriptions.subscription import Subscription
|
||||||
|
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
|
||||||
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubscriptionValidation:
|
||||||
|
def test_preset_error__source_variable_does_not_exist(
|
||||||
|
self, config_file, output_options, youtube_video
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
StringFormattingVariableNotFoundException,
|
||||||
|
match="contains the following variables that do not exist: dne_var",
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": {"output_directory": "dir", "file_name": "{dne_var}"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error__override_variable_does_not_exist(
|
||||||
|
self, config_file, output_options, youtube_video
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
StringFormattingVariableNotFoundException,
|
||||||
|
match="contains the following variables that do not exist: dne_var",
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": {"output_directory": "{dne_var}", "file_name": "file"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error__dict_source_variable_does_not_exist(
|
||||||
|
self, config_file, output_options, youtube_video
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
StringFormattingVariableNotFoundException,
|
||||||
|
match="contains the following variables that do not exist: dne_var",
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": {"output_directory": "dir", "file_name": "file"},
|
||||||
|
"nfo_tags": {
|
||||||
|
"nfo_name": "the nfo name",
|
||||||
|
"nfo_root": "the root",
|
||||||
|
"tags": {"tag_a": "{dne_var}"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error__dict_override_variable_does_not_exist(
|
||||||
|
self, config_file, output_options, youtube_video
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
StringFormattingVariableNotFoundException,
|
||||||
|
match="contains the following variables that do not exist: dne_var",
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": output_options,
|
||||||
|
"output_directory_nfo_tags": {
|
||||||
|
"nfo_name": "the nfo name",
|
||||||
|
"nfo_root": "the root",
|
||||||
|
"tags": {"tag_a": "{dne_var}"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error__dict_override_variable_not_static(
|
||||||
|
self, config_file, output_options, youtube_video
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
StringFormattingVariableNotFoundException,
|
||||||
|
match="static formatters must contain variables that "
|
||||||
|
"have no dependency to entry variables",
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": {
|
||||||
|
"output_directory": "{title}",
|
||||||
|
"file_name": "{uid}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error_added_url_variable_cannot_resolve(self, config_file, output_options):
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationException,
|
||||||
|
match=re.escape(
|
||||||
|
"variable the_bad_one cannot use the variables subtitles_ext because it "
|
||||||
|
"depends on other variables that are computed later in execution"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": {
|
||||||
|
"url": "youtube.com/watch?v=123abc",
|
||||||
|
"variables": {"the_bad_one": "{subtitles_ext}"},
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"embed_subtitles": True,
|
||||||
|
},
|
||||||
|
"output_options": {"output_directory": "dir", "file_name": "acjk"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preset_error_override_name_conflicts_with_plugin(self, config_file, output_options):
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationException,
|
||||||
|
match=re.escape(
|
||||||
|
"Override variable with name throttle_protection cannot be used since it is the "
|
||||||
|
"name of a plugin. Perhaps you meant to define it as a plugin? If so, indent it "
|
||||||
|
"left to make it at the same level as overrides."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
_ = Subscription.from_preset(
|
||||||
|
preset=Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test",
|
||||||
|
value={
|
||||||
|
"download": {
|
||||||
|
"url": "youtube.com/watch?v=123abc",
|
||||||
|
},
|
||||||
|
"subtitles": {
|
||||||
|
"embed_subtitles": True,
|
||||||
|
},
|
||||||
|
"output_options": {"output_directory": "dir", "file_name": "acjk"},
|
||||||
|
"overrides": {"throttle_protection": "nope"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config=config_file,
|
||||||
|
)
|
||||||
|
|
@ -37,10 +37,12 @@ def should_filter_property(property_name: str) -> bool:
|
||||||
"dict",
|
"dict",
|
||||||
"keys",
|
"keys",
|
||||||
"dict_with_format_strings",
|
"dict_with_format_strings",
|
||||||
|
"dict_with_parsed_format_strings",
|
||||||
"subscription_name",
|
"subscription_name",
|
||||||
"list",
|
"list",
|
||||||
"script",
|
"script",
|
||||||
"unresolvable",
|
"unresolvable",
|
||||||
|
"leaf_name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue