WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
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:
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The pyRevit YAML reader, faked HONESTLY -- strings only, like the real one.
|
||||
|
||||
`pyrevit/coreutils/yaml.py` is a YamlDotNet wrapper and its ENTIRE scalar
|
||||
conversion is:
|
||||
|
||||
elif isinstance(ynode, libyaml.RepresentationModel.YamlScalarNode):
|
||||
return ynode.Value
|
||||
|
||||
Every scalar therefore comes back as a STRING: `1.5` as `'1.5'`, `true` as
|
||||
`'true'`, and `null` as the four characters `'null'`. Two modules in this tree
|
||||
read through it -- `knowledge._merge_file`, which runs `_coerce()` afterwards,
|
||||
and `ticketpage._read_yaml`, which until 2026-08-31 did not.
|
||||
|
||||
WHY THIS FILE EXISTS: THE OLD FAKE WAS KINDER THAN PRODUCTION.
|
||||
The suite's only pyRevit YAML double was backed by `vibe.yamlkit`, which TYPES
|
||||
its scalars -- `_parse_scalar` turns 'null' into None. So every guard in this
|
||||
repository that reads YAML read it through a typing reader while the button
|
||||
read it through one that types nothing. The master checklist consequently
|
||||
arrived at the button with `CHECKLIST_FOR_ENGINEER` derived EMPTY (5.3 and 5.5
|
||||
red on the owner's screen) and the WORD 'null' in 37 of 39 rows' reason slots
|
||||
-- with the whole suite green, because the corruption is a non-empty string and
|
||||
so satisfies the very guard that demands a sentence from every row.
|
||||
|
||||
A double that is more forgiving than the thing it doubles is not a test double,
|
||||
it is a blindfold. Everything faking this reader now comes here, so the
|
||||
kindness cannot creep back one test file at a time.
|
||||
|
||||
AND THE SAME LESSON, AGAIN, ON DUPLICATE KEYS (2026-09-01).
|
||||
YamlDotNet -- what pyRevit's reader is -- REFUSES a mapping key stated twice:
|
||||
`load_as_dict` raises `Duplicate key`. This fake read through `yamlkit`, which
|
||||
takes the last one and says nothing, so the production reader and the double
|
||||
disagreed about whether a file loads AT ALL. A nested duplicate in one plant
|
||||
file therefore threw away that entire 255KB layer inside Revit -- every
|
||||
parameter role None, the sweep enumerating 0 assemblies, every report normal --
|
||||
and nothing on the host could reproduce it. `_UniqueKeyLoader` below closes
|
||||
that: this reader now raises on a duplicate at any depth, exactly like the one
|
||||
it doubles. `knowledge._merge_file` is the code that had to learn to say so,
|
||||
and `tools/knowledgelint.py` is the same strictness as a host-side gate.
|
||||
|
||||
WHY NOT conftest.py: `panellift/tests/` has a conftest of its own, so a
|
||||
whole-repo `python -m pytest` leaves exactly one module named `conftest`
|
||||
importable and it is whichever pytest reached first. `from conftest import ...`
|
||||
inside a test file is a coin flip; this name is unique in the tree.
|
||||
"""
|
||||
# pylint: disable=import-error,invalid-name
|
||||
import io
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
def stringify_like_yamldotnet(node):
|
||||
"""Every scalar as text, the way the real pyRevit reader returns it.
|
||||
|
||||
Maps and sequences keep their shape; only the leaves change.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
return dict([(k, stringify_like_yamldotnet(v))
|
||||
for k, v in node.items()])
|
||||
if isinstance(node, (list, tuple)):
|
||||
return [stringify_like_yamldotnet(v) for v in node]
|
||||
if node is None:
|
||||
return u'null'
|
||||
if node is True:
|
||||
return u'true'
|
||||
if node is False:
|
||||
return u'false'
|
||||
if isinstance(node, bytes):
|
||||
return node.decode('utf-8')
|
||||
if isinstance(node, str):
|
||||
return node
|
||||
return u'{0}'.format(node)
|
||||
|
||||
|
||||
def install_pyrevit_yaml(monkeypatch, load_as_dict):
|
||||
"""Register a fake `pyrevit.coreutils.yaml` exposing `load_as_dict`.
|
||||
|
||||
The readers under test import the module chain and swallow every
|
||||
exception, so a half-registered chain makes the read quietly do nothing
|
||||
and the test passes for the wrong reason. All three names are set.
|
||||
"""
|
||||
yaml_mod = types.ModuleType('pyrevit.coreutils.yaml')
|
||||
yaml_mod.load_as_dict = load_as_dict
|
||||
coreutils = types.ModuleType('pyrevit.coreutils')
|
||||
coreutils.yaml = yaml_mod
|
||||
pyrevit = types.ModuleType('pyrevit')
|
||||
pyrevit.coreutils = coreutils
|
||||
monkeypatch.setitem(sys.modules, 'pyrevit', pyrevit)
|
||||
monkeypatch.setitem(sys.modules, 'pyrevit.coreutils', coreutils)
|
||||
monkeypatch.setitem(sys.modules, 'pyrevit.coreutils.yaml', yaml_mod)
|
||||
return yaml_mod
|
||||
|
||||
|
||||
class DuplicateKeyError(ValueError):
|
||||
"""A mapping key stated twice, refused the way YamlDotNet refuses it.
|
||||
|
||||
A `ValueError` so a caller guarding a parse with `except ValueError` still
|
||||
catches it, and its own class so a test can name exactly this failure.
|
||||
The message leads with the same two words the real reader's does --
|
||||
`Duplicate key` -- because that string is what a person sees in Self Check
|
||||
and in `config_problems()`, and a double that produces a different one
|
||||
teaches the wrong thing about what to search for.
|
||||
"""
|
||||
|
||||
|
||||
def raise_on_duplicate_keys(text, path=None):
|
||||
"""Refuse `text` if any mapping in it states a key twice, at any depth.
|
||||
|
||||
PyYAML with the standard unique-key loader recipe: `construct_mapping` is
|
||||
called once per mapping node, so nesting is covered without walking the
|
||||
tree by hand, and `key_node.start_mark` gives the line and column of the
|
||||
SECOND statement of the key -- the one to delete.
|
||||
|
||||
PyYAML is used only as the duplicate DETECTOR; the value still comes back
|
||||
from `yamlkit`, whose grammar is the one the rest of this fake is built
|
||||
on. So a document PyYAML cannot parse for some other reason is passed
|
||||
through here rather than refused: a grammar disagreement between PyYAML
|
||||
and yamlkit is not evidence about YamlDotNet, and this checker has exactly
|
||||
one question. The cost is honest and worth writing down -- a duplicate key
|
||||
inside a document PyYAML rejects for an unrelated reason is not seen here.
|
||||
`tools/knowledgelint.py` reports that case, because it strict-parses the
|
||||
real files with PyYAML and nothing else.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
class _UniqueKeyLoader(yaml.SafeLoader):
|
||||
pass
|
||||
|
||||
def _construct_mapping(loader, node, deep=False):
|
||||
seen = set()
|
||||
for key_node, _value_node in node.value:
|
||||
key = loader.construct_object(key_node, deep=deep)
|
||||
try:
|
||||
duplicate = key in seen
|
||||
except TypeError: # an unhashable key: not our
|
||||
continue # question, and not our error
|
||||
if duplicate:
|
||||
mark = key_node.start_mark
|
||||
raise DuplicateKeyError(
|
||||
'Duplicate key: {0!r} at line {1}, column {2}{3} -- a '
|
||||
'mapping may state a key only once.'.format(
|
||||
key, mark.line + 1, mark.column + 1,
|
||||
' of {0}'.format(path) if path else ''))
|
||||
seen.add(key)
|
||||
return yaml.SafeLoader.construct_mapping(loader, node, deep=deep)
|
||||
|
||||
_UniqueKeyLoader.add_constructor(
|
||||
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping)
|
||||
|
||||
try:
|
||||
yaml.load(text, Loader=_UniqueKeyLoader)
|
||||
except DuplicateKeyError:
|
||||
raise
|
||||
except Exception: # see the docstring: not this question
|
||||
return
|
||||
|
||||
|
||||
def strings_only_yaml_reader():
|
||||
"""`load_as_dict` with yamlkit's grammar and YamlDotNet's (absent) types.
|
||||
|
||||
Strict about duplicate keys as well as untyped about scalars, because the
|
||||
reader it stands in for is both. See the module docstring.
|
||||
"""
|
||||
from vibe import yamlkit
|
||||
|
||||
def load_as_dict(path):
|
||||
handle = io.open(str(path), 'r', encoding='utf-8')
|
||||
try:
|
||||
text = handle.read()
|
||||
finally:
|
||||
handle.close()
|
||||
raise_on_duplicate_keys(text, path)
|
||||
return stringify_like_yamldotnet(yamlkit.parse(text))
|
||||
|
||||
return load_as_dict
|
||||
Reference in New Issue
Block a user