ssato / python-anyconfig

Python library provides common APIs to load and dump configuration files in various formats
MIT License
277 stars 31 forks source link

better support for YAML #96

Closed AvdN closed 5 years ago

AvdN commented 5 years ago

ruamel.yaml is a super-set of PyYAML(both in that it supports 1.2 and 1.1, and that it has features to round-trip (load, modify, dump) data. anyconfig is currently not exploiting those features resulting in e.g. the restriction that output results are not ordered as indicated for the YAML backend.

There is a PyYAML backward compatible interface to ruamel.yaml, which you can get by using the RoundTripLoader and RoundTripDumper as Loader and Dumper:

    from ruamel.yaml import RoundTripLoader as Loader
    from ruamel.yaml import RoundTripDumper as Dumper

These are subclasses of the safe versions not of the (PyYAML default) unsafe versions.

In particular YAML mappings are loaded as a subclass of (ordered)dict and YAML sequences as a subclass of list. Therefor the key ordering of the original mappings is preserved and not sorted (as with PyYAML). These subclasses are CommentedMap and CommentedSeq. The subclassing also allows:

There is a newer API used as:

from ruamel.yaml import YAML
yaml = YAML()
yaml.preserve_quotes = True
yaml.indent(mapping=4, sequence=6, offset=3)
data = yaml.load(string_stream_Path)
# modify data
yaml.dump(data, stream_Path)

There are also other, less important features, only available through the new API. The major restriction in the new API is that print(yaml.dump(data)) is no longer possible. This was much abused by people not understanding the streaming nature of the YAML dumper (which is in exact contrast to the standard library JSON dumper, where dump() calls dumps() anyway)

Supporting roundtriploading certainly requires changes as at least CommentedSeq gets dumped with all its metadata by anyconfig. It is probably best to first split the functionality between PyYAML and ruamel.yaml based on whether the latter can be imported and then adapt the functionality from there on. As indicated the loaded data can be dumped to JSON, but I have not tested doing so with anyconfig, nor how this influence the other front-backend combinations in the matrix of supported formats.

ssato commented 5 years ago

Great thanks for your details!

ssato commented 5 years ago

Sorry to late.

I add and enable basic support of ruamel.yaml with some commits such as 90abb01 in the next branch.

ssato@x1-carbon-gen6% ls                                                                          ~/repos/public/github.com/ssato/python-anyconfig.git
AUTHORS.txt  LICENSE.MIT  MANIFEST.in  NEWS  README.rst  anyconfig  anyconfig.egg-info  build  dist  docs  pkg  setup.cfg  setup.py  tests  tox.ini
ssato@x1-carbon-gen6% bpython3
bpython version 0.17.1 on top of Python 3.7.1 /usr/bin/python3
>>> import sys, os, pprint
>>> sys.path.insert(0, os.curdir)
>>> import anyconfig
>>> anyconfig.backend.yaml.ryaml
<module 'anyconfig.backend.yaml.ruamel_yaml' from './anyconfig/backend/yaml/ruamel_yaml.py'>
>>> d = anyconfig.load("tests/res/00-scm.yml")
>>> d
CommentedMap([('type', 'object'), ('properties', CommentedMap([('name', CommentedMap([('type', 'string')])), ('a', CommentedMap([('type', 'integer')]))
, ('b', CommentedMap([('type', 'object'), ('properties', CommentedMap([('b', CommentedMap([('type', 'array'), ('items', CommentedMap([('type', 'integer
')]))]))]))]))]))])
>>> s = anyconfig.dumps(d, default_flow_style=False, ac_parser="yaml")
>>> pprint.pprint(s)
('type: object\n'

 'properties:\n'

 '  name:\n'

 '    type: string\n'

 '  a:\n'

 '    type: integer\n'

 '  b:\n'

 '    type: object\n'

 '    properties:\n'

 '      b:\n'

 '        type: array\n'

 '        items:\n'

 '          type: integer\n')
>>>
ssato commented 5 years ago

Updated. Now users can specify the backend parser explicitly with the commits such as 67b8600. If it's OK, I'll clean up the code and merge changes into the master branch.

ssato@x1-carbon-gen6% bpython3                                                                    ~/repos/public/github.com/ssato/python-anyconfig.git
bpython version 0.17.1 on top of Python 3.7.1 /usr/bin/python3
>>> import sys,os
>>> sys.path.insert(0, os.curdir)
>>> import anyconfig
>>> print(open("tests/res/00-scm.yml").read())
# comment-0
type: object
properties:
  # comment-1
  name:
    # comment-2
    type: string
  a:
    type: integer
  b:
    type: object
    properties:
      b:
        type: array
        items:
          type: integer
>>> d = anyconfig.load("tests/res/00-scm.yml", ac_parser="ruamel.yaml", typ="rt")
>>> d
CommentedMap([('type', 'object'), ('properties', CommentedMap([('name', CommentedMap([('type', 'string')])), ('a', CommentedMap([('type', 'integer')]))
, ('b', CommentedMap([('type', 'object'), ('properties', CommentedMap([('b', CommentedMap([('type', 'array'), ('items', CommentedMap([('type', 'integer
')]))]))]))]))]))])
>>> print(anyconfig.dumps(d, ac_parser="ruamel.yaml", explicit_start=True, indent=4))
---
type: object
properties:
    name:
        type: string
    a:
        type: integer
    b:
        type: object
        properties:
            b:
                type: array
                items:
                    type: integer
>>>
ssato commented 5 years ago

I've just released 0.9.8 contains this changes and fixes, and let this issue closed. Please let me know if any further issues found related to this.

Thanks again for your suggestion!

gsemet commented 5 years ago

Hi. I have configuration where is does not work (ruamel.yaml without pyyaml package)

Python 3.5.2 (default, Nov 12 2018, 13:43:14) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import anyconfig
>>> anyconfig.backend.yaml.ryaml
False
>>> dir(anyconfig.backend.yaml)
['PARSERS', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'absolute_import', 'ryaml']
>>> anyconfig.backend.yaml.PARSERS
[]
>>> import ruamel.yaml
>>> ruamel.yaml
<module 'ruamel.yaml' from '/home/username/projects/myproject/.venv/lib/python3.5/site-packages/ruamel/yaml/__init__.py'>

pip freeze shows ruamel.yaml==0.15.87 and anyconfig==0.9.8.

Installing pyyaml seem to enable the ruamel backend:

$ pip install pyyaml
$ python
>>> import anyconfig.backend.yaml
>>> anyconfig.backend.yaml.PARSERS
[<class 'anyconfig.backend.yaml.pyyaml.Parser'>, <class 'anyconfig.backend.yaml.ruamel_yaml.Parser'>]
ssato commented 5 years ago

@gsemet Thanks a lot for your (possible bug) report! I opened another issue for this and will look into it.

ssato commented 5 years ago

@gsemet FYI. https://github.com/ssato/python-anyconfig/issues/99#issuecomment-461838387

gsemet commented 5 years ago

look great thanks!