Skip to content

The shiboken6 bindings

Note

Paths on this page are relative to packages/pyside6-scintilla/, except repo-level ones (docs/, tools/, .github/, examples/).

How _pyside6_scintilla is built, what shiboken generates, problems encountered while wrapping ScintillaEditBase (Phase 1), and how to update everything when a new Scintilla release is vendored.

How it fits together

  • src/scintilla/ -- vendored Scintilla release tarball, unmodified (see .gitattributes; see docs/auditing.md for how to verify this).
  • src/scintilla_qt/CMakeLists.txt -- builds the vendored Scintilla core plus its Qt widget glue (PlatQt, ScintillaQt, ScintillaEditBase) as a shared library, scintilla_qt.
  • src/pyside6_scintilla/bindings/
  • bindings.h -- header shiboken's ApiExtractor parses to discover the C++ declarations referenced by bindings.xml.
  • bindings.xml -- the shiboken typesystem: declares which C++ types/enums/classes get Python wrappers.
  • CMakeLists.txt -- runs shiboken to generate the wrapper sources below, then compiles and links them (with scintilla_qt and Qt) into _pyside6_scintilla.{pyd,so,dylib}.
  • scintilla_edit_base_fixed.h/.cpp, scintilla_document_fixed.h/.cpp -- hand-written subclasses of the vendored ScintillaEditBase/ ScintillaDocument, fixing their signals and their str/bytes types (see issues 4 and 5 below).
  • scintilla_edit_fixed.h/.cpp -- generated (checked in) by tools/generate_scintilla_edit.py from Scintilla.iface: the typed method-per-message editor class, replacing upstream's WidgetGen.py-generated ScintillaEdit (see issue 5).
  • scintilla_text_encoding.h/.cpp -- str <-> document-bytes conversions; not included by bindings.h, so shiboken never binds them.
  • src/pyside6_scintilla/__init__.py -- re-exports the compiled extension's public API as the pyside6_scintilla package.

Generated files

shiboken6 generates these files (under build/<tag>/src/pyside6_scintilla/bindings/pyside6_scintilla/_pyside6_scintilla/, one directory per component of the typesystem's dotted package name) from bindings.h + bindings.xml. They are build artifacts, not checked in, and are not hand-edited or hand-commented.

File Purpose Generated by
_pyside6_scintilla_module_wrapper.cpp Module init (PyInit__pyside6_scintilla); registers all wrapped types with the extension module shiboken6, from the <typesystem> root
scintilla_wrapper.cpp / .h Wrapper for the Scintilla namespace and its enums (Update, Message, KeyMod, ...) shiboken6, from <namespace-type name="Scintilla">
scintilla_notificationdata_wrapper.cpp / .h Wrapper for the Scintilla::NotificationData value type shiboken6, from <value-type name="NotificationData">
scintillaeditbase_wrapper.cpp / .h Wrapper for the ScintillaEditBase widget's public methods/signals/slots shiboken6, from <object-type name="ScintillaEditBase"/>
scintillaeditbasefixed_wrapper.cpp, scintillaeditfixed_wrapper.cpp, scintilladocument_wrapper.cpp, scintilladocumentfixed_wrapper.cpp / .h Wrappers for the other bound classes (the *Fixed ones are what __init__.py exposes) shiboken6, from their <object-type> entries
_pyside6_scintilla_python.h Master header included by every wrapper .cpp; includes bindings.h plus the PySide6/shiboken headers it needs shiboken6

The GENERATED_SOURCES list in bindings/CMakeLists.txt only needs the .cpp files -- the .h files are pulled in by those .cpp files.

_pyside6_scintilla_python.h is the key one to remember: it has its own include list, separate from bindings.h. Anything done in bindings.h (e.g. #define/#undef tricks) only affects what shiboken's ApiExtractor sees while generating code -- it has no effect on what the generated .cpp files compile against. Fixes that need to apply at compile time belong in bindings/CMakeLists.txt (e.g. as target_compile_definitions).

shiboken's generation step only depends on bindings.h/bindings.xml (its scan of included headers only works with Makefile generators), so bindings/CMakeLists.txt adds the hand-written/generated *_fixed.h headers as explicit dependencies -- otherwise editing one leaves stale wrappers.

Issues encountered (Phase 1: ScintillaEditBase)

1. Duplicate Scintilla::Message enum codegen

ScintillaStructures.h forward-declares enum class Message; ("in case ScintillaMessages.h isn't included") in addition to the full definition in ScintillaMessages.h. Both are in namespace Scintilla, and <enum-type name="Message"/> matches both. shiboken's clang-based parser treats them as two separate EnumDecls and emits duplicate converters/type-index entries for Scintilla::Message (SBK_Scintilla_Message_IDX defined twice, etc.) -- this fails to compile. Reordering includes does not help.

Fix, in bindings.h: include ScintillaMessages.h first (the real Message definition), then wrap #include <ScintillaStructures.h> in #define Message ScintillaMessageFwdDeclUnused / #undef Message so its forward declaration becomes an unrelated, unused enum instead of a second Scintilla::Message. As a side effect, NotificationData::message (whose type is now that renamed dummy enum) has to be dropped via <modify-field name="message" remove="true"/> on NotificationData in bindings.xml.

A leftover, mostly cosmetic warning remains: "Scoped enum Scintilla::ScintillaMessageFwdDeclUnused does not have a type entry" -- consistent with several other pre-existing warnings for unbound scoped enums and safe to ignore.

2. FindText/FindTextW macro collision (Windows)

<windows.h> is pulled in transitively via Python.h before any Qt header gets a chance to define WIN32_LEAN_AND_MEAN. That drags in <commdlg.h>, which #defines FindText to FindTextA/FindTextW. This mangles Scintilla::Message::FindText in the generated wrapper .cpp files -- and since those files don't include bindings.h (see "Generated files" above), an #undef FindText there has no effect on the compile step.

Fix, in bindings/CMakeLists.txt (Windows only): target_compile_definitions(_pyside6_scintilla PRIVATE WIN32_LEAN_AND_MEAN). This stops <commdlg.h> from ever being pulled in, so FindText is never redefined.

3. Qt6Core5Compat.dll not bundled by the PySide6 wheel (Windows)

scintilla_qt links Qt6::Core5Compat (vendored Scintilla's PlatQt.cpp uses QTextCodec, removed from QtCore in Qt6). Qt6Core5Compat.dll is the one Qt6*.dll the PySide6 wheel does not bundle, so _pyside6_scintilla.pyd -> scintilla_qt.dll -> Qt6Core5Compat.dll fails to load at import time.

Fix, in bindings/CMakeLists.txt (Windows only): install(FILES $<TARGET_FILE:Qt6::Core5Compat> DESTINATION pyside6_scintilla) ships Qt6Core5Compat.dll alongside scintilla_qt.dll/_pyside6_scintilla.pyd, where Windows' default same-directory DLL search finds it. Also requires scintilla_qt itself to be installed (RUNTIME/LIBRARY DESTINATION pyside6_scintilla), and src/pyside6_scintilla/__init__.py to import PySide6.QtWidgets before importing the compiled extension, so PySide6 registers its own DLL search directory for Qt6Widgets.dll/Qt6Core.dll/pyside6.abi3.dll/ shiboken6.abi3.dll first.

Not yet addressed for Linux/macOS wheels: Qt6Core5Compat bundling there will need delvewheel/auditwheel/delocate in CI, or an install(FILES ...) step analogous to the Windows one. _pyside6_scintilla's INSTALL_RPATH is already set to $ORIGIN (Linux)/@loader_path (macOS) in anticipation of this.

4. ScintillaEditBase/ScintillaEdit signals carrying Scintilla::Position/enum-typed parameters never reach a Python slot

shiboken marshals Scintilla::Position/ModificationFlags/FoldLevel/etc. fine for ordinary method arguments (they're registered as <primitive-type>/<enum-type> in bindings.xml), but Qt's generic meta-call path -- used to invoke a Python slot from a C++ signal emission -- can't convert them. Connecting a Python slot to e.g. ScintillaEditBase.modified looked like it worked (connect() raised nothing), but the slot was never actually called: Qt's event loop swallows the resulting TypeError. Registering these as Qt meta-types was considered and rejected in favor of re-emission (below), to avoid baking Scintilla-specific Q_DECLARE_METATYPE registration into a generic binding layer.

Fix: bindings/scintilla_edit_base_fixed.h (a hand-written, non-vendored header, included from bindings.h) declares a subclass, ScintillaEditBaseFixed, that connects each affected signal in C++ to a lambda re-emitting the same data as a plain-typed (int/QString) signal of the same name -- the same pattern already used by ScintillaDocument::modified (see src/scintilla/qt/ScintillaEdit/ScintillaDocument.h). The generated ScintillaEditFixed derives from it, so it inherits the fix. PySide creates a Signal descriptor for each signal it finds in a class's own QMetaObject, so the subclass's redeclaration simply shadows the inherited broken-signature one through normal Python attribute/MRO lookup -- the same mechanism that lets a Python subclass override a method by just redefining it, no typesystem changes involved. src/pyside6_scintilla/__init__.py imports the Fixed classes aliased to the public ScintillaEditBase/ScintillaEdit names, so this is invisible from the public API.

A few non-obvious wrinkles along the way:

  • generate="no" breaks header include order. The first attempt set generate="no" on ScintillaEditBase/ScintillaEdit (since ScintillaEditBaseFixed/ScintillaEditFixed are the only Python-visible classes) but that drops the header from shiboken's "Bound library includes" list in the generated module header, which is sorted alphabetically -- losing ScintillaEditBase.h (which internally includes ScintillaTypes.h before ScintillaStructures.h) left ScintillaStructures.h included before ScintillaTypes.h with nothing to satisfy its need for Position first. Fix: keep ScintillaEditBase fully bound (default generate="yes") -- a <modify-function remove="all"/> on the broken signal overloads was tried too, on the assumption it'd avoid an ambiguous Python overload, but turned out to be a no-op (see the next point) and was dropped as dead weight.
  • <modify-function> doesn't apply to Qt signals. Unlike ordinary methods, Qt signals are exposed to Python via PySide's own metaobject introspection at class-creation time, not via shiboken's typesystem-driven method generation -- so a <modify-function signature="..." remove="all"/> targeting a signal silently has no effect (confirmed: removing the dozen remove="all" rules entirely, the build and full test suite behaved identically). The fix above never needed them -- the shadowing happens purely through normal Python class attribute/MRO lookup, the same mechanism a Python subclass uses to override any other inherited attribute by just redefining it.
  • moc can't parse the vendored headers. ScintillaEditBaseFixed/ ScintillaEditFixed need Q_OBJECT, so CMake's AUTOMOC runs moc over scintilla_edit_base_fixed.h -- but moc's lightweight parser chokes on ScintillaStructures.h's more intricate C++ once it's transitively included. moc only actually needs the base class names for the generated metaobject, not their full definitions. Fix: guard the real includes with #ifdef Q_MOC_RUN / #else, giving moc bare forward declarations of ScintillaEditBase instead.
  • EXPORT_IMPORT_API only makes sense across a DLL boundary. Vendored ScintillaEditBase/ScintillaDocument use this macro because they live in scintilla_qt.dll and are imported by _pyside6_scintilla. Applying the same macro to the *Fixed classes -- which are defined and compiled entirely within _pyside6_scintilla, no DLL boundary crossed -- resolved to __declspec(dllimport) on Windows and collided with moc's own definition of staticMetaObject (error C2491: definition of dllimport static data member not allowed). Fix: don't apply EXPORT_IMPORT_API to classes fully contained within one compiled module.
  • <modify-function> only applies to the declaring class -- but a remove="all" on a base class method also removes a subclass method with the same signature. shiboken ignores a <modify-function> rule registered against a subclass that merely inherits the method rather than redeclaring it. Conversely, removing ScintillaDocument's get_char_range(int,int)const also dropped ScintillaDocumentFixed's same-signature replacement, so only base members whose parameter types change are removed (C++ name hiding takes care of the rest).

See tests/test_scintilla_edit_base.py::test_modified_signal_reaches_python_slot for the regression test.

5. Text vs bytes (issue #16)

Upstream's ScintillaEdit (generated by src/scintilla/qt/ScintillaEdit/WidgetGen.py, and stale: about 40 messages behind the vendored Scintilla.iface) maps every string parameter to const char * and every string result to QByteArray. Through shiboken that became an API that took str (always UTF-8 encoded, whatever the document's code page), returned bytes (getText(n) even padded with NULs), had byte-length parameters the caller had to compute in an encoding they never chose, and whose stub claimed bytes was accepted too (it wasn't). A wrong argument type then failed inside shiboken's error reporting with NameError: name '_pyside6_scintilla' is not defined, since the typesystem package was the bare module name.

Fix: a strict, consistent contract, with no implicit conversions:

Kind Examples Python type
Text: document text, names, display strings setText/getText, insertText, textRange, getLine, setKeyWords, styleSetFont, annotationSetText, ScintillaDocument.insert_string/get_char_range, modified's text str in and out
Raw data: style bytes, images setStylingEx, addStyledText, marginSetStyles/marginStyles, registerImage, registerRGBAImage bytes in (bytes/bytearray/QByteArray), QByteArray out
Raw messages ScintillaEditBase.send/sends/sendr int; sends takes bytes, sendr returns bytes
Document bytes, for byte-offset work getBytes()/getBytes(start, end), ScintillaDocument.get_byte_range QByteArray out

Scintilla's own API is uniformly byte-based (every string a char *, every position a byte offset); ScintillaEdit translates it by one rule, so ScintillaDoc.html stays usable as the reference:

  1. SCI_<NAME> becomes a camelCased method (SCI_ADDTEXT -> addText; getters drop Get: SCI_GETLEXERLANGUAGE -> lexerLanguage).
  2. char * text becomes str, in the document's encoding; raw data stays bytes. Colours become QColor (issue #17, see below).
  3. A length argument that only measures the string argument is dropped.
  4. A result buffer is returned instead of passed in.
  5. Positions and lengths stay byte counts.

Every generated method's docstring names the message it sends and spells out how its signature differs (from Method.doc_note() in the generator, stitched in by tools/generate_pyi.py), e.g. addText: "Sends SCI_ADDTEXT. No length argument: it's the byte length of the encoded text."

  • Encoding is the widget's job, like Qt's text widgets: scintilla_text_encoding.cpp converts with the document's encoding -- UTF-8 in Unicode mode; otherwise a DBCS code page (932/936/949/950) names the codec, else the default style's character set does (Latin-1 for a standalone ScintillaDocument, which has no styles). One lookup serves both the editor and the document, so text written through one reads back the same through the other. (ScintillaQt itself only consults the character set, so a DBCS document whose default style still has the default character set is handled by us but drawn as Latin-1 by Scintilla until styleSetCharacterSet matches.) Font names and the font locale are always UTF-8, as PlatQt.cpp decodes them.
  • None is Scintilla's NULL where NULL means "clear" (annotationSetText, eOLAnnotationSetText, marginSetText take str | None, as upstream's const char * did; an empty string sets an empty, still present annotation). A QString parameter can't be NULL, so these are overridden in __init__.py's ScintillaEdit.
  • Range getters clamp like Scintilla does: getBytes(start, end) (and textRange) resolve end == -1 to the document end and clamp out-of-range bounds, so the buffer matches what Editor::GetTextRange writes.
  • Length parameters that only describe the string argument are dropped (addText(text), appendText, replaceTarget, searchInTarget, ...): the binding computes them from the encoded text. Getters take no buffer size; getCurLine() returns (text, caret).
  • Positions stay byte positions -- Scintilla's model. Where byte offsets matter, read the document's stored bytes directly with getBytes() (as the tree-sitter example does) instead of encoding getText() back, or use positionRelative/countCharacters.
  • str never loses data silently. SCI_SETTEXT, SCI_INSERTTEXT and SCI_REPLACESEL take NUL-terminated strings, so text containing "\0" would be cut short; setText/insertText/replaceSel then fall back to a length-based equivalent built on SCI_ADDTEXT (the generator's NUL_SAFE_FALLBACKS), with the same caret and undo behaviour. Other NUL-terminated messages carry names or display strings, where a NUL has no meaning.
  • Colours are QColor (issue #17). Scintilla's colour messages take a packed 0xBBGGRR int and colouralpha ones 0xAABBGGRR -- the opposite byte order from Qt's 0xAARRGGBB, so QColor("red").rgb() passed through unchanged would come out blue. The 45 generated methods take and return QColor instead (scintilla_colour.h converts; a plain colour ignores alpha and reads back opaque). PySide6 still converts an int (and str, Qt.GlobalColor) to QColor implicitly, in Qt's order: kept deliberately for consistency with the rest of PySide6, though it means old code passing Scintilla's packed ints now gets red and blue swapped, and loses alpha (QColor(QRgb) ignores it). For the same reason an invalid colour name becomes black there rather than an error. send() keeps Scintilla's packed ints; colour_to_scintilla()/ colour_from_scintilla() (in __init__.py) convert for it, and reject an int since its byte order would be ambiguous.
  • Pixels are logical pixels, as in upstream's WidgetGen.py since Scintilla 5.6.4, which types pixel values as pixels in Scintilla.iface (setMarginWidthN, textWidth, pointXFromPosition, setCaretWidth, ...). Under Scintilla.ScaleTechnique.PixelAligned (setScaleTechnique, Qt only) the engine works in device pixels; the generated methods then scale by the viewport's ScintillaScale property, which ScintillaQt sets to the device-pixel ratio in that mode and to 0 (no scaling) otherwise. So they're unchanged with the default technique, and send() always sees what the engine uses. The integer methods round; each has a ...F twin (textWidthF, setMarginWidthNF, ...) with unrounded float pixels. See tests/test_scale_technique.py.
  • targetAsUTF8/encodedFromUTF8 aren't generated: ScintillaQt doesn't handle those messages (only the Win32/GTK/Cocoa platform layers do).

tools/generate_scintilla_edit.py generates ScintillaEditFixed from Scintilla.iface, with a small classification table (bytes vs. text vs. UTF-8-only messages); make scintilla-generate regenerates it and CI checks the checked-in copy is current. ScintillaEditBaseFixed replaces the vendored send/sends (removed in bindings.xml) with qint64/quint64-typed, non-virtual versions plus sendr: shiboken's signature support doesn't know Scintilla's sptr_t/uptr_t (an "UNRECOGNIZED" RuntimeWarning per method), and PySide converts qintptr to Python through a 32-bit long on Windows, truncating pointers such as GetDocPointer's result. bindings.xml's package is now the dotted pyside6_scintilla._pyside6_scintilla, so failed calls raise a proper TypeError. See tests/test_text_api.py.

Type stubs (_pyside6_scintilla.pyi)

src/pyside6_scintilla/_pyside6_scintilla.pyi (plus the PEP 561 src/pyside6_scintilla/py.typed marker) gives Pylance/mypy/ruff/pylint full signatures and autocomplete for Scintilla.*/ScintillaEditBase. Unlike the generated wrapper .cpp/.h files above, it's checked into git -- it directly improves the local dev/IDE experience (e.g. for examples/simple_scintilla_base_edit/main.py) and is picked up by wheel.packages for published wheels, without adding a PySide6-importing step to the CMake/CI build.

Regenerate it with make scintilla-stubs (tools/generate_pyi.py) after rebuilding the extension (make scintilla-install or uv sync --reinstall-package pyside6-scintilla), and whenever bindings.xml/bindings.h change the public API surface. The script:

  1. Runs shiboken6-genpyi against the built _pyside6_scintilla extension, working around two issues with the generic shiboken6-genpyi entry point on a PySide6-based binding (both detailed in the script's docstring):
  2. it re-imports the extension from a bare path, which fails with "DLL load failed" unless pyside6_scintilla (and therefore PySide6.QtWidgets, see issue 3 above) has been imported first;
  3. its find_imports() references a PySide6 global that only PySide6.support.generate_pyi sets up -- the generic entry point raises NameError: name 'PySide6' is not defined without it.
  4. Drops genpyi's pyside6_scintilla._pyside6_scintilla. qualifier from the stub's references to its own classes, and its bare import _pyside6_scintilla self-import -- pyright resolves neither, and an unresolved base class silently makes every inherited member Unknown (pinned by tests/test_typing_stubs.py). It also drops memoryview from QByteArray parameter types (PySide6 annotates it but rejects one at runtime), and types ScintillaDocument's pdoc_ as Shiboken.VoidPtr.
  5. Aliases Scintilla.Position/sptr_t/uptr_t to int. These are primitive typedefs (<primitive-type> in bindings.xml), not nested types, but genpyi emits unresolvable Scintilla.Position-style forward references for them (NotificationData's # type: comments). If a future Scintilla release adds another such primitive typedef, genpyi will warn (RuntimeWarning: ... UNRECOGNIZED: 'Scintilla.<name>') -- add it to PRIMITIVE_ALIASES in tools/generate_pyi.py.
  6. Stitches the # ... doc comments from src/scintilla/include/Scintilla.iface into Scintilla.Message enum members as docstrings (per-member hover docs). val/enum-style features generally don't have these comments, so this mainly benefits Message.

_pyside6_scintilla.pyi is excluded from ruff (extend-exclude in pyproject.toml) since it's machine-generated and not ruff-format-compliant.

Lifetime & ownership

Audit of the wrapped types for memory-safety/lifetime surprises beyond standard PySide6/Qt semantics (Phase 3 Tracer 6).

ScintillaEditBase/ScintillaEdit as QWidgets

No caveat: standard Qt/PySide6 ownership rules apply. A parentless widget is kept alive by Python references, and is destroyed -- its signals included -- once the last reference is dropped. A widget with a Qt parent is kept alive by that parent regardless of Python references, and its signals (notify, command, macroRecord, etc.) keep firing as long as the widget exists.

NotificationData (delivered via notify)

ScintillaEditBase.notify(NotificationData *pscn) hands Python a wrapper around a transient struct that Scintilla reuses across notifications.

  • Reading a field during the handler call is safe -- e.g. text (a const char * in C++) is eagerly copied into an independent QByteArray at access time, valid for as long as you like afterwards. It's raw document bytes, length of them for SCN_MODIFIED (deleted text points into the document buffer, with no NUL terminator) -- a textBytes() add-function in bindings.xml that __init__.py exposes as the text attribute, since shiboken would otherwise read the field as a NUL-terminated UTF-8 str (wrong for other code pages, and over-reading deletions).
  • The NotificationData object itself must not be retained past the handler call: once later notifications overwrite the shared struct, its fields no longer reflect the notification it was originally delivered for (see tests/test_scintilla_notify.py).

Where available, prefer ScintillaEditBase's typed per-notification signals (modified, charAdded, marginClicked, etc.) instead -- e.g. modified's text parameter is already a decoded str, with no such caveat.

ScintillaDocument from get_doc()

Already documented on ScintillaEdit.get_doc()'s docstring and its reference page: the returned ScintillaDocument has no Qt parent, so it's kept alive only by your Python reference to it -- drop it and its modified/ save_point/etc. signals stop firing, even though the underlying document buffer stays alive via Scintilla's own refcounting as long as an editor uses it (see tests/test_scintilla_document.py).

Updating to a new Scintilla release

Per docs/specs/mission.md, Scintilla updates are deliberate and tested, not automatic. When vendoring a new release tarball:

  1. Replace src/scintilla/ with the new release's contents (remove the old tree first so deleted upstream files are actually removed, then extract the new tarball with --strip-components=1). .gitattributes (src/scintilla/** -text) keeps the diff clean.
  2. Diff src/scintilla/qt/ScintillaEditBase/ScintillaEditBase.pro against the previous release's copy. Update the source list in src/scintilla_qt/CMakeLists.txt to match any added/removed/renamed .cxx/.h files.
  3. Check src/scintilla/include/ScintillaTypes.h, ScintillaMessages.h, and ScintillaStructures.h for new enums, typedefs, or struct fields that ScintillaEditBase's public API (or ScintillaEdit, for Phase 2) now references. Add the corresponding <primitive-type>, <enum-type>, or <value-type> entries to bindings.xml -- this is iterative: run the build, read the shiboken error naming the missing type, add the minimal entry, repeat.
  4. Run make scintilla-generate (tools/generate_scintilla_edit.py) to regenerate ScintillaEditFixed from the new Scintilla.iface. Review new string-carrying messages (string/stringresult/cells parameters) in the diff: raw data (style bytes, images) belongs in the generator's BYTES_FEATURES, always-UTF-8 text in UTF8_FEATURES; anything else is document text, which is the default (see issue 5).
  5. Re-check whether the duplicate-Message-enum workaround (issue 1 above) still applies as written. If Scintilla restructures its headers, the #define <Type> ... / #include / #undef pattern in bindings.h may need to move, target a different type, or no longer be needed -- the same pattern applies if a different enum/struct gets a forward-declaration collision in a future release.
  6. Bump __version__ in src/pyside6_scintilla/__init__.py from X.Y.Z.N to the new Scintilla release's X.Y.Z.0 (binding-only changes against the same Scintilla release increment N).
  7. Rebuild with uv sync --reinstall-package pyside6-scintilla (uv sync alone doesn't always pick up CMake/typesystem changes), then uv run pytest and uv run ruff check .. See docs/build.md.