diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9b667557..5b3d4909de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Clicking a non focusable widget focus ancestors https://github.com/Textualize/textual/pull/4236 -- BREAKING: Querying and TCSS expect widget class names to start with a capital letter or an underscore `_` https://github.com/Textualize/textual/pull/4252 +- BREAKING: widget class names must start with a capital letter or an underscore `_` https://github.com/Textualize/textual/pull/4252 ## [0.52.1] - 2024-02-20 diff --git a/src/textual/widget.py b/src/textual/widget.py index 2b5796af55..29d522453b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -4,7 +4,6 @@ from __future__ import annotations -import warnings from asyncio import Lock, create_task, wait from collections import Counter from contextlib import asynccontextmanager @@ -13,7 +12,6 @@ from types import TracebackType from typing import ( TYPE_CHECKING, - Any, AsyncGenerator, Awaitable, ClassVar, @@ -22,7 +20,6 @@ Iterable, NamedTuple, Sequence, - Type, TypeVar, cast, overload, @@ -75,7 +72,6 @@ ) from .layouts.vertical import VerticalLayout from .message import Message -from .message_pump import _MessagePumpMeta from .messages import CallbackType from .notifications import Notification, SeverityLevel from .reactive import Reactive @@ -247,35 +243,12 @@ def __get__(self, obj: Widget, objtype: type[Widget] | None = None) -> str | Non return title.markup -_WidgetMetaSub = TypeVar("_WidgetMetaSub", bound="_WidgetMeta") - - -class _WidgetMeta(_MessagePumpMeta): - """Metaclass for widgets. - - Used to issue a warning if a widget subclass is created with naming that's - incompatible with TCSS/querying. - """ - - def __new__( - mcs: Type[_WidgetMetaSub], - name: str, - *args: Any, - **kwargs: Any, - ) -> _WidgetMetaSub: - """Hook into widget subclass creation to check the subclass name.""" - if not name[0].isupper() and not name.startswith("_"): - warnings.warn( - SyntaxWarning( - f"Widget subclass {name!r} should be capitalised or start with '_'." - ), - stacklevel=2, - ) - return super().__new__(mcs, name, *args, **kwargs) +class BadWidgetName(Exception): + """Raised when widget class names do not satisfy the required restrictions.""" @rich.repr.auto -class Widget(DOMNode, metaclass=_WidgetMeta): +class Widget(DOMNode): """ A Widget is the base class for Textual widgets. @@ -2919,11 +2892,17 @@ def __init_subclass__( inherit_css: bool = True, inherit_bindings: bool = True, ) -> None: - base = cls.__mro__[0] + name = cls.__name__ + if not name[0].isupper() and not name.startswith("_"): + raise BadWidgetName( + f"Widget subclass {name!r} should be capitalised or start with '_'." + ) + super().__init_subclass__( inherit_css=inherit_css, inherit_bindings=inherit_bindings, ) + base = cls.__mro__[0] if issubclass(base, Widget): cls.can_focus = base.can_focus if can_focus is None else can_focus cls.can_focus_children = ( diff --git a/tests/test_widget.py b/tests/test_widget.py index 7fcb264a31..182de27b7a 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -11,7 +11,7 @@ from textual.css.query import NoMatches from textual.geometry import Offset, Size from textual.message import Message -from textual.widget import MountError, PseudoClasses, Widget +from textual.widget import BadWidgetName, MountError, PseudoClasses, Widget from textual.widgets import Label, LoadingIndicator @@ -513,3 +513,12 @@ def compose(self) -> ComposeResult: "l1", "l3", ] + + +def test_bad_widget_name_raised() -> None: + """Ensure error is raised when bad class names are used for widgets.""" + + with pytest.raises(BadWidgetName): + + class lowercaseWidget(Widget): + pass