Skip to content

Instantly share code, notes, and snippets.

@andreif
Last active July 16, 2023 08:56
Show Gist options
  • Save andreif/4109020cc854939fadaab1eda1009b26 to your computer and use it in GitHub Desktop.
Save andreif/4109020cc854939fadaab1eda1009b26 to your computer and use it in GitHub Desktop.
Convert JSON to YAML in Python without any dependency.
"""
Convert JSON to colorful YAML.
Example:
$ alias jaml="python .../jaml.py"
$ curl https://api.thecatapi.com/v1/images/search -s | jaml
License: MIT
"""
def dump(data, iter=False, color=False, indent=2):
indent = ' ' * indent
def clr(code, value):
if not color:
return value
return f'\033[{code}m{value}\033[0m'
def _dump(value):
if value is None:
yield clr('3;31', 'null')
elif isinstance(value, bool):
yield clr('3;31', str(value).lower())
elif isinstance(value, (int, float)):
yield clr(34, str(value))
elif isinstance(value, str):
if '\n' in value:
yield clr(3, '|')
for line in value.splitlines():
yield clr(3, line)
else:
yield clr(3, value or "''")
elif isinstance(value, list):
if value:
for item in value:
for n, line in enumerate(_dump(item)):
yield (indent if n else clr(36, '- ')) + line
else:
yield '[]'
elif isinstance(value, dict):
if value:
for key, val in value.items():
key = clr(32, str(key) + ':')
if isinstance(val, (list, dict)) and val:
yield key
for line in _dump(val):
yield indent + line
else:
for n, line in enumerate(_dump(val)):
yield (indent if n else key + ' ') + line
else:
yield '{}'
else:
raise NotImplementedError(type(value))
generator = _dump(value=data)
if iter:
return generator
else:
return '\n'.join(list(generator))
if __name__ == '__main__':
import json
import sys
if not sys.stdin.isatty():
text = sys.stdin.read()
try:
data = json.loads(text)
except Exception:
print(text)
else:
print(dump(data, color=True))
@andreif
Copy link
Author

andreif commented Jul 15, 2023

TODO:

  •   Support multiline strings
  •  Handle trailing whitespace in strings.
  • Handle special characters in strings, e.g. yaml/yaml-test-suite#125.

@andreif
Copy link
Author

andreif commented Jul 16, 2023

Testing is done by converting JSON to YAML and back and comparing the result to the initial input:

import datetime
import json
import sys

import ruamel.yaml  # pip install ruamel.yaml
import yaml         # pip install PyYAML

import jaml


def default(x):
    if isinstance(x, datetime.datetime):
        return x.isoformat().replace('000+00:00', 'Z')
    return repr(x)


def test(text):
    data = json.loads(text)
    v1 = json.dumps(data, indent=2, default=default)
    print(jaml.dump(data, color=True))
    yml = jaml.dump(data)
    data = ruamel.yaml.YAML(typ='safe', pure=True).load(yml)  # YAML 1.2
    # data = yaml.safe_load(yml)  # YAML 1.1
    v2 = json.dumps(data, indent=2, default=default)
    if v1 != v2:
        print('v1:', v1)
        print('v2:', v2)
        print("DIFF", file=sys.stderr)
        exit(1)


if __name__ == '__main__':
    if not sys.stdin.isatty():
        test(sys.stdin.read())
        exit()

    1 and test('''
        {"a": {"b": {"c": {"d": 1}}}}
    ''')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment