Python 3: language-layer cleanup, portable CLR loading and out-params, plus compat tests - #3484
Python 3: language-layer cleanup, portable CLR loading and out-params, plus compat tests#3484ChrisCrosley wants to merge 19 commits into
Conversation
a static AST checker (pipenv run check-py3), a Revit-hosted test suite run by twin DevTools buttons on IronPython and CPython, and the checker's 30-finding baseline.
iteritems in ElementWrapper.__repr__, bare unicode uses in the vendored markdown package, missing __bool__ aliases in forms, and (str, unicode) isinstance tuples. Checker baseline 30 -> 15.
- markdown: resolve extension short names against the vendored package (the hardcoded top-level `markdown.extensions.*` paths never worked on any engine); replace Element.getchildren(), removed in CPython 3.9; skip conversions under IronPython 3, where fat DLR frames overflow the host thread's stack (~768KB needed, measured standalone) — an uncatchable StackOverflowException that hard-crashes Revit. - suite: split native-binary interop imports (rhino/adc) into an opt-in test so a bad native load cannot kill the host; skip managed interop imports on .NET 8 hosts (IxMilia.Dxf/Ifc.Net are not shipped for netcore); replace the grid-based out-param test with Curve.Intersect on pure geometry (Grid.Curve is view-dependent on this host); skip markdown extra under IronPython (re.Scanner unavailable in both 2 and 3). - runner: print failing tests' tracebacks to the output window (printErrors was a no-op).
framework.add_reference_to_file() wraps clr.AddReferenceToFileAndPath (IronPython) / Assembly.LoadFrom (pythonnet); all 13 active call sites converted. Checker IPY-CLR findings 12 -> 0.
query.intersect_curves() wraps Curve.Intersect; load_family branches LoadFamily; FamilyLoaderOptionsHandler gains __namespace__ + dual ref-param conventions for pythonnet interface callbacks. IronPython paths unchanged; CPython suite run fully green (12 pass / 0 errors). Three extension scripts converted to the new wrappers (Section Box Navigator, Load More Types, Load Families). Testing those surfaced two more fixes: itertools.ifilterfalse in file_utils.py (gone in Py3) and deprecated '\d' escapes in non-raw strings (rpws, Logs button). Checker: new CLR-REF rule (clr.Reference outside the sanctioned dispatch sites), Py2-only itertools coverage in all usage forms, and parse-time SyntaxWarnings reported as SYNTAX-WARN findings. Baseline is now 3 (SORT-KEY only).
Apply the pyrevitlabs#3477 sort-key pattern to Get RVT Info, ParametersValuesToParameter, and ExportIFC. Checker is now at 0 findings across the tree
From a review of the branch: - check_py3_compat.py now exits non-zero on a missing or zero-file path (a typo'd path was passing green, neutering the gate) - markdown convert() decodes bytes explicitly instead of stringifying them to "b'...'" - markdown extra.py resolves its package via __name__, not the IronPython-unreliable __package__ - TemplateListItem.__bool__ returns a real bool (Py3 requires it) - opt-in reload test (TEST_HANDLER_REBAKE) for the __namespace__ re-bake question on FamilyLoaderOptionsHandler Wires check-py3 as a CI gate (py3-compat job) now that the tree is at zero findings. The generic-collection List[T](pylist) class surfaced in review (~55 sites, mostly extensions) is deferred to its own PR.
pyrevit.coreutils.markdown has no first-party runtime consumers (output.print_md renders via C#), so stop treating it as a maintained surface: remove its four compat tests and the IPY3 skip helper, drop it from the import test, and exclude coreutils/markdown from check_py3_compat. Unlike rpw (engine-locked WPF legacy) it is pure Python and portable - a deprecated unbundling candidate, not frozen legacy. The Stage 1 syntax/vendoring fixes are kept: they are correct and help third-party callers on every engine; #! python3 scripts should use pip markdown.
dict.keys()/values()/items() and map/filter/zip are non-indexable views in Python 3 (lists in Python 2), breaking on any Py3 engine. Fixes the IPY3 Search button (search_matches indexed a bare KeysView), the grouped SelectFromList (.index() on a view), five functions returning bare iterators, and two ItemsSource stores. New PY3-VIEW checker rule flags bare views that escape local scope (indexed/returned/yielded/attr-stored). Call-arg is intentionally not flagged: the core consumers (SelectFromList.show, print_table) already iterate inputs, so passing a view is safe.
Add three statically-detectable Py2->3 rules: PY2-MODULE (stdlib renames - StringIO/Queue/urllib2/..., guard-aware), PY2-BUILTIN (removed builtins when called - raw_input/execfile/reduce/..., skipped when the name is imported), and PY2-NEXT (iterator class with next() but no __next__). StandardError added to PY2-NAME, and local-name detection is now import-aware. Fixes the 4 residuals found, all in extensions: three unused `import urllib2` (removed) and one `import StringIO` -> plain-list accumulator (sidesteps io.StringIO str/unicode ambiguity on the default IPY2 engine)
From a review of the checker: - recognize try/except ImportError as a guard, so the canonical portable module shim (try: from StringIO import StringIO / except ImportError: from io import StringIO) is no longer flagged PY2-MODULE - match the immediate receiver name for CLR-REF and PY2-ITER instead of walking the whole receiver subtree, so foo[clr].Reference and obj[itertools].imap() no longer false-match (framework.clr.Reference and real itertools.imap still do) Receiver-blindness of PY3-VIEW / SORT-KEY (.items() on a non-dict) is left as-is: the suggested list()/key= fix is always safe and the first-party false-positive rate is zero. No real detection lost; the tree stays at 0.
There was a problem hiding this comment.
PR Summary:
- Cleans up Python-2-only language idioms (
iteritems, bareunicode,__nonzero__/__bool__, keyless heterogeneous sorts, bare views/iterators) acrosspyrevitliband shipped extensions so the code runs correctly under IronPython 3. - Adds an engine-portable CLR loading shim (
framework.add_reference_to_file) replacingclr.AddReferenceToFileAndPathat all first-party call sites outsideframework.py. - Ports the two
clr.Referenceout-param sites (query.intersect_curves,create.load_family/FamilyLoaderOptionsHandler) to dispatch between IronPython'sStrongBoxidiom and pythonnet's tuple-return convention. - Ships a stdlib-only AST checker (
dev/scripts/check_py3_compat.py) and an in-Revitunittestsuite (test_py3_compat.py, two DevTools buttons) to verify parity across IronPython2/IronPython3/CPython engines.
Review Summary:
Traced the core marshaling changes against the actual Revit API interface signatures (IFamilyLoadOptions.OnFamilyFound/OnSharedFamilyFound ref-param ordering, Curve.Intersect out-param) and confirmed the IronPython vs. pythonnet dispatch branches are consistent with each other and with prior behavior. Verified pyrevit.compat.IRONPY correctly spans both IronPython 2 and 3 so the clr.Reference branches remain exercised on IPY3 as intended. Spot-checked all converted CLR-loading call sites (adc.py, dxf.py, ifc.py, rhino.py, git.py, mathnet.py, asmmaker.py) for leftover unused clr imports and extension-handling edge cases in the new framework.add_reference_to_file helper — none found. Reviewed the vendored markdown fixes, the extension script call-site conversions (sectionbox_navigation.py, family_utils.py, file_utils.py, etc.), and the new checker/test infrastructure for logic correctness. The only issue found is a low-severity dangling documentation reference to a file (IRONPYTHON_TO_PYTHON3_ANALYSIS.md) that isn't shipped in the repo or this PR. Overall this is a well-scoped, thoroughly self-tested change with no functional regressions identified.
Suggestions
|
Hi @ChrisCrosley , can you do me a favor? don't write essays for PR descriptions. Get straight to the point. It took me less time to read the actual code edits than the description itself 🤣 |
|
🙃🙃 point taken! 🙃🙃 |
sanzoghenzo
left a comment
There was a problem hiding this comment.
A few minor comments, overall it looks good!
…Examples.panel/ExportIFC.pushbutton/script.py Co-authored-by: Andrea Ghensi <andrea.ghensi@gmail.com>
Co-authored-by: Andrea Ghensi <andrea.ghensi@gmail.com>
|
There are some conflicts to solve, then we should be ready to go! How can we make sure that future PRs for the python side have run |
TLDR: This is a long description but Claude did a pretty good job reasoning through this and I thought it would be worth sharing in full. This PR attempts to pick up all of the Python3 specific language changes to make IronPython3.4 stable, and is a first step towards CPython + pythonnet migration. The PR comes with a command line script to search for Python2 issues that extension developers can also run on their repos and 2 test buttons. There are some know issues at the bottom that may be worth discussion.
Description
One small step along the migration path from IronPython 2.7 -> CPython + pythonnet. That path splits into (at least...) two layers: Python-3 language idioms, which break on any Python 3 engine (IronPython 3 included), and pythonnet-bridge marshaling idioms, which break only on CPython+pythonnet (IronPython has native .NET integration and never hits them).
This branch attempts to clear the language layer in full and the first two of roughly eight bridge classes - IronPython-only CLR loading (
clr.AddReferenceToFileAndPath) andclr.Referenceout-params - plus the test infrastructure to verify them per engine. It deliberately does not complete the bridge: the remaining marshaling classes (generic-collection construction, interface__namespace__, enum/indexer/IDisposable/overload idioms) are surveyed and tracked as a measured backlog, not fixed here - see Scope. It does not attempt thepyrevit/formsWPF/CPython port or change engine selection; language-layer residuals insideforms/_ipy.pyare fixed where found (e.g. the__bool__aliases and the Python-3 view/iterator bugs below).Net effect: this makes
pyrevitliband the shipped extensions correct under IronPython 3 (the language layer is exactly what IPY3 exposes), and lands the first layer of CPython support - not CPython-correctness, which the bridge backlog gates.Every change here keeps working on the default IronPython 2 engine; nothing in this branch changes engine selection or public API signatures.
The changes are covered by a static checker (
pipenv run check-py3) and an in-Revit suite run by two DevTools buttons (Stage 0). The suite is green on all three engines, with documented skips for the known issues below.Stages
iteritems, bareunicode,__nonzero__/__bool__aliases)clr.AddReferenceToFileAndPathfrom all call sites outsidepyrevit.framework)clr.Referencesites inrevit/db/create.pyandrevit/db/query.py)PY3-VIEWview/iterator class + the2to3/pylint --py3krulesPY2-MODULE/PY2-BUILTIN/PY2-NEXT, and the residuals they found)Stage 0 - test infrastructure
Static checker:
dev/scripts/check_py3_compat.pyAST-based scanner for the Python-2-only and IronPython-only patterns this branch targets. Stdlib-only and path-agnostic: third-party extension authors can run it against their own
.extensionfolders with a stock Python 3.SYNTAXSYNTAX-WARN'\d'escapes in non-raw strings - a hard error in future Pythons)PY2-ITER.iteritems()/.iterkeys()/.itervalues(); Py2-only itertools members (ifilterfalse,izip, ...) in any usage formPY2-HASKEY.has_key()PY2-NAMExrange/basestring/unicode/unichr/long/StandardErrorPY2-BUILTINraw_input/execfile/apply/cmp/coerce/intern/buffer/reduce/reload/file(skipped when the name is imported)PY2-MODULEStringIO,Queue,cPickle,ConfigParser,urllib2, ...); guard-awarePY2-NEXTnext()(with__iter__) but no__next__PY2-BOOL__nonzero__without__bool__IPY-CLRclr.AddReferenceToFileAndPathoutsidepyrevit.frameworkCLR-REFclr.Referenceoutside the sanctioned dispatch sites (query.py,create.py)SORT-KEYsorted()over dict.items()/.values()PY3-VIEWkeys/values/items,map/filter/zip) that escapes local scope - indexed, returned, yielded, or stored to an attribute (was a list in Python 2)Engine-guarded shims are recognized and skipped (
if PY2:branches,@skipUnless(PY2, ...)decorators,try/except NameErrorfallbacks, local redefinitions).pyrevitlib/rpwandpyrevit.coreutils.markdownare excluded (see Vendored-library policy).Baseline at Stage 0 was over 30 findings.
In-Revit test suite:
pyrevit.unittests.test_py3_compatRevit-hosted suite exercising the compatibility hotspots. Two DevTools buttons under pyRevitDev → Debug → Unit Tests run the identical suite on the attached IronPython engine and on CPython (
#! python3), making it a per-engine parity dashboard: a failing test is a coverage signal marking exactly what a later stage fixes.Test procedure: run both buttons under the default (IPY2) attach; switch the attached engine to IPY342 in Settings, reload, and run again. The out-param tests build fixtures inside rolled-back transactions (no trace left in the document) and reuse the existing DevTools
A.rfa.Results on a Revit 2025+ (.NET 8) host - all cells verified in Revit through Stage 2, with the CPython column re-verified fully green after Stages 3 and 4:
test_core_module_importspyrevit.*modules importtest_interop_module_importstest_interop_native_module_importsTEST_NATIVE_INTEROPtest_framework_asm_file_referencetest_basewrapper_reprElementWrapper.__repr__(iteritems)test_forms_listitem_truthinessTemplateListItem__nonzero__test_forms_paramdef_truthinessParamDef__nonzero__test_get_param_by_name/test_get_category_by_nametest_load_family_out_paramclr.Referenceincreate.load_familytest_curve_intersect_out_paramclr.Referenceout-param ofquery.get_gridpoints, on pure geometrytest_envvars_sortable_by_name† On .NET 8 hosts the assemblies are not shipped (see known issue below). On netfx hosts (Revit ≤2024) the managed interop test runs: expected pass under IronPython, and it is the test that proves the Stage 2 shim for interop under CPython - i.e. that verification requires a Revit ≤2024 host.
The rhino/adc import tests are opt-in because importing them loads unmanaged and version-uncontrolled binaries into the Revit process:
rhino3dmio_native.dll(rhino3dm's native C++ core; also netfx-only, so loading it into a .NET 8 host is untested territory) and whatever assemblies the machine's Desktop Connector install carries. Managed load failures are catchable and just turn a test red; a bad native load is an access violation nothing can catch. A test suite's worst failure mode must be a red row, not a dead host. This gating is precautionary - these imports were the initial suspect for the IPY342 hard-crashes but were exonerated (the actual cause was the markdown stack overflow below); the risk class is real regardless. Certifying these modules means flippingTEST_NATIVE_INTEROPdeliberately, one module at a time.Stage 1 - Python-2 syntax residuals
All
PY2-ITER,PY2-NAME, andPY2-BOOLfindings cleared. Flips thetest_basewrapper_reprmatrix cells green on IPY342/CPython andtest_forms_listitem_truthinesson IPY342.revit/db/__init__.py-pdata.iteritems()→.items(); this was the crash insideBaseWrapper.__repr__on any Python 3 engine.forms/_ipy.py-__bool__ = __nonzero__aliases onTemplateListItemandParamDef. Python 3 ignores__nonzero__, so under IPY342 those objects were silently always-truthy, breaking checked-state logic.coreutils/markdown/- restored thetext_typedefinition inutil.py's Python-2 branch (it was commented out) and routed all bareunicodeuses through it, includingclass AtomicString(unicode)→(text_type);__version__.pyuses plainstr(ASCII by construction). The package no longer depends oncompat.py'sunicodebuiltins injection.revit/db/query.py-isinstance(x, (str, unicode))→isinstance(x, str). Sound on every engine pyRevit ships: IronPython 2 has a single string type (str is unicode), and CPython 2 was never a pyRevit engine.Override Text.pushbutton-unicode(el.Text)→str(el.Text), same reasoning.Two additional vendored-markdown bugs, found by running the new suite in Revit (the CPython cell showed 2 errors beyond the expected Stage 2/3 reds):
extensions/footnotes.py-Element.getchildren()was removed in CPython 3.9 (still present in IronPython 3.4, which is why only the CPython run errored); replaced withlen(li)/list(parent).extra.py's sub-extension list andbuild_extension's fallback hardcoded a top-levelmarkdown.extensions.path that does not exist in the vendored copy. Both now resolve against the vendored package, somarkdown.markdown(text, extensions=["tables"])works for the first time. Verified under local CPython by importing the package standalone.The markdown fixes are kept (they are correct and help third-party callers on every engine - the extension-loading bug was broken under IPY2 too), but the four markdown tests were removed and
coreutils/markdownis now excluded from the checker: the package has no first-party runtime consumers and is a deprecated unbundling candidate (see the vendored-library policy), so it is no longer part of the maintained surface. The two known-issue sections below document why.Stage 2 - framework CLR shim
framework.add_reference_to_file(asm_file), the engine-portable replacement for IronPython'sclr.AddReferenceToFileAndPath. It dispatches to that API under IronPython (byte-for-byte legacy behavior); under CPython/pythonnet it loads the file viaAssembly.LoadFrom, appending.dllwhen missing (detected by suffix, notsplitext- assembly names contain dots:IxMilia.Dxf). Documented as public surface: "how do I load a DLL portably" is the first question a#! python3extension author hits.interop/adc.py(×6),dxf.py,ifc.py,rhino.py,coreutils/git.py(which also loses its inline engine branch),coreutils/mathnet.py, andloader/asmmaker.py's Lokad.ILPack load. Not converted:framework.pyinternals (the shim site, already engine-gated),rpw(frozen legacy), and the DevTools IronPython-compile test (IronPython-only by design).coreutils.assmutils.load_asm_file, which bare-loads viaAssembly.LoadFromand does not register the assembly forimportunder IronPython - a same-sounding name with a different contract, which is why the new shim lives inframework(the CLR-loading hub) under a distinct name.test_framework_asm_file_referenceloads a shipped managed assembly (pyRevitLabs.Json) through the shim via an extensionless path, on every engine.With the shim in place,
pyrevit.interopbecomes importable under#! python3- verifiable only on a netfx host (Revit ≤2024), since .NET 8 builds do not ship the interop assemblies (see known issue).Stage 3 - out-param marshaling
Ports the two
clr.Referenceout-param sites inpyrevitlibto engine-dispatched code: IronPython keeps the explicitclr.Reference/StrongBox path byte-for-byte; CPython/pythonnet uses the tuple-return convention with aNonefiller for the out argument (plain omission would resolve to the wrong overload where a no-out sibling exists, e.g.Curve.Intersect(Curve)).query.py- new public helperquery.intersect_curves(c1, c2)wrappingCurve.Intersect's out-param;get_gridpointsnow uses it, and the suite tests it directly on pure geometry.create.py-load_familybranches theDocument.LoadFamilycall, andFamilyLoaderOptionsHandlergets the two things pythonnet requires of Python-implemented .NET interfaces: a__namespace__attribute, and ref-param handling by convention - IronPython mutates the passed StrongBox, pythonnet returns the updated values in a tuple.Verified in Revit: the CPython suite run came back fully green (12 pass / 4 skip / 0 errors), proving on pyRevit's pythonnet fork the None-filler out-arg convention, the tuple return, and a Python-implemented .NET interface (
IFamilyLoadOptionswith__namespace__and tuple-returned ref-params) receiving live callbacks from Revit.Extension follow-through. A full-tree sweep confirmed
pyrevitlib's two sites were the only library sites, but three shipped extension scripts carried the same idiom; all three now use the new wrappers:query.intersect_curves(its code carried a "weird ironpython stuff - change when cpython works" comment; that day came)revit.create.load_family, which also replaces crash-on-failed-load with a graceful empty listAlso verified: no other Python-implemented .NET interface in first-party code has ref/out params in its callbacks -
IFamilyLoadOptionswas the only one. A new checker rule (CLR-REF) flagsclr.Referenceanywhere outside the two sanctioned dispatch sites (query.py,create.py) so the idiom cannot creep back.Manual testing of the converted Load Families tool under IPY342 surfaced one more pre-existing Py2 residual in a sibling lib:
file_utils.pyuseditertools.ifilterfalse(renamedfilterfalsein Python 3). It was replaced with a set comprehension, which also restores the documented set type that the iterator had silently replaced. This exposed a checker blind spot, now closed: Py2-only itertools members (ifilter,ifilterfalse,imap,izip,izip_longest) are flagged in all three usage forms (attribute access,from itertools import ..., bare names), guard-aware as usual. A tree-wide re-scan found no other instances.Stage 4 - heterogeneous-sort hardening
Applies the #3477 fix pattern (
key=lambda kv: str(kv[0])) to the three remaining keyless dict-view sorts: Get RVT Info (over a .NET dictionary - the closest analog of the original Settings crash), ParametersValuesToParameter (category name → BuiltInCategory pairs), and the ExportIFC dev example. The survey's other suspects (multilinesorted()calls informs/_ipy.py) turned out to be false alarms from line-based grep - each carries akey=on its continuation line, which the AST checker had correctly not flagged.This cleared the checker's original baseline to 0, wired into CI as the
py3-compatjob - any reintroduced Py2 idiom, IronPython-only CLR call, strayclr.Reference, deprecated escape sequence, or risky keyless sort fails the build. Manual test for the three fixed tools: run each button; only the sort order of displayed entries could differ.Stage 5 - extended checker coverage
Stages 1-4 drained the checker's original rule set. Two things then extended it: an IronPython-3 Search-button bug report, and a review of whether the checker was comprehensive against the canonical Py2→3 catalog. Both added detection and drained the residuals they surfaced, keeping the tree at zero.
Python-3 view/iterator residuals
Chasing an IronPython-3 Search-button report ("not subscriptable") surfaced a language-layer class the original sweep missed -
dict.keys()/.values()/.items()andmap/filter/zipreturn lists in Python 2 but non-indexable lazy views in Python 3 (breaks on any Py3 engine, IronPython 3 included; it is not the bridge "collections are views" item, which is pythonnet-only .NET collections). Fixes:forms/_ipy.py-search_matchesreturned a bareKeysViewthat the search loop then indexed (results[i]); the groupedSelectFromListsimilarly did.index()on a bare view. Both wrapped inlist(). (These are language residuals in_ipy.py, in the same category as Stage 1's__bool__fix - not the deferred WPF/CPython port.)pyutils.pairwise→zip,query.get_schedules→filter, two extensionload_configs→filter) and two that stored one to a WPFItemsSource(later.index()-ed) - all wrapped inlist().New checker rule
PY3-VIEW, designed around escape: a bare view is flagged only when it leaves local scope where its use is unknowable - indexed, returned, yielded, or stored to an attribute. It deliberately does not flag a plain-localx = d.keys()(usually just iterated - too noisy) norfoo(d.keys())call-arguments. The call-argument case was measured (15 sites, mostlySelectFromList.show/output.print_table) and left out for a reason worth recording: all the core consumers already iterate their inputs rather than index them (for item in ctx_items), so passing a view is already safe - and a call-arg rule couldn't be satisfied by the correct fix (hardening the consumer) anyway. That is the "stricter on the core library" answer in practice: the library is defensive in both directions - it does not return bare views, and it iterates (never indexes) collection inputs, which protects every caller without touching call sites. Rule limitation: it catches syntactic escape shapes, not data-flow (a view assigned to a local and then indexed elsewhere, like the grouped-select bug, still needs code review - that one was found by hand).Rounding out the rule set (2to3 / pylint --py3k)
A completeness review against the canonical catalog: all pure-syntax removals were already caught free by the
SYNTAXparse step, but three statically-detectable classes were still missing and are now covered:PY2-MODULE- stdlib module renames (StringIO,Queue,cPickle,ConfigParser,urllib2, ~35 total), guard-aware socompat.py'sif PY2:imports are skipped.PY2-BUILTIN- removed builtins when called (raw_input,execfile,apply,cmp,reduce,reload,file, ...), import-aware sofrom functools import reduce; reduce(...)is not flagged.PY2-NEXT- iterator classes withnext()+__iter__but no__next__(thePY2-BOOLanalog). PlusStandardErroradded toPY2-NAME, and local-name detection made import-aware.These found and fixed 4 residuals, all
PY2-MODULE, all in extensions: three unusedimport urllib2(removed) and oneimport StringIOin Wipe Site Designer Line Styles → a plain-list accumulator (sidestepsio.StringIOstr/unicode ambiguity on the default IPY2 engine).PY2-BUILTINandPY2-NEXTfound nothing - clean, so they are pure regression insurance. (The semantic-only changes the checker deliberately leaves to the in-Revit suite - division, str/bytes, dict-ordering - are listed under Stage 0.)Vendored-libraries
Two vendored libraries came up repeatedly. They are different cases and get different treatment; stating both explicitly so it survives into the migration docs.
pyrevitlib/rpw(revitpythonwrapper 1.7.4, unmaintained upstream) - engine-locked legacy. It subclasses WPF types and loadsIronPython.Wpf, so it genuinely cannot run on CPython. Supported only under the opt-in legacy IronPython engine; excluded from the Python-3-supported surface, this branch's checker, and the shim conversions.pyrevit.formsis the documented replacement for its UI layer. Its real constituency is third-party IronPython scripts, which keep the legacy engine regardless.pyrevit.coreutils.markdown(python-markdown 2.6.8) - deprecated, portable orphan (unbundling candidate). Unlike rpw it is pure Python (regex + ElementTree, noclr/WPF), so it runs on any engine by nature - it was never engine-locked. It was vendored in December 2016 as a genuine core dependency (the output window renderedprint_mdthrough it), then orphaned in June 2026 when the output refactor moved rendering to C# (ScriptOutput.cs), leaving zero first-party runtime consumers. Because it has no internal consumer, this branch stops investing in it: the four markdown compat tests were removed andcoreutils/markdownis excluded from the checker. The Stage 1 syntax and vendoring fixes are kept - they are correct, cheap, and help third-party callers on every engine (the extension-loading bug was broken under IPY2 too) - but reverting them would be pointless churn, not a reason to keep maintaining the package. It stays at its current path rather than moving tosite-packages/, because a top-levelmarkdownthere would both break everypyrevit.coreutils.markdownimporter and collide with pip-installed markdown 3.x under the shared CPython interpreter.#! python3scripts should use the realmarkdownfrom pip - PyPI access being the point of the migration - and the vendored copy is a candidate to unbundle and delete.Both libraries are deletion candidates when the legacy IronPython engine sunsets.
Known issue: markdown conversion hard-crashes Revit under IPY342
Running the suite under IPY342 hard-crashed Revit (no error dialog). Reproduced and root-caused outside Revit by hosting the repo's
bin/netfx/engines/IPY342DLLs in a disposable process: markdown conversion dies with a .NETStackOverflowException(exit0xC00000FD), the one exception .NET cannot catch. IronPython 3.4's DLR stack frames are much fatter than IPY2's or CPython's, and markdown's parser is recursion-heavy; measured standalone, the conversions need ~768KB of fresh stack. On Revit's already-deep UI thread that headroom does not exist.This was originally handled with
skipIf(IRONPY3)markdown tests, but those tests were later removed entirely (see the vendored-library policy): the crash is not test-specific - any script callingpyrevit.coreutils.markdownunder IPY342 inside Revit risks it - and the package has no first-party runtime consumers (verified:output.print_mdrenders via C# inScriptOutput.cs). This crash is itself part of why markdown is treated as a deprecated, unbundling candidate rather than a maintained surface. The same investigation surfaced a related managed (non-crash) issue in theextraextension, below.Known issue: markdown
extra/attr_listbroken under IronPython (2 and 3)attr_list.pyrequiresre.Scanner, which IronPython's .NET-regex-basedredoes not provide on either engine version; itsfrom sre import Scannerfallback is dead in Python 3 and unavailable in the shipped IPY2 stdlib. The Stage 1 vendoring fix exposed this: previously, name-based extension loading failed so early thatextranever worked on any engine - which is also evidence it has no users, and another data point for deprecating the vendored copy. No fix is planned; scripts needingextraunder a Python-3 engine should use pipmarkdown.Known issue: interop assemblies missing on .NET 8 hosts (Revit 2025+)
Found by running the suite on a .NET 8 Revit:
IxMilia.Dxf,Ifc.NET, andRhino3dmIOship only indev/libs/netfx/→bin/netfx/; the netcore lib set omits them.pyrevit.interop.dxf/ifc/rhinotherefore cannot import on Revit 2025+ on any engine - a packaging gap in shipped pyRevit that predates this branch (the docs site advertises these modules). The managed interop test isskipIf(NETCORE)with this reason. Closing the gap needs either Stage 2 verification on a netfx host (Revit ≤2024) or a decision to ship netcore copies (IxMilia.Dxftargets netstandard and should port;Ifc.NETandRhino3dmIOneed evaluation).Third-party impact
None breaking. Details were reviewed change by change: public signatures and return types are preserved; the Stage 2 shim is additive (and makes
pyrevit.interopimportable under#! python3where it previously hard-failed); the Stage 1__bool__fix restores documented IPY2 truthiness behavior under IPY3. Thecompat.pyunicodebuiltins injection stays - its removal would be a separate, announced deprecation. The checker doubles as a "check your extension" tool for the eventual community migration guide.Links: