Metadata-Version: 2.4
Name: composeui
Version: 0.3.0
Summary: A framework-agnostic MVVM layer for Python UIs: one app for Qt, Textual and the web
Author-email: El Hadi Moussi <50993404+ehmoussi@users.noreply.github.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ehmoussi/composeui
Project-URL: Documentation, https://ehmoussi.github.io/composeui/
Project-URL: Source, https://github.com/ehmoussi/composeui
Project-URL: Issues, https://github.com/ehmoussi/composeui/issues
Project-URL: Changelog, https://github.com/ehmoussi/composeui/blob/main/CHANGELOG.md
Keywords: mvvm,gui,qt,pyside6,textual,tui,web,htmx,attrs,cattrs,reactive
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Environment :: X11 Applications :: Qt
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: User Interfaces
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: attrs>=26.1.0
Requires-Dist: cattrs>=25.3.0
Requires-Dist: typing-extensions>=4.15.0
Provides-Extra: pyside6
Requires-Dist: pyqtgraph>=0.13.7; extra == "pyside6"
Requires-Dist: pyside6>=6.10.3; extra == "pyside6"
Requires-Dist: qtpy>=2.4.3; extra == "pyside6"
Provides-Extra: tui
Requires-Dist: textual>=8.2.8; extra == "tui"
Requires-Dist: textual-fspicker>=0.6.0; extra == "tui"
Requires-Dist: textual-plotext>=1.0.1; extra == "tui"
Provides-Extra: web
Requires-Dist: itsdangerous>=2.2.0; extra == "web"
Requires-Dist: markdown-it-py>=3.0.0; extra == "web"
Requires-Dist: markupsafe>=3.0.2; extra == "web"
Requires-Dist: python-multipart>=0.0.20; extra == "web"
Requires-Dist: starlette>=0.49.3; extra == "web"
Requires-Dist: uvicorn>=0.39.0; extra == "web"
Provides-Extra: report
Requires-Dist: matplotlib>=3.9; extra == "report"
Provides-Extra: reload
Requires-Dist: watchfiles>=1.1.1; extra == "reload"
Provides-Extra: dataframe
Requires-Dist: narwhals>=2.21.0; extra == "dataframe"
Dynamic: license-file

<h1>
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ehmoussi/composeui/main/docs/_static/composeui-logo-dark.svg">
    <img alt="ComposeUI" src="https://raw.githubusercontent.com/ehmoussi/composeui/main/docs/_static/composeui-logo-light.svg" width="268" height="64">
  </picture>
</h1>

