test in place

This commit is contained in:
Jesse Bannon 2026-03-13 10:38:45 -07:00
parent 96bdd30a7e
commit cc170a1c13
5 changed files with 63 additions and 253 deletions

View file

@ -15,8 +15,9 @@ from ytdl_sub.cli.output_transaction_log import (
)
from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, InspectArguments, parser
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.validators.variable_validation import ResolutionLevel
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled, ValidationException
from ytdl_sub.utils.file_handler import FileHandler
@ -215,14 +216,6 @@ def _parse_inspect_mocks(mocks: Optional[List[str]]) -> Dict[str, str]:
return out
def _parse_inspect_level(inspect_level: str) -> int:
for val, name in InspectArguments.LevelChoices.items():
if inspect_level == val or inspect_level == name:
return int(val)
raise ValueError("should not reach here")
def _inspect(
config: ConfigFile,
subscription_paths: List[str],
@ -288,7 +281,7 @@ def main() -> List[Subscription]:
subscription_paths=args.subscription_paths,
subscription_matches=args.match,
subscription_override_dict=subscription_override_dict,
inspection_level=_parse_inspect_level(args.inspection_level),
inspection_level=ResolutionLevel.level_number(args.inspection_level),
mocks=_parse_inspect_mocks(args.mock),
)
return []

View file

