Skip to content

Migrate FlexGet to Loguru’s predefined log levels #5144

Description

@vivodi

Summary

All uses of logger.verbose currently trigger the following Pyright error:

Cannot access attribute "verbose" for class "Logger"
  Attribute "verbose" is unknown

Instead of suppressing these errors individually, I suggest migrating FlexGet away from the custom verbose level and using Loguru’s predefined levels.

There are several reasons for this:

1. verbose has unclear semantics

FlexGet currently uses:

critical
error
warning
info
verbose
debug
trace

It is difficult to consistently distinguish the intended semantics of info, verbose, debug, and trace.

In particular, verbose describes the amount of detail rather than the nature of the event. Loguru's succeed, on the other hand, has a much clearer and more specific meaning.

Therefore, rather than mechanically replacing every verbose with succeed, I suggest reviewing the existing info and verbose messages:

  • Messages indicating a successful operation → succeed
  • Other informational messages → info
  • Detailed diagnostic messages → debug / trace

This would make the logging levels more consistent and easier to understand.

2. Custom levels cause Pyright errors

verbose is a custom Loguru level and is not part of Loguru's typed Logger API. As a result, every logger.verbose(...) call produces a Pyright error.

Loguru's predefined levels are already included in its type definitions, so using them would eliminate these errors without requiring individual # type: ignore comments or other suppressions.

3. Custom log levels should generally be avoided

The Python logging documentation recommends avoiding custom logging levels when the existing levels are sufficient, and specifically warns that custom levels can be problematic for libraries because different libraries may define levels with conflicting meanings.

Although this documentation is for the standard logging module, the same principle applies here: if Loguru already provides suitable predefined levels, there is little reason for FlexGet to maintain its own custom level.

Proposal

I suggest removing FlexGet's custom verbose level and migrating its current uses to Loguru's predefined levels based on their actual semantics:

  • succeed for messages indicating successful operations
  • info for other informational messages currently using info or verbose
  • debug / trace for messages that are primarily diagnostic

This would improve the semantic clarity of FlexGet's logging, eliminate the Pyright errors caused by logger.verbose, and avoid maintaining a custom log level when suitable predefined levels already exist.

Activity

  1. added theissue type on Aug 9, 2026
  2. vivodi commented on Aug 9, 2026

    @vivodi
    ContributorAuthor

    @paranoidi, I noticed that your Pyright fix PR suppresses all warnings related to logger.verbose. However, I think a better approach would be to migrate FlexGet to Loguru’s predefined log levels instead of suppressing these warnings.

    If you agree with this approach, I can prepare a PR to migrate FlexGet to Loguru’s predefined log levels.

  3. gazpachoking commented on Aug 9, 2026

    @gazpachoking
    Member

    Where would the verbose logs go? That's the level that's automatically used when FlexGet is run interactively as opposed to in cron or daemon mode.

  4. vivodi commented on Aug 9, 2026

    @vivodi
    ContributorAuthor

    Note

    FlexGet currently uses the following log levels:
    critical, error, warning, info, verbose, debug, and trace.
    Loguru’s predefined log levels are:
    critical, error, warning, succeed, info, debug, and trace.

    @gazpachoking A simple approach would be to map FlexGet’s seven log levels directly to Loguru’s seven predefined levels: info → succeed and verbose → info.

    This mapping is straightforward, but not all existing info logs represent successful operations. We could therefore review the affected logs and:

    • move non-successful info logs to the new info level;
    • move successful verbose logs to succeed.

    If you think this additional review could cause confusion, we could instead keep the mapping as a simple replacement. The downside is that some succeed logs may not actually represent successful operations.

  5. gazpachoking commented on Aug 9, 2026

    @gazpachoking
    Member

    I dunno, I feel like succeeded doesn't fit our purposes quite right. Can we maybe move the logger import to somewhere internal and override when TYPE_CHECKING to add the method?

  6. vivodi commented on Aug 14, 2026

    @vivodi
    ContributorAuthor

    I dunno, I feel like succeeded doesn't fit our purposes quite right.

    I counted the occurrences of logger.info and logger.verbose in each file and found that 65 files use logger.info, 60 use logger.verbose, and 33 use both.

    The vast majority of files use only one of the two, so I think it is reasonable to conclude that most plugin authors do not distinguish between the semantics of logger.info and logger.verbose, but simply choose whichever they prefer. Therefore, it seems reasonable to move the logs that represent completed operations to the new logger.success level, while keeping the remaining logs at logger.info.

    success has a stronger semantic meaning, which should also make it easier for plugin authors to distinguish between different log levels.

    Table of the number of info and verbose occurrences in each file

    File info verbose
    flexget\components\archive\archive.py 0 1
    flexget\components\archive\cli.py 5 3
    flexget\components\archive\db.py 2 1
    flexget\components\archives\decompress.py 2 0
    flexget\components\archives\utils.py 0 2
    flexget\components\backlog\backlog.py 0 1
    flexget\components\backlog\db.py 2 0
    flexget\components\bittorrent\convert_magnet.py 1 0
    flexget\components\bittorrent\torrent_scrub.py 1 0
    flexget\components\bittorrent\trackers.py 3 0
    flexget\components\estimate_release\estimators\est_released_movies.py 0 1
    flexget\components\ftp\ftp_download.py 2 0
    flexget\components\ftp\ftp_list.py 0 1
    flexget\components\ftp\sftp_client.py 0 5
    flexget\components\history\db.py 0 1
    flexget\components\imdb\from_imdb.py 5 2
    flexget\components\imdb\imdb.py 1 0
    flexget\components\imdb\imdb_lookup.py 0 5
    flexget\components\imdb\imdb_watchlist.py 0 3
    flexget\components\irc\irc.py 3 2
    flexget\components\managed_lists\list_add.py 1 1
    flexget\components\managed_lists\list_clear.py 1 1
    flexget\components\managed_lists\list_match.py 1 1
    flexget\components\managed_lists\list_remove.py 1 1
    flexget\components\managed_lists\lists\couchpotato_list.py 2 6
    flexget\components\managed_lists\lists\imdb_list.py 0 1
    flexget\components\managed_lists\lists\radarr_list.py 0 5
    flexget\components\managed_lists\lists\sonarr_list.py 0 3
    flexget\components\managed_lists\lists\subtitle_list.py 2 0
    flexget\components\managed_lists\lists\thetvdb_list.py 0 2
    flexget\components\notify\notification_framework.py 0 1
    flexget\components\notify\notifiers\discord.py 4 0
    flexget\components\notify\notifiers\ifttt.py 1 0
    flexget\components\notify\notifiers\matrix.py 1 0
    flexget\components\notify\notifiers\mqtt.py 0 11
    flexget\components\notify\notifiers\telegram.py 1 0
    flexget\components\notify\notify.py 0 1
    flexget\components\pending_approval\db.py 1 0
    flexget\components\pending_approval\pending_approval.py 0 1
    flexget\components\rejected\db.py 3 1
    flexget\components\rejected\remember_rejected.py 1 0
    flexget\components\scheduler\scheduler.py 3 0
    flexget\components\seen\db.py 6 0
    flexget\components\seen\seen.py 1 0
    flexget\components\seen\seen_movies.py 1 0
    flexget\components\series\configure_series.py 1 1
    flexget\components\series\db.py 10 5
    flexget\components\series\gen_series.py 3 0
    flexget\components\series\next_series_episodes.py 0 2
    flexget\components\series\next_series_seasons.py 0 2
    flexget\components\series\series.py 3 7
    flexget\components\series\series_begin.py 1 0
    flexget\components\series\series_remove.py 2 0
    flexget\components\sites\sites\allyoulike.py 0 1
    flexget\components\sites\sites\filelist_api.py 0 2
    flexget\components\sites\sites\lostfilm.py 8 6
    flexget\components\sites\sites\newznab.py 4 1
    flexget\components\sites\sites\rlsbb.py 0 1
    flexget\components\sites\sites\site_1337x.py 1 0
    flexget\components\sites\sites\site_rutracker.py 1 0
    flexget\components\sites\sites\solidtorrents.py 1 0
    flexget\components\sites\sites\uindex.py 0 2
    flexget\components\sites\urlrewrite_search.py 0 2
    flexget\components\sites\urlrewriting.py 1 0
    flexget\components\status\db.py 0 2
    flexget\components\thetvdb\api_tvdb.py 1 1
    flexget\components\thetvdb\thetvdb_lookup.py 0 1
    flexget\components\tmdb\api_tmdb.py 1 1
    flexget\components\trakt\db.py 0 1
    flexget\components\trakt\trakt_list.py 1 3
    flexget\db_schema.py 2 0
    flexget\entry.py 1 0
    flexget\ipc.py 0 1
    flexget\manager.py 16 2
    flexget\plugins\cli\check.py 0 4
    flexget\plugins\cli\explain_sql.py 2 0
    flexget\plugins\cli\perf_tests.py 3 0
    flexget\plugins\cli\performance.py 4 0
    flexget\plugins\clients\aria2.py 0 1
    flexget\plugins\clients\deluge.py 6 3
    flexget\plugins\clients\nzbget.py 2 0
    flexget\plugins\clients\pyload.py 2 0
    flexget\plugins\clients\qbittorrent.py 14 0
    flexget\plugins\clients\rtorrent.py 6 5
    flexget\plugins\clients\transmission.py 10 0
    flexget\plugins\daemon\web_server.py 4 0
    flexget\plugins\filter\content_filter.py 7 0
    flexget\plugins\filter\content_size.py 2 0
    flexget\plugins\filter\delay.py 0 2
    flexget\plugins\filter\exists.py 0 1
    flexget\plugins\filter\exists_movie.py 0 4
    flexget\plugins\filter\limit_new.py 1 1
    flexget\plugins\filter\only_new.py 0 1
    flexget\plugins\filter\proper_movies.py 1 1
    flexget\plugins\filter\rottentomatoes.py 1 0
    flexget\plugins\filter\timeframe.py 1 0
    flexget\plugins\generic\cron_env.py 1 0
    flexget\plugins\generic\db_analyze.py 1 0
    flexget\plugins\generic\db_vacuum.py 1 0
    flexget\plugins\generic\log_start.py 2 0
    flexget\plugins\generic\welcome.py 2 0
    flexget\plugins\input\anidb_list.py 0 4
    flexget\plugins\input\betaseries_list.py 0 1
    flexget\plugins\input\discover.py 1 7
    flexget\plugins\input\filesystem.py 8 2
    flexget\plugins\input\filmweb_watchlist.py 0 3
    flexget\plugins\input\from_task.py 0 2
    flexget\plugins\input\gazelle.py 2 0
    flexget\plugins\input\html.py 1 3
    flexget\plugins\input\inputs.py 0 1
    flexget\plugins\input\letterboxd.py 0 1
    flexget\plugins\input\next_sonarr_episodes.py 0 2
    flexget\plugins\input\npo_watchlist.py 3 4
    flexget\plugins\input\rlslog.py 0 1
    flexget\plugins\input\rss.py 0 3
    flexget\plugins\input\sickbeard.py 2 0
    flexget\plugins\input\tail.py 6 0
    flexget\plugins\input\text.py 3 0
    flexget\plugins\input\torznab.py 2 0
    flexget\plugins\internal\api_bluray.py 0 1
    flexget\plugins\internal\api_rottentomatoes.py 0 1
    flexget\plugins\metainfo\assume_quality.py 0 3
    flexget\plugins\modify\manipulate.py 0 4
    flexget\plugins\modify\regex_extract.py 1 0
    flexget\plugins\modify\sort_by_weight.py 0 1
    flexget\plugins\operate\debug_warnings.py 1 0
    flexget\plugins\operate\interval.py 3 1
    flexget\plugins\operate\proxy.py 0 1
    flexget\plugins\operate\run_task.py 1 0
    flexget\plugins\operate\sleep.py 0 1
    flexget\plugins\operate\spy_headers.py 5 0
    flexget\plugins\operate\verbose.py 0 1
    flexget\plugins\operate\verbose_details.py 0 3
    flexget\plugins\output\download.py 5 0
    flexget\plugins\output\exec.py 3 1
    flexget\plugins\output\file_operations.py 15 3
    flexget\plugins\output\html.py 0 1
    flexget\plugins\output\mock_output.py 0 1
    flexget\plugins\output\rss.py 2 1
    flexget\plugins\output\rtorrent_magnet.py 2 0
    flexget\plugins\output\sabnzbd.py 2 0
    flexget\plugins\output\sns.py 2 0
    flexget\plugins\output\subtitles_periscope.py 1 0
    flexget\plugins\output\subtitles_subliminal.py 2 2
    flexget\plugins\output\symlink.py 0 2
    flexget\plugins\output\utorrent.py 3 0
    flexget\plugins\services\kodi_library.py 2 0
    flexget\plugins\services\myepisodes.py 3 0
    flexget\plugins\services\pogcal_acquired.py 0 4
    flexget\task.py 5 0
    flexget\task_queue.py 0 1
    flexget\tray_icon.py 0 2
    flexget\utils\cached_input.py 0 3
    flexget\utils\lazy_dict.py 1 0
    flexget\utils\log.py 1 1
    flexget\utils\simple_persistence.py 1 0
    flexget\utils\tools.py 0 1
    flexget\webserver.py 3 0
    Total 281 205
    Code used for the statistics ```py from pathlib import Path import re import sys

    def count_logger_calls(directory: str):
    info_pattern = re.compile(r"\blogger.info\s*(")
    verbose_pattern = re.compile(r"\blogger.verbose\s*(")

    results = []
    
    for path in Path(directory).rglob("*.py"):
        try:
            content = path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            content = path.read_text(encoding="utf-8", errors="ignore")
    
        info_count = len(info_pattern.findall(content))
        verbose_count = len(verbose_pattern.findall(content))
    
        if info_count or verbose_count:
            results.append((path, info_count, verbose_count))
    
    return results
    

    if name == "main":
    directory = sys.argv[1] if len(sys.argv) > 1 else "flexget"

    results = count_logger_calls(directory)
    
    total_info = 0
    total_verbose = 0
    
    print(f"{'File':80} {'info':>8} {'verbose':>8}")
    print("-" * 100)
    only_info, only_verbose, both=0,0,0
    
    for path, info, verbose in sorted(results):
        print(f"{str(path):80} {info:8} {verbose:8}")
        total_info += info
        total_verbose += verbose
        if info and not verbose:
            only_info += 1
        elif verbose and not info:
            only_verbose += 1
        elif verbose and info:
            both += 1
    
    print("-" * 100)
    print(f"{'Total':80} {total_info:8} {total_verbose:8}")
    print(f"The number of files using `logger.info` is {only_info}, the number using `logger.verbose` is {only_verbose}, and the number using both is {both}.")
    
    </details>
    
    
    
    
  7. vivodi commented on Aug 14, 2026

    @vivodi
    ContributorAuthor

    Can we maybe move the logger import to somewhere internal and override when TYPE_CHECKING to add the method?

    I did some quick testing, and this approach should work fine. However, all logger imports will need to be changed from from loguru import logger to from flexget.log import logger.
    Update: This is a bit hacky. Although I did a quick test and it seems to work, there could still be obstacles to implementing it fully.

    There is actually an even simpler solution: change logger.verbose(message) to logger.log("verbose", message), avoiding the need for monkey-patching.

    So there are three ways to solve this issue now:

    1. Migrate FlexGet’s log levels to Loguru’s predefined log levels.
    2. Use the approach you mentioned: move the logger import to somewhere internal and override it under TYPE_CHECKING to add the method.
    3. Change logger.verbose(message) to logger.log("verbose", message).

    @gazpachoking Which approach would you prefer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions