From feec3271f6cfc4560917f5c81cd19628c83c64d7 Mon Sep 17 00:00:00 2001 From: getzze Date: Thu, 17 Oct 2024 18:07:39 +0100 Subject: [PATCH 1/8] add dynamic options to click for providers and refiners. --- src/subliminal/cli.py | 111 +++++++++++++++++++++++++--------------- src/subliminal/utils.py | 53 +++++++++++++++++++ tests/test_utils.py | 85 +++++++++++++++++++++++++++++- 3 files changed, 208 insertions(+), 41 deletions(-) diff --git a/src/subliminal/cli.py b/src/subliminal/cli.py index ec419261..0fbc7c1c 100644 --- a/src/subliminal/cli.py +++ b/src/subliminal/cli.py @@ -40,10 +40,12 @@ ) from subliminal.core import ARCHIVE_EXTENSIONS, scan_name, search_external_subtitles from subliminal.extensions import get_default_providers, get_default_refiners -from subliminal.utils import merge_extend_and_ignore_unions +from subliminal.utils import get_parameters_from_signature, merge_extend_and_ignore_unions if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Mapping, Sequence + + from subliminal.utils import Parameter logger = logging.getLogger(__name__) @@ -197,6 +199,56 @@ def plural(quantity: int, name: str, *, bold: bool = True, **kwargs: Any) -> str refiners_config = OptionGroup('Refiners configuration') +def options_from_managers( + group_name: str, + options: Mapping[str, Sequence[Parameter]], + group: OptionGroup | None = None, +) -> Callable[[Callable], Callable]: + """Add click options dynamically from providers and refiners keyword arguments.""" + click_option = click.option if group is None else group.option + + def decorator(f: Callable) -> Callable: + for plugin_name, opt_params in options.items(): + for opt in reversed(opt_params): + name = opt['name'] + # CLI option has dots, variable has double-underscores to differentiate + # with simple underscore in provider name or keyword argument. + param_decls = ( + f'--{group_name}.{plugin_name}.{name}', + f'_{group_name}__{plugin_name}__{name}', + ) + attrs = { + 'default': opt['default'], + 'help': opt['desc'], + 'show_default': True, + } + click_option(*param_decls, **attrs)(f) + return f + + return decorator + + +# Options from providers +provider_options = { + name: get_parameters_from_signature(provider_manager[name].plugin) for name in provider_manager.names() +} + +refiner_options = { + name: [ + opt + for opt in get_parameters_from_signature(refiner_manager[name].plugin) + if opt['name'] not in ('video', 'kwargs', 'embedded_subtitles', 'providers', 'languages') + ] + for name in refiner_manager.names() +} + +# Decorator to add click options from providers +options_from_providers = options_from_managers('providers', provider_options, group=providers_config) + +# Decorator to add click options from refiners +options_from_refiners = options_from_managers('refiners', refiner_options, group=refiners_config) + + @click.group( context_settings={'max_content_width': 100}, epilog='Suggestions and bug reports are greatly appreciated: https://github.com/Diaoul/subliminal/', @@ -220,28 +272,8 @@ def plural(quantity: int, name: str, *, bold: bool = True, **kwargs: Any) -> str expose_value=True, help='Path to the cache directory.', ) -@providers_config.option( - '--addic7ed', - type=click.STRING, - nargs=2, - metavar='USERNAME PASSWORD', - help='Addic7ed configuration.', -) -@providers_config.option( - '--opensubtitles', - type=click.STRING, - nargs=2, - metavar='USERNAME PASSWORD', - help='OpenSubtitles configuration.', -) -@providers_config.option( - '--opensubtitlescom', - type=click.STRING, - nargs=2, - metavar='USERNAME PASSWORD', - help='OpenSubtitles.com configuration.', -) -@refiners_config.option('--omdb', type=click.STRING, nargs=1, metavar='APIKEY', help='OMDB API key.') +@options_from_providers +@options_from_refiners @click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.') @click.version_option(__version__) @click.pass_context @@ -249,10 +281,7 @@ def subliminal( ctx: click.Context, cache_dir: str, debug: bool, - addic7ed: tuple[str, str], - opensubtitles: tuple[str, str], - opensubtitlescom: tuple[str, str], - omdb: str, + **kwargs: Any, ) -> None: """Subtitles, faster than your thoughts.""" cache_dir = os.path.expanduser(cache_dir) @@ -281,21 +310,23 @@ def subliminal( logger.info(msg) ctx.obj['debug'] = debug + # provider configs provider_configs = ctx.obj['provider_configs'] - if addic7ed: - provider_configs['addic7ed'] = {'username': addic7ed[0], 'password': addic7ed[1]} - if opensubtitles: - provider_configs['opensubtitles'] = {'username': opensubtitles[0], 'password': opensubtitles[1]} - provider_configs['opensubtitlesvip'] = {'username': opensubtitles[0], 'password': opensubtitles[1]} - if opensubtitlescom: - provider_configs['opensubtitlescom'] = {'username': opensubtitlescom[0], 'password': opensubtitlescom[1]} - provider_configs['opensubtitlescomvip'] = {'username': opensubtitlescom[0], 'password': opensubtitlescom[1]} - # refiner configs refiner_configs = ctx.obj['refiner_configs'] - if omdb: - refiner_configs['omdb'] = {'apikey': omdb} + + for k, v in kwargs.items(): + try_split = k.split('__') + if len(try_split) != 3: + click.echo(f'Unknown option: {k}={v}') + continue + group, plugin, key = try_split + if group == '_providers': + provider_configs.setdefault(plugin, {})[key] = v + + elif group == '_refiners': + refiner_configs.setdefault(plugin, {})[key] = v @subliminal.command() @@ -478,7 +509,7 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None: show_default=True, help=f'Scan archives for videos (supported extensions: {", ".join(ARCHIVE_EXTENSIONS)}).', ) -@providers_config.option( +@click.option( '-n', '--name', type=click.STRING, diff --git a/src/subliminal/utils.py b/src/subliminal/utils.py index 915e9fd5..32806626 100644 --- a/src/subliminal/utils.py +++ b/src/subliminal/utils.py @@ -10,6 +10,7 @@ import socket from collections.abc import Iterable from datetime import datetime, timedelta, timezone +from inspect import signature from types import GeneratorType from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast, overload from xmlrpc.client import ProtocolError @@ -32,6 +33,14 @@ class ExtendedLists(Generic[S], TypedDict): extend: Sequence[S] ignore: Sequence[S] + class Parameter(TypedDict): + """Parameter of a function.""" + + name: str + default: Any + annotation: str | None + desc: str | None + T = TypeVar('T') R = TypeVar('R') @@ -353,3 +362,47 @@ def clip(value: float, minimum: float | None, maximum: float | None) -> float: if minimum is not None: value = max(value, minimum) return value + + +def split_doc_args(args: str | None) -> list[str]: + """Split the arguments of a docstring (in Sphinx docstyle).""" + if not args: + return [] + split_regex = re.compile(r'(?m):((param|type)\s|(return|yield|raise|rtype|ytype)s?:)') + split_indices = [m.start() for m in split_regex.finditer(args)] + if len(split_indices) == 0: + return [] + next_indices = [*split_indices[1:], None] + parts = [args[i:j].strip() for i, j in zip(split_indices, next_indices)] + return [p for p in parts if p.startswith(':param')] + + +def get_argument_doc(fun: Callable) -> dict[str, str]: + """Get documentation for the arguments of the function.""" + param_regex = re.compile( + r'^:param\s+(?P[\w\s\[\].,:`~!]+\s+|\([^\)]+\)\s+)?(?P\w+):\s+(?P[^:]*)$' + ) + + parts = split_doc_args(fun.__doc__) + + ret = {} + for p in parts: + m = param_regex.match(p) + if not m: + continue + _, name, desc = m.groups() + if name is None: + continue + ret[name] = ' '.join(desc.strip().split()) + + return ret + + +def get_parameters_from_signature(fun: Callable) -> list[Parameter]: + """Get keyword arguments with default and type.""" + sig = signature(fun) + doc = get_argument_doc(fun) + return [ + {'name': name, 'default': p.default, 'annotation': p.annotation, 'desc': doc.get(name)} + for name, p in sig.parameters.items() + ] diff --git a/tests/test_utils.py b/tests/test_utils.py index f9c71f70..7a8990aa 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable from xmlrpc.client import ProtocolError import pytest @@ -14,7 +14,9 @@ decorate_imdb_id, ensure_list, get_age, + get_argument_doc, get_extend_and_ignore_union, + get_parameters_from_signature, handle_exception, matches_extended_title, merge_extend_and_ignore_unions, @@ -22,15 +24,36 @@ sanitize, sanitize_id, sanitize_release_group, + split_doc_args, ) if TYPE_CHECKING: from pathlib import Path + from babelfish import Language + # Core test pytestmark = pytest.mark.core +@pytest.fixture() +def docstring() -> str: + """Generate a docstring with arguments.""" + return """Some class or function. + + This object has only one purpose: parsing. + + :param :class:`~babelfish.language.Language` language: the language of the subtitles. + :param str keyword: the query term. + :param (int | None) season: the season number. + :param episode: the episode number. + :type episode: int + :param (int | None) year: the video year and + on another line. + :return: something + """ + + def test_sanitize(): assert sanitize(None) is None assert sanitize("Marvel's Agents of S.H.I.E.L.D.") == 'marvels agents of s h i e l d' @@ -238,3 +261,63 @@ def test_merge_extend_and_ignore_unions( def test_clip(value: float, minimum: float | None, maximum: float | None, expected: float) -> None: out = clip(value, minimum, maximum) assert out == expected + + +def test_split_doc_args(docstring: str) -> None: + parts = split_doc_args(docstring) + assert len(parts) == 5 + assert all(p.startswith(':param ') for p in parts) + assert '\n' in parts[4] + + +@pytest.mark.parametrize('is_class', [False, True]) +def test_get_argument_doc(docstring: str, is_class: bool) -> None: + obj: Callable + if is_class: + + def obj(): + pass + + else: + + class obj: + pass + + obj.__doc__ = docstring + + d = get_argument_doc(obj) + assert d == { + 'language': 'the language of the subtitles.', + 'keyword': 'the query term.', + 'season': 'the season number.', + 'episode': 'the episode number.', + 'year': 'the video year and on another line.', + } + + +def test_get_parameters_from_signature(docstring: str) -> None: + def fun( + language: Language | None = None, + keyword: str = 'key', + season: int = 0, + episode: int | None = None, + year: str | None = None, + no_desc: bool = False, + ) -> None: + pass + + fun.__doc__ = docstring + + params = get_parameters_from_signature(fun) + + assert len(params) == 6 + + assert params[0]['name'] == 'language' + assert params[0]['default'] is None + assert params[0]['annotation'] == 'Language | None' + assert params[0]['desc'] == 'the language of the subtitles.' + + assert params[5]['name'] == 'no_desc' + assert params[5]['default'] is False + assert params[5]['annotation'] == 'bool' + assert params[5]['desc'] is None From 16e499c6ba6deeff8e0a1d2efa843f0821dab212 Mon Sep 17 00:00:00 2001 From: getzze Date: Thu, 17 Oct 2024 18:08:13 +0100 Subject: [PATCH 2/8] add documentation for arguments in refiners --- src/subliminal/refiners/hash.py | 4 ++++ src/subliminal/refiners/metadata.py | 1 + src/subliminal/refiners/omdb.py | 5 +++++ src/subliminal/refiners/tmdb.py | 5 +++++ src/subliminal/refiners/tvdb.py | 5 +++++ 5 files changed, 20 insertions(+) diff --git a/src/subliminal/refiners/hash.py b/src/subliminal/refiners/hash.py index 72b0a87e..4aedd165 100644 --- a/src/subliminal/refiners/hash.py +++ b/src/subliminal/refiners/hash.py @@ -73,6 +73,10 @@ def refine( * :attr:`~subliminal.video.Video.hashes` + :param Video video: the Video to refine. + :param providers: list of providers for which the video hash should be computed. + :param languages: set of languages that need to be compatible with the providers. + """ if video.size is None or video.size <= 10485760: logger.warning('Size is lower than 10MB: hashes not computed') diff --git a/src/subliminal/refiners/metadata.py b/src/subliminal/refiners/metadata.py index 9e93b74c..574dd8a6 100644 --- a/src/subliminal/refiners/metadata.py +++ b/src/subliminal/refiners/metadata.py @@ -61,6 +61,7 @@ def refine( * :attr:`~subliminal.video.Video.audio_codec` * :attr:`~subliminal.video.Video.subtitles` + :param Video video: the Video to refine. :param bool embedded_subtitles: search for embedded subtitles. :param (str | None) metadata_provider: provider used to retrieve information from video metadata. Should be one of ['mediainfo', 'ffmpeg', 'mkvmerge', 'enzyme']. None defaults to `mediainfo`. diff --git a/src/subliminal/refiners/omdb.py b/src/subliminal/refiners/omdb.py index a8486bc5..f31c1320 100644 --- a/src/subliminal/refiners/omdb.py +++ b/src/subliminal/refiners/omdb.py @@ -305,6 +305,11 @@ def refine(video: Video, *, apikey: str | None = None, force: bool = False, **kw * :attr:`~subliminal.video.Movie.year` * :attr:`~subliminal.video.Video.imdb_id` + :param Video video: the Video to refine. + :param (str | None) apikey: a personal API key to use OMDb. + :param bool force: if True, refine even if IMDB id is already known for a Movie or + if both the IMDB ids of the series and of the episodes are known for an Episode. + """ # update the API key if apikey is not None: diff --git a/src/subliminal/refiners/tmdb.py b/src/subliminal/refiners/tmdb.py index 903f3b65..a7a1d74b 100644 --- a/src/subliminal/refiners/tmdb.py +++ b/src/subliminal/refiners/tmdb.py @@ -320,6 +320,11 @@ def refine(video: Video, *, apikey: str | None = None, force: bool = False, **kw * :attr:`~subliminal.video.Video.tmdb_id` * :attr:`~subliminal.video.Video.imdb_id` + :param Video video: the Video to refine. + :param (str | None) apikey: a personal API key to use TMDB. + :param bool force: if True, refine even if TMDB id is already known for a Movie or + if both the TMDB ids of the series and of the episodes are known for an Episode. + """ # update the API key if apikey is not None: diff --git a/src/subliminal/refiners/tvdb.py b/src/subliminal/refiners/tvdb.py index a1dc1bbc..5df1f068 100644 --- a/src/subliminal/refiners/tvdb.py +++ b/src/subliminal/refiners/tvdb.py @@ -353,6 +353,11 @@ def refine(video: Video, *, apikey: str | None = None, force: bool = False, **kw * :attr:`~subliminal.video.Video.imdb_id` * :attr:`~subliminal.video.Episode.tvdb_id` + :param Video video: the Video to refine. + :param (str | None) apikey: a personal API key to use TheTVDB. + :param bool force: if True, refine even if both the IMDB ids of the series and + of the episodes are known for an Episode. + """ # only deal with Episode videos if not isinstance(video, Episode): From 03d58dc92fd1ec2c6ed013e76df0649638e2081b Mon Sep 17 00:00:00 2001 From: getzze Date: Sun, 24 Nov 2024 15:41:04 +0000 Subject: [PATCH 3/8] document Addic7ed parameters for the CLI --- src/subliminal/providers/addic7ed.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/subliminal/providers/addic7ed.py b/src/subliminal/providers/addic7ed.py index d6e7acb9..728e9ff0 100644 --- a/src/subliminal/providers/addic7ed.py +++ b/src/subliminal/providers/addic7ed.py @@ -202,7 +202,15 @@ def get_matches(self, video: Video) -> set[str]: class Addic7edProvider(Provider): - """Addic7ed Provider.""" + """Addic7ed Provider. + + :param (str | None) username: addic7ed username (not mandatory) + :param (str | None) password: addic7ed password (not mandatory) + :param int timeout: request timeout + :param bool allow_searches: allow using Addic7ed search API, it's very slow and + using it can result in blocking access to the website (default to False). + + """ languages: ClassVar[Set[Language]] = addic7ed_languages video_types: ClassVar = (Episode,) From 2d2f5673019dbf5c17c8bf56328e6161d85d5551 Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 18 Oct 2024 00:24:07 +0100 Subject: [PATCH 4/8] make sure values from config files are not erased by cli defaults --- src/subliminal/cli.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/subliminal/cli.py b/src/subliminal/cli.py index 0fbc7c1c..2434c8b1 100644 --- a/src/subliminal/cli.py +++ b/src/subliminal/cli.py @@ -9,6 +9,7 @@ import pathlib import re from collections import defaultdict +from collections.abc import Mapping from datetime import timedelta from typing import TYPE_CHECKING, Any @@ -43,7 +44,7 @@ from subliminal.utils import get_parameters_from_signature, merge_extend_and_ignore_unions if TYPE_CHECKING: - from collections.abc import Callable, Mapping, Sequence + from collections.abc import Callable, Sequence from subliminal.utils import Parameter @@ -118,6 +119,11 @@ def convert(self, value: str, param: click.Parameter | None, ctx: click.Context return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()}) +PROVIDERS_OPTIONS_TEMPLATE = '_{ext}__{plugin}__{key}' +PROVIDERS_OPTIONS_CLI_TEMPLATE = '--{ext}.{plugin}.{key}' +PROVIDERS_OPTIONS_ENVVAR_TEMPLATE = 'SUBLIMINAL_{ext}_{plugin}_{key}' + + def configure(ctx: click.Context, param: click.Parameter | None, filename: str | os.PathLike) -> None: """Read a configuration file.""" filename = pathlib.Path(filename).expanduser() @@ -163,15 +169,18 @@ def configure(ctx: click.Context, param: click.Parameter | None, filename: str | options['download'] = download_dict # make provider and refiner options - providers_dict = toml_dict.setdefault('provider', {}) - refiners_dict = toml_dict.setdefault('refiner', {}) + for ext in ('provider', 'refiner'): + for plugin, d in toml_dict.setdefault(ext, {}).items(): + if not isinstance(d, Mapping): + continue + for k, v in d.items(): + name = PROVIDERS_OPTIONS_TEMPLATE.format(ext=ext, plugin=plugin, key=k) + options[name] = v ctx.obj = { 'debug_message': msg, 'provider_lists': provider_lists, 'refiner_lists': refiner_lists, - 'provider_configs': providers_dict, - 'refiner_configs': refiners_dict, } ctx.default_map = options @@ -214,8 +223,8 @@ def decorator(f: Callable) -> Callable: # CLI option has dots, variable has double-underscores to differentiate # with simple underscore in provider name or keyword argument. param_decls = ( - f'--{group_name}.{plugin_name}.{name}', - f'_{group_name}__{plugin_name}__{name}', + PROVIDERS_OPTIONS_CLI_TEMPLATE.format(ext=group_name, plugin=plugin_name, key=name), + PROVIDERS_OPTIONS_TEMPLATE.format(ext=group_name, plugin=plugin_name, key=name), ) attrs = { 'default': opt['default'], @@ -243,10 +252,10 @@ def decorator(f: Callable) -> Callable: } # Decorator to add click options from providers -options_from_providers = options_from_managers('providers', provider_options, group=providers_config) +options_from_providers = options_from_managers('provider', provider_options, group=providers_config) # Decorator to add click options from refiners -options_from_refiners = options_from_managers('refiners', refiner_options, group=refiners_config) +options_from_refiners = options_from_managers('refiner', refiner_options, group=refiners_config) @click.group( @@ -311,23 +320,25 @@ def subliminal( ctx.obj['debug'] = debug - # provider configs - provider_configs = ctx.obj['provider_configs'] - # refiner configs - refiner_configs = ctx.obj['refiner_configs'] + # create provider and refiner configs + provider_configs = {} + refiner_configs = {} for k, v in kwargs.items(): try_split = k.split('__') - if len(try_split) != 3: + if len(try_split) != 3: # pragma: no cover click.echo(f'Unknown option: {k}={v}') continue group, plugin, key = try_split - if group == '_providers': + if group == '_provider': provider_configs.setdefault(plugin, {})[key] = v - elif group == '_refiners': + elif group == '_refiner': # pragma: no branch refiner_configs.setdefault(plugin, {})[key] = v + ctx.obj['provider_configs'] = provider_configs + ctx.obj['refiner_configs'] = refiner_configs + @subliminal.command() @click.option( From 9a838c47e38405cf52545d49d25694694cbc20fa Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 18 Oct 2024 00:26:00 +0100 Subject: [PATCH 5/8] cli takes options from environment variables --- pyproject.toml | 2 +- src/subliminal/cli.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 41e14a4f..25061877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ repository = "https://github.com/Diaoul/subliminal" documentation = "https://subliminal.readthedocs.org" [project.scripts] -subliminal = "subliminal.cli:subliminal" +subliminal = "subliminal.cli:cli" [project.entry-points."subliminal.providers"] addic7ed = "subliminal.providers.addic7ed:Addic7edProvider" diff --git a/src/subliminal/cli.py b/src/subliminal/cli.py index 2434c8b1..192aeaa3 100644 --- a/src/subliminal/cli.py +++ b/src/subliminal/cli.py @@ -230,6 +230,12 @@ def decorator(f: Callable) -> Callable: 'default': opt['default'], 'help': opt['desc'], 'show_default': True, + 'show_envvar': True, + 'envvar': PROVIDERS_OPTIONS_ENVVAR_TEMPLATE.format( + ext=group_name.upper(), + plugin=plugin_name.upper(), + key=name.upper(), + ), } click_option(*param_decls, **attrs)(f) return f @@ -271,6 +277,7 @@ def decorator(f: Callable) -> Callable: show_default=True, is_eager=True, expose_value=False, + show_envvar=True, help='Path to the TOML configuration file.', ) @click.option( @@ -421,6 +428,7 @@ def cache(ctx: click.Context, clear_subliminal: bool) -> None: 'use_ctime', is_flag=True, default=False, + show_envvar=True, help=( 'Use the latest of modification date and creation date to calculate the age. ' 'Otherwise, just use the modification date.' @@ -811,3 +819,8 @@ def download( if verbose == 0: click.echo(f"Downloaded {plural(total_subtitles, 'subtitle')}") + + +def cli() -> None: + """CLI that recognizes environment variables.""" + subliminal(auto_envvar_prefix='SUBLIMINAL') From fd2eaed9a1feaa58e836ecb593e70ca54d2239bd Mon Sep 17 00:00:00 2001 From: getzze Date: Fri, 18 Oct 2024 00:42:07 +0100 Subject: [PATCH 6/8] add news --- changelog.d/1179.breaking.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/1179.breaking.rst diff --git a/changelog.d/1179.breaking.rst b/changelog.d/1179.breaking.rst new file mode 100644 index 00000000..22692659 --- /dev/null +++ b/changelog.d/1179.breaking.rst @@ -0,0 +1,4 @@ +Deprecate the `--addic7ed USERNAME PASSWORD`, `--opensubtitles` and `--opensubtitlescom` CLI options +in favor of `--provider.addic7ed.username USERNAME`, `--provider.addic7ed.password PASSWORD`, etc... +Add a generic way of passing arguments to the providers using CLI options. +Use environment variables to pass options to the CLI. From b2a9c3c263ee6dec18525e3fc36e8e19a874221f Mon Sep 17 00:00:00 2001 From: getzze Date: Tue, 22 Oct 2024 22:46:08 +0100 Subject: [PATCH 7/8] fix typing --- src/subliminal/cli.py | 8 +++++--- tests/test_utils.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/subliminal/cli.py b/src/subliminal/cli.py index 192aeaa3..654ffb11 100644 --- a/src/subliminal/cli.py +++ b/src/subliminal/cli.py @@ -226,6 +226,7 @@ def decorator(f: Callable) -> Callable: PROVIDERS_OPTIONS_CLI_TEMPLATE.format(ext=group_name, plugin=plugin_name, key=name), PROVIDERS_OPTIONS_TEMPLATE.format(ext=group_name, plugin=plugin_name, key=name), ) + # Setting the default value also decides on the type attrs = { 'default': opt['default'], 'help': opt['desc'], @@ -237,7 +238,7 @@ def decorator(f: Callable) -> Callable: key=name.upper(), ), } - click_option(*param_decls, **attrs)(f) + f = click_option(*param_decls, **attrs)(f) # type: ignore[operator] return f return decorator @@ -295,6 +296,7 @@ def decorator(f: Callable) -> Callable: @click.pass_context def subliminal( ctx: click.Context, + /, cache_dir: str, debug: bool, **kwargs: Any, @@ -328,8 +330,8 @@ def subliminal( ctx.obj['debug'] = debug # create provider and refiner configs - provider_configs = {} - refiner_configs = {} + provider_configs: dict[str, dict[str, Any]] = {} + refiner_configs: dict[str, dict[str, Any]] = {} for k, v in kwargs.items(): try_split = k.split('__') diff --git a/tests/test_utils.py b/tests/test_utils.py index 7a8990aa..30cd3bc7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from pathlib import Path - from babelfish import Language + from babelfish import Language # type: ignore[import-untyped] # Core test pytestmark = pytest.mark.core @@ -280,7 +280,7 @@ def obj(): else: - class obj: + class obj: # type: ignore[no-redef] pass obj.__doc__ = docstring From 016a0cdbff4a4272785720095a575713ad533a64 Mon Sep 17 00:00:00 2001 From: getzze Date: Sun, 24 Nov 2024 15:46:42 +0000 Subject: [PATCH 8/8] fix ruff --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 30cd3bc7..dae0748c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -36,7 +36,7 @@ pytestmark = pytest.mark.core -@pytest.fixture() +@pytest.fixture def docstring() -> str: """Generate a docstring with arguments.""" return """Some class or function.