Repository: Textualize/textual

Issue:
Setting inline styles width or height to an integer causes a crash
`self.styles.height = 10` doesn't work, but `self.styles.height = 10.0` does.

Traceback:

```
╭─────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────╮
│                                                                                                                                                 │
│ /Users/darrenburns/code/textual/src/textual/widget.py:539 in get_render_lines                                                                   │
│                                                                                                                                                 │
│   536 │   │   │   Lines: A list of lists of segments.                                         ╭───── locals ─────╮                              │
│   537 │   │   """                                                                             │   end = None     │                              │
│   538 │   │   if self._dirty_regions:                                                         │  self = Button() │                              │
│ ❱ 539 │   │   │   self._render_lines()                                                        │ start = None     │                              │
│   540 │   │   │   if self.is_container:                                                       ╰──────────────────╯                              │
│   541 │   │   │   │   self.horizontal_scrollbar.refresh()                                                                                       │
│   542 │   │   │   │   self.vertical_scrollbar.refresh()                                                                                         │
│ /Users/darrenburns/code/textual/src/textual/widget.py:520 in _render_lines                                                                      │
│                                                                                                                                                 │
│   517 │   def _render_lines(self) -> None:                                                    ╭───── locals ──────╮                             │
│   518 │   │   """Render all lines."""                                                         │ height = 80       │                             │
│   519 │   │   width, height = self.size                                                       │   self = Button() │                             │
│ ❱ 520 │   │   renderable = self.render_styled()                                               │  width = 20       │                             │
│   521 │   │   options = self.console.options.update_dimensions(width, height)                 ╰───────────────────╯                             │
│   522 │   │   lines = self.console.render_lines(renderable, options)                                                                            │
│   523 │   │   self._render_cache = RenderCache(self.size, lines)                                                                                │
│                                                                                                                                                 │
│ /Users/darrenburns/code/textual/src/textual/widget.py:386 in render_styled                                                                      │
│                                                                                                                                                 │
│   383 │   │   │   RenderableType: A new renderable.                                           ╭──── locals ─────╮                               │
│   384 │   │   """                                                                             │ self = Button() │                               │
│   385 │   │                                                                                   ╰─────────────────╯                               │
│ ❱ 386 │   │   renderable = self.render()                                                                                                        │
│   387 │   │   styles = self.styles                                                                                                              │
│   388 │   │   parent_styles = self.parent.styles                                                                                                │
│   389                                                                                                                                           │
│                                                                                                                                                 │
│ /Users/darrenburns/code/textual/src/textual/widgets/_button.py:53 in render                                                                     │
│                                                                                                                                                 │
│   50 │                                                                                       ╭───── locals ─────╮                               │
│   51 │   def render(self) -> RenderableType:                                                 │  self = Button() │                               │
│   52 │   │   width = cell_len(self.label)                                                    │ width = 9        │                               │
│ ❱ 53 │   │   self.styles.width = width                                                       ╰──────────────────╯                               │
│   54 │   │   return ButtonRenderable(                                                                                                           │
│   55 │   │   │   self.label,                                                                                                                    │
│   56 │   │   )                                                                                                                                  │
│                                                                                                                                                 │
│ /Users/darrenburns/code/textual/src/textual/css/_style_properties.py:103 in __set__                                                             │
│                                                                                                                                                 │
│   100 │   │   │   except ScalarParseError:                                                                                                      │
│   101 │   │   │   │   raise StyleValueError("unable to parse scalar from {value!r}")                                                            │
│   102 │   │   else:                                                                                                                             │
│ ❱ 103 │   │   │   raise StyleValueError("expected float, Scalar, or None")                                                                      │
│   104 │   │   if new_value is not None and new_value.unit not in self.units:                                                                    │
│   105 │   │   │   raise StyleValueError(                                                                                                        │
│   106 │   │   │   │   f"{self.name} units must be one of {friendly_list(get_symbols(self.unit                                                   │
│                                                                                                                                                 │
│ ╭───────────────────────────────────────── locals ─────────────────────────────────────────╮                                                    │
│ │   obj = RenderStyles(                                                                    │                                                    │
│ │         │   color=Color(192, 192, 192, a=1.0),                                           │                                                    │
│ │         │   background=Color(0, 0, 128, a=1.0),                                          │                                                    │
│ │         │   border_top=('solid', Color(0, 128, 0, a=1.0)),                               │                                                    │
│ │         │   border_right=('solid', Color(0, 128, 0, a=1.0)),                             │                                                    │
│ │         │   border_bottom=('solid', Color(0, 128, 0, a=1.0)),                            │                                                    │
│ │         │   border_left=('solid', Color(0, 128, 0, a=1.0)),                              │                                                    │
│ │         │   width=Scalar(                                                                │                                                    │
│ │         │   │   value=20.0,                                                              │                                                    │
│ │         │   │   unit=<Unit.CELLS: 1>,                                                    │                                                    │
│ │         │   │   percent_unit=<Unit.WIDTH: 4>                                             │                                                    │
│ │         │   )                                                                            │                                                    │
│ │         )                                                                                │                                                    │
│ │  self = <textual.css._style_properties.ScalarProperty object at 0x101e62860>             │                                                    │
│ │ value = 9                                                                                │                                                    │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯                                                    │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
StyleValueError: expected float, Scalar, or None
```


