Repository: litestar-org/litestar

Issue:
Bug: Query parameter values of "true"/"false" become title-cased when used as string query parameter
### Description

When defining a query parameter as `param: str`, and passing it a value of `true`, the value received by the route handler is `True`, having been title cased. The same is true for the value of `false`.

The issue lies within the `parse_query_string_function`:

https://github.com/litestar-org/litestar/blob/ae2b8fcde70566bb9929c984525ee6bf98d16f0f/litestar/_parsers.py#L28-L38

The value is converted to a `bool` here, and later on back to a string since the parameter is annotated as such.

The conversion of query string parameters to `bool`s should not happen there, but instead be directed by the type annotation of the parameter, and occur within the signature model.

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import get
from litestar.testing import create_test_client


@get("/")
def handler(param: str) -> str:
    return param


with create_test_client(handler) as client:
    assert client.get("/?param=true").text == "True"
```


### Steps to reproduce

_No response_

### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

-

### Platform

- [ ] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)
StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:

I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/_parsers.py b/litestar/_parsers.py
--- a/litestar/_parsers.py
+++ b/litestar/_parsers.py
@@ -34,8 +34,7 @@ def parse_query_string(query_string: bytes) -> tuple[tuple[str, Any], ...]:
     Returns:
         A tuple of key value pairs.
     """
-    _bools = {"true": True, "false": False, "True": True, "False": False}
-    return tuple((k, v if v not in _bools else _bools[v]) for k, v in fast_parse_query_string(query_string, "&"))
+    return tuple(fast_parse_query_string(query_string, "&"))
 
 
 @lru_cache(1024)
diff --git a/litestar/_signature/models/attrs_signature_model.py b/litestar/_signature/models/attrs_signature_model.py
--- a/litestar/_signature/models/attrs_signature_model.py
+++ b/litestar/_signature/models/attrs_signature_model.py
@@ -48,9 +48,10 @@
 if TYPE_CHECKING:
     from litestar.utils.signature import ParsedSignature
 
-key_re = re.compile("@ attribute (.*)|'(.*)'")
-
 __all__ = ("AttrsSignatureModel",)
+key_re = re.compile("@ attribute (.*)|'(.*)'")
+TRUE_SET = {"1", "true", "on", "t", "y", "yes"}
+FALSE_SET = {"0", "false", "off", "f", "n", "no"}
 
 try:
     import pydantic
@@ -75,6 +76,22 @@ def _pass_through_unstructure_hook(value: Any) -> Any:
     return value
 
 
+def _structure_bool(value: Any, _: type[bool]) -> bool:
+    if isinstance(value, bytes):
+        value = value.decode("utf-8").lower()
+
+    if isinstance(value, str):
+        value = value.lower()
+
+    if value == 0 or value in FALSE_SET:
+        return False
+
+    if value == 1 or value in TRUE_SET:
+        return True
+
+    raise ValueError(f"Cannot convert {value} to bool")
+
+
 def _structure_datetime(value: Any, cls: type[datetime]) -> datetime:
     if isinstance(value, datetime):
         return value
@@ -151,6 +168,7 @@ def _structure_str(value: Any, cls: type[str]) -> str:
     (UUID, _structure_uuid),
     (UploadFile, _pass_through_structure_hook),
     (WebSocket, _pass_through_structure_hook),
+    (bool, _structure_bool),
     (date, _structure_date),
     (datetime, _structure_datetime),
     (str, _structure_str),


Test patch:
diff --git a/tests/datastructures/test_url.py b/tests/datastructures/test_url.py
--- a/tests/datastructures/test_url.py
+++ b/tests/datastructures/test_url.py
@@ -34,7 +34,7 @@ def test_url() -> None:
     assert url.password == "hunter2"
     assert url.port == 81
     assert url.hostname == "example.org"
-    assert url.query_params.dict() == {"query": ["param"], "bool": [True]}
+    assert url.query_params.dict() == {"query": ["param"], "bool": ["true"]}
 
 
 @pytest.mark.parametrize(
diff --git a/tests/signature/test_attrs_signature_modelling.py b/tests/signature/test_attrs_signature_modelling.py
--- a/tests/signature/test_attrs_signature_modelling.py
+++ b/tests/signature/test_attrs_signature_modelling.py
@@ -13,6 +13,58 @@
 person = PersonFactory.build()
 
 
+@pytest.mark.parametrize(
+    "value,expected",
+    (
+        ("1", True),
+        (b"1", True),
+        ("True", True),
+        (b"True", True),
+        ("on", True),
+        (b"on", True),
+        ("t", True),
+        (b"t", True),
+        ("true", True),
+        (b"true", True),
+        ("y", True),
+        (b"y", True),
+        ("yes", True),
+        (b"yes", True),
+        (1, True),
+        (True, True),
+        ("0", False),
+        (b"0", False),
+        ("False", False),
+        (b"False", False),
+        ("f", False),
+        (b"f", False),
+        ("false", False),
+        (b"false", False),
+        ("n", False),
+        (b"n", False),
+        ("no", False),
+        (b"no", False),
+        ("off", False),
+        (b"off", False),
+        (0, False),
+        (False, False),
+    ),
+)
+def test_cattrs_converter_structure_bool(value: Any, expected: Any) -> None:
+    result = _converter.structure(value, bool)
+    assert result == expected
+
+
+def test_cattrs_converter_structure_bool_value_error() -> None:
+    with pytest.raises(ValueError):
+        _converter.structure(None, bool)
+        _converter.structure("foofoofoo", bool)
+        _converter.structure(object(), bool)
+        _converter.structure(type, bool)
+        _converter.structure({}, bool)
+        _converter.structure([], bool)
+
+
 @pytest.mark.parametrize(
     "value,cls,expected",
     (
diff --git a/tests/signature/test_parsing.py b/tests/signature/test_parsing.py
--- a/tests/signature/test_parsing.py
+++ b/tests/signature/test_parsing.py
@@ -1,4 +1,5 @@
 from typing import TYPE_CHECKING, Any, Iterable, List, Literal, Optional, Sequence
+from unittest.mock import MagicMock
 
 import pytest
 from pydantic import BaseModel
@@ -265,3 +266,18 @@ def fn(a: Sequence[int], b: OptionalSequence[int]) -> None:
 
     assert model.fields["a"].is_non_string_sequence
     assert model.fields["b"].is_non_string_sequence
+
+
+@pytest.mark.parametrize("signature_backend", ["pydantic", "attrs"])
+@pytest.mark.parametrize("query,expected", [("1", True), ("true", True), ("0", False), ("false", False)])
+def test_query_param_bool(query: str, expected: bool, signature_backend: Literal["pydantic", "attrs"]) -> None:
+    mock = MagicMock()
+
+    @get("/")
+    def handler(param: bool) -> None:
+        mock(param)
+
+    with create_test_client(route_handlers=[handler], preferred_validation_backend=signature_backend) as client:
+        response = client.get(f"/?param={query}")
+        assert response.status_code == HTTP_200_OK, response.json()
+        mock.assert_called_once_with(expected)
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -87,8 +87,8 @@ def test_parse_query_string() -> None:
         "value": ["10"],
         "veggies": ["tomato", "potato", "aubergine"],
         "calories": ["122.53"],
-        "healthy": [True],
-        "polluting": [False],
+        "healthy": ["True"],
+        "polluting": ["False"],
     }