ytdl-sub 2026.1.6__py3-none-any.whl → 2026.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ytdl_sub/__init__.py CHANGED
@@ -1 +1 @@
1
- __pypi_version__ = "2026.01.06";__local_version__ = "2026.01.06+6b99c31"
1
+ __pypi_version__ = "2026.01.13";__local_version__ = "2026.01.13+530d2f7"
@@ -22,7 +22,6 @@ VariableT = TypeVar("VariableT", bound="Variable")
22
22
 
23
23
 
24
24
  def _get(
25
- cast: str,
26
25
  metadata_variable_name: str,
27
26
  metadata_key: str,
28
27
  variable_name: Optional[str],
@@ -47,7 +46,7 @@ def _get(
47
46
  return as_type(
48
47
  variable_name=variable_name or metadata_key,
49
48
  metadata_key=metadata_key,
50
- definition=f"{{ %legacy_bracket_safety(%{cast}({out})) }}",
49
+ definition=f"{{ {out} }}",
51
50
  )
52
51
 
53
52
 
@@ -182,7 +181,6 @@ class MapMetadataVariable(MetadataVariable, MapVariable):
182
181
  Creates a map variable from entry metadata
183
182
  """
184
183
  return _get(
185
- "map",
186
184
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
187
185
  metadata_key=metadata_key,
188
186
  variable_name=variable_name,
@@ -204,7 +202,6 @@ class ArrayMetadataVariable(MetadataVariable, ArrayVariable):
204
202
  Creates an array variable from entry metadata
205
203
  """
206
204
  return _get(
207
- "array",
208
205
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
209
206
  metadata_key=metadata_key,
210
207
  variable_name=variable_name,
@@ -226,7 +223,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
226
223
  Creates a string variable from entry metadata
227
224
  """
228
225
  return _get(
229
- "string",
230
226
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
231
227
  metadata_key=metadata_key,
232
228
  variable_name=variable_name,
@@ -245,7 +241,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
245
241
  Creates a string variable from playlist metadata
246
242
  """
247
243
  return _get(
248
- "string",
249
244
  metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
250
245
  metadata_key=metadata_key,
251
246
  variable_name=variable_name,
@@ -264,7 +259,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable):
264
259
  Creates a string variable from source metadata
265
260
  """
266
261
  return _get(
267
- "string",
268
262
  metadata_variable_name=SOURCE_METADATA_VARIABLE_NAME,
269
263
  metadata_key=metadata_key,
270
264
  variable_name=variable_name,
@@ -301,7 +295,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable):
301
295
  Creates an int variable from entry metadata
302
296
  """
303
297
  return _get(
304
- "int",
305
298
  metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME,
306
299
  metadata_key=metadata_key,
307
300
  variable_name=variable_name,
@@ -320,7 +313,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable):
320
313
  Creates an int variable from playlist metadata
321
314
  """
322
315
  return _get(
323
- "int",
324
316
  metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME,
325
317
  metadata_key=metadata_key,
326
318
  variable_name=variable_name,
ytdl_sub/script/script.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=missing-raises-doc
2
+ import copy
2
3
  from typing import Dict
3
4
  from typing import List
4
5
  from typing import Optional
@@ -7,6 +8,7 @@ from typing import Set
7
8
  from ytdl_sub.script.functions import Functions
8
9
  from ytdl_sub.script.parser import parse
9
10
  from ytdl_sub.script.script_output import ScriptOutput
11
+ from ytdl_sub.script.types.resolvable import Argument
10
12
  from ytdl_sub.script.types.resolvable import BuiltInFunctionType
11
13
  from ytdl_sub.script.types.resolvable import Lambda
12
14
  from ytdl_sub.script.types.resolvable import Resolvable
@@ -14,6 +16,7 @@ from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree
14
16
  from ytdl_sub.script.types.syntax_tree import SyntaxTree
15
17
  from ytdl_sub.script.types.variable import FunctionArgument
16
18
  from ytdl_sub.script.types.variable import Variable
19
+ from ytdl_sub.script.types.variable_dependency import VariableDependency
17
20
  from ytdl_sub.script.utils.exceptions import UNREACHABLE
18
21
  from ytdl_sub.script.utils.exceptions import CycleDetected
19
22
  from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
@@ -695,3 +698,59 @@ class Script:
695
698
  Names of all functions within the Script.
696
699
  """
697
700
  return set(to_function_definition_name(name) for name in self._functions.keys())
701
+
702
+ def resolve_partial(
703
+ self,
704
+ unresolvable: Optional[Set[str]] = None,
705
+ ) -> "Script":
706
+ """
707
+ Returns
708
+ -------
709
+ New (deep-copied) script that resolves inner variables as much
710
+ as possible.
711
+ """
712
+ unresolvable: Set[str] = unresolvable or {}
713
+ resolved: Dict[Variable, Resolvable] = {}
714
+ unresolved: Dict[Variable, Argument] = {
715
+ Variable(name): definition
716
+ for name, definition in self._variables.items()
717
+ if name not in unresolvable
718
+ }
719
+
720
+ partially_resolved = True
721
+ while partially_resolved:
722
+
723
+ partially_resolved = False
724
+
725
+ for variable in list(unresolved.keys()):
726
+ definition = unresolved[variable]
727
+
728
+ maybe_resolved = definition
729
+ if isinstance(definition, Variable) and definition.name not in unresolvable:
730
+ maybe_resolved = resolved.get(definition, unresolved[definition])
731
+ elif isinstance(definition, VariableDependency):
732
+ maybe_resolved = definition.partial_resolve(
733
+ resolved_variables=resolved,
734
+ unresolved_variables=unresolved,
735
+ custom_functions=self._functions,
736
+ )
737
+
738
+ if isinstance(maybe_resolved, Resolvable):
739
+ resolved[variable] = maybe_resolved
740
+ del unresolved[variable]
741
+ partially_resolved = True
742
+ else:
743
+ unresolved[variable] = maybe_resolved
744
+
745
+ # If the definition changed, then the script changed
746
+ # which means we can iterate again
747
+ partially_resolved |= definition != maybe_resolved
748
+
749
+ return copy.deepcopy(self).add_parsed(
750
+ {var_name: self._variables[var_name] for var_name in unresolvable}
751
+ | {
752
+ var.name: ResolvedSyntaxTree(ast=[definition])
753
+ for var, definition in resolved.items()
754
+ }
755
+ | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()}
756
+ )
@@ -28,7 +28,7 @@ class UnresolvedArray(_Array, VariableDependency, FutureResolvable):
28
28
  value: List[Argument]
29
29
 
30
30
  @property
31
- def _iterable_arguments(self) -> List[Argument]:
31
+ def iterable_arguments(self) -> List[Argument]:
32
32
  return self.value
33
33
 
34
34
  def resolve(
@@ -47,6 +47,27 @@ class UnresolvedArray(_Array, VariableDependency, FutureResolvable):
47
47
  ]
48
48
  )
49
49
 
50
+ def partial_resolve(
51
+ self,
52
+ resolved_variables: Dict[Variable, Resolvable],
53
+ unresolved_variables: Dict[Variable, Argument],
54
+ custom_functions: Dict[str, VariableDependency],
55
+ ) -> Argument | Resolvable:
56
+ maybe_resolvable_values, is_resolvable = VariableDependency.try_partial_resolve(
57
+ args=self.value,
58
+ resolved_variables=resolved_variables,
59
+ unresolved_variables=unresolved_variables,
60
+ custom_functions=custom_functions,
61
+ )
62
+
63
+ out = UnresolvedArray(value=maybe_resolvable_values)
64
+ if is_resolvable:
65
+ return out.resolve(
66
+ resolved_variables=resolved_variables, custom_functions=custom_functions
67
+ )
68
+
69
+ return out
70
+
50
71
  def future_resolvable_type(self) -> Type[Resolvable]:
51
72
  return Array
52
73
 
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Callable
5
5
  from typing import Dict
6
6
  from typing import List
7
+ from typing import Optional
7
8
  from typing import Type
8
9
  from typing import Union
9
10
 
@@ -11,9 +12,11 @@ from ytdl_sub.script.functions import Functions
11
12
  from ytdl_sub.script.types.array import Array
12
13
  from ytdl_sub.script.types.array import UnresolvedArray
13
14
  from ytdl_sub.script.types.resolvable import Argument
15
+ from ytdl_sub.script.types.resolvable import Boolean
14
16
  from ytdl_sub.script.types.resolvable import BuiltInFunctionType
15
17
  from ytdl_sub.script.types.resolvable import FunctionType
16
18
  from ytdl_sub.script.types.resolvable import FutureResolvable
19
+ from ytdl_sub.script.types.resolvable import Integer
17
20
  from ytdl_sub.script.types.resolvable import Lambda
18
21
  from ytdl_sub.script.types.resolvable import NamedCustomFunction
19
22
  from ytdl_sub.script.types.resolvable import Resolvable
@@ -35,7 +38,7 @@ from ytdl_sub.script.utils.type_checking import is_union
35
38
  @dataclass(frozen=True)
36
39
  class Function(FunctionType, VariableDependency, ABC):
37
40
  @property
38
- def _iterable_arguments(self) -> List[Argument]:
41
+ def iterable_arguments(self) -> List[Argument]:
39
42
  return self.args
40
43
 
41
44
 
@@ -84,6 +87,52 @@ class CustomFunction(Function, NamedCustomFunction):
84
87
  # been checked in the parser with
85
88
  raise UNREACHABLE
86
89
 
90
+ def partial_resolve(
91
+ self,
92
+ resolved_variables: Dict[Variable, Resolvable],
93
+ unresolved_variables: Dict[Variable, Argument],
94
+ custom_functions: Dict[str, VariableDependency],
95
+ ) -> Argument | Resolvable:
96
+ maybe_resolvable_args, _ = VariableDependency.try_partial_resolve(
97
+ args=self.args,
98
+ resolved_variables=resolved_variables,
99
+ unresolved_variables=unresolved_variables,
100
+ custom_functions=custom_functions,
101
+ )
102
+
103
+ for i in range(len(self.args)):
104
+ function_arg = FunctionArgument.from_idx(idx=i, custom_function_name=self.name)
105
+ function_value = maybe_resolvable_args[i]
106
+
107
+ if isinstance(function_value, Resolvable):
108
+ resolved_variables[function_arg] = function_value
109
+ else:
110
+ unresolved_variables[function_arg] = function_value
111
+
112
+ assert len(custom_functions[self.name].iterable_arguments) == 1
113
+ custom_function_definition = custom_functions[self.name].iterable_arguments[0]
114
+
115
+ maybe_resolvable_custom_function, is_resolvable = VariableDependency.try_partial_resolve(
116
+ args=[custom_function_definition],
117
+ resolved_variables=resolved_variables,
118
+ unresolved_variables=unresolved_variables,
119
+ custom_functions=custom_functions,
120
+ )
121
+
122
+ for i in range(len(self.args)):
123
+ function_arg = FunctionArgument.from_idx(idx=i, custom_function_name=self.name)
124
+
125
+ if isinstance(maybe_resolvable_args[i], Resolvable):
126
+ del resolved_variables[function_arg]
127
+ else:
128
+ del unresolved_variables[function_arg]
129
+
130
+ if is_resolvable:
131
+ return maybe_resolvable_custom_function[0]
132
+
133
+ # Did not resolve custom function arguments, do not proceed
134
+ return CustomFunction(name=self.name, args=maybe_resolvable_args)
135
+
87
136
 
88
137
  class BuiltInFunction(Function, BuiltInFunctionType):
89
138
  def validate_args(self) -> "BuiltInFunction":
@@ -312,5 +361,133 @@ class BuiltInFunction(Function, BuiltInFunctionType):
312
361
  f"Runtime error occurred when executing the function %{self.name}: {str(exc)}"
313
362
  ) from exc
