WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user