WillTools: precast shop-ticket QC and panel handling tools
ci / test (push) Has been cancelled

Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
This commit is contained in:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
+791
View File
@@ -0,0 +1,791 @@
# -*- coding: utf-8 -*-
"""A small, hand-rolled YAML subset: write it, read it back, nothing else.
Two things this is not: a general YAML library, and a wrapper around one.
Neither exists for free here -- PyYAML is a C extension, not available under
IronPython 2.7, and the one YAML reader this extension already had
(`pyrevit.coreutils.yaml`) only exists inside a running Revit/pyRevit process,
which is exactly the environment a unit test does not have.
So this module writes ONE well-defined, restricted shape of YAML -- block
mappings, block and flow lists, quoted/plain scalars, full-line comments --
and reads back exactly that shape. It has no Revit dependency and no
third-party dependency, so it is the same code under IronPython 2.7 inside
Revit and under CPython in a test. `configdump.py` (the project-config dump)
and `designspec.py` / `specformat.py` (the design spec) both write through
`emit()` and read through `parse()`; neither has its own YAML code.
The contract `parse()` relies on: every document `emit()` produces is a tree
of dicts, lists and scalars, indented two spaces per level, with a list
item's dash occupying exactly one indent unit (`'- '` is two characters, the
same width as `' '`) so a list item's own fields line up under it. That is
not a guess about YAML in general -- it is what `emit()` itself does, checked
against its own output below.
"""
# pylint: disable=invalid-name
import re
from vibe.compat import string_types, text_type
_PLAIN = re.compile(r'^[A-Za-z0-9_][A-Za-z0-9_ ./+-]*$')
_RESERVED = ('true', 'false', 'null', 'yes', 'no', 'on', 'off', '~')
_NUMBER_LOOKING = re.compile(r'^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$')
def scalar(value):
"""One value as it is written on the right of a `key:`."""
if value is None:
return 'null'
if value is True:
return 'true'
if value is False:
return 'false'
if isinstance(value, float):
text = repr(value)
return text[:-2] if text.endswith('.0') else text
if not isinstance(value, string_types):
return str(value)
text = text_type(value)
if not text:
return "''"
if text.lower() in _RESERVED or not _PLAIN.match(text) \
or text != text.strip() or text.endswith(':'):
return "'" + text.replace("'", "''") + "'"
# A bare number-looking string has to be quoted or it reads back as a
# number, and a part number like 4.10 would come back as 4.1.
if _NUMBER_LOOKING.match(text):
return "'" + text + "'"
return text
def is_flat(node):
"""A list of plain scalars, short enough to sit on one line."""
if not isinstance(node, list) or not node:
return False
if any(isinstance(v, (dict, list)) for v in node):
return False
return sum(len(scalar(v)) + 2 for v in node) <= 66
def emit(node, indent=0):
"""A dict/list/scalar tree as YAML lines. Deterministic key order."""
pad = ' ' * indent
out = []
if isinstance(node, dict):
if not node:
return [pad + '{}']
for key in sorted(node):
value = node[key]
label = pad + scalar(key) + ':'
if isinstance(value, dict) and value:
out.append(label)
out.extend(emit(value, indent + 1))
elif isinstance(value, list) and value:
if is_flat(value):
out.append('{0} [{1}]'.format(
label, ', '.join(scalar(v) for v in value)))
else:
out.append(label)
out.extend(emit(value, indent + 1))
elif isinstance(value, (dict, list)):
out.append('{0} {1}'.format(label,
'{}' if isinstance(value, dict)
else '[]'))
else:
out.append('{0} {1}'.format(label, scalar(value)))
return out
if isinstance(node, list):
for item in node:
if isinstance(item, dict) and item:
lines = emit(item, indent + 1)
# The first key rides on the dash, the rest line up under it.
out.append(pad + '- ' + lines[0].strip())
out.extend(lines[1:])
elif isinstance(item, list) and item:
lines = emit(item, indent + 1)
out.append(pad + '-')
out.extend(lines)
else:
out.append(pad + '- ' + scalar(item))
return out
return [pad + scalar(node)]
# ------------------------------------------------------------------- parse
class YamlKitError(ValueError):
"""A document this parser cannot read, said out loud instead of cut short.
A `ValueError` subclass so a caller that already guards a parse with
`except ValueError` keeps working, and its own class so one that wants
to name this failure can.
WHY THIS EXISTS AT ALL -- the lesson, kept where the code is.
A parser that stops early without saying so is worse than one that
raises, and this module has now proved it repeatedly. Every one of
these is ordinary YAML, none of them is anything `emit()` writes, and
each of them silently swallowed the rest of a real file:
* `knowledge/catalogue.yaml` carried one `note: >-` two thirds of the
way down, and every line under it -- the whole rebar branch -- was
invisible while the file "parsed fine" (fixed: `_is_block_scalar`);
* a job knowledge file carried `parts: [P76, P89,` wrapping onto the
next line, and the three sections below it were simply absent;
* the same file wrote a block sequence at its key's own indentation
(`notes:` then `- ...` in the same column);
* it wrapped a long list item's text onto a second, indented line;
* a plant file put a `# comment` on the end of a `key:` line.
16 top-level keys in that job file, 13 in what `parse()` returned.
The second one cost a real press. The config editor cross-checks its
tree against the one the Revit-side reader builds, key by key, and the
key it happened to be writing was `snapshot.detached_dir` -- so a
WHOLE-FILE failure was reported as "the two YAML readers disagree about
snapshot.detached_dir", a sentence about an innocent key that sent the
reader to the wrong file entirely. Every key past the cut would have
produced the same message and the same wrong destination.
So the rule for anything added to this parser: a construct it cannot
read raises. It never returns the part it managed to get through.
"""
def parse(text):
"""The inverse of `emit()`. Reads back exactly the shape it produces.
Comments (any line whose stripped text starts with '#') and blank lines
are ignored wherever they appear -- a spec file is meant to carry a
comment above every section, and this is what makes that free. The
document is assumed to be one mapping at the top, which is true of every
file this module writes (a knowledge dump, a design spec).
Beyond what `emit()` writes, `_logical_lines` folds the hand-written
shapes a knowledge file really contains -- a wrapped flow sequence, a
trailing comment, a plain scalar continued on the next line -- before
any of the block grammar runs. Every one of them used to truncate the
document in silence; see `YamlKitError`.
Raises `YamlKitError` on a flow sequence that is never closed.
"""
lines = _logical_lines(text)
if not lines:
return {}
top_indent = lines[0][0]
value, _idx = _parse_dict(lines, 0, top_indent)
return value
def _logical_lines(text):
"""Physical lines folded into the (level, text) pairs the grammar reads.
Blank lines and whole-line comments are dropped wherever they appear --
a spec file is meant to carry a comment above every section, and this
is what makes that free. What is left is one entry per LOGICAL line,
which is not always one physical line: see `_join_flow` and
`_fold_plain`.
The one place none of this applies is the body of a block scalar, where
a '#' is text and an indented line is content rather than structure.
Those lines are passed through verbatim for `_parse_dict` to gather.
"""
raw = (text or '').split('\n')
total = len(raw)
out = []
index = 0
block_level = None
while index < total:
line = raw[index]
number = index + 1
index += 1
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# emit() always indents two spaces per level (a list item's dash is
# the same width as one level, see the module docstring), so integer
# division recovers the level exactly -- no half-levels exist here.
level = (len(line) - len(line.lstrip(' '))) // 2
if block_level is not None:
if level > block_level:
out.append((level, stripped))
continue
block_level = None
body = _strip_comment(stripped)
if not body:
continue
offset = _flow_value_offset(body)
if offset >= 0:
body, index = _join_flow(raw, index, body, offset, number)
elif _plain_value(body):
body, index = _fold_plain(raw, index, body, level)
if _is_block_scalar(_line_value(body) or ''):
block_level = level
out.append((level, body))
return out
def _line_value(text):
"""The value half of a line, read the way the block grammar reads it.
`None` when the line has no value half at all (a bare `-`, or a line
with no colon). Mirrors `_parse_dict`'s `partition(':')` and
`_parse_list`'s `_is_mapping_line` exactly, so the pre-pass can never
disagree with the parse that follows about what a line's value is.
"""
if text == '-':
return None
if text[:2] == '- ':
remainder = text[2:]
if _is_mapping_line(remainder):
return remainder.partition(':')[2].strip()
return remainder.strip()
_key, sep, rest = text.partition(':')
if not sep:
return None
return rest.strip()
def _plain_value(text):
"""True when this line's value is a plain scalar that could continue.
Not a block-scalar indicator (`_parse_dict` gathers those itself), not
a flow collection (`_join_flow` has already dealt with those), and not
empty (an empty value means the value is the indented block below).
"""
value = _line_value(text)
return bool(value) and not _is_block_scalar(value)
def _fold_plain(raw, index, body, level):
"""A plain scalar continued on the following, more-indented lines.
YAML folds those into the one scalar with a space, and a hand-written
note that ran long is exactly where it happens. This parser used to do
something far worse than misread it: an indented line after a `key:
value` matches no branch of `_parse_dict`'s loop, so it BROKE the loop
and every enclosing one, and the document ended there.
A continuation stops at the first line that is not more indented, at a
blank or comment line, and at any line that is itself a list item or a
`key: value` -- that last guard is what keeps the second field of a
`- key: value` list item a field instead of swallowing it into the
first one's text.
"""
total = len(raw)
while index < total:
line = raw[index]
stripped = line.strip()
if not stripped or stripped.startswith('#'):
break
if (len(line) - len(line.lstrip(' '))) // 2 <= level:
break
text = _strip_comment(stripped)
if not text or _is_list_line(text) or _is_mapping_line(text):
break
body = body + ' ' + text
index += 1
return body, index
def _strip_comment(text):
"""One line with an unquoted trailing `# comment` removed.
A '#' only starts a comment where YAML says it does -- at the start of
the line or after whitespace, and never inside a quoted scalar -- so
`sheet_re: ^WP#\\d+` keeps its hash and `dims: [3", 3"]` keeps its inch
marks. `emit()` quotes any value containing a '#' (the plain-scalar
pattern excludes it), so nothing this module writes is touched here;
this is entirely for the files people edit by hand, where a comment on
the end of a `key:` line used to end the document.
"""
return _flow_scan(text, 0, 0, '')[0]
def _join_flow(raw_lines, index, first, offset, number):
"""One logical line from a flow sequence that wraps. Returns (text, idx).
`emit()` never writes one -- `is_flat` keeps a flow list to 66
characters on a single line -- so this shape only ever arrives by a
human hand-editing the file, which is exactly why it has to be read.
The continuation lines are joined with a single space, the way YAML
folds them, and their own indentation is discarded: the level of the
logical line is the level of the line the `[` opened on.
Raises `YamlKitError` when the bracket is never closed. THAT IS THE
DELIBERATE CHOICE, not an oversight: running off the end of the file
and returning the mapping built so far is precisely the silent
truncation this whole path exists to end. An unclosed `[` is a broken
file and the caller has to be told, not handed two thirds of a tree.
"""
body, depth, quote = _flow_scan(first, offset, 0, '')
parts = [body]
total = len(raw_lines)
while depth > 0:
if index >= total:
raise YamlKitError(
'unterminated flow sequence: the "[" opened on line {0} is '
'never closed, so the rest of the document cannot be read. '
'Nothing was returned -- a partial tree is how this bug hid '
'the last time. The line: {1!r}'.format(number, first[:70]))
nxt = raw_lines[index].strip()
index += 1
# A blank line or a whole-line comment inside a flow sequence is
# skipped, the same as anywhere else -- unless a quoted scalar is
# still open across the line break, where a '#' is just text.
if not quote and (not nxt or nxt.startswith('#')):
continue
body, depth, quote = _flow_scan(nxt, 0, depth, quote)
parts.append(body)
return ' '.join([p for p in parts if p]), index
def _flow_value_offset(text):
"""Where a flow collection opens on this line, or -1 for none.
Only a value that BEGINS with `[` (or `{`) opens one. YAML lets a plain
scalar carry a bracket in the middle of itself -- `note: see [1` is the
string "see [1" and nothing is unclosed about it -- so counting every
bracket on every line would swallow the rest of the document hunting a
`]` that was never a bracket at all.
`{` counts as an opener the same way `[` does -- see `_parse_flow_map`.
Joining it matters for the same reason: an unjoined one that wrapped
would end the enclosing mapping and lose the rest of the file, and
losing one value beats losing the document.
"""
body = text
offset = 0
if body[:2] == '- ':
rest = body[2:].lstrip()
offset = len(body) - len(rest)
body = rest
elif body == '-':
return -1
# `x[:1] in '[{'` is True for the empty string -- guard the head first.
if body[:1] and body[0] in '[{':
return offset
# `key: [...]`. Partitioned exactly the way _parse_dict partitions it,
# so this can never disagree with the parse that follows.
key, sep, rest = body.partition(':')
if not sep:
return -1
value = rest.lstrip()
if not value or value[0] not in '[{':
return -1
return offset + len(key) + 1 + (len(rest) - len(value))
# A quote character only OPENS a quoted scalar where a scalar can start:
# at the head of the value, or after a separator. Anywhere else it is
# ordinary text, which YAML agrees with and this project needs -- the
# shipped catalogue writes `dims: [3", 3", 3"]`, where every `"` is an inch
# mark. Toggling on those turns half the line into "a quoted string" and
# hides the closing bracket, which is the truncation bug again wearing a
# different hat. `it's` in an unquoted scalar is the same case.
_QUOTE_OPENS = ' \t[{,:'
def _opens_quote(ch, prev):
return (ch == "'" or ch == '"') and (prev == '' or prev in _QUOTE_OPENS)
def _flow_scan(text, start, depth, quote):
"""Walk one physical line of a flow collection.
Returns (body, depth, quote): `body` is the line with an unquoted
trailing comment removed, `depth` the running bracket depth, and
`quote` the quote character still open at the end of the line ('' when
none). Everything before `start` is left untouched -- that is the
`key:` half of the line, which the block grammar still has to see.
Quotes are tracked so a value that contains a bracket or a comma
("'P76, [spare]'") neither closes the sequence early nor splits, and so
a '#' inside a quoted scalar stays part of the scalar instead of
starting a comment. A '#' only starts a comment where YAML says it
does: at the start of the line or after whitespace.
"""
i = start
n = len(text)
while i < n:
ch = text[i]
if quote:
if ch == quote:
if quote == "'" and text[i + 1:i + 2] == "'":
i += 2
continue
quote = ''
elif quote == '"' and ch == '\\':
i += 2
continue
i += 1
continue
if _opens_quote(ch, text[i - 1] if i else ''):
quote = ch
i += 1
continue
if ch == '#' and (i == 0 or text[i - 1] in ' \t'):
return text[:i].rstrip(), depth, ''
if ch == '[' or ch == '{':
depth += 1
elif ch == ']' or ch == '}':
if depth > 0:
depth -= 1
i += 1
return text.rstrip(), depth, quote
def loads(text):
"""Alias for parse(), read as "load a string" the way json.loads reads."""
return parse(text)
def _parse_dict(lines, idx, indent, first=None):
result = {}
n = len(lines)
pending = [first] if first is not None else []
while True:
if pending:
text = pending.pop(0)
consume = False
else:
if idx >= n:
break
cur_indent, text = lines[idx]
if cur_indent != indent or _is_list_line(text):
break
consume = True
key, sep, rest = text.partition(':')
if not sep:
# Malformed line for this grammar -- skip rather than raise, so
# one stray line does not take the whole file down.
if consume:
idx += 1
continue
key = _parse_key(key)
rest = rest.strip()
if consume:
idx += 1
if _is_block_scalar(rest):
# `key: >-` and friends. emit() never writes one, so for a long
# time this parser did not read one either -- and a file with a
# block scalar in it did not fail, it TRUNCATED: the indicator
# was taken as the value and every more-indented line after it
# ended the enclosing mapping. `knowledge/catalogue.yaml` had one
# `note: >-` two thirds of the way down, and everything below it
# -- the whole rebar branch -- was invisible to every host test
# that read the shipped file, while the file itself parsed
# "fine". Hand-written knowledge files are exactly where a folded
# scalar gets used, so reading them is not optional.
#
# The subset: folded (`>`) joins with spaces, literal (`|`) with
# newlines. Chomping is read, because the default is NOT the
# obvious one and the difference is a whole reader
# disagreement: a bare `>` CLIPS, which keeps exactly one
# trailing newline, and only `>-` strips it. A `+` (keep all)
# is read as clip -- parse() has already dropped the blank
# lines that would tell us how many, and one is the nearest
# true answer available. What this cannot see, for the same
# reason, is a blank line or a `#` line INSIDE a block, so a
# literal block loses its paragraph breaks. Fine for a sentence
# in a knowledge file, which is all anyone writes here.
text_lines = []
while idx < n and lines[idx][0] > indent:
text_lines.append(lines[idx][1])
idx += 1
joiner = u'\n' if rest[:1] == '|' else u' '
body = joiner.join(text_lines)
if body and '-' not in rest[1:]:
body += u'\n'
result[key] = body
elif rest == '':
if idx < n and lines[idx][0] > indent:
child_indent = lines[idx][0]
if _is_list_line(lines[idx][1]):
child, idx = _parse_list(lines, idx, child_indent)
else:
child, idx = _parse_dict(lines, idx, child_indent)
result[key] = child
elif idx < n and lines[idx][0] == indent \
and _is_list_line(lines[idx][1]):
# A block sequence written at its KEY'S OWN indentation:
#
# notes:
# - the first note
# - the second
#
# Valid YAML, never what emit() writes (it indents a list
# one level in), and the THIRD construct found truncating a
# hand-edited file: the key took None, then the `- ` line at
# the mapping's own indent broke the loop and every enclosing
# one with it, so the rest of the document vanished. There is
# no ambiguity to weigh -- a bare `- ` item cannot be a
# sibling of mapping keys, so a list here belongs to the key
# immediately above it and to nothing else.
child, idx = _parse_list(lines, idx, indent)
result[key] = child
else:
result[key] = None
elif rest == '{}':
result[key] = {}
elif rest == '[]':
result[key] = []
elif _is_flow(rest):
result[key] = _parse_flow(rest)
else:
result[key] = _parse_scalar(rest)
return result, idx
def _parse_list(lines, idx, indent):
result = []
n = len(lines)
while idx < n:
cur_indent, text = lines[idx]
if cur_indent != indent or not _is_list_line(text):
break
if text == '-':
idx += 1
if idx < n and lines[idx][0] > indent:
child_indent = lines[idx][0]
if _is_list_line(lines[idx][1]):
child, idx = _parse_list(lines, idx, child_indent)
else:
child, idx = _parse_dict(lines, idx, child_indent)
result.append(child)
else:
result.append(None)
continue
remainder = text[2:]
idx += 1
if remainder == '{}':
result.append({})
elif _is_flow(remainder):
result.append(_parse_flow(remainder))
elif _is_mapping_line(remainder):
item, idx = _parse_dict(lines, idx, indent + 1, first=remainder)
result.append(item)
else:
result.append(_parse_scalar(remainder))
return result, idx
_BLOCK_SCALAR = re.compile(r'^[|>][+-]?\d*[+-]?$')
def _is_block_scalar(text):
"""`>`, `>-`, `|`, `|+`, `|2-` -- the whole of the indicator grammar."""
return bool(text) and bool(_BLOCK_SCALAR.match(text))
def _is_list_line(text):
return text == '-' or text.startswith('- ')
def _is_mapping_line(text):
"""'key: value' vs a scalar that happens to contain a colon.
Every key `emit()` writes is a plain field name -- the scalar() rules
quote anything else, including a string ending in ':' -- so a colon
following an identifier, unquoted, is a key/value pair and nothing else
reaching this parser can look like one.
"""
if text[:1] == "'":
return False
if ':' not in text:
return False
key = text.split(':', 1)[0]
return bool(re.match(r'^[A-Za-z0-9_]+$', key))
def _is_flow(text):
"""A complete `[...]` or `{...}` -- one line, by the time it gets here."""
return ((text[:1] == '[' and text[-1:] == ']') or
(text[:1] == '{' and text[-1:] == '}')) and len(text) >= 2
def _parse_flow(text):
"""A flow collection: `[a, b]` as a list, `{a: 1, b: 2}` as a dict."""
if text[:1] == '{':
return _parse_flow_map(text)
return _parse_flow_list(text)
def _split_flow(inner):
"""The top-level items of a flow collection's interior.
Splits on commas that are neither inside a quoted scalar nor inside a
nested collection: a comma in `[[1, 2], [3]]` or in `'P76, spare'` is
not a separator of the outer collection, and a bracket inside a quoted
scalar does not open one.
"""
parts = []
current = []
quote = ''
depth = 0
i = 0
while i < len(inner):
ch = inner[i]
if quote:
if ch == quote:
if quote == "'" and inner[i + 1:i + 2] == "'":
current.append("'")
i += 2
continue
quote = ''
current.append(ch)
i += 1
continue
if _opens_quote(ch, inner[i - 1] if i else ''):
quote = ch
current.append(ch)
i += 1
continue
if ch == '[' or ch == '{':
depth += 1
elif ch == ']' or ch == '}':
if depth > 0:
depth -= 1
elif ch == ',' and not depth:
parts.append(''.join(current).strip())
current = []
i += 1
continue
current.append(ch)
i += 1
tail = ''.join(current).strip()
if tail:
parts.append(tail)
return parts
def _parse_flow_list(text):
"""`[a, b, 'c, d']` as a list. Complete on one line by now.
A wrapped sequence has already been folded onto one line by
`_join_flow`, so this only ever sees a closed `[...]`.
"""
inner = text[1:-1].strip()
if not inner:
return []
return [_flow_item(p) for p in _split_flow(inner)]
def _parse_flow_map(text):
"""`{a: 1, b: 2}` as a dict.
THIS USED TO COME BACK AS THE RAW STRING, which is the module's other
silent-wrong-answer shape: nothing failed, the value simply was not a
mapping, and every key under it was missing from the tree without a
word. A real job knowledge file writes 29 of them
(`bar_notes: {SR356: OPPOSING PAIRS}`), and the config editor's
whole-file coverage check counts paths -- so those 29 read as 41
settings the editor could not see and refused to write beside.
An item with no colon is not a mapping entry and is dropped rather than
guessed at; `_parse_dict` treats a colon-less line the same way.
"""
inner = text[1:-1].strip()
result = {}
if not inner:
return result
for part in _split_flow(inner):
key, sep, rest = _split_flow_pair(part)
if not sep:
continue
result[_parse_key(key)] = _flow_item(rest.strip())
return result
def _split_flow_pair(part):
"""One `key: value` of a flow mapping, split at the first free colon."""
quote = ''
i = 0
while i < len(part):
ch = part[i]
if quote:
if ch == quote:
if quote == "'" and part[i + 1:i + 2] == "'":
i += 2
continue
quote = ''
i += 1
continue
if _opens_quote(ch, part[i - 1] if i else ''):
quote = ch
i += 1
continue
if ch == ':':
return part[:i], ':', part[i + 1:]
i += 1
return part, '', ''
def _flow_item(text):
"""One item of a flow collection: nested collection, or scalar."""
if text == '{}':
return {}
if text == '[]':
return []
if _is_flow(text):
return _parse_flow(text)
return _parse_scalar(text)
def _unquote(text):
"""A quoted scalar's content, or None when the text is not quoted.
Single quotes are what `emit()` writes and `''` is the escape. DOUBLE
quotes are read too, for the same reason everything else in this
pre-pass is: a person writing a knowledge file by hand uses them, and
a value the two readers disagree about is a value the config editor
refuses to write beside. The escape handled is the one that matters
for this subset -- a backslash before a quote or a backslash -- not the
whole of YAML's double-quoted grammar (\\u, \\n and friends are left
alone rather than half-implemented).
Inch marks are safe: `3"` neither starts nor ends the way this needs.
"""
if len(text) < 2:
return None
if text[0] == "'" and text[-1] == "'":
return text[1:-1].replace("''", "'")
if text[0] == '"' and text[-1] == '"':
return text[1:-1].replace('\\"', '"').replace('\\\\', '\\')
return None
def _parse_key(text):
text = text.strip()
inner = _unquote(text)
return text if inner is None else inner
def _parse_scalar(text):
text = text.strip()
inner = _unquote(text)
if inner is not None:
return inner
if text == 'null':
return None
if text == 'true':
return True
if text == 'false':
return False
if re.match(r'^[+-]?\d+$', text):
return int(text)
if re.match(r'^[+-]?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?$', text) and \
('.' in text or 'e' in text or 'E' in text):
try:
return float(text)
except ValueError:
pass
return text