314
363
 
364
+ def _partial_resolve_conditional(
365
+ self,
366
+ resolved_variables: Dict[Variable, Resolvable],
367
+ unresolved_variables: Dict[Variable, Argument],
368
+ custom_functions: Dict[str, "VariableDependency"],
369
+ ):
370
+ """
371
+ If the conditional partially resolvable enough to warrant evaluation,
372
+ perform it here.
373
+ """
374
+ if self.is_subset_of(
375
+ variables=resolved_variables, custom_function_definitions=custom_functions
376
+ ):
377
+ return self.resolve(
378
+ resolved_variables=resolved_variables,
379
+ custom_functions=custom_functions,
380
+ )
381
+
382
+ if self.name == "if":
383
+ maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve(
384
+ args=[self.args[0]],
385
+ resolved_variables=resolved_variables,
386
+ unresolved_variables=unresolved_variables,
387
+ custom_functions=custom_functions,
388
+ )
389
+ if is_resolvable:
390
+ boolean_output = maybe_resolvable_arg[0]
391
+ assert isinstance(boolean_output, Boolean)
392
+ return self.args[1] if boolean_output.native else self.args[2]
393
+
394
+ if self.name == "elif":
395
+ for idx in range(0, len(self.args), 2):
396
+ maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve(
397
+ args=[self.args[idx]],
398
+ resolved_variables=resolved_variables,
399
+ unresolved_variables=unresolved_variables,
400
+ custom_functions=custom_functions,
401
+ )
402
+ if is_resolvable:
403
+ boolean_output = maybe_resolvable_arg[0]
404
+ assert isinstance(boolean_output, Boolean)
405
+ if boolean_output.native:
406
+ return self.args[idx + 1]
407
+ else:
408
+ break
409
+
410
+ if self.name == "assert_then":
411
+ maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve(
412
+ args=[self.args[0]],
413
+ resolved_variables=resolved_variables,
414
+ unresolved_variables=unresolved_variables,
415
+ custom_functions=custom_functions,
416
+ )
417
+ if is_resolvable:
418
+ boolean_output = maybe_resolvable_arg[0]
419
+ assert isinstance(boolean_output, Boolean)
420
+ if boolean_output.native:
421
+ return self.args[1]
422
+
423
+ return self
424
+
425
+ def _try_optimized_partial_resolve(
426
+ self,
427
+ resolved_variables: Dict[Variable, Resolvable],
428
+ unresolved_variables: Dict[Variable, Argument],
429
+ custom_functions: Dict[str, "VariableDependency"],
430
+ ) -> Optional[Argument]:
431
+ """
432
+ If a function has enough (but not all) resolved parameters to warrant a return,
433
+ perform it here.
434
+ """
435
+ if self.name == "array_at":
436
+ if (
437
+ isinstance(self.args[0], UnresolvedArray)
438
+ and isinstance(self.args[1], Integer)
439
+ and len(self.args[0].value) >= self.args[1].value
440
+ ):
441
+ maybe_resolvable_values, _ = VariableDependency.try_partial_resolve(
442
+ args=[self.args[0].value[self.args[1].value]],
443
+ resolved_variables=resolved_variables,
444
+ unresolved_variables=unresolved_variables,
445
+ custom_functions=custom_functions,
446
+ )
447
+
448
+ return maybe_resolvable_values[0]
449
+
450
+ return None
451
+
452
+ def partial_resolve(
453
+ self,
454
+ resolved_variables: Dict[Variable, Resolvable],
455
+ unresolved_variables: Dict[Variable, Argument],
456
+ custom_functions: Dict[str, VariableDependency],
457
+ ) -> Argument | Resolvable:
458
+ conditional_return_args = self.function_spec.conditional_arg_indices(
459
+ num_input_args=len(self.args)
460
+ )
461
+
462
+ if conditional_return_args:
463
+ return self._partial_resolve_conditional(
464
+ resolved_variables=resolved_variables,
465
+ unresolved_variables=unresolved_variables,
466
+ custom_functions=custom_functions,
467
+ )
468
+
469
+ if partial_resolved := self._try_optimized_partial_resolve(
470
+ resolved_variables=resolved_variables,
471
+ unresolved_variables=unresolved_variables,
472
+ custom_functions=custom_functions,
473
+ ):
474
+ return partial_resolved
475
+
476
+ maybe_resolvable_values, is_resolvable = VariableDependency.try_partial_resolve(
477
+ args=self.args,
478
+ resolved_variables=resolved_variables,
479
+ unresolved_variables=unresolved_variables,
480
+ custom_functions=custom_functions,
481
+ )
482
+
483
+ out = BuiltInFunction(name=self.name, args=maybe_resolvable_values)
484
+ if is_resolvable:
485
+ return out.resolve(
486
+ resolved_variables=resolved_variables,
487
+ custom_functions=custom_functions,
488
+ )
489
+
490
+ return out
491
+
315
492
  def __hash__(self):
316
493
  return hash((self.name, *self.args))
@@ -31,7 +31,7 @@ class UnresolvedMap(_Map, VariableDependency, FutureResolvable):
31
31
  value: Dict[Argument, Argument]
32
32
 
33
33
  @property
34
- def _iterable_arguments(self) -> List[Argument]:
34
+ def iterable_arguments(self) -> List[Argument]:
35
35
  return list(itertools.chain(*self.value.items()))
36
36
 
37
37
  def resolve(
@@ -55,6 +55,35 @@ class UnresolvedMap(_Map, VariableDependency, FutureResolvable):
55
55
 
56
56
  return Map(output)
57
57
 
58
+ def partial_resolve(
59
+ self,
60
+ resolved_variables: Dict[Variable, Resolvable],
61
+ unresolved_variables: Dict[Variable, Argument],
62
+ custom_functions: Dict[str, VariableDependency],
63
+ ) -> Argument | Resolvable:
64
+ maybe_resolvable_keys, is_keys_resolvable = VariableDependency.try_partial_resolve(
65
+ args=self.value.keys(),
66
+ resolved_variables=resolved_variables,
67
+ unresolved_variables=unresolved_variables,
68
+ custom_functions=custom_functions,
69
+ )
70
+
71
+ maybe_resolvable_values, is_values_resolvable = VariableDependency.try_partial_resolve(
72
+ args=self.value.values(),
73
+ resolved_variables=resolved_variables,
74
+ unresolved_variables=unresolved_variables,
75
+ custom_functions=custom_functions,
76
+ )
77
+
78
+ out = UnresolvedMap(value=dict(zip(maybe_resolvable_keys, maybe_resolvable_values)))
79
+ if is_keys_resolvable and is_values_resolvable:
80
+ return out.resolve(
81
+ resolved_variables=resolved_variables,
82
+ custom_functions=custom_functions,
83
+ )
84
+
85
+ return out
86
+
58
87
  def future_resolvable_type(self) -> Type[Resolvable]:
59
88
  return Map
60
89
 
@@ -3,6 +3,7 @@ from typing import Dict
3
3
  from typing import List
4
4
  from typing import Optional
5
5
 
6
+ from ytdl_sub.script.types.function import BuiltInFunction
6
7
  from ytdl_sub.script.types.resolvable import Argument
7
8
  from ytdl_sub.script.types.resolvable import Resolvable
8
9
  from ytdl_sub.script.types.resolvable import String
@@ -15,7 +16,7 @@ class SyntaxTree(VariableDependency):
15
16
  ast: List[Argument]
16
17
 
17
18
  @property
18
- def _iterable_arguments(self) -> List[Argument]:
19
+ def iterable_arguments(self) -> List[Argument]:
19
20
  return self.ast
20
21
 
21
22
  def resolve(
@@ -40,6 +41,26 @@ class SyntaxTree(VariableDependency):
40
41
  # Otherwise, to concat multiple resolved outputs, we must concat as strings
41
42
  return String("".join([str(res) for res in resolved]))
42
43
 
44
+ def partial_resolve(
45
+ self,
46
+ resolved_variables: Dict[Variable, Resolvable],
47
+ unresolved_variables: Dict[Variable, Argument],
48
+ custom_functions: Dict[str, VariableDependency],
49
+ ) -> Argument | Resolvable:
50
+ # Ensure this does not get returned as a SyntaxTree since nesting them is not supported.
51
+ maybe_resolvable_values, _ = VariableDependency.try_partial_resolve(
52
+ args=self.ast,
53
+ resolved_variables=resolved_variables,
54
+ unresolved_variables=unresolved_variables,
55
+ custom_functions=custom_functions,
56
+ )
57
+
58
+ # Mimic the above resolve behavior
59
+ if len(maybe_resolvable_values) > 1:
60
+ return BuiltInFunction(name="concat", args=maybe_resolvable_values)
61
+
62
+ return maybe_resolvable_values[0]
63
+
43
64
  @property
44
65
  def maybe_resolvable(self) -> Optional[Resolvable]:
45
66
  """
@@ -76,3 +97,11 @@ class ResolvedSyntaxTree(SyntaxTree):
76
97
  @property
77
98
  def maybe_resolvable(self) -> Optional[Resolvable]:
78
99
  return self.ast[0]
100
+
101
+ def partial_resolve(
102
+ self,
103
+ resolved_variables: Dict[Variable, Resolvable],
104
+ unresolved_variables: Dict[Variable, Argument],
105
+ custom_functions: Dict[str, VariableDependency],
106
+ ) -> Argument | Resolvable:
107
+ return self.ast[0]
@@ -5,6 +5,7 @@ from typing import Dict
5
5
  from typing import Iterable
6
6
  from typing import List
7
7
  from typing import Set
8
+ from typing import Tuple
8
9
  from typing import Type
9
10
  from typing import TypeVar
10
11
  from typing import final
@@ -27,7 +28,7 @@ TypeT = TypeVar("TypeT")
27
28
  class VariableDependency(ABC):
28
29
  @property
29
30
  @abstractmethod
30
- def _iterable_arguments(self) -> List[Argument]:
31
+ def iterable_arguments(self) -> List[Argument]:
31
32
  """
32
33
  Returns
33
34
  -------
@@ -38,7 +39,7 @@ class VariableDependency(ABC):
38
39
  self, ttype: Type[TypeT], subclass: bool = False, instance: bool = True
39
40
  ) -> List[TypeT]:
40
41
  output: List[TypeT] = []
41
- for arg in self._iterable_arguments:
42
+ for arg in self.iterable_arguments:
42
43
  if subclass and issubclass(type(arg), ttype):
43
44
  output.append(arg)
44
45
  elif instance and isinstance(arg, ttype):
@@ -104,7 +105,7 @@ class VariableDependency(ABC):
104
105
  All CustomFunctions that this depends on.
105
106
  """
106
107
  output: Set[ParsedCustomFunction] = set()
107
- for arg in self._iterable_arguments:
108
+ for arg in self.iterable_arguments:
108
109
  if isinstance(arg, NamedCustomFunction):
109
110
  if not isinstance(arg, FunctionType):
110
111
  # A NamedCustomFunction should also always be a FunctionType
@@ -138,6 +139,28 @@ class VariableDependency(ABC):
138
139
  Resolved value
139
140
  """
140
141
 
142
+ @abstractmethod
143
+ def partial_resolve(
144
+ self,
145
+ resolved_variables: Dict[Variable, Resolvable],
146
+ unresolved_variables: Dict[Variable, Argument],
147
+ custom_functions: Dict[str, "VariableDependency"],
148
+ ) -> Argument | Resolvable:
149
+ """
150
+ Parameters
151
+ ----------
152
+ resolved_variables
153
+ Lookup of variables that have been resolved
154
+ unresolved_variables
155
+ Lookup of variables that have not been resolved
156
+ custom_functions
157
+ Lookup of any custom functions that have been parsed
158
+
159
+ Returns
160
+ -------
161
+ Either a fully resolved value or partially resolved value of the same type.
162
+ """
163
+
141
164
  @classmethod
142
165
  def _resolve_argument_type(
143
166
  cls,
@@ -222,3 +245,43 @@ class VariableDependency(ABC):
222
245
  ):
223
246
  return True
224
247
  return len(self.variables.intersection(variables)) > 0
248
+
249
+ @classmethod
250
+ def try_partial_resolve(
251
+ cls,
252
+ args: Iterable[Argument],
253
+ resolved_variables: Dict[Variable, Resolvable],
254
+ unresolved_variables: Dict[Variable, Argument],
255
+ custom_functions: Dict[str, "VariableDependency"],
256
+ ) -> Tuple[List[Argument], bool]:
257
+ """
258
+ Attempts to resolve a list of arguments. Returns a tuple of them post partially resolved,
259
+ and a boolean indicating whether all of them are fully resolved.
260
+ """
261
+ maybe_resolvable_args: List[Argument] = []
262
+ is_resolvable = True
263
+ for arg in args:
264
+ maybe_resolvable_args.append(arg)
265
+
266
+ if isinstance(arg, Lambda) and arg.value in custom_functions:
267
+ if not custom_functions[arg.value].is_subset_of(
268
+ variables=resolved_variables,
269
+ custom_function_definitions=custom_functions,
270
+ ):
271
+ is_resolvable = False
272
+ elif isinstance(arg, VariableDependency):
273
+ maybe_resolvable_args[-1] = arg.partial_resolve(
274
+ resolved_variables=resolved_variables,
275
+ unresolved_variables=unresolved_variables,
276
+ custom_functions=custom_functions,
277
+ )
278
+
279
+ if not isinstance(maybe_resolvable_args[-1], Resolvable):
280
+ is_resolvable = False
281
+ elif isinstance(arg, Variable):
282
+ if arg not in resolved_variables:
283
+ is_resolvable = False
284
+ if arg in unresolved_variables:
285
+ maybe_resolvable_args[-1] = unresolved_variables[arg]
286
+
287
+ return maybe_resolvable_args, is_resolvable
ytdl_sub/utils/script.py CHANGED
@@ -2,11 +2,14 @@ import json
2
2
  import re
3
3
  from typing import Any
4
4
  from typing import Dict
5
+ from typing import Optional
5
6
 
6
7
  from ytdl_sub.script.parser import parse
8
+ from ytdl_sub.script.types.array import Array
7
9
  from ytdl_sub.script.types.array import UnresolvedArray
8
10
  from ytdl_sub.script.types.function import BuiltInFunction
9
11
  from ytdl_sub.script.types.function import Function
12
+ from ytdl_sub.script.types.map import Map
10
13
  from ytdl_sub.script.types.map import UnresolvedMap
11
14
  from ytdl_sub.script.types.resolvable import Argument
12
15
  from ytdl_sub.script.types.resolvable import Boolean
@@ -137,9 +140,9 @@ class ScriptUtils:
137
140
  out = f"%bool({arg.native})"
138
141
  elif isinstance(arg, Float):
139
142
  out = f"%float({arg.native})"
140
- elif isinstance(arg, UnresolvedArray):
143
+ elif isinstance(arg, (Array, UnresolvedArray)):
141
144
  out = f"[ {', '.join(cls._to_script_code(val) for val in arg.value)} ]"
142
- elif isinstance(arg, UnresolvedMap):
145
+ elif isinstance(arg, (Map, UnresolvedMap)):
143
146
  kv_list = (
144
147
  f"{cls._to_script_code(key)}: {cls._to_script_code(val)}"
145
148
  for key, val in arg.value.items()
@@ -155,11 +158,43 @@ class ScriptUtils:
155
158
  raise UNREACHABLE
156
159
  return f"{{ {out} }}" if top_level else out
157
160
 
161
+ @classmethod
162
+ def _is_top_level_string(cls, tree: SyntaxTree) -> Optional[str]:
163
+ if not (
164
+ len(tree.ast) == 1
165
+ and isinstance(tree.ast[0], BuiltInFunction)
166
+ and tree.ast[0].name == "concat"
167
+ ):
168
+ return None
169
+
170
+ output = ""
171
+ for arg in tree.ast[0].args:
172
+ if isinstance(arg, BuiltInFunction) and arg.name == "string" and len(arg.args) == 1:
173
+ output += cls._to_script_code(arg.args[0], top_level=True)
174
+ else:
175
+ output += cls._to_script_code(arg, top_level=True)
176
+
177
+ return output
178
+
179
+ @classmethod
180
+ def _syntax_tree_to_native_script(cls, tree: SyntaxTree) -> str:
181
+
182
+ if (output := cls._is_top_level_string(tree)) is not None:
183
+ return output
184
+
185
+ output = ""
186
+ for arg in tree.ast:
187
+ output += cls._to_script_code(arg, top_level=True)
188
+ return output
189
+
158
190
  @classmethod
159
191
  def to_native_script(cls, value: Any) -> str:
160
192
  """
161
193
  Converts any JSON-compatible value into equivalent script syntax
162
194
  """
195
+ if isinstance(value, SyntaxTree):
196
+ return cls._syntax_tree_to_native_script(value)
197
+
163
198
  return cls._to_script_code(cls._to_script_argument(value), top_level=True)
164
199
 
165
200
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ytdl-sub
3
- Version: 2026.1.6
3
+ Version: 2026.1.13
4
4
  Summary: Automate downloading metadata generation with YoutubeDL
5
5
  Author: Jesse Bannon
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -703,7 +703,7 @@ Requires-Dist: black==24.10.0; extra == "lint"
703
703
  Requires-Dist: isort==7.0.0; extra == "lint"
704
704
  Requires-Dist: pylint==4.0.1; extra == "lint"
705
705
  Provides-Extra: docs
706
- Requires-Dist: sphinx<9,>=7; extra == "docs"
706
+ Requires-Dist: sphinx<10,>=7; extra == "docs"
707
707
  Requires-Dist: sphinx-rtd-theme<4,>=2; extra == "docs"
708
708
  Requires-Dist: sphinx-book-theme~=1.0; extra == "docs"
709
709
  Requires-Dist: sphinx-copybutton~=0.5; extra == "docs"
@@ -1,4 +1,4 @@
1
- ytdl_sub/__init__.py,sha256=nLml9KZHf1Nc577PKf16c0ZGjsSoLjrMPVHCbI69wBw,73
1
+ ytdl_sub/__init__.py,sha256=gB8fvSniQWtSTiQbvgSB9n_Ing5ceOHUg-DjaWhecp0,73
2
2
  ytdl_sub/main.py,sha256=4Rf9wXxSKW7IPnWqG5YtTZ814PjP1n9WtoFDivaainE,1004
3
3
  ytdl_sub/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  ytdl_sub/cli/entrypoint.py,sha256=XXjUH4HiOP_BB2ZA_bNcyt5-o6YLAdZmj0EP3xtOtD8,9496
@@ -40,7 +40,7 @@ ytdl_sub/entries/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
40
40
  ytdl_sub/entries/script/custom_functions.py,sha256=TyfiZilUKB424NXjDCwgOmYBMyqji_QIyk0TTjPC_zU,6963
41
41
  ytdl_sub/entries/script/function_scripts.py,sha256=iPIgTpIzXv5PRnfG8gi42zP6YKUIGPtN9KuMpjHWRTM,719
42
42
  ytdl_sub/entries/script/variable_definitions.py,sha256=dz4znpZ4op4ALtnYw2vy7ePW6z76A6TI994BFlBaOmo,43561
43
- ytdl_sub/entries/script/variable_types.py,sha256=6urKgOIOKYjScM5yZu-gF8sYi1HaVrTwaYJkJJg9hi8,9826
43
+ ytdl_sub/entries/script/variable_types.py,sha256=SMPm52pYbw-c9uFX65pzpIYCecVd0oO0w1htmV6oTTM,9634
44
44
  ytdl_sub/entries/variables/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  ytdl_sub/entries/variables/override_variables.py,sha256=LjWtora5FT9hSka3COP6RySBdpQ39ogiwbJgE-Z_X5Y,5923
46
46
  ytdl_sub/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -93,7 +93,7 @@ ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml,sha256=0OgIOzxSPNVqwcZnL6
93
93
  ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml,sha256=MRAxKnh_MvQFJTisRYGsbsUlDmHNLMuV5tlK6iczmqs,49826
94
94
  ytdl_sub/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  ytdl_sub/script/parser.py,sha256=LUvmUIxg346njE8-uDx06nOW6ZDX5QS6aUtp4lsgwpk,22115
96
- ytdl_sub/script/script.py,sha256=ufW-Zr0amTh0vkdW63oOca6MndojgaTaXxpOFk25U48,27688
96
+ ytdl_sub/script/script.py,sha256=r5QJOt0Dw1GE9qy8hHr5-bjr2HWs1rv9g-dsHrSBZrc,29998
97
97
  ytdl_sub/script/script_output.py,sha256=5SIamnI-1D3xMA0qQzjf9xrIy8j6BVhGCKrl_Q1d2M8,1381
98
98
  ytdl_sub/script/functions/__init__.py,sha256=rzl6O5G0IEqFYHQECIM9bRvcuQj-ytC_p-Xl2TTa6j0,2932
99
99
  ytdl_sub/script/functions/array_functions.py,sha256=yg9rcZP67-aVzhu4oZZzUu3-AHiHltbQDWEs8GW1DJ4,7193
@@ -108,13 +108,13 @@ ytdl_sub/script/functions/print_functions.py,sha256=s-EhqdUvDcH3zIkt95LIDGxvlACY
108
108
  ytdl_sub/script/functions/regex_functions.py,sha256=d6omjhD9FjkP0BVjUaMsJfXVt-reh-MI52cUhXdL4S4,6744
109
109
  ytdl_sub/script/functions/string_functions.py,sha256=rZbOuP2V9FvoKzMG94R7vsvj-GxHK_hwgVDMa_Yeftw,6095
110
110
  ytdl_sub/script/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
- ytdl_sub/script/types/array.py,sha256=1yEJEWtwS6T4W8KZN2IdNAGpi3SUa1qqzCXQfjo-P2M,1711
112
- ytdl_sub/script/types/function.py,sha256=rnKb5L8frQ6sG95lMFj_AS6qQw7tt88z26tyWrCWPhY,12341
113
- ytdl_sub/script/types/map.py,sha256=Y3zlm__IHcnycGFv0xbU_P7wgiQgtgddatPLQypjCvU,2254
111
+ ytdl_sub/script/types/array.py,sha256=2NQhAzAE5aRtlKh7elSFwDgN6-cQLYK1rteLlQE2GYI,2475
112
+ ytdl_sub/script/types/function.py,sha256=Yq6CL7eZEvJNW4agERwjB8Bmaw-GZFhVsZPn81ai_48,19433
113
+ ytdl_sub/script/types/map.py,sha256=Hw053W-2kxqqW0ece2QsBu_xs7HmsKlNkZwYoBiYlc0,3405
114
114
  ytdl_sub/script/types/resolvable.py,sha256=YeMEhPTRTDSr5AVmK4NRpUbqxh1YQT6a2dGUIPKoFkI,5482
115
- ytdl_sub/script/types/syntax_tree.py,sha256=4xWluTMPJqA6yYuQ4XCcncSFt5cDZ9ZIT2AIOOH4bT0,2336
115
+ ytdl_sub/script/types/syntax_tree.py,sha256=bkZqKLOj_yKUFBq8uLQ2i4Rcre01aD1_A-yOrHQt_9w,3479
116
116
  ytdl_sub/script/types/variable.py,sha256=aVJ3ocUr3WpDoolOq6y3NV71b3EQQYPAGrIT0FtIqc4,813
117
- ytdl_sub/script/types/variable_dependency.py,sha256=w0NrgSoom9joLaLMWHbma0zekmljYVXdhYW-k_g1n0I,7419
117
+ ytdl_sub/script/types/variable_dependency.py,sha256=ZKDDY9AeYhH_kVJgY-Td-cmt3USDkmMEBLpcn0-8pLs,9861
118
118
  ytdl_sub/script/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  ytdl_sub/script/utils/exception_formatters.py,sha256=hZDX2w90vU4lY2xiEM-qxQofQz2dmqPwhkSeWaJRsQ4,4645
120
120
  ytdl_sub/script/utils/exceptions.py,sha256=cag5ZLM6as1w-RsrOwO-oe4YFpwlFu_8U0QNGv_jQ68,2774
@@ -138,7 +138,7 @@ ytdl_sub/utils/file_lock.py,sha256=VlDl8QjBEUAHDBHsfp7Q0tR1Me0fFYELqzMtlyZ0MO4,2
138
138
  ytdl_sub/utils/file_path.py,sha256=7RWc4fGj7HH6srzGJ5ph1l5euJfWpIX-2cyC-03YXEs,2539
139
139
  ytdl_sub/utils/logger.py,sha256=g_23ddk1WpQ3-_MOHz-Rlz7xCydK66BbtqkualX1zAQ,8593
140
140
  ytdl_sub/utils/retry.py,sha256=eyhtrfmzwph4uGf4Bwk0UThEIGk4cobwLLuWowGHyK4,1313
141
- ytdl_sub/utils/script.py,sha256=M0EagR15LNkP0lg0NLAvGkBXLRfprCofj8SLcaLz0_g,6277
141
+ ytdl_sub/utils/script.py,sha256=g-Jrfm4WHOhll9QYVx3vuOFJmiQnG2dvbdCbf80kbTI,7439
142
142
  ytdl_sub/utils/scriptable.py,sha256=mSMLVOnBzTAQLZXyBozx2spLwNi6llaT_c2zdS2l1rk,3365
143
143
  ytdl_sub/utils/subtitles.py,sha256=0ICFw7C9H9BIZvsZf0YhD9ygwR68XCdOQWerspR2g3I,85
144
144
  ytdl_sub/utils/system.py,sha256=I9dH46ZhRRpaO0pEPfeOKp968Ub3SAdyZASVavRHrvc,58
@@ -158,9 +158,9 @@ ytdl_sub/validators/string_select_validator.py,sha256=KFXNKWX2J80WGt08m5gVYphPMH
158
158
  ytdl_sub/validators/validators.py,sha256=JC3-c9fSrozFADUY5jqZEhXpM2q3sfserlooQxT2DK8,9133
159
159
  ytdl_sub/ytdl_additions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
160
  ytdl_sub/ytdl_additions/enhanced_download_archive.py,sha256=Lsc0wjHdx9d8dYJCskZYAUGDAQ_QzQ-_xbQlyrBSzfk,24884
161
- ytdl_sub-2026.1.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
- ytdl_sub-2026.1.6.dist-info/METADATA,sha256=gVJGeZNSYZ_Cq1wnwVdwkQkJsnVAnYNM-scbVs1zttw,51418
163
- ytdl_sub-2026.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
164
- ytdl_sub-2026.1.6.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
- ytdl_sub-2026.1.6.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
- ytdl_sub-2026.1.6.dist-info/RECORD,,
161
+ ytdl_sub-2026.1.13.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
162
+ ytdl_sub-2026.1.13.dist-info/METADATA,sha256=5_cVGotelpxrRCV6DMgOUjKdFQvd71CxxjkMm9jUtkE,51420
163
+ ytdl_sub-2026.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
164
+ ytdl_sub-2026.1.13.dist-info/entry_points.txt,sha256=K3T5235NlAI-WLmHCg5tzLZHqc33OLN5IY5fOGc9t10,48
165
+ ytdl_sub-2026.1.13.dist-info/top_level.txt,sha256=6z-JWazl6jXspC2DNyxOnGnEqYyGzVbgcBDoXfbkUhI,9
166
+ ytdl_sub-2026.1.13.dist-info/RECORD,,