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 bybindings.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 (withscintilla_qtand Qt) into_pyside6_scintilla.{pyd,so,dylib}.scintilla_edit_base_fixed.h/.cpp,scintilla_document_fixed.h/.cpp-- hand-written subclasses of the vendoredScintillaEditBase/ScintillaDocument, fixing their signals and their str/bytes types (see issues 4 and 5 below).scintilla_edit_fixed.h/.cpp-- generated (checked in) bytools/generate_scintilla_edit.pyfromScintilla.iface: the typed method-per-message editor class, replacing upstream'sWidgetGen.py-generatedScintillaEdit(see issue 5).scintilla_text_encoding.h/.cpp--str<-> document-bytes conversions; not included bybindings.h, so shiboken never binds them.src/pyside6_scintilla/__init__.py-- re-exports the compiled extension's public API as thepyside6_scintillapackage.
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 setgenerate="no"onScintillaEditBase/ScintillaEdit(sinceScintillaEditBaseFixed/ScintillaEditFixedare 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 -- losingScintillaEditBase.h(which internally includesScintillaTypes.hbeforeScintillaStructures.h) leftScintillaStructures.hincluded beforeScintillaTypes.hwith nothing to satisfy its need forPositionfirst. Fix: keepScintillaEditBasefully bound (defaultgenerate="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 dozenremove="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/ScintillaEditFixedneedQ_OBJECT, so CMake'sAUTOMOCruns moc overscintilla_edit_base_fixed.h-- but moc's lightweight parser chokes onScintillaStructures.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 ofScintillaEditBaseinstead. EXPORT_IMPORT_APIonly makes sense across a DLL boundary. VendoredScintillaEditBase/ScintillaDocumentuse this macro because they live inscintilla_qt.dlland are imported by_pyside6_scintilla. Applying the same macro to the*Fixedclasses -- 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 ofstaticMetaObject(error C2491: definition of dllimport static data member not allowed). Fix: don't applyEXPORT_IMPORT_APIto classes fully contained within one compiled module.<modify-function>only applies to the declaring class -- but aremove="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, removingScintillaDocument'sget_char_range(int,int)constalso droppedScintillaDocumentFixed'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:
SCI_<NAME>becomes a camelCased method (SCI_ADDTEXT->addText; getters dropGet:SCI_GETLEXERLANGUAGE->lexerLanguage).char *text becomesstr, in the document's encoding; raw data stays bytes. Colours becomeQColor(issue #17, see below).- A length argument that only measures the string argument is dropped.
- A result buffer is returned instead of passed in.
- 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.cppconverts 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 standaloneScintillaDocument, 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. (ScintillaQtitself 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 untilstyleSetCharacterSetmatches.) Font names and the font locale are always UTF-8, asPlatQt.cppdecodes them. Noneis Scintilla's NULL where NULL means "clear" (annotationSetText,eOLAnnotationSetText,marginSetTexttakestr | None, as upstream'sconst char *did; an empty string sets an empty, still present annotation). AQStringparameter can't be NULL, so these are overridden in__init__.py'sScintillaEdit.- Range getters clamp like Scintilla does:
getBytes(start, end)(andtextRange) resolveend == -1to the document end and clamp out-of-range bounds, so the buffer matches whatEditor::GetTextRangewrites. - 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 encodinggetText()back, or usepositionRelative/countCharacters. strnever loses data silently.SCI_SETTEXT,SCI_INSERTTEXTandSCI_REPLACESELtake NUL-terminated strings, so text containing"\0"would be cut short;setText/insertText/replaceSelthen fall back to a length-based equivalent built onSCI_ADDTEXT(the generator'sNUL_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'scolourmessages take a packed0xBBGGRRint andcolouralphaones0xAABBGGRR-- the opposite byte order from Qt's0xAARRGGBB, soQColor("red").rgb()passed through unchanged would come out blue. The 45 generated methods take and returnQColorinstead (scintilla_colour.hconverts; a plaincolourignores alpha and reads back opaque). PySide6 still converts anint(andstr,Qt.GlobalColor) toQColorimplicitly, 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 anintsince its byte order would be ambiguous. - Pixels are logical pixels, as in upstream's
WidgetGen.pysince Scintilla 5.6.4, which types pixel values aspixelsinScintilla.iface(setMarginWidthN,textWidth,pointXFromPosition,setCaretWidth, ...). UnderScintilla.ScaleTechnique.PixelAligned(setScaleTechnique, Qt only) the engine works in device pixels; the generated methods then scale by the viewport'sScintillaScaleproperty, whichScintillaQtsets to the device-pixel ratio in that mode and to 0 (no scaling) otherwise. So they're unchanged with the default technique, andsend()always sees what the engine uses. The integer methods round; each has a...Ftwin (textWidthF,setMarginWidthNF, ...) with unroundedfloatpixels. Seetests/test_scale_technique.py. targetAsUTF8/encodedFromUTF8aren't generated:ScintillaQtdoesn'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:
- Runs
shiboken6-genpyiagainst the built_pyside6_scintillaextension, working around two issues with the genericshiboken6-genpyientry point on a PySide6-based binding (both detailed in the script's docstring): - it re-imports the extension from a bare path, which fails with "DLL
load failed" unless
pyside6_scintilla(and thereforePySide6.QtWidgets, see issue 3 above) has been imported first; - its
find_imports()references aPySide6global that onlyPySide6.support.generate_pyisets up -- the generic entry point raisesNameError: name 'PySide6' is not definedwithout it. - Drops genpyi's
pyside6_scintilla._pyside6_scintilla.qualifier from the stub's references to its own classes, and its bareimport _pyside6_scintillaself-import -- pyright resolves neither, and an unresolved base class silently makes every inherited memberUnknown(pinned bytests/test_typing_stubs.py). It also dropsmemoryviewfromQByteArrayparameter types (PySide6 annotates it but rejects one at runtime), and typesScintillaDocument'spdoc_asShiboken.VoidPtr. - Aliases
Scintilla.Position/sptr_t/uptr_ttoint. These are primitive typedefs (<primitive-type>inbindings.xml), not nested types, but genpyi emits unresolvableScintilla.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 toPRIMITIVE_ALIASESintools/generate_pyi.py. - Stitches the
# ...doc comments fromsrc/scintilla/include/Scintilla.ifaceintoScintilla.Messageenum members as docstrings (per-member hover docs).val/enum-style features generally don't have these comments, so this mainly benefitsMessage.
_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(aconst char *in C++) is eagerly copied into an independentQByteArrayat access time, valid for as long as you like afterwards. It's raw document bytes,lengthof them for SCN_MODIFIED (deleted text points into the document buffer, with no NUL terminator) -- atextBytes()add-function inbindings.xmlthat__init__.pyexposes as thetextattribute, since shiboken would otherwise read the field as a NUL-terminated UTF-8str(wrong for other code pages, and over-reading deletions). - The
NotificationDataobject 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 (seetests/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:
- 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. - Diff
src/scintilla/qt/ScintillaEditBase/ScintillaEditBase.proagainst the previous release's copy. Update the source list insrc/scintilla_qt/CMakeLists.txtto match any added/removed/renamed.cxx/.hfiles. - Check
src/scintilla/include/ScintillaTypes.h,ScintillaMessages.h, andScintillaStructures.hfor new enums, typedefs, or struct fields thatScintillaEditBase's public API (orScintillaEdit, for Phase 2) now references. Add the corresponding<primitive-type>,<enum-type>, or<value-type>entries tobindings.xml-- this is iterative: run the build, read the shiboken error naming the missing type, add the minimal entry, repeat. - Run
make scintilla-generate(tools/generate_scintilla_edit.py) to regenerateScintillaEditFixedfrom the newScintilla.iface. Review new string-carrying messages (string/stringresult/cellsparameters) in the diff: raw data (style bytes, images) belongs in the generator'sBYTES_FEATURES, always-UTF-8 text inUTF8_FEATURES; anything else is document text, which is the default (see issue 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 / #undefpattern inbindings.hmay 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. - Bump
__version__insrc/pyside6_scintilla/__init__.pyfromX.Y.Z.Nto the new Scintilla release'sX.Y.Z.0(binding-only changes against the same Scintilla release incrementN). - Rebuild with
uv sync --reinstall-package pyside6-scintilla(uv syncalone doesn't always pick up CMake/typesystem changes), thenuv run pytestanduv run ruff check .. Seedocs/build.md.