@ -266,12 +266,12 @@ inspect_parser.add_argument(
metavar=",".join(str(i) for i in range(4)),
type=str,
help="""level of inspection to perform:
0 - original present the subscription as-is (default)
0 - original present the subscription as-is
1 - fill fill in defined values
2 - resolve resolve all variables
2 - resolve resolve all possible variables (default)
3 - internal resolve all variables to their internal representation
""",
default="0",
default="resolve",
choices=list(InspectArguments.LevelChoices.keys())
+ list(InspectArguments.LevelChoices.values()),
dest="inspection_level",

View file

@ -35,6 +35,21 @@ class ResolutionLevel:
return "internal"
raise ValueError("Invalid resolution level")
@classmethod
def level_number(cls, resolution_arg: str) -> int:
"""
Numeric resolution level
"""
if resolution_arg in ("0", "original"):
return 0
if resolution_arg in ("1", "fill"):
return 1
if resolution_arg in ("2", "resolve"):
return 2
if resolution_arg in ("3", "internal"):
return 3
raise ValueError("Invalid resolution level")
@classmethod
def all(cls) -> List[int]:
"""
@ -81,8 +96,6 @@ class VariableValidation:
unresolvable=self.unresolved_variables,
)
self.script = self.script.resolve_partial(
unresolvable=self.unresolved_variables,
output_filter=self._get_resolve_partial_filter(),
@ -132,9 +145,7 @@ class VariableValidation:
value = self.script.definition_of(name)
if name in self.script.function_names:
# Keep custom functions as-is
output[name] = self.overrides.dict_with_format_strings[
name
]
output[name] = self.overrides.dict_with_format_strings[name]
elif resolved := value.maybe_resolvable:
output[name] = resolved.native
else:

View file

@ -3,10 +3,16 @@ from pathlib import Path
import pytest
import yaml
from conftest import mock_run_from_cli
from unit.config.test_subscription_resolution import compare_resolved_yaml
from ytdl_sub.config.validators.variable_validation import ResolutionLevel
class TestInspect:
@pytest.mark.parametrize("config_provided", [True, False])
@pytest.mark.parametrize(
"inspect_level", ["0", "original", "1", "fill", "2", "resolve", "3", "internal"]
)
def test_inspect_command(
self,
capsys,
@ -14,238 +20,21 @@ class TestInspect:
tv_show_subscriptions_path: Path,
output_directory: str,
config_provided: bool,
inspect_level: str,
):
# Shares same test fixture as `test_subscription_resolution.py`
args = f"--config {default_config_path} " if config_provided else ""
args += f"inspect {tv_show_subscriptions_path} --match 'Jake Trains'"
args += f"inspect {tv_show_subscriptions_path} --match 'NOVA PBS' "
args += f"--level {inspect_level}"
subscriptions = mock_run_from_cli(args=args)
assert len(subscriptions) == 0
captured_as_dict = yaml.safe_load(capsys.readouterr().out)
assert captured_as_dict == {
"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/@JakeTrains",
"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-Jake Trains-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}/Jake Trains",
"preserve_mtime": False,
"thumbnail_name": "{thumbnail_file_name}",
},
"overrides": {
"%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }",
"%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }",
"%is_bilateral_url": '{ %contains( $0, "youtube.com/playlist" ) }',
"%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }",
"%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }",
"assert_not_collection": '{ %assert( %and( %not( %bool( s01_url ) ), %not( %bool( s01_name ) ) ), "Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it expects `url`. Perhaps you meant to use the `TV Show Collection` preset?" ) }',
"avatar_uncropped_thumbnail_file_name": "{ tv_show_poster_file_name }",
"banner_uncropped_thumbnail_file_name": "{ tv_show_fanart_file_name }",
"enable_bilateral_scraping": "{ %bool(True) }",
"enable_resolution_assert": "{ %bool(True) }",
"enable_throttle_protection": "{ %bool(True) }",
"episode_content_rating": "{ tv_show_content_rating }",
"episode_date_standardized": '{ %if( %contains( tv_show_by_date_season_ordering, "release" ), release_date_standardized, upload_date_standardized ) }',
"episode_file_name": '{ %concat( %string( "s" ), %string( season_number_padded ), %string( ".e" ), %string( episode_number_padded ), %string( " - " ), %string( file_title ) ) }',
"episode_file_path": '{ %concat( %string( season_directory_name_sanitized ), %string( "/" ), %string( episode_file_name_sanitized ) ) }',
"episode_number": "{ %array_at( episode_number_and_padded_, 0 ) }",
"episode_number_and_padded_": '{ %elif( %episode_ordering_( "upload-day" ), [ %concat( upload_day, upload_date_index_padded ), 4 ], %episode_ordering_( "upload-month-day" ), [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ], %episode_ordering_( "upload-month-day-reversed" ), [ %concat( upload_day_of_year_reversed, upload_date_index_reversed_padded ), 5 ], %episode_ordering_( "release-day" ), [ %concat( release_day, upload_date_index_padded ), 4 ], %episode_ordering_( "release-month-day" ), [ %concat( release_month, release_day_padded, upload_date_index_padded ), 6 ], %episode_ordering_( "release-month-day-reversed" ), [ %concat( release_day_of_year_reversed, upload_date_index_reversed_padded ), 5 ], %episode_ordering_( "download-index" ), [ download_index, 6 ], %throw( \'tv_show_by_date_episode_ordering must be one of the following: "upload-day", "upload-month-day", "upload-month-day-reversed", "release-day", "release-month-day", "release-month-day-reversed", "download-index"\' ) ) }',
"episode_number_padded": "{ %pad_zero( %int( episode_number ), %int( %array_at( episode_number_and_padded_, 1 ) ) ) }",
"episode_plot": '{ %concat( %string( webpage_url ), %string( "\n\n" ), %string( description ) ) }',
"episode_title": '{ %concat( %string( episode_date_standardized ), %string( " - " ), %string( title ) ) }',
"episode_year": "{ %slice( episode_date_standardized, 0, 4 ) }",
"file_title": "{ title_sanitized_plex }",
"file_uid": "{ uid_sanitized_plex }",
"include_sibling_metadata": "{ %bool(False) }",
"modified_webpage_url": "{ webpage_url }",
"only_recent_date_range": "2months",
"only_recent_max_files": "{ %int(30) }",
"resolution_assert": '{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( "Entry ", title, " downloaded at a low resolution (", resolution_readable, "), you\'ve probably been throttled. ", "Stopping further downloads, wait a few hours and try again. ", "Disable using the override variable `enable_resolution_assert: False`." ) ), "false is no-op" ) }',
"resolution_assert_height_gte": "{ %int(361) }",
"resolution_assert_ignore_titles": "{ [ ] }",
"resolution_assert_is_ignored": '{ %print_if_true( %concat( title, " has a match in resolution_assert_ignore_titles, skipping resolution assert." ), %contains_any( title, resolution_assert_ignore_titles ) ) }',
"resolution_assert_print": '{ %print( %if( enable_resolution_assert, "Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`", "Resolution assert is disabled. Use at your own risk!" ), enable_resolution_assert, -1 ) }',
"resolution_readable": '{ %concat( %string( width ), %string( "x" ), %string( height ) ) }',
"s01_name": "",
"s01_url": "",
"season_directory_name": '{ %concat( %string( "Season " ), %string( season_number_padded ) ) }',
"season_number": '{ %elif( %season_ordering_( "upload-year" ), upload_year, %season_ordering_( "upload-year-month" ), %concat( upload_year, upload_month_padded ), %season_ordering_( "release-year" ), release_year, %season_ordering_( "release-year-month" ), %concat( release_year, release_month_padded ), %throw( \'tv_show_by_date_season_ordering must be one of the following: "upload-year", "upload-year-month", "release-year", "release-year-month"\' ) ) }',
"season_number_padded": "{ season_number }",
"season_poster_file_name": '{ %concat( %string( season_directory_name_sanitized ), %string( "/Season" ), %string( season_number_padded ), %string( ".jpg" ) ) }',
"subscription_array": "{ %from_json( '[\"https://www.youtube.com/@JakeTrains\"]' ) }",
"subscription_indent_1": "Kids",
"subscription_indent_2": "TV-Y",
"subscription_value": "https://www.youtube.com/@JakeTrains",
"subscription_value_1": "https://www.youtube.com/@JakeTrains",
"thumbnail_file_name": '{ %concat( %string( episode_file_path ), %string( "-thumb.jpg" ) ) }',
"tv_show_by_date_episode_ordering": "upload-month-day",
"tv_show_by_date_ordering_pair_validation_": '{ %assert_then( %or( %ordering_pair_eq( "upload-year", "upload-month-day" ), %ordering_pair_eq( "upload-year", "upload-month-day-reversed" ), %ordering_pair_eq( "upload-year", "download-index" ), %ordering_pair_eq( "upload-year-month", "upload-day" ), %ordering_pair_eq( "release-year", "release-month-day" ), %ordering_pair_eq( "release-year", "release-month-day-reversed" ), %ordering_pair_eq( "release-year", "download-index" ), %ordering_pair_eq( "release-year-month", "release-day" ) ), episode_number_and_padded_, "Detected incompatibility between tv_show_by_date_season_ordering and tv_show_by_date_episode_ordering. Ensure you are not using both upload and release date, and that the year/month/day are included in the combined season and episode." ) }',
"tv_show_by_date_season_ordering": "upload-year",
"tv_show_content_rating": "{ subscription_indent_2 }",
"tv_show_content_rating_default": "TV-14",
"tv_show_date_range_type": '{ %if( %contains( tv_show_by_date_season_ordering, "release" ), "release_date", "upload_date" ) }',
"tv_show_directory": output_directory,
"tv_show_fanart_file_name": "fanart.jpg",
"tv_show_genre": "{ subscription_indent_1 }",
"tv_show_genre_default": "ytdl-sub",
"tv_show_name": "{ subscription_name }",
"tv_show_poster_file_name": "poster.jpg",
"url": "{ subscription_value }",
"url10": "",
"url100": "",
"url11": "",
"url12": "",
"url13": "",
"url14": "",
"url15": "",
"url16": "",
"url17": "",
"url18": "",
"url19": "",
"url2": "",
"url20": "",
"url21": "",
"url22": "",
"url23": "",
"url24": "",
"url25": "",
"url26": "",
"url27": "",
"url28": "",
"url29": "",
"url3": "",
"url30": "",
"url31": "",
"url32": "",
"url33": "",
"url34": "",
"url35": "",
"url36": "",
"url37": "",
"url38": "",
"url39": "",
"url4": "",
"url40": "",
"url41": "",
"url42": "",
"url43": "",
"url44": "",
"url45": "",
"url46": "",
"url47": "",
"url48": "",
"url49": "",
"url5": "",
"url50": "",
"url51": "",
"url52": "",
"url53": "",
"url54": "",
"url55": "",
"url56": "",
"url57": "",
"url58": "",
"url59": "",
"url6": "",
"url60": "",
"url61": "",
"url62": "",
"url63": "",
"url64": "",
"url65": "",
"url66": "",
"url67": "",
"url68": "",
"url69": "",
"url7": "",
"url70": "",
"url71": "",
"url72": "",
"url73": "",
"url74": "",
"url75": "",
"url76": "",
"url77": "",
"url78": "",
"url79": "",
"url8": "",
"url80": "",
"url81": "",
"url82": "",
"url83": "",
"url84": "",
"url85": "",
"url86": "",
"url87": "",
"url88": "",
"url89": "",
"url9": "",
"url90": "",
"url91": "",
"url92": "",
"url93": "",
"url94": "",
"url95": "",
"url96": "",
"url97": "",
"url98": "",
"url99": "",
"urls": "{ subscription_array }",
},
"throttle_protection": {
"enable": True,
"sleep_per_download_s": {"max": "28.4", "min": "13.8"},
"sleep_per_request_s": {"max": "0.75", "min": "0.0"},
"sleep_per_subscription_s": {"max": "26.1", "min": "16.3"},
},
"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}",
},
}
out = yaml.safe_load(capsys.readouterr().out)
compare_resolved_yaml(
out=out,
output_directory=output_directory,
subscription_name="NOVA PBS",
preset_type="tv_show",
resolution_level=ResolutionLevel.level_number(resolution_arg=inspect_level),
)

View file

@ -1,4 +1,5 @@
from pathlib import Path
from typing import Dict
import pytest
import yaml
@ -10,12 +11,13 @@ from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.file_path import FilePathTruncater
def _ensure_resolved_yaml(
sub: Subscription, output_directory: str, preset_type: str, resolution_level: int
def compare_resolved_yaml(
out: Dict,
output_directory: str,
subscription_name: str,
preset_type: str,
resolution_level: int,
) -> None:
output_yaml = sub.resolved_yaml(resolution_level=resolution_level)
out = yaml.safe_load(output_yaml)
expected_out_filename = (
f"{preset_type}/inspect_sub_{ResolutionLevel.name_of(resolution_level)}.json"
)
@ -24,7 +26,7 @@ def _ensure_resolved_yaml(
if resolution_level > ResolutionLevel.ORIGINAL:
output_path = Path(output_directory)
if "tv_show_directory" in expected_out["overrides"]:
output_path = output_path / sub.name
output_path = output_path / subscription_name
expected_out["output_options"]["output_directory"] = FilePathTruncater.to_native_filepath(
str(output_path)
@ -40,6 +42,21 @@ def _ensure_resolved_yaml(
assert out == expected_out
def _ensure_resolved_yaml(
sub: Subscription, output_directory: str, preset_type: str, resolution_level: int
) -> None:
output_yaml = sub.resolved_yaml(resolution_level=resolution_level)
out = yaml.safe_load(output_yaml)
compare_resolved_yaml(
out=out,
output_directory=output_directory,
subscription_name=sub.name,
preset_type=preset_type,
resolution_level=resolution_level,
)
@pytest.mark.parametrize("resolution_level", ResolutionLevel.all())
class TestResolution:
def test_resolution_tv_show(