Skip to content

Commit

Permalink
Updated json5_write so that the resulting file is mostly Json5 (unquo…
Browse files Browse the repository at this point in the history
…ted keys, non-escaped unicode), where allowed.
  • Loading branch information
eisDNV committed Feb 25, 2025
1 parent d5b191c commit e7e0998
Show file tree
Hide file tree
Showing 13 changed files with 1,899 additions and 3,279 deletions.
90 changes: 65 additions & 25 deletions src/sim_explorer/utils/json5.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import codecs
import logging
import re
from pathlib import Path
Expand Down Expand Up @@ -26,45 +27,83 @@ def json5_check(js5: dict[str, Any]) -> bool:
return json5.encode_noop(js5)


def json5_write(js5: dict, file: Path | str, *, indent: int = 3):
def json5_write(js5: dict, file: Path | str, *, indent: int = 3, compact: bool = True):
"""Use pyjson5 to print the json5 code to file, optionally using indenting the code to make it human-readable.
Args:
file (Path): Path (file) object. Is overwritten if it exists.
indent (int): indentation length. Raw dump if set to -1
indent (int) = 3: indentation length. Raw dump if set to -1
compact (bool) = True: compact file writing, i.e. try to keep keys unquoted and avoid escapes
"""

def _unescape(chars: str):
"""Try to unescape chars. If that results in a valid Json5 value we keep the unescaped version."""
if len(chars) and chars[0] in ("[", "{"):
pre = chars[0]
chars = chars[1:]
else:
pre = ""
if len(chars) and chars[-1] in ("]", "}", ","):
post = chars[-1]
chars = chars[:-1]
else:
post = ""

unescaped = codecs.decode(chars, "unicode-escape")
try:
json5.decode("{ key : " + unescaped + "}")
except Json5Exception:
return pre + chars + post # need to keep the escaping to have valid Json5
else: # unescaped this is still valid Json5
return pre + unescaped + post

def _pretty_print(chars: str):
nonlocal fp, level, indent, _list
nonlocal fp, level, indent, _list, _collect

# first all actions with explicit fp.write
if chars == ":":
fp.write(": ")
return
if compact:
_c = _collect.strip()
no_quote = _c.strip('"').strip("'").strip('"').strip("'")
try:
json5.decode("{" + no_quote + " : 'dummy'}")
except Json5Exception:
_collect += ": "
else:
_collect = no_quote + ": "
fp.write(_collect)
_collect = ""
elif chars == "{":
level += 1
fp.write("{\n" + " " * (level * indent))
return
_collect += "{\n" + " " * (level * indent)
elif chars == "," and _list == 0:
fp.write(",\n" + " " * (level * indent))
return
# the the actions below add fp_write in the end
elif chars == "}":
_collect += ",\n" + " " * (level * indent)
else: # default
_collect += chars
# level and _list handling
if chars == "}":
level -= 1
elif chars == "[":
_list += 1
elif chars == "]":
_list -= 1
fp.write(chars)
# write to file and reset _collect
if chars in ("{", "}", "[", "]", ","):
fp.write(_unescape(_collect))
_collect = ""

assert json5.encode_noop(js5), f"Python object {js5} is not serializable as Json5"
level: int = 0
_list: int = 0
with Path.open(Path(file), "w") as fp:
if indent == -1:
json5.encode_io(js5, fp, supply_bytes=False, quotationmark="'") # type: ignore # pyright? TextIOWrapper??
elif indent >= 0:
json5.encode_callback(js5, _pretty_print, supply_bytes=False)
if indent == -1: # just dump it no pretty print, Json5 features, ...
txt = json5.encode(js5, quotationmark="'")
with Path.open(Path(file), "w") as fp:
fp.write(txt)

elif indent >= 0: # pretty-print and other features are taken into account
level: int = 0
_list: int = 0
_collect: str = ""
with Path.open(Path(file), "w") as fp:
json5.encode_callback(js5, _pretty_print, supply_bytes=False, quotationmark="'")


def json5_find_identifier_start(txt: str, pos: int):
Expand Down Expand Up @@ -104,10 +143,10 @@ def json5_try_correct(txt: str, pos: int):
return (success, txt)


def json5_read(file: Path | str, *, save_if_changed: bool = False) -> dict:
def json5_read(file: Path | str, *, save: int = 0) -> dict:
"""Read the Json5 file.
If key or comment errors are encountered they are tried fixed 'en route'.
If corrections have been performed and save_if_changed=True the file is saved with the same name.
save: 0: do not save, 1: save if changed, 2: save in any case. Overwrite file when saving.
"""

def get_line(txt: str, pos: int) -> int:
Expand Down Expand Up @@ -154,10 +193,11 @@ def get_line(txt: str, pos: int) -> int:
raise ValueError(f"Unhandled problem in Json5 file {file}: {err.args[0]}") from err
else:
break
if num_warn > 0:
logger.warning(f"Decoding the file {file}, {num_warn} illegal characters were detected and fixed")
if save_if_changed:
json5_write(js5, file)
if save == 0 and num_warn > 0:
logger.warning(f"Decoding the file {file}, {num_warn} illegal characters were detected. Not saved.")
elif (save == 1 and num_warn > 0) or save == 2:
logger.warning(f"Decoding the file {file}, {num_warn} illegal characters were detected. File re-saved.")
json5_write(js5, file, indent=3, compact=True)
return js5