A small, framework-agnostic MVVM layer for building UIs in Python. You declare views as typed
[`attrs`](https://www.attrs.org/) classes. They hold the state of the UI, but no toolkit code: in
MVVM terms, they are the view-models
([how ComposeUI maps to MVVM](https://ehmoussi.github.io/composeui/user-guide/core-concepts.html#how-composeui-maps-to-mvvm)).
A `Renderer` binds each view to a toolkit widget, which plays the View role of MVVM:

- a desktop window with [Qt](https://pypi.org/project/QtPy/), through `qtpy`;
- a terminal with [Textual](https://textual.textualize.io/);
- a web page served with [Starlette](https://www.starlette.io/) and [htmx](https://htmx.org/).

`Signal` objects carry events between view, model, and application code.

> **Status:** early-stage / pre-alpha. The API is small and still moving.

## Why

- **Declarative views.** A view is an `attrs` class with typed fields. You don't build
  widgets or wire their signals by hand.
- **Reactive by construction.** When you set an attribute of a view (`view.text = "..."`),
  the view pushes the change to its renderer. Normal state changes need no `update()` or
  `refresh()` calls.
- **Toolkit-agnostic core.** `composeui.core` doesn't import Qt, Textual, or Starlette
  when Python imports it. Each backend is imported only when a view renders with it.
- **Typed callback injection.** `Signal.connect` and `Application.connect` read the
  parameter names of a callback. They inject `view`, `main_view`, or `model` when the
  callback asks for them. So callbacks stay short, without manual lookups.

## When to use it

- **Choose ComposeUI** when one app must show as a desktop window, in a terminal, and in a
  browser, or may move from one to another later. It also suits an app whose state must be
  typed, saved as a file, and tested without a UI
  ([Testing your app](https://ehmoussi.github.io/composeui/user-guide/testing.html)).
- **Compared with Streamlit,** the views are explicit classes that keep their state, and
  nothing runs the script again on each change. The same app also runs as a desktop
  window or in a terminal.
- **Compared with Qt, Textual, or a web framework alone,** the app gives up the widgets
  that only one toolkit has, and keeps one code base for the three.
- **Not yet** for an app that needs a stable API, or a widget that is still missing, such
  as a context menu or a wizard (see
  [`ROADMAP.md`](https://github.com/ehmoussi/composeui/blob/main/ROADMAP.md)).

## Installation

Requires Python >= 3.9. Install ComposeUI from PyPI with the extras of the backends that you
want, for example the web and Qt backends:

```bash
uv add "composeui[web,pyside6]"     # or: pip install "composeui[web,pyside6]"
```

Each extra adds a backend or a feature:

| Extra | Installs | For |
| --- | --- | --- |
| `web` | Starlette, uvicorn | The web backend, the default |
| `pyside6` | PySide6 | The Qt backend, a desktop window |
| `tui` | Textual | The Textual backend, a terminal |
| `report` | matplotlib | The report images of charts |
| `dataframe` | narwhals | A large table of a pandas, Polars, or PyArrow frame |
| `reload` | watchfiles | A faster `--reload` in development |

To try ComposeUI and its example apps, clone the repository and install every extra:

```bash
git clone https://github.com/ehmoussi/composeui
cd composeui
uv sync --all-extras
```

To choose extras, give them all in one command, for example
`uv sync --extra web --extra pyside6`. Each `uv sync` removes the extras that it does not
name, so a second `uv sync --extra tui` would remove the web and Qt backends.

## Quickstart

A minimal window with a text input. As you type your name, the status bar greets you:

```python
from attrs import Factory

from composeui.core.application import run
from composeui.core.view import InputView, MainView, define_view


@define_view
class GreetingInputView(InputView):
    def __attrs_post_init__(self) -> None:
        super().__attrs_post_init__()
        self.placeholder = "Type your name..."
        self.text_edited.connect(self._on_name_changed)

    # `main_view` is injected automatically because it's a requested parameter.
    def _on_name_changed(self, text: str, *, main_view: MainView) -> None:
        main_view.status_bar.show_message(f"Hello, {text}!" if text else "")


@define_view
class HelloMainView(MainView):
    app_title = "Hello ComposeUI"
    extension = ".hello"  # the file type of its studies, which the File menu saves

    greeting: GreetingInputView = Factory(GreetingInputView)


if __name__ == "__main__":
    run(HelloMainView)
```

Run it with one of the backends. Each backend needs its extra:

```bash
uv run python hello.py --backend qt        # a window: the pyside6 extra
uv run python hello.py --backend textual   # a terminal: the tui extra
uv run python hello.py                     # a web page, the default: the web extra
```

The web backend serves the page at http://127.0.0.1:8000. `run` imports only the backend
that you choose, and leaves the rest of the command line to the app (`--help` lists it).

<img alt="The quickstart on the web: a text field holds Ada, and the status bar says Hello, Ada!" src="https://raw.githubusercontent.com/ehmoussi/composeui/main/docs/images/quickstart-web.png" width="600">

### Next steps

1. Read the [core concepts](https://ehmoussi.github.io/composeui/user-guide/core-concepts.html)
   of the user guide: views, signals, the model, and studies.
2. Build a form with its layout and its checks, in
   [Building views](https://ehmoussi.github.io/composeui/user-guide/building-views.html).
3. Run the example apps, in the order of the
   [examples guide](https://ehmoussi.github.io/composeui/examples-guide.html#where-to-start).
   The first,
   [`src/examples/pipeform.py`](https://github.com/ehmoussi/composeui/blob/main/src/examples/pipeform.py),
   has forms, radio groups, a check box, a read-only table, checks, and a File menu that
   saves and opens:
   `uv run python -m examples.pipeform --backend qt`, or `textual`, or `web`.

## Documentation

The documentation site, at https://ehmoussi.github.io/composeui/, has the guides below, a
widget catalog, and the API reference.

- The [user guide](https://ehmoussi.github.io/composeui/user-guide.html) explains how to build an app: the
  views, their layout, forms and their checks, the widgets, pages, tabs, dialogs, tables,
  charts, [maps](https://ehmoussi.github.io/composeui/user-guide/maps.html), threads, SQLite models, CSV
  imports, translations, the differences between the backends, testing an app, and a skill
  for coding agents.
- The [examples guide](https://ehmoussi.github.io/composeui/examples-guide.html) says what each example app
  shows, and how to run it.
- The [technical guide](https://ehmoussi.github.io/composeui/technical-guide.html) explains how ComposeUI
  works inside, and how to develop it: the architecture, how to add a widget or a backend,
  the tests, and the checks.
- [`CHANGELOG.md`](https://github.com/ehmoussi/composeui/blob/main/CHANGELOG.md) lists the changes of each
  release.
- [`ROADMAP.md`](https://github.com/ehmoussi/composeui/blob/main/ROADMAP.md) lists the work that is left.

## License

MIT, see [`LICENSE`](https://github.com/ehmoussi/composeui/blob/main/LICENSE).
