WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s

History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
This commit is contained in:
2026-09-09 08:32:15 -05:00
commit 8e6ee31596
406 changed files with 223435 additions and 0 deletions
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
"""Every extension .py with a non-ASCII byte must declare its encoding.
This is a test because it already cost a live failure. On 2026-09-03, after a
Revit restart, pressing Panel Analysis threw
Revit encountered a non-ASCII character '\\xe2' in file ...
PanelAnalysis.pushbutton/script.py
-- an IronPython 2.7 SyntaxError, raised at compile time, before a line of the
button ran. The file carried deliberate typography in `u'...'` labels and
comments (the prime in f'c, subscripts, the sqrt sign, an em-dash, a Chinese
note) but no `# -*- coding: utf-8 -*-` cookie, and IronPython 2.7 refuses a
source file that mixes non-ASCII bytes with no declared encoding (PEP 263).
CPython 3 -- what this test runs on, and what `tools/rvt.py` runs on -- defaults
to UTF-8 and never raises this, so the trap is invisible on the host and only
appears inside Revit. That asymmetry is exactly why it needs a guard: DSV4F, a
branch merge, or anyone pasting a nice-looking dash into a comment reintroduces
it silently, and the next person to feel it is the owner pressing a button.
The engine that matters here is IronPython 2.7, so the scan covers the files it
loads -- everything under the extension except the host-only `tests/` tree
(those run on CPython 3, where the cookie is not required).
"""
import os
import re
# tests/ -> WillTools.extension (mirrors test_button_tooltips._EXT).
_EXT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
# PEP 263: the cookie is honoured only on the first OR second line, and must
# match `coding[:=]\s*([-\w.]+)`. We check the first two lines the same way
# IronPython does, so a cookie on line 3 (too late) still counts as missing.
_COOKIE = re.compile(rb'coding[:=]')
def _extension_py_files():
"""Every .py IronPython 2.7 loads from the extension -- tests excluded."""
for root, dirs, files in os.walk(_EXT):
# Do not descend into the host-only tests tree.
if os.path.abspath(root) == _TESTS_DIR:
dirs[:] = []
continue
for name in files:
if name.endswith('.py'):
yield os.path.join(root, name)
def _has_non_ascii(data):
return any(byte >= 0x80 for byte in bytearray(data))
def _declares_encoding(data):
first_two = b'\n'.join(data.split(b'\n')[:2])
return bool(_COOKIE.search(first_two))
def _first_non_ascii_line(data):
for lineno, line in enumerate(data.split(b'\n'), 1):
if _has_non_ascii(line):
return lineno
return None
def test_the_walk_found_scripts():
"""A scan that silently matched nothing is not a passing guard.
If the walk stops finding the button scripts -- a renamed container, a
walk that no longer descends -- this test must fail rather than pass by
vacuously scanning an empty set.
"""
found = list(_extension_py_files())
assert found, 'the extension .py walk found nothing -- the scan is broken'
assert any(p.endswith('script.py') for p in found), (
'the walk found no button script.py -- it stopped descending; fix the '
'walk before trusting a green run here')
def test_non_ascii_files_declare_an_encoding():
"""No IronPython-loaded .py may carry a non-ASCII byte without a cookie."""
offenders = []
for path in _extension_py_files():
with open(path, 'rb') as handle:
data = handle.read()
if _has_non_ascii(data) and not _declares_encoding(data):
offenders.append((os.path.relpath(path, _EXT),
_first_non_ascii_line(data)))
assert not offenders, (
'these files carry a non-ASCII byte but declare no encoding, so '
"IronPython 2.7 throws a SyntaxError at compile time (\"non-ASCII "
'character ... but no encoding declared\") the moment Revit loads '
'them. Add `# -*- coding: utf-8 -*-` as the first line:\n' +
'\n'.join(' {0} (first non-ASCII on line {1})'.format(p, ln)
for p, ln in offenders))