8e6ee31596
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.
176 lines
7.5 KiB
Python
176 lines
7.5 KiB
Python
# -*- 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
|