Discussion:

Resolving patch:
diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py
--- a/src/textual/css/_style_properties.py
+++ b/src/textual/css/_style_properties.py
@@ -115,12 +115,12 @@ def __set__(
         ):
             raise StyleValueError("'auto' not allowed here")
 
-        if new_value.unit != Unit.AUTO:
-            if new_value is not None and new_value.unit not in self.units:
+        if new_value is not None and new_value.unit != Unit.AUTO:
+            if new_value.unit not in self.units:
                 raise StyleValueError(
                     f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}"
                 )
-            if new_value is not None and new_value.is_percent:
+            if new_value.is_percent:
                 new_value = Scalar(
                     float(new_value.value), self.percent_unit, Unit.WIDTH
                 )


Test patch:
diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py
--- a/tests/css/test_styles.py
+++ b/tests/css/test_styles.py
@@ -1,11 +1,15 @@
+from decimal import Decimal
+
 import pytest
 
 from rich.style import Style
 
 from textual.color import Color
-from textual.css.errors import StyleTypeError
+from textual.css.errors import StyleTypeError, StyleValueError
+from textual.css.scalar import Scalar, Unit
 from textual.css.styles import Styles, RenderStyles
 from textual.dom import DOMNode
+from textual.widget import Widget
 
 
 def test_styles_reset():
@@ -131,3 +135,53 @@ def test_opacity_set_invalid_type_error():
     styles = RenderStyles(DOMNode(), Styles(), Styles())
     with pytest.raises(StyleTypeError):
         styles.opacity = "invalid value"
+
+
+@pytest.mark.parametrize(
+    "size_dimension_input,size_dimension_expected_output",
+    [
+        # fmt: off
+        [None, None],
+        [1, Scalar(1, Unit.CELLS, Unit.WIDTH)],
+        [1.0, Scalar(1.0, Unit.CELLS, Unit.WIDTH)],
+        [1.2, Scalar(1.2, Unit.CELLS, Unit.WIDTH)],
+        [1.2e3, Scalar(1200.0, Unit.CELLS, Unit.WIDTH)],
+        ["20", Scalar(20, Unit.CELLS, Unit.WIDTH)],
+        ["1.4", Scalar(1.4, Unit.CELLS, Unit.WIDTH)],
+        [Scalar(100, Unit.CELLS, Unit.WIDTH), Scalar(100, Unit.CELLS, Unit.WIDTH)],
+        [Scalar(10.3, Unit.CELLS, Unit.WIDTH), Scalar(10.3, Unit.CELLS, Unit.WIDTH)],
+        [Scalar(10.4, Unit.CELLS, Unit.HEIGHT), Scalar(10.4, Unit.CELLS, Unit.HEIGHT)],
+        [Scalar(10.5, Unit.PERCENT, Unit.WIDTH), Scalar(10.5, Unit.WIDTH, Unit.WIDTH)],
+        [Scalar(10.6, Unit.PERCENT, Unit.PERCENT), Scalar(10.6, Unit.WIDTH, Unit.WIDTH)],
+        [Scalar(10.7, Unit.HEIGHT, Unit.PERCENT), Scalar(10.7, Unit.HEIGHT, Unit.PERCENT)],
+        # percentage values are normalised to floats and get the WIDTH "percent_unit":
+        [Scalar(11, Unit.PERCENT, Unit.HEIGHT), Scalar(11.0, Unit.WIDTH, Unit.WIDTH)],
+        # fmt: on
+    ],
+)
+def test_widget_style_size_can_accept_various_data_types_and_normalize_them(
+    size_dimension_input, size_dimension_expected_output
+):
+    widget = Widget()
+
+    widget.styles.width = size_dimension_input
+    assert widget.styles.width == size_dimension_expected_output
+
+
+@pytest.mark.parametrize(
+    "size_dimension_input",
+    [
+        "a",
+        "1.4e3",
+        3.14j,
+        Decimal("3.14"),
+        list(),
+        tuple(),
+        dict(),
+    ],
+)
+def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_input):
+    widget = Widget()
+
+    with pytest.raises(StyleValueError):
+        widget.styles.width = size_dimension_input
diff --git a/tests/test_widget.py b/tests/test_widget.py
--- a/tests/test_widget.py
+++ b/tests/test_widget.py
@@ -1,16 +1,22 @@
+from contextlib import nullcontext as does_not_raise
+from decimal import Decimal
+
 import pytest
 
 from textual.css.errors import StyleValueError
+from textual.css.scalar import Scalar, Unit
 from textual.widget import Widget
 
 
 @pytest.mark.parametrize(
-    "set_val, get_val, style_str", [
+    "set_val, get_val, style_str",
+    [
         [True, True, "visible"],
         [False, False, "hidden"],
         ["hidden", False, "hidden"],
         ["visible", True, "visible"],
-    ])
+    ],
+)
 def test_widget_set_visible_true(set_val, get_val, style_str):
     widget = Widget()
     widget.visible = set_val