# -*- 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