Repository: litestar-org/litestar

Issue:
Enhancement(Infra): Move away from Poetry
### Summary

This issue serves as a launchpad to potentially swap from Poetry to another solution. We don't have to change or go anywhere, but this is just to get an idea for options, thoughts, and concerns.

Poetry causes many issues, its dependency resolution sometimes takes multiple minutes, they have more than once made questionable changes (https://github.com/python-poetry/poetry/pull/6297), dependencies leak outside of their intended target (https://discord.com/channels/919193495116337154/919193495690936353/1131650830060245094, https://github.com/python-poetry/poetry/issues/4401).

### Basic Example

_No response_

### Drawbacks and Impact

- New tool = new learning curve for everyone
- CI Changes

### Unresolved questions

#### Where to?

- [PDM](https://pdm.fming.dev/latest/) 
- [Hatch](https://hatch.pypa.io/latest/)
- PDM + Hatch's build system, Hatchling
- [Rye](https://github.com/mitsuhiko/rye/) _(experimental)_
- [Huak](https://github.com/cnpryer/huak) _(experimental)_

#### Other

This doesn't really solve the "The Python packaging ecosystem is terrible overall" issue...

> **__NOTE__**: PDM does have a potential issue around [PEP582](https://peps.python.org/pep-0582/) being rejected.

<!-- POLAR PLEDGE BADGE START -->
## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/2025">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2025/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2025/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
I noticed recently that `hatch` has been adopted by [attrs](https://github.com/python-attrs/attrs/pull/1094) (they are a project I've always viewed as setting a good standard as far as project/build/testing configuration goes).

One thing hatch doesn't have is lock files: [ref](https://github.com/pypa/hatch/issues/749), [ref](https://github.com/pypa/hatch/issues/716). However, that's not really an end-user issue, and seems we could do something to work around that by using `pip-tools` to compile a hashed set of requirements for development environments.

Given that hatch is a PyPA project - it probably will be a bit more "boring" than other package managers out there - but sometimes boring is a good thing.

Just my 2c.
Why is this an issue? Sorry, this is not decided. 

Resolving patch:
diff --git a/litestar/contrib/sqlalchemy/dto.py b/litestar/contrib/sqlalchemy/dto.py
--- a/litestar/contrib/sqlalchemy/dto.py
+++ b/litestar/contrib/sqlalchemy/dto.py
@@ -37,7 +37,7 @@
 
 T = TypeVar("T", bound="DeclarativeBase | Collection[DeclarativeBase]")
 
-ElementType: TypeAlias = "Column[Any] | RelationshipProperty[Any]"
+ElementType: TypeAlias = "Column | RelationshipProperty"
 SQLA_NS = {**vars(orm), **vars(sql)}
 
 
diff --git a/litestar/contrib/sqlalchemy/plugins/init/config/common.py b/litestar/contrib/sqlalchemy/plugins/init/config/common.py
--- a/litestar/contrib/sqlalchemy/plugins/init/config/common.py
+++ b/litestar/contrib/sqlalchemy/plugins/init/config/common.py
@@ -52,7 +52,7 @@ class GenericSessionConfig(Generic[ConnectionT, EngineT, SessionT]):
     bind: EngineT | ConnectionT | None | EmptyType = Empty
     """The :class:`Engine <sqlalchemy.engine.Engine>` or :class:`Connection <sqlalchemy.engine.Connection>` that new
     :class:`Session <sqlalchemy.orm.Session>` objects will be bound to."""
-    binds: dict[type[Any] | Mapper[Any] | TableClause | str, EngineT | ConnectionT] | None | EmptyType = Empty
+    binds: dict[type[Any] | Mapper | TableClause | str, EngineT | ConnectionT] | None | EmptyType = Empty
     """A dictionary which may specify any number of :class:`Engine <sqlalchemy.engine.Engine>` or :class:`Connection
     <sqlalchemy.engine.Connection>` objects as the source of connectivity for SQL operations on a per-entity basis. The
     keys of the dictionary consist of any series of mapped classes, arbitrary Python classes that are bases for mapped


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -57,7 +57,7 @@ jobs:
           path: ~/.cache
           key: cache-${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.pydantic-version }}-${{ hashFiles('**/poetry.lock') }}
       - name: Install dependencies
-        run: poetry install --no-interaction
+        run: poetry install --no-interaction --extras full
       - if: ${{ inputs.pydantic-version == '1' }}
         name: Install pydantic v1
         run: poetry remove pydantic-extra-types && poetry add "pydantic>=1.10.10,<2" piccolo beanie
diff --git a/tests/unit/test_cli/test_cli.py b/tests/unit/test_cli/test_cli.py
--- a/tests/unit/test_cli/test_cli.py
+++ b/tests/unit/test_cli/test_cli.py
@@ -45,7 +45,7 @@ def test_register_commands_from_entrypoint(mocker: "MockerFixture", runner: "Cli
     def custom_group() -> None:
         pass
 
-    @custom_group.command()  # type: ignore[attr-defined]
+    @custom_group.command()
     def custom_command(app: Litestar) -> None:
         mock_command_callback()
 
diff --git a/tests/unit/test_middleware/test_csrf_middleware.py b/tests/unit/test_middleware/test_csrf_middleware.py
--- a/tests/unit/test_middleware/test_csrf_middleware.py
+++ b/tests/unit/test_middleware/test_csrf_middleware.py
@@ -199,7 +199,7 @@ def form_handler(data: dict = Body(media_type=RequestEncodingType.URL_ENCODED))
         _ = client.get("/")
         response = client.get("/")
         html_soup = BeautifulSoup(html.unescape(response.text), features="html.parser")
-        data = {"_csrf_token": html_soup.body.div.form.input.attrs.get("value")}  # type: ignore
+        data = {"_csrf_token": html_soup.body.div.form.input.attrs.get("value")}  # type: ignore[union-attr]
         response = client.post("/", data=data)
         assert response.status_code == HTTP_201_CREATED
         assert response.json() == data