Expand Down
76 changes: 38 additions & 38 deletions tests/data/BouncingBall0/BouncingBall.cases
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
{
"header": {
"name": "BouncingBall",
"description": "Simple sim explorer with the basic BouncingBall FMU (ball dropped from h=1m",
"modelFile": "OspSystemStructure.xml",
"simulator": "OSP",
"logLevel": "fatal",
"timeUnit": "second",
"variables": {
"g": ["bb","g","Gravity acting on the ball"],
"e": ["bb","e","Coefficient of restitution"],
"v_min": ["bb","v_min","Velocity below which the ball stops bouncing"],
"h": ["bb","h","Position (z) of the ball"],
"v_z": ["bb","der(h)","Derivative of h (speed in z-direction"],
"v": ["bb","v","Velocity of ball"],
"a_z": ["bb","der(v)","Derivative of v (acceleration in z-direction)"]}},
"base": {
"description": "Variable settings for the base case. All other cases are based on that",
"spec": {
"stepSize": 0.01,
"stopTime": 3.0,
"g": -9.81,
"e": 1.0,
"h": 1.0,
"h@step": "result",
"[email protected]": "result"}},
"restitution": {
"description": "Smaller coefficient of restitution e",
"spec": {
"e": 0.5}},
"restitutionAndGravity": {
"description": "Based restitution (e change), change also the gravity g",
"parent": "restitution",
"spec": {
"g": -1.5}},
"gravity": {
"description": "Gravity like on the moon",
"spec": {
"g": -1.5}}}
header: {
name: 'BouncingBall',
description: 'Simple sim explorer with the basic BouncingBall FMU (ball dropped from h=1m',
modelFile: 'OspSystemStructure.xml',
simulator: 'OSP',
logLevel: 'fatal',
timeUnit: 'second',
variables: {
g: ['bb','g','Gravity acting on the ball'],
e: ['bb','e','Coefficient of restitution'],
v_min: ['bb','v_min','Velocity below which the ball stops bouncing'],
h: ['bb','h','Position (z) of the ball'],
v_z: ['bb','der(h)','Derivative of h (speed in z-direction'],
v: ['bb','v','Velocity of ball'],
a_z: ['bb','der(v)','Derivative of v (acceleration in z-direction)']}},
base: {
description: 'Variable settings for the base case. All other cases are based on that',
spec: {
stepSize: 0.01,
stopTime: 3.0,
g: -9.81,
e: 1.0,
h: 1.0,
'h@step': 'result',
'[email protected]': 'result'}},
restitution: {
description: 'Smaller coefficient of restitution e',
spec: {
e: 0.5}},
restitutionAndGravity: {
description: 'Based restitution (e change), change also the gravity g',
parent: 'restitution',
spec: {
g: -1.5}},
gravity: {
description: 'Gravity like on the moon',
spec: {
g: -1.5}}}
88 changes: 44 additions & 44 deletions tests/data/BouncingBall3D/BouncingBall3D.cases
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
{
"header": {
"name": "BouncingBall3D",
"description": "Simple sim explorer with the 3D BouncingBall FMU (3D position and speed)",
"modelFile": "OspSystemStructure.xml",
"simulator": "OSP",
"logLevel": "fatal",
"timeUnit": "second",
"variables": {
"g": ["bb","g","Gravity acting on the ball"],
"e": ["bb","e","Coefficient of restitution"],
"x": ["bb","pos","3D Position of the ball in meters"],
"v": ["bb","speed","3D speed of ball in meters/second"],
"x_b": ["bb","p_bounce","Expected 3D Position where the next bounce will occur (in meters)"]}},
"base": {
"description": "Ball dropping from height 1 m. Results should be the same as the basic BouncingBall",
"spec": {
"stepSize": 0.01,
"stopTime": 3,
"g": 9.81,
"e": 1.0,
"x[2]": 39.37007874015748,
"x@step": "result",
"v@step": "result",
"x_b@step": "res"}},
"restitution": {
"description": "Smaller coefficient of restitution e",
"spec": {
"e": 0.5}},
"restitutionAndGravity": {
"description": "Based restitution (e change), change also the gravity g",
"parent": "restitution",
"spec": {
"g": 1.5},
"assert": {
"1@A": ["g==1.5","Check setting of gravity (about 1/7 of earth)"],
"2@ALWAYS": ["e==0.5","Check setting of restitution"],
"3@F": ["x[2] \u003c 3.0","For long times the z-position of the ball remains small (loss of energy)"],
"[email protected]": ["abs(x[2]) \u003c 0.4","Close to bouncing time the ball should be close to the floor"]}},
"gravity": {
"description": "Gravity like on the moon",
"spec": {
"g": 1.5},
"assert": {
"6@ALWAYS": ["g==9.81","Check wrong gravity."]}}}
header: {
name: 'BouncingBall3D',
description: 'Simple sim explorer with the 3D BouncingBall FMU (3D position and speed)',
modelFile: 'OspSystemStructure.xml',
simulator: 'OSP',
logLevel: 'fatal',
timeUnit: 'second',
variables: {
g: ['bb','g','Gravity acting on the ball'],
e: ['bb','e','Coefficient of restitution'],
x: ['bb','pos','3D Position of the ball in meters'],
v: ['bb','speed','3D speed of ball in meters/second'],
x_b: ['bb','p_bounce','Expected 3D Position where the next bounce will occur (in meters)']}},
base: {
description: 'Ball dropping from height 1 m. Results should be the same as the basic BouncingBall',
spec: {
stepSize: 0.01,
stopTime: 3,
g: 9.81,
e: 1.0,
'x[2]': 39.37007874015748,
'x@step': 'result',
'v@step': 'result',
'x_b@step': 'res'}},
restitution: {
description: 'Smaller coefficient of restitution e',
spec: {
e: 0.5}},
restitutionAndGravity: {
description: 'Based restitution (e change), change also the gravity g',
parent: 'restitution',
spec: {
g: 1.5},
assert: {
'1@A': ['g==1.5','Check setting of gravity (about 1/7 of earth)'],
'2@ALWAYS': ['e==0.5','Check setting of restitution'],
'3@F': ['x[2] < 3.0','For long times the z-position of the ball remains small (loss of energy)'],
'[email protected]': ['abs(x[2]) < 0.4','Close to bouncing time the ball should be close to the floor']}},
gravity: {
description: 'Gravity like on the moon',
spec: {
g: 1.5},
assert: {
'6@ALWAYS': ['g==9.81','Check wrong gravity.']}}}
Loading

0 comments on commit e7e0998

Please sign in to comment.