Metadata-Version: 2.4
Name: witty_for_python
Version: 0.1.2
Summary: Python bindings for the Wt (Web Toolkit) C++ library, generated with nanobind. Built wheels bundle Wt (GPLv2), TinyMCE 6.8.4 (MIT), and link against permissively-licensed system libraries (Boost, libharu, libpng, zlib) — see THIRD_PARTY_LICENSES.md for details.
Author: Adam DePrince
License-Expression: GPL-2.0-only
License-File: LICENSE
License-File: THIRD_PARTY_LICENSES.md
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries
Project-URL: Homepage, https://github.com/adamdeprince/witty_for_python
Project-URL: Repository, https://github.com/adamdeprince/witty_for_python
Project-URL: Issues, https://github.com/adamdeprince/witty_for_python/issues
Project-URL: Wt upstream, https://www.webtoolkit.eu/wt
Project-URL: TinyMCE upstream, https://github.com/tinymce/tinymce
Requires-Python: >=3.12
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Description-Content-Type: text/markdown

# witty_for_python

[![CI](https://github.com/adamdeprince/witty_for_python/actions/workflows/ci.yml/badge.svg)](https://github.com/adamdeprince/witty_for_python/actions/workflows/ci.yml)

Python bindings for [Wt (Web Toolkit)](https://www.webtoolkit.eu/wt) — a C++ widget-tree web framework — generated with [nanobind](https://github.com/wjakob/nanobind) and built against C++23.

> **Independent, unofficial wrapper.** witty_for_python is a personal project by [Adam DePrince](https://adamdeprince.com). It is **not** produced by, endorsed by, sponsored by, or otherwise affiliated with Emweb bv, the authors and copyright holders of Wt. "Wt" is referenced here only in its descriptive sense — to identify the library this software wraps — and remains the property of Emweb. For Wt itself (source, official binaries, support, commercial licensing), go directly to [www.webtoolkit.eu/wt](https://www.webtoolkit.eu/wt).

## Status

Pre-alpha scaffold. Initial bindings cover:

- **Lifecycle**: `WServer`, `WApplication`, `WEnvironment`, `EntryPointType`
- **Containers**: `WContainerWidget`, plus layouts `WBoxLayout` (`WHBoxLayout` / `WVBoxLayout`), `WGridLayout`
- **Widgets**: `WText`, `WPushButton`, `WLineEdit`, `WCheckBox`, `WAnchor`, `WImage`, `WTable` (`WTableCell`, `WTableRow`, `WTableColumn`)
- **Signals**: `Signal`, `EventSignal`, `MouseEventSignal`, `KeyEventSignal` — Python callables, GIL-aware

## Requirements

- C++23 toolchain (gcc ≥ 13, clang ≥ 17)
- CMake ≥ 3.26
- Python ≥ 3.10 (or a free-threaded 3.13t / 3.14t — auto-detected)
- Boost dev headers + zlib + libharu + libpng (Wt's build-time deps; libharu backs `WPdfImage`)
  - Debian / Ubuntu: `sudo apt install libboost-dev libboost-system-dev libboost-thread-dev libboost-filesystem-dev libboost-program-options-dev zlib1g-dev libhpdf-dev libpng-dev`
- Node ≥ 16 + Yarn 1.x (to build the vendored TinyMCE; only needed when `WITTY_FOR_PYTHON_BUILD_TINYMCE=ON`, the default)
  - On Ubuntu with Corepack: `sudo corepack enable yarn`

Two third-party libraries are **vendored** as git submodules and built as part of `pip install` — you do not install either of them separately:

| Submodule         | Version          | License | Bundled as                                              |
| ----------------- | ---------------- | ------- | ------------------------------------------------------- |
| `extern/wt`       | Wt 4.13.2        | GPLv2   | `_libs/libwt.so`, `libwthttp.so`, `_wt_resources/*`     |
| `extern/tinymce`  | TinyMCE 6.8.4    | MIT     | `_wt_resources/tinymce/` (powers `WTextEdit`)           |

See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) for attribution details and the upstream license texts that ride along inside each wheel.

## Build & install

```bash
git clone --recursive git@github.com:adamdeprince/witty_for_python.git
cd witty_for_python
pip install --no-build-isolation -e ".[test]"   # editable + pytest
```

For a non-editable install: `pip install --no-build-isolation .`.

scikit-build-core drives CMake; CMake builds Wt from the `extern/wt` submodule, builds TinyMCE from the `extern/tinymce` submodule (via `yarn build`), links our extension against Wt, and bundles `libwt.so` + `libwthttp.so` + Wt's static resources + the TinyMCE distribution into the package directory. The extension's RPATH is `$ORIGIN/_libs`, so `import witty_for_python` works without any `LD_LIBRARY_PATH` or `~/.local` setup.

First build takes ~13 minutes cold (Wt ≈ 8 min, TinyMCE ≈ 5 min). Both are cached across rebuilds — incremental edits to our own sources finish in under a minute. Pass `-DWITTY_FOR_PYTHON_BUILD_TINYMCE=OFF` to skip the TinyMCE step (the wheel still builds; `WTextEdit` won't have a working asset path).

See [docs/building_wt.md](docs/building_wt.md) for details on the vendored-Wt layout, the CMake options we set on it, and how to bump the pin.

## Run the example

```bash
python examples/gallery.py --docroot . \
    --http-address 127.0.0.1 --http-port 8080
```

Then open <http://localhost:8080>. The static resources Wt serves to the browser are bundled with the package; the example finds them via `witty_for_python.resources_dir`.

## Project layout

```
CMakeLists.txt
pyproject.toml
ext/                    # C++ source — one bind_*.cpp per topic
  common.hpp            # nanobind includes + WString <-> str caster
  module.cpp            # NB_MODULE entrypoint
  bind_signals.cpp      # Signal, EventSignal, MouseEventSignal, KeyEventSignal
  bind_application.cpp  # WObject, WWidget, WInteractWidget, WFormWidget, WApplication, WEnvironment
  bind_container.cpp    # WContainerWidget
  bind_widgets.cpp      # WText, WPushButton, WLineEdit, WCheckBox, WAnchor, WImage, WLink
  bind_table.cpp        # WTable, WTableCell, WTableRow, WTableColumn
  bind_layout.cpp       # WLayout, WBoxLayout, WHBoxLayout, WVBoxLayout, WGridLayout
  bind_server.cpp       # WServer, EntryPointType
src/witty_for_python/            # Python package (compiled extension installed here)
examples/               # Sample Wt apps written in Python
```

## Ownership model

Wt 4 uses `std::unique_ptr` for widget ownership; the bindings mirror this:

- A widget you construct in Python is owned by Python.
- `container.add_widget(widget)` **transfers ownership** of the underlying C++ instance to the container, then re-arms the Python wrapper as a *non-owning alias*. The wrapper stays usable, the call **returns the same Python object** (`identity` and `subtype` preserved), and the fluent shape works:
  ```python
  btn = wt.WPushButton("ok")
  same = container.add_widget(btn)
  assert same is btn                    # identity preserved
  same.clicked.connect(handler)         # wrapper still usable
  container.add_widget(wt.WPushButton("x")).clicked.connect(other)  # chains
  ```
  The same `re-arm after transfer` pattern is applied across the bindings — see [docs/binding_design.md §4](docs/binding_design.md) for the binding-side rules (every widget class is built via `heap_init` so it can transfer to `unique_ptr`; every `add_*`/`set_*`/`insert_*` method calls `nb::inst_set_state` to mark the source wrapper non-owning after the transfer).
- Returning a `WApplication` from an entry-point factory hands ownership to the Wt session manager (the factory is invoked through a hand-written closure that pins `WEnvironment` to `rv_policy::reference` so the non-copyable env isn't copied — see §4.3 of the binding-design doc).

Static typing: nanobind's stubgen sees the fluent methods as `(widget: object) -> object` because the C++ binding takes `nb::object`. `scripts/regenerate_stubs.py` runs a post-processing pass that rewrites those signatures to `(widget: _T) -> _T` with an unbounded `TypeVar`, so:

```python
btn: wt.WPushButton = container.add_widget(wt.WPushButton("ok"))  # type-checker keeps WPushButton
container.add_widget(wt.WPushButton("x")).clicked.connect(handler)  # .clicked resolves
```

The rewrite covers `add_widget`/`add_widgets`/`add_item`/`add_items`/`add_marker`/`add_popup`/`add_tooltip`/`add_button`/`add_series`/`bind_widget`/`add_tab` and a handful of others — anything matching `(self, …, x: object) -> object` or `(self, …, xs: list) -> list` in the raw stub. The bulk variants lift to `list[_T] -> list[_T]`.

## Signal binding

Signals expose a single `connect(callable)` method. The callable's positional-arg count is inspected once at connect time:

- **0 args** → the slot is invoked with no arguments and any payload is dropped.
- **1+ args** → the payload is forwarded through `nb::cast` with `rv_policy::copy`, so the Python object survives outside the synchronous slot call.

```python
# Both forms work — witty_for_python introspects each callable.
button.clicked.connect(lambda: print("clicked"))
button.clicked.connect(lambda evt: print(evt.widget.x, evt.widget.y))
```

The same `connect` works for `Signal[<T>]` payloads (`IntSignal`, `BoolSignal`, `DoubleSignal`, `StringSignal`, `DialogCodeSignal`, `StandardButtonSignal`, `MenuItemSignal`) and `EventSignal` types (`EventSignal`, `MouseEventSignal`, `KeyEventSignal`). Slot exceptions are surfaced via `PyErr_WriteUnraisable` rather than swallowed.

## Shutdown warnings

If you ever see this on interpreter exit:

```
nanobind: leaked N instances!
nanobind: leaked M types!
nanobind: leaked K functions!
...
nanobind: this is likely caused by a reference counting issue in the binding code.
```

it is **cosmetic** — the OS reclaims the memory normally; nothing actually leaks at runtime. It is nanobind's *liveness check at module finalization*: any bound C++ instance still alive when its module is torn down is reported. The root cause is structural: Wt signals hold Python callables (via `std::shared_ptr<nb::object>` in the connection slot lambdas), and that holder chain keeps both the callables and any bound widgets they capture alive past the point where nanobind takes its census.

To prevent the warning, the bindings maintain a process-wide registry of every connection opened through witty_for_python's `connect()` and expose two helpers:

```python
witty_for_python._live_connection_count()   # diagnostic — how many slot holders we keep
witty_for_python._cleanup_all_connections() # disconnect every one of them
```

`witty_for_python/__init__.py` registers `_cleanup_all_connections()` as an `atexit` handler, so under normal interpreter shutdown the connection registry is flushed *before* nanobind's finalizer runs. The slot lambdas are destroyed, their `shared_ptr<nb::object>` holders drop their references to the Python callables, Python's module-clear pass then reclaims the bound wrappers, and the leak check finds nothing.

In practice this means: under a clean `sys.exit(0)` or end-of-script termination, **the warning does not appear**. It can still surface if you crash hard, call `os._exit()` (which skips `atexit`), or unregister our handler. You can verify the cleanup is wired up by inspecting `witty_for_python._live_connection_count()` in your own atexit handler.

You may also call `witty_for_python._cleanup_signal_slots()` directly between tests, or call `signal.disconnect_all_slots()` on an individual signal — both go through the same registry.

## License

witty_for_python is licensed under the **GNU General Public License, Version 2 only** — the same restriction Wt itself imposes ("Other versions of the GPL do not apply"). The full text is in [LICENSE](LICENSE).

A Wt commercial license obtained from Emweb does **not** grant any commercial license to witty_for_python. The two are independent works with independent copyright holders; a license to one is not a license to the other. witty_for_python is currently available **only** under GPLv2.

### Bundled third-party software

Built wheels redistribute several upstream open-source projects — see [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) for full attribution and license texts.

**Vendored at source level** (git submodules under `extern/`, so the exact source for any binary we ship is traceable to a specific upstream commit):

- **Wt 4.13.2** — GPL-2.0-only ("Wt OSS license"). The licensing peer of this project.
- **TinyMCE 6.8.4** — MIT license. Backs `WTextEdit`.

**Linked transitively** (pulled from the build environment's system packages; the resulting `.so` files end up inside the wheel via `auditwheel repair` during the manylinux release pipeline):

- **Boost** — Boost Software License 1.0. Used by Wt for threading, filesystem, and command-line parsing.
- **libharu** — zlib/libpng license. Backs `WPdfImage`.
- **libpng** — libpng license. Pulled in by libharu for PNG image embedding.
- **zlib** — zlib license. Compression used pervasively across Wt + libharu.

All linked libraries are permissively licensed and combine cleanly with this project's GPL-2.0-only license; the wheel can be redistributed under our license without additional restrictions from these libraries.

Copyright (C) 2026 Adam DePrince. All rights reserved.
