diff --git a/src/ytdl_sub/entries/base_entry.py b/src/ytdl_sub/entries/base_entry.py index a2663381..c4243d63 100644 --- a/src/ytdl_sub/entries/base_entry.py +++ b/src/ytdl_sub/entries/base_entry.py @@ -1,3 +1,4 @@ +# pylint: disable=protected-access from abc import ABC from pathlib import Path from typing import Any @@ -35,7 +36,7 @@ class BaseEntry(ABC): self._kwargs = entry_dict @property - def uid(self: "BaseEntry") -> str: + def uid(self) -> str: """ Returns ------- @@ -45,7 +46,7 @@ class BaseEntry(ABC): return str(self._kwargs[v.uid.metadata_key]) @property - def download_archive_extractor(self: "BaseEntry") -> str: + def download_archive_extractor(self) -> str: """ The extractor name used in yt-dlp download archives """ @@ -59,14 +60,14 @@ class BaseEntry(ABC): ).lower() @property - def title(self: "BaseEntry") -> str: + def title(self) -> str: """ The title of the entry. If a title does not exist, returns its unique ID. """ return self._kwargs_get(v.title.metadata_key, self.uid) @property - def webpage_url(self: "BaseEntry") -> str: + def webpage_url(self) -> str: """ The url to the webpage. """ @@ -78,7 +79,7 @@ class BaseEntry(ABC): return "info.json" @property - def uploader_id(self: "BaseEntry") -> str: + def uploader_id(self) -> str: """ The uploader id if it exists, otherwise return the unique ID. """ diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index 029b98ed..b996c20b 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -1,3 +1,4 @@ +# pylint: disable=protected-access import copy import json import os @@ -75,6 +76,10 @@ class Entry(BaseEntry, Scriptable): def add_injected_variables( self, download_entry: "Entry", download_idx: int, upload_date_idx: int ) -> "Entry": + """ + Adds variables that get injected into the Entry script that aren't available at + metadata scrape time (only after the actual download). + """ self.add( { # Tracks number of entries downloaded @@ -211,7 +216,7 @@ class Entry(BaseEntry, Scriptable): return maybe_prior_variables @final - def to_dict(self) -> Dict[str, str]: + def to_dict(self) -> Dict[str, Any]: """ Returns ------- diff --git a/src/ytdl_sub/script/functions/__init__.py b/src/ytdl_sub/script/functions/__init__.py index a7ff8489..bda1216e 100644 --- a/src/ytdl_sub/script/functions/__init__.py +++ b/src/ytdl_sub/script/functions/__init__.py @@ -31,10 +31,25 @@ class Functions( @classmethod def is_built_in(cls, name: str) -> bool: + """ + Returns + ------- + True if the name exists as a built-in function or custom function. False otherwise. + """ return hasattr(cls, name) or hasattr(cls, f"{name}_") or name in cls._custom_functions @classmethod def get(cls, name: str) -> Callable[..., Resolvable]: + """ + Returns + ------- + The actual Python callable for the function of the given name. + + Raises + ------ + FunctionDoesNotExistRuntimeException + If the function does not exist. + """ if hasattr(cls, name): return getattr(cls, name) if hasattr(cls, f"{name}_"): @@ -46,6 +61,14 @@ class Functions( @classmethod def register_function(cls, function: Callable[..., Resolvable]) -> None: + """ + Adds a function to the suite of offered functions. + + Parameters + ---------- + function + A static function whose name will be used as the offered function name. + """ if cls.is_built_in(function.__name__): raise ValueError( f"Cannot register a function with name {function.__name__} " diff --git a/src/ytdl_sub/script/functions/array_functions.py b/src/ytdl_sub/script/functions/array_functions.py index 389d1d65..ffff0da7 100644 --- a/src/ytdl_sub/script/functions/array_functions.py +++ b/src/ytdl_sub/script/functions/array_functions.py @@ -18,6 +18,14 @@ from ytdl_sub.script.utils.exceptions import FunctionRuntimeException class ArrayFunctions: @staticmethod def array(maybe_array: AnyArgument) -> Array: + """ + Tries to cast an unknown variable type to an Array. + + Raises + ------ + FunctionRuntimeException + If the input type is not actually an Array. + """ if not isinstance(maybe_array, Array): raise FunctionRuntimeException( f"Tried and failed to cast {maybe_array.type_name()} as an Array" @@ -26,6 +34,9 @@ class ArrayFunctions: @staticmethod def array_size(array: Array) -> Integer: + """ + Returns the size of an Array. + """ return Integer(len(array.value)) @staticmethod diff --git a/src/ytdl_sub/script/functions/json_functions.py b/src/ytdl_sub/script/functions/json_functions.py index e0d4f4d2..915fedf1 100644 --- a/src/ytdl_sub/script/functions/json_functions.py +++ b/src/ytdl_sub/script/functions/json_functions.py @@ -33,5 +33,8 @@ def _from_json(out: Any) -> Resolvable: class JsonFunctions: @staticmethod - def from_json(argument: AnyArgument) -> AnyArgument: + def from_json(argument: String) -> AnyArgument: + """ + Converts a JSON string into an actual type. + """ return _from_json(json.loads(argument.value)) diff --git a/src/ytdl_sub/script/functions/map_functions.py b/src/ytdl_sub/script/functions/map_functions.py index 1aa5609f..45ca13f1 100644 --- a/src/ytdl_sub/script/functions/map_functions.py +++ b/src/ytdl_sub/script/functions/map_functions.py @@ -17,6 +17,14 @@ from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException class MapFunctions: @staticmethod def map(maybe_mapping: AnyArgument) -> Map: + """ + Tries to cast an unknown variable type to a Map. + + Raises + ------ + FunctionRuntimeException + If the input type is not actually a Map. + """ if not isinstance(maybe_mapping, Map): raise FunctionRuntimeException( f"Tried and failed to cast {maybe_mapping.type_name()} as a Map" @@ -25,6 +33,9 @@ class MapFunctions: @staticmethod def map_size(mapping: Map) -> Integer: + """ + Returns the size of a Map. + """ return Integer(len(mapping.value)) @staticmethod diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index 533a8224..33ca2e1b 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -144,11 +144,9 @@ class Script: if lambda_type := spec.is_lambda_like: lambda_function_names = set( - [ - lamb.value - for lamb in SyntaxTree(function.args).lambdas - if isinstance(lamb, Lambda) - ] + lamb.value + for lamb in SyntaxTree(function.args).lambdas + if isinstance(lamb, Lambda) ) # Only case len(lambda_function_names) > 1 is when used in if-statements @@ -342,11 +340,46 @@ class Script: unresolvable: Optional[Set[str]] = None, update: bool = False, ) -> ScriptOutput: + """ + Resolves the script + + Parameters + ---------- + 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 internal values with the resolved variables instead of + their original definition. This helps avoid re-evaluated the same variables repeatedly. + + Returns + ------- + ScriptOutput + Containing all resolved variables. + """ return self._resolve( pre_resolved=resolved, unresolvable=unresolvable, update=update, output_filter=None ) def add(self, variables: Dict[str, str], unresolvable: Optional[Set[str]] = None) -> "Script": + """ + Adds parses and adds new variables to the script. + + Parameters + ---------- + variables + Mapping containing variable name to definition. + unresolvable + Optional. Set of unresolved variables that the new variables may contain, but the + script does not (yet). + + Returns + ------- + Script + self + """ added_variables_to_validate: Set[str] = set() for variable_name, variable_definition in variables.items(): self._variables[variable_name] = parse( diff --git a/src/ytdl_sub/script/script_output.py b/src/ytdl_sub/script/script_output.py index 68a240d2..d8275e6e 100644 --- a/src/ytdl_sub/script/script_output.py +++ b/src/ytdl_sub/script/script_output.py @@ -10,13 +10,25 @@ from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved class ScriptOutput: output: Dict[str, Resolvable] - def as_resolvable(self) -> Dict[str, Resolvable]: - return self.output - def as_native(self) -> Dict[str, Any]: + """ + Returns + ------- + The script output as native python types + """ return {name: out.native for name, out in self.output.items()} def get(self, name: str) -> Resolvable: + """ + Returns + ------- + The script output's variable as a resolvable type + + Raises + ------ + ScriptVariableNotResolved + The variable name requested did not resolve + """ if name not in self.output: raise ScriptVariableNotResolved( f"Tried to access resolved variable {name}, but it has not resolved" @@ -24,12 +36,17 @@ class ScriptOutput: return self.output[name] def get_native(self, name: str) -> Any: + """ + Returns + ------- + The script output's variable as native python type + """ return self.get(name).native def get_str(self, name: str) -> str: + """ + Returns + ------- + The script output's variable as a string + """ return str(self.get(name)) - - def get_int(self, name: str) -> int: - out = self.get_native(name) - assert isinstance(out, int) - return out diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index 9ebff025..50c57ff3 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -73,22 +73,36 @@ class ReturnableArgumentB(ValueArgument, NamedType, ABC): @dataclass(frozen=True) class AnyArgument(ReturnableArgument, ReturnableArgumentA, ReturnableArgumentB, ABC): """ - Human-readable name for FutureResolvable + Human-readable name for Resolvable """ @dataclass(frozen=True) class Resolvable(AnyArgument, ABC): + """ + A type that is resolved into a native Python type (and have no dependencies to other types). + """ + def __str__(self) -> str: return str(self.value) @property def native(self) -> Any: + """ + Returns + ------- + The resolvable in its native form + """ return self.value @dataclass(frozen=True) class FutureResolvable(AnyArgument, ABC): + """ + Used when parsing, it is an unresolved type that will eventually resolve to a known type + (i.e. Maps, Arrays) + """ + @abstractmethod def future_resolvable_type(self) -> Type[Resolvable]: pass @@ -96,27 +110,47 @@ class FutureResolvable(AnyArgument, ABC): @dataclass(frozen=True) class Hashable(Resolvable, ABC): + """ + Resolvable type that can be used as hashes (i.e. in Maps) + """ + pass @dataclass(frozen=True) class NonHashable(NamedType, ABC): + """ + Type that is known to never be hashable. + """ + pass @dataclass(frozen=True) class ResolvableToJson(Resolvable, ABC): + """ + Types whose string values should be resolved to JSON (i.e. Maps, Arrays) + """ + def __str__(self): return json.dumps(self.native) @dataclass(frozen=True) class ResolvableT(Hashable, ABC, Generic[T]): + """ + Resolvable types that resolve to the generic T + """ + value: T @dataclass(frozen=True) -class Numeric(ResolvableT[NumericT], Hashable, ABC, Generic[NumericT]): +class Numeric(ResolvableT[NumericT], ABC, Generic[NumericT]): + """ + Resolvable numeric types (int/float) + """ + pass @@ -131,12 +165,12 @@ class Float(Numeric[float], Argument): @dataclass(frozen=True) -class Boolean(ResolvableT[bool], Hashable, Argument): +class Boolean(ResolvableT[bool], Argument): pass @dataclass(frozen=True) -class String(ResolvableT[str], Hashable, Argument): +class String(ResolvableT[str], Argument): pass @@ -199,4 +233,8 @@ class LambdaThree(Lambda): @dataclass(frozen=True) class LambdaReduce(LambdaTwo): + """ + Type-hinting for functions that apply a reduce-operation using a lambda (two arguments) + """ + pass diff --git a/src/ytdl_sub/script/types/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py index b6092c34..d058852c 100644 --- a/src/ytdl_sub/script/types/syntax_tree.py +++ b/src/ytdl_sub/script/types/syntax_tree.py @@ -42,6 +42,11 @@ class SyntaxTree(VariableDependency): @property def maybe_resolvable(self) -> Optional[Resolvable]: + """ + Returns + ------- + A resolvable if the AST contains a single type that is resolvable. None otherwise. + """ if len(self.ast) == 1 and isinstance(self.ast[0], Resolvable): return self.ast[0] return None diff --git a/src/ytdl_sub/script/utils/type_checking.py b/src/ytdl_sub/script/utils/type_checking.py index 016239f0..2c2ccf7c 100644 --- a/src/ytdl_sub/script/utils/type_checking.py +++ b/src/ytdl_sub/script/utils/type_checking.py @@ -106,6 +106,11 @@ def is_type_compatible( arg: NamedType, expected_arg_type: Type[Resolvable | Optional[Resolvable]], ) -> bool: + """ + Returns + ------- + True if arg is compatible with expected_arg_type. False otherwise. + """ arg_type: Type[NamedType] = arg.__class__ if isinstance(arg, BuiltInFunctionType): arg_type = arg.output_type() # built-in function