Skip to content

Conversation

@snejus
Copy link
Member

@snejus snejus commented Jan 3, 2026

This PR adds types to the library and includes py.typed so that mypy discovers them in packages that depend on confuse.

I have tested this against beets: it makes complicated templates, such as MappingTemplate, OneOf and Choices transparent, for example:

ColorName = Literal[
    "text_success",
    "text_warning",
    "text_error",
    "text_highlight",
    "text_highlight_minor",
    "action_default",
    "action",
    # New Colors
    "text_faint",
    "import_path",
    "import_path_items",
    "action_description",
    "changed",
    "text_diff_added",
    "text_diff_removed",
]


@cache
def get_color_config() -> dict[ColorName, str]:
    """Parse and validate color configuration, converting names to ANSI codes.

    Processes the UI color configuration, handling both new list format and
    legacy single-color format. Validates all color names against known codes
    and raises an error for any invalid entries.
    """
    template_dict: dict[ColorName, confuse.OneOf[str | list[str]]] = {
        n: confuse.OneOf(
            [
                confuse.Choice(sorted(LEGACY_COLORS)),
                confuse.Sequence(confuse.Choice(sorted(CODE_BY_COLOR))),
            ]
        )
        for n in ColorName.__args__  # type: ignore[attr-defined]
    }
    template = confuse.MappingTemplate(template_dict)
    colors_by_color_name = {
        k: (v if isinstance(v, list) else LEGACY_COLORS.get(v, [v]))
        for k, v in config["ui"]["colors"].get(template).items()
    }

    reveal_type(colors_by_color_name)  # mypy: Revealed type is "builtins.dict[Literal['text_success'] | Literal['text_warning'] | Literal['text_error'] | Literal['text_highlight'] | Literal['text_highlight_minor'] | Literal['action_default'] | Literal['action'] | Literal['text_faint'] | Literal['import_path'] | Literal['import_path_items'] | Literal['action_description'] | Literal['changed'] | Literal['text_diff_added'] | Literal['text_diff_removed'], builtins.list[builtins.str]]
            max_rec = max_rec_view[key].as_choice(
                {
                    "strong": Recommendation.strong,
                    "medium": Recommendation.medium,
                    "low": Recommendation.low,
                    "none": Recommendation.none,
                }
            )
            reveal_type(max_rec)  # mypy: Revealed type is "beets.autotag.match.Recommendation"

@snejus snejus requested review from sampsyo and semohr January 3, 2026 18:00
@github-actions
Copy link

github-actions bot commented Jan 3, 2026

Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.

@snejus snejus requested review from a team and removed request for sampsyo and semohr January 3, 2026 18:00
@snejus snejus force-pushed the type-choices branch 3 times, most recently from 9ca9be5 to 2d0b9b6 Compare January 4, 2026 16:54
Copy link

@amogus07 amogus07 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much experience with libraries, but here are some ideas and suggestions

@@ -1 +1 @@
import yaml
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import yaml.scanner

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the underlying loaded modules in sys.modules it should make no difference. The yaml module does not use any lazy loading afaiks.

For maintenance reasons I would just import what we acutely use instead of importing the full module if there is no specific reason for it. I.e.from yaml.scanner import ScannerError.

Copy link

@amogus07 amogus07 Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't importing yaml execute it's __init__.py, which is unnecessary overhead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but importing yaml.scanner will also execute the yaml/__init__.py file. You can readup on the exact semantics here.

@snejus snejus force-pushed the lint branch 2 times, most recently from f7ffcd3 to 8afaaab Compare January 5, 2026 03:26
@snejus snejus force-pushed the type-choices branch 4 times, most recently from df5fe80 to 3f7059e Compare January 5, 2026 06:12
@snejus snejus added this to the v2.2.0 milestone Jan 5, 2026
@snejus
Copy link
Member Author

snejus commented Jan 5, 2026

@amogus07 I noticed that several of your comments are based on issues raised by basedpyright, however we use mypy in all beetbox packages, and they have different philosophies.

Mypy balances strictness with pragmatism, while basedpyright enforces rules that often conflict with real-world Python usage:

  • Import Cycles: Mypy treats TYPE_CHECKING guards as a valid solution. Basedpyright still flags them as architectural flaws.
  • Abstract Attributes: PEP 526 defines name: str in class bodies as a standard contract. Mypy understands this; basedpyright demands explicit initialization.
  • Cognitive Burden: Basedpyright's strictness creates constant friction, forcing code to appease the tool rather than serve the project.

We prefer Mypy's approach for library maintenance. If you're contributing to beetbox projects, configuring your editor to use mypy will align with our standards and avoid these false positives.

@amogus07
Copy link

amogus07 commented Jan 5, 2026

@snejus I suppose it's just a matter of preference. Whereas you see stricter rules as a cognitive burden, I often have a hard time understanding untyped or leniently typed code (as it's easy to misunderstand it from an outsider perspective). I understand that this project uses mypy, I just thought some of (based)pyright's suggestions are also worth considering as long as they don't conflict with mypy. But if you're strictly against it I'll try to keep that in mind.

@snejus snejus requested a review from semohr January 6, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants