Metadata-Version: 2.5
Name: wagtail-block-reference
Version: 0.3.2
Summary: BlockReference support for Wagtail - enables forward and cyclic block references
Project-URL: Repository, https://github.com/joeyjurjens/wagtail-block-reference
Author-email: Joey Jurjens <joeyjurjens@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Wagtail
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Requires-Dist: wagtail>=7.0
Requires-Dist: wrapt>=2.3.0
Description-Content-Type: text/markdown

# wagtail-block-reference

BlockReference support for Wagtail - enables forward and cyclic block references.

> **Heads up:** this package works by patching Wagtail internals (Python and JS) to support
> forward and cyclic block references, which Wagtail doesn't do out of the box. It exists
> because the upstream PR ([wagtail#14279](https://github.com/wagtail/wagtail/pull/14279))
> may take a while to land (if it ever does), and waiting wasn't an option for me :). Relying
> on internals means relying on internals - so the test suite deliberately targets the
> patched code paths, and a future Wagtail upgrade that breaks something will fail loudly
> rather than silently misbehave.

## Installation

```bash
pip install wagtail-block-reference
```

Add to `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    "wagtail_block_reference",
]
```

## Usage

```python
from wagtail import blocks
from wagtail_block_reference import BlockReference

class CommentBlock(blocks.StructBlock):
    text = blocks.CharBlock()
    replies = blocks.ListBlock(BlockReference(lambda: CommentBlock))
```

## Targets must resolve to the same instance

A cycle ends when a walk meets a target it has already seen, and "already seen" means the
same object. So a reference must resolve to the *same* block instance every time.

Declaring the target as a class, a dotted path, or a class attribute gives you that for
free. A factory does not:

```python
# Fine: one CommentBlock instance, memoised on the reference.
replies = blocks.ListBlock(BlockReference(lambda: CommentBlock))

# Broken: a new block on every resolution, so the graph never closes.
replies = blocks.ListBlock(BlockReference(lambda: registry.build("comment")))
```

The second form is an infinitely deep graph rather than a cyclic one, and no visited-set
can bound it. It shows up most easily with a dynamic block registry - memoise the registry
so a given name always yields the same instance.

This is caught rather than left to exhaust the stack: past
`WAGTAIL_BLOCK_REFERENCE_MAX_WALK_DEPTH` levels (default 100), `check()` reports
`wagtailcore.E010` and other walks raise `BlockReferenceDepthExceeded`. Raise the setting
if a graph really is nested that deeply through references.

## How it works

`BlockReference` is a lazy proxy that resolves its target block on first access. The target
can be a lambda (for forward/cyclic references), a dotted import path, or a block class.

The package ships two patches:

**Python patch** (`patches.py`, applied at import time): hooks into Wagtail's block
metaclass so that `BlockReference` attributes declared on a `StructBlock` are collected as
child blocks, and registers a telepath adapter so the block serialises as its resolved target.
Patches are applied with [`wrapt`](https://github.com/GrahamDumpleton/wrapt) rather than
plain attribute assignment - it wraps the live descriptor (including the metaclass's
implicit-staticmethod `__new__`) and hands each wrapper the current original via `wrapped`,
which is more reliable than reassigning `SomeClass.method = ...` outright.

**JS patch** (`patch.js`, injected via `insert_global_admin_js`): waits for Wagtail to set
`window.telepath`, then replaces it outright with a vendored `Telepath` implementation
([wagtail/telepath-unpack#5](https://github.com/wagtail/telepath-unpack/pull/5)) that has
cyclic/back-reference (`_ref`) support built into `unpack` itself, carrying over any
constructors already registered on the instance it replaces. It also wraps `StructBlock` and
`StreamBlock` prototypes with a lazy `childBlockDefsByName` getter - without that, the cyclic
telepath graph causes a `Maximum call stack size exceeded` crash when the editor tries to
build the block name map at construction time.

## Supported versions

All Wagtail versions that are currently under active or security support: 7.0 LTS, 7.3, and 7.4 LTS. CI tests against each of these. Versions drop off the matrix as they go end-of-life.

CI also runs a non-blocking preview job against the latest Wagtail 8.0 pre-release (currently `8.0rc1`), so breakage surfaces before the actual release rather than after.

## Development

```bash
# Install dependencies
uv sync

# Lint, format & type-check
uv run ruff check .
uv run ruff format .
uv run ty check

# Run all tests (unit + E2E) against a single Wagtail version
uv run playwright install chromium
uv run pytest tests/

# Run all tests against all supported Wagtail versions (installs Chromium automatically)
uv run tox

# Run all tests against all supported Wagtail versions (installs Chromium automatically)
# and see visually what the browser is doing
uv run tox  -- --headed --slowmo=500

# Run against a specific version
uv run tox -e wagtail74

# Run against a specific version and see visually what the browser is doing
uv run tox -e wagtail74 -- --headed --slowmo=500
```

## License

MIT
