Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e42ac1
refactor(types): introduce TagifiedTagList/TagifiedTag subclasses (#116)
schloerke May 19, 2026
84397cf
refactor(types): unquote TagifiedNode forward ref; add new names to _…
schloerke May 19, 2026
fa7fd5b
refactor(types): TagList.tagify() constructs TagifiedTagList directly…
schloerke May 19, 2026
7d16b34
docs(_core): clarify why TagList.tagify boundary check casts cp (#116)
schloerke May 19, 2026
2a3e02e
refactor(types): Tag.tagify() constructs TagifiedTag directly (#116)
schloerke May 19, 2026
0cf5d44
fix(_core): make _equals_impl symmetric across subclass relationships…
schloerke May 19, 2026
8337889
refactor(types): JSXTag.tagify() returns TagifiedTag (#116)
schloerke May 19, 2026
a9e169a
feat(types): narrow TagifiedTagList mutators to TagifiedChild (#116)
schloerke May 19, 2026
c210fd8
refactor(_core): drop redundant quotes; document LSP narrowing on Tag…
schloerke May 19, 2026
562e0f4
feat(types): narrow TagifiedTag mutators to TagifiedChild (#116)
schloerke May 19, 2026
b725fe2
test(types): flip TagifiedTagList.append test to negative form (#116)
schloerke May 19, 2026
399498d
test(types): document pyright's permissive variance on Tagified flows…
schloerke May 19, 2026
a6379ff
feat(types): export TagifiedTag, TagifiedTagList (now class), Tagifie…
schloerke May 19, 2026
cfb7e2e
docs(_core): refresh TagChild non-generic rationale comment (#116)
schloerke May 19, 2026
3246446
docs(changelog): document #116 subclass migration
schloerke May 19, 2026
246ea0d
feat(types): collapse Tagified/TagifiedChild and normalize child.tagi…
schloerke May 19, 2026
5754005
docs(changelog): consolidate #116 / #117 entries
schloerke May 19, 2026
4d31a1e
docs(changelog): clean up TagifiedTagList mutator entry wording (#116)
schloerke May 19, 2026
992cc65
docs(changelog): consolidate 0.7.0 entries
schloerke May 19, 2026
44f6979
fix(_core): tighten post-tagify boundary to reject bare Tag/TagList
schloerke May 19, 2026
b7c0416
refactor(_core): define TagifiedChild internally; Tagified = Tagified…
schloerke May 19, 2026
eef3353
docs(_core): trim Tagified-aliases header; demote LSP note to comment
schloerke May 19, 2026
b39be60
docs(_jsx): drop was/now diff annotation from JSXTag.tagify comment
schloerke May 19, 2026
502b3fc
docs(_core): rewrite LSP override notes for readability
schloerke May 19, 2026
032e3d9
docs(_core): note Liskov-clean alternative if pyright recursive-alias…
schloerke May 19, 2026
c8cdd1a
test(types): add canary for LSP-narrowing override suppression
schloerke May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 43 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking changes

* `Tagifiable.tagify()` now returns `Tagified`, a tighter type that
excludes the un-resolved `Tagifiable` arm of `TagNode`. Custom
`.tagify()` implementations annotated with bare `TagList` or `Tag`
return types will fail static type checking; update them to
`-> Tagified` (or omit the return annotation). Runtime behavior of
correct `.tagify()` implementations is unchanged. (#105)
* `Tagifiable.tagify()` now returns `Tagified` — a non-generic union
that mirrors `TagChild`'s shape (including the flattening
conveniences `float`, `None`, `Sequence[Tagified]`) but excludes the
un-resolved `Tagifiable` arm. Custom `.tagify()` implementations
annotated with bare `TagList` or `Tag` return types will fail
static type checking; update them to `-> Tagified` (or omit the
return annotation). Runtime behavior of correct implementations is
unchanged. (#105, #116, #117)

* `Tag.tagify()` no longer preserves the caller's `Tag` subclass in
its return type. Code relying on the previous subclass-preserving
signature should `cast` the result. (#105)

* `TagifiedTagList` is now a real subclass of `TagList["TagifiedNode"]`
(previously a `TypeAliasType` alias). `TagifiedTag` is a new parallel
subclass of `Tag["TagifiedNode"]`. Both are returned by `.tagify()`
and are runtime-`isinstance`-checkable. Their `__init__` / `append`
/ `extend` / `insert` are statically narrowed to `Tagified`, so
pyright flags `tagified.append(SomeTagifiable())` at the call site.
The classes stay internal to `htmltools._core` — they are not
re-exported from the top-level `htmltools` namespace — so code that
wants to `isinstance`-check imports them explicitly from there.
Code that depended on `TagifiedTagList` being an alias (e.g.,
`typing.get_type_hints` introspection) needs to treat it as a
class instead.

In practice, pyright remains permissive about flowing a
`TagifiedTagList` into a parameter typed as bare `TagList` (or
`TagList[TagNode]`), so most downstream consumers of `.tagify()`'s
return do NOT need migration. (#116)

### New features

* `Tag` and `TagList` are now generic in their child type, defaulting
to `TagNode`. Bare `Tag` / `TagList` retain today's meaning. Mutation
methods (`append` / `extend` / `insert`) still accept `Tagifiable` at
static-type-check time even on tagified containers — the invariant
is enforced at runtime instead (`TagList.tagify()` raises `TypeError`
and `get_html_string` raises `RuntimeError` for an un-tagified
subtree). See `tests/test_types.py` for the rationale. (#105)

* Added the public type alias `Tagified` — the union of all
fully-tagified shapes — for use as the return annotation of
`Tagifiable.tagify()` implementations. (#105)
to `TagNode`. Bare `Tag` / `TagList` retain today's meaning. (#105)

### Bug fixes

* `TagList.tagify()` now raises `TypeError` at the boundary when a child's `.tagify()` returns a `TagList` containing an un-tagified `Tagifiable` object. The error names the offending class and slot index so buggy `.tagify()` implementations surface at the source rather than later at render time. The render-time `RuntimeError` raised by `get_html_string()` for an un-tagified child has also been clarified to include the offending class name and a hint that the tree was likely mutated after `.tagify()` was called. (#7, #105, #112)
* `TagList.tagify()` now defensively normalizes every shape a child's
`.tagify()` can return. A return whose contents still include an
un-tagified `Tagifiable`, or a return that is itself a bare `Tag` /
`TagList` rather than a `TagifiedTag` / `TagifiedTagList`, raises
`TypeError` at the boundary, naming the offending class and slot
index. (Pyright already rejects these at the call site via the
`Tagified` return-type annotation; the runtime guard catches
implementations that bypass static checking.) `None` is dropped,
`float`/`int` is str-ified, and `Sequence` is flattened —
previously these last three shapes either crashed the render path
(`None` → `TypeError` in `html_escape`) or silently corrupted the
tag tree. The render-time `RuntimeError` raised by
`get_html_string()` for an un-tagified child has also been
clarified to include the offending class name and a hint that the
tree was likely mutated after `.tagify()` was called. (#7, #105,
#112, #117)

### Dependencies

Expand Down
Loading
Loading