Repository: Textualize/textual

Issue:
Validators and watchers should run hand-in-hand
When `init=True` in a `reactive` attribute, the `watch_` method for that attribute run, but the `validate_` method does not.
This seems rather confusing and we think that validators and watchers should always run hand-in-hand.
If the watcher is called, you should be confident that the incoming `new_value` has already been validated by the `validate_` method.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8a4fa91e..96d5e88ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Fixed
 
+- Fixed validator not running on first reactive set https://github.com/Textualize/textual/pull/1359
 - Ensure only printable characters are used as key_display https://github.com/Textualize/textual/pull/1361
 
 ## [0.6.0] - 2022-12-11
diff --git a/docs/widgets/list_item.md b/docs/widgets/list_item.md
index 1ceb65e31..5a9ed4435 100644
--- a/docs/widgets/list_item.md
+++ b/docs/widgets/list_item.md
@@ -2,8 +2,8 @@
 
 `ListItem` is the type of the elements in a `ListView`.
 
-- [] Focusable
-- [] Container
+- [ ] Focusable
+- [ ] Container
 
 ## Example
 
diff --git a/docs/widgets/text_log.md b/docs/widgets/text_log.md
index 8a8f690d8..ccc7ea78c 100644
--- a/docs/widgets/text_log.md
+++ b/docs/widgets/text_log.md
@@ -41,4 +41,4 @@ This widget sends no messages.
 
 ## See Also
 
-* [TextLog](../api/textlog.md) code reference
+* [TextLog](../api/text_log.md) code reference
diff --git a/mkdocs.yml b/mkdocs.yml
index 4f3247d92..7f73787b3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -112,7 +112,6 @@ nav:
     - "api/color.md"
     - "api/containers.md"
     - "api/data_table.md"
-    - "api/text_log.md"
     - "api/directory_tree.md"
     - "api/dom_node.md"
     - "api/events.md"
@@ -130,6 +129,7 @@ nav:
     - "api/reactive.md"
     - "api/screen.md"
     - "api/static.md"
+    - "api/text_log.md"
     - "api/timer.md"
     - "api/walk.md"
     - "api/widget.md"
diff --git a/src/textual/reactive.py b/src/textual/reactive.py
index c71070ba3..88b10cd04 100644
--- a/src/textual/reactive.py
+++ b/src/textual/reactive.py
@@ -177,8 +177,8 @@ class Reactive(Generic[ReactiveType]):
         validate_function = getattr(obj, f"validate_{name}", None)
         # Check if this is the first time setting the value
         first_set = getattr(obj, f"__first_set_{self.internal_name}", True)
-        # Call validate, but not on first set.
-        if callable(validate_function) and not first_set:
+        # Call validate
+        if callable(validate_function):
             value = validate_function(value)
         # If the value has changed, or this is the first time setting the value
         if current_value != value or first_set or self._always_update:


Test patch:
diff --git a/tests/test_reactive.py b/tests/test_reactive.py
index dc3ca9958..6ccfa0656 100644
--- a/tests/test_reactive.py
+++ b/tests/test_reactive.py
@@ -2,9 +2,8 @@ import asyncio
 
 import pytest
 
-from textual.app import App, ComposeResult
+from textual.app import App
 from textual.reactive import reactive, var
-from textual.widget import Widget
 
 OLD_VALUE = 5_000
 NEW_VALUE = 1_000_000
@@ -81,14 +80,16 @@ async def test_watch_async_init_true():
         try:
             await asyncio.wait_for(app.watcher_called_event.wait(), timeout=0.05)
         except TimeoutError:
-            pytest.fail("Async watcher wasn't called within timeout when reactive init = True")
+            pytest.fail(
+                "Async watcher wasn't called within timeout when reactive init = True")
 
     assert app.count == OLD_VALUE
     assert app.watcher_old_value == OLD_VALUE
     assert app.watcher_new_value == OLD_VALUE  # The value wasn't changed
 
 
-@pytest.mark.xfail(reason="Reactive watcher is incorrectly always called the first time it is set, even if value is same [issue#1230]")
+@pytest.mark.xfail(
+    reason="Reactive watcher is incorrectly always called the first time it is set, even if value is same [issue#1230]")
 async def test_watch_init_false_always_update_false():
     class WatcherInitFalse(App):
         count = reactive(0, init=False)
@@ -173,20 +174,45 @@ async def test_reactive_with_callable_default():
     assert app.watcher_called_with == OLD_VALUE
 
 
-@pytest.mark.xfail(reason="Validator methods not running when init=True [issue#1220]")
 async def test_validate_init_true():
     """When init is True for a reactive attribute, Textual should call the validator
     AND the watch method when the app starts."""
+    validator_call_count = 0
 
     class ValidatorInitTrue(App):
         count = var(5, init=True)
 
         def validate_count(self, value: int) -> int:
+            nonlocal validator_call_count
+            validator_call_count += 1
             return value + 1
 
     app = ValidatorInitTrue()
     async with app.run_test():
+        app.count = 5
         assert app.count == 6  # Validator should run, so value should be 5+1=6
+        assert validator_call_count == 1
+
+
+async def test_validate_init_true_set_before_dom_ready():
+    """When init is True for a reactive attribute, Textual should call the validator
+    AND the watch method when the app starts."""
+    validator_call_count = 0
+
+    class ValidatorInitTrue(App):
+        count = var(5, init=True)
+
+        def validate_count(self, value: int) -> int:
+            nonlocal validator_call_count
+            validator_call_count += 1
+            return value + 1
+
+    app = ValidatorInitTrue()
+    app.count = 5
+    async with app.run_test():
+        assert app.count == 6  # Validator should run, so value should be 5+1=6
+        assert validator_call_count == 1
+
 
 
 @pytest.mark.xfail(reason="Compute methods not called when init=True [issue#1227]")