Repository: Textualize/textual

Issue:
[Bug] Describe failures for Integer validator
I encountered a small issue using input validators.
I have an `Input` widget with an `Integer` validator set. The error occurs when i try to get the `ValidationResult.failure_descriptions` property when the input is empty. 

Since the `Integer` validator inherits from the `Number` validator, failures of type `Number.NotANumber` and `Number.NotInRange` may occur. The failures are found when the input is empty but when I try to get access to the failure descriptions, it is empty because of the following code : 

```python
class Integer(Number):
    """Validator which ensures the value is an integer which falls within a range."""

    class NotAnInteger(Failure):
        """Indicates a failure due to the value not being a valid integer."""

    def validate(self, value: str) -> ValidationResult:
        """Ensure that `value` is an integer, optionally within a range.

        Args:
            value: The value to validate.

        Returns:
            The result of the validation.
        """
        # First, check that we're dealing with a number in the range.
        number_validation_result = super().validate(value)
        if not number_validation_result.is_valid:
            return number_validation_result

        # We know it's a number, but is that number an integer?
        is_integer = float(value).is_integer()
        if not is_integer:
            return ValidationResult.failure([Integer.NotAnInteger(self, value)])

        return self.success()

    def describe_failure(self, failure: Failure) -> str | None:
        """Describes why the validator failed.

        Args:
            failure: Information about why the validation failed.

        Returns:
            A string description of the failure.
        """
        if isinstance(failure, Integer.NotAnInteger):
            return f"Must be a valid integer."
        elif isinstance(failure, Integer.NotInRange):
            if self.minimum is None and self.maximum is not None:
                return f"Must be less than or equal to {self.maximum}."
            elif self.minimum is not None and self.maximum is None:
                return f"Must be greater than or equal to {self.minimum}."
            else:
                return f"Must be between {self.minimum} and {self.maximum}."
        else:
            return None
```

In the method `describe_failure`, only `Integer.NotAnInteger` and `Integer.NotInRange` failures are tested, but not the ones from Number...
Otherwise, the `Integer.NotInRange` does not seem to be raised in the `Integer.validate()` method.
Feel free to ask more information if I'm not clear enough. :)


# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.55.0 |
| Rich    | 13.7.1 |

## Python

| Name           | Value                                                |
|----------------|------------------------------------------------------|
| Version        | 3.11.8                                               |
| Implementation | CPython                                              |
| Compiler       | GCC 11.2.0                                           |
| Executable     | /home/alexandre/miniconda3/envs/analytics/bin/python |

## Operating System

| Name    | Value                                       |
|---------|---------------------------------------------|
| System  | Linux                                       |
| Release | 5.15.0-60-generic                           |
| Version | #66-Ubuntu SMP Fri Jan 20 14:29:49 UTC 2023 |

## Terminal

| Name                 | Value          |
|----------------------|----------------|
| Terminal Application | GNOME Terminal |
| TERM                 | xterm-256color |
| COLORTERM            | truecolor      |
| FORCE_COLOR          | *Not set*      |
| NO_COLOR             | *Not set*      |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=204, height=53 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 204                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 53                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |




Discussion:
We found the following entries in the [FAQ](https://textual.textualize.io/FAQ/) which you may find helpful:


- [How do I center a widget in a screen?](https://textual.textualize.io/FAQ/#how-do-i-center-a-widget-in-a-screen)
- [How do I pass arguments to an app?](https://textual.textualize.io/FAQ/#how-do-i-pass-arguments-to-an-app)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)

Resolving patch:
diff --git a/src/textual/validation.py b/src/textual/validation.py
--- a/src/textual/validation.py
+++ b/src/textual/validation.py
@@ -363,7 +363,7 @@ def describe_failure(self, failure: Failure) -> str | None:
         Returns:
             A string description of the failure.
         """
-        if isinstance(failure, Integer.NotAnInteger):
+        if isinstance(failure, (Integer.NotANumber, Integer.NotAnInteger)):
             return "Must be a valid integer."
         elif isinstance(failure, Integer.NotInRange):
             if self.minimum is None and self.maximum is not None:


Test patch:
diff --git a/tests/test_validation.py b/tests/test_validation.py
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -214,3 +214,11 @@ def test_Function_validate(function, failure_description, is_valid):
     assert result.is_valid is is_valid
     if result.failure_descriptions:
         assert result.failure_descriptions[0] == failure_description
+
+
+def test_Integer_failure_description_when_NotANumber():
+    """Regression test for https://github.com/Textualize/textual/issues/4413"""
+    validator = Integer()
+    result = validator.validate("x")
+    assert result.is_valid is False
+    assert result.failure_descriptions[0] == "Must be a valid integer."