diff options
author | Anthon van der Neut <anthon@mnt.org> | 2017-06-04 14:27:02 +0200 |
---|---|---|
committer | Anthon van der Neut <anthon@mnt.org> | 2017-06-04 14:27:02 +0200 |
commit | a0ff58af5242fddc39236168b18aafb7c8b324da (patch) | |
tree | b7ba0da53b720dde7be6e3b32ad927456c6f5dcf | |
parent | 6ba66a44af41d072f5ceddfcdf2c21611c2a7cd0 (diff) | |
download | ruamel.yaml-a0ff58af5242fddc39236168b18aafb7c8b324da.tar.gz |
initial version with YAML class0.15.0
Some new tests are still required
-rw-r--r-- | CHANGES | 16 | ||||
-rw-r--r-- | README.rst | 17 | ||||
-rw-r--r-- | __init__.py | 4 | ||||
-rw-r--r-- | _doc/api.rst | 123 | ||||
-rw-r--r-- | _doc/basicuse.rst | 56 | ||||
-rw-r--r-- | _doc/detail.rst | 6 | ||||
-rw-r--r-- | _doc/index.rst | 7 | ||||
-rw-r--r-- | _test/data/duplicate-anchor-1.loader-warning (renamed from _test/data/duplicate-anchor-1.loader-error) | 0 | ||||
-rw-r--r-- | _test/data/duplicate-anchor-2.loader-warning (renamed from _test/data/duplicate-anchor-2.loader-error) | 0 | ||||
-rw-r--r-- | _test/lib/test_errors.py | 2 | ||||
-rw-r--r-- | _test/test_z_data.py | 5 | ||||
-rw-r--r-- | composer.py | 6 | ||||
-rw-r--r-- | constructor.py | 6 | ||||
-rw-r--r-- | cyaml.py | 3 | ||||
-rw-r--r-- | emitter.py | 50 | ||||
-rw-r--r-- | error.py | 1 | ||||
-rw-r--r-- | main.py | 380 | ||||
-rw-r--r-- | parser.py | 6 | ||||
-rw-r--r-- | reader.py | 42 | ||||
-rw-r--r-- | representer.py | 2 | ||||
-rw-r--r-- | resolver.py | 11 | ||||
-rw-r--r-- | scanner.py | 4 | ||||
-rw-r--r-- | serializer.py | 4 |
23 files changed, 699 insertions, 52 deletions
@@ -1,3 +1,19 @@ +[0, 15, 0]: 2017-06-04 + - it is no allowed to pass in a ``pathlib.Path`` as "stream" parameter to all + load/dump functions + - passing in a non-supported object (e.g. a string) as "stream" will result in a + much more meaningful YAMLStreamError. + - assigning a normal string value to an existing CommentedMap key or CommentedSeq + element will result in a value cast to the previous value's type if possible. + +[0, 15, 0]: 2017-06-04 + - it is no allowed to pass in a ``pathlib.Path`` as "stream" parameter to all + load/dump functions + - passing in a non-supported object (e.g. a string) as "stream" will result in a + much more meaningful YAMLStreamError. + - assigning a normal string value to an existing CommentedMap key or CommentedSeq + element will result in a value cast to the previous value's type if possible. + [0, 14, 12]: 2017-05-14 - fix for issue 119, deepcopy not returning subclasses (reported and PR by Constantine Evans <cevans@evanslabs.org>) @@ -4,10 +4,24 @@ ruamel.yaml ``ruamel.yaml`` is a YAML 1.2 loader/dumper package for Python. +Starting with version 0.15.0 the way YAML files are loaded and dumped +is changing. See the API doc for details. Currently existing +functionality will throw a warning before being changed/removed. +**For production systems you should pin the version being used with +``ruamel.yaml<=0.15``**. There might be bug fixes in the 0.14 series, +but new functionality is likely only to be available via the new API. + +If your package uses ``ruamel.yaml`` and is not listed on PyPI, drop +me an email, preferably with some infomormation on how you use the +package (or a link to bitbucket/github) and I'll keep you informed +when the status of the API is stable enough to make the transition. + * `Overview <http://yaml.readthedocs.org/en/latest/overview.html>`_ * `Installing <http://yaml.readthedocs.org/en/latest/install.html>`_ +* `Basic Usage <http://yaml.readthedocs.org/en/latest/basicuse.html>`_ * `Details <http://yaml.readthedocs.org/en/latest/detail.html>`_ * `Examples <http://yaml.readthedocs.org/en/latest/example.html>`_ +* `API <http://yaml.readthedocs.org/en/latest/api.html>`_ * `Differences with PyYAML <http://yaml.readthedocs.org/en/latest/pyyaml.html>`_ .. image:: https://readthedocs.org/projects/yaml/badge/?version=stable @@ -18,13 +32,14 @@ ChangeLog .. should insert NEXT: at the beginning of line for next key -NEXT: +0.15.0 (2017-06-04): - it is no allowed to pass in a ``pathlib.Path`` as "stream" parameter to all load/dump functions - passing in a non-supported object (e.g. a string) as "stream" will result in a much more meaningful YAMLStreamError. - assigning a normal string value to an existing CommentedMap key or CommentedSeq element will result in a value cast to the previous value's type if possible. + - added ``YAML`` class for new API 0.14.12 (2017-05-14): - fix for issue 119, deepcopy not returning subclasses (reported and PR by diff --git a/__init__.py b/__init__.py index e33f19e..14cf4e2 100644 --- a/__init__.py +++ b/__init__.py @@ -11,8 +11,8 @@ if False: # MYPY _package_data = dict( full_package_name='ruamel.yaml', - version_info=(0, 14, 13, 'dev'), - __version__='0.14.13.dev', + version_info=(0, 15, 0), + __version__='0.15.0', author='Anthon van der Neut', author_email='a.van.der.neut@ruamel.eu', description='ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order', # NOQA diff --git a/_doc/api.rst b/_doc/api.rst new file mode 100644 index 0000000..61d74a1 --- /dev/null +++ b/_doc/api.rst @@ -0,0 +1,123 @@ + + +Departure from previous API +--------------------------- + +With version 0.15.0 `ruamel.yaml` starts to depart from the previous (PyYAML) way +of loading and dumping. During a transition period the original +`load()` and `dump()` in its various formats will still be supported, +but this is not guaranteed to be so with the transition to 1.0. + +At the latest with 1.0, but possible earlier transition error and +warning messages will be issued, so any packages depending on +ruamel.yaml should pin the version with which they are testing. + +Up to 0.15.0, the loaders (``load()``, ``safe_load()``, +``round_trip_load()``, ``load_all``, etc.) took, apart from the input +stream, a ``version`` argument to allow downgrading to YAML 1.1, +sometimes needed for +documents without directive. When round-tripping, there was an option to +preserve quotes. + +Up to 0.15.0, the dumpers (``dump()``, ``safe_dump``, +``round_trip_dump()``, ``dump_all()``, etc.) had a plethora of +arguments, some inhereted from ``PyYAML``, some added in +``ruamel.yaml``. The only required argument is the ``data`` to be +dumped. If the stream argument is not provided to the dumper, then a +string representation is build up in memory and returned to the +caller. + +Starting with 0.15.0 ``load()`` and ``dump()`` are methods on a +``YAML`` instance and only take the stream, +resp. the data and stram argument. All other parameters are set on the instance +of ``YAML`` before calling ``load()`` or ``dump()`` + +Before 0.15.0:: + + from pathlib import Path + from ruamel import yaml + + data = yaml.safe_load("abc: 1") + out = path('/tmp/out.yaml') + with out.open('w') as fp: + yaml.safe_dump(data, fp, default_flow_style=False) + +after: + + from ruamel.yaml import YAML + + yaml = YAML(typ='safe') + yaml.default_flow_style = False + data = yaml.load("abc: 1") + out = path('/tmp/out.yaml') + yaml.dump(data, out) + +If you previously used an keyword argument `explicit_start=True` you +now do ``yaml.explicit_start = True`` before calling ``dump()``. The +``Loader`` and ``Dumper`` keyword arguments are not supported that +way. You can provide the `typ` keyword to `rt` (default), +`safe`, `unsafe` or `base` (for round-trip load/dump, safe_load/dump, +load/dump resp. using the BaseLoader / BaseDumper. More fine-control +is possible by setting the attributes `.Parser`, `.Constructor`, +`.Emitter`, etc., to the class of the type to create for that stage +(typically a subclass of an existing class implementing that). + +All data is dumped (not just for round-trip-mode) with `.allow_unicode += True` + +You can of course have multiple YAML instances active at the same +time, with different load and/or dump behaviour. + +Initially only the typical operations are supported, but in principle +all functionality of the old interface will be available via +``YAML`` instances (if you are using something that isn't let me know). + +Reason for API change +--------------------- + +``ruamel.yaml`` inherited the way of doing things from ``PyYAML``. In +particular when calling the function ``load()`` or ``dump()`` a +temporary instances of ``Loader()`` resp. ``Dumper()`` were +created that were discarded on termination of the function. + +This way of doing things leads to several problems: + +- it is impossible to return information to the caller apart from the + constructed data structure. E.g. if you would get a YAML document + version number from a directive, there is no way to let the caller + know apart from handing back special data structures. The same + problem exists when trying to do on the fly + analysis of a document for indentation width. + +- these instances were composites of the various load/dump steps and + if you wanted to enhance one of the steps, you needed e.g. subclass + the emitter and make a new composite (dumper) as well, providing all + of the parameters (i.e. copy paste + +- many routines (like ``add_representer()``) have a direct global + impact on all of the following calls to ``dump()`` and those are + difficult if not impossible to turn back. This forces the need to + subclass ``Loaders`` and ``Dumpers``, a long time problem in PyYAML + as some attributes were not `deep_copied`` although a bug-report + (and fix) had been available a long time. + +- If you want to set an attribute, e.g. to control whether literal + block style scalars are allowed to have trailing spaces on a line + instead of being dumped as double quoted scalars, you have to change + the ``dump()`` family of routines, all of the ``Dumpers()`` as well + as the actual functionality change in `emitter.Emitter()`. The + functionality change takes changing 4 (four!) lines in one file, and being able + to enable that another 50+ line changes (non-contiguous) in 3 more files resulting + in diff that is far over 200 lines long. + +- replacing libyaml with something that doesn't both support `0o52` + and `052` for the integer ``42`` (instead of ``52`` as per YAML 1.2) + is difficult + + +With ``ruamel.yaml>=0.15.0`` the various steps "know" about the +``YAML`` instance and can pick up setting, as well as report back +information via that instance. Representers, etc., are added to a +reusable instance and different YAML instances can co-exists. + +This change eases development and helps prevent regressions. diff --git a/_doc/basicuse.rst b/_doc/basicuse.rst new file mode 100644 index 0000000..935e83a --- /dev/null +++ b/_doc/basicuse.rst @@ -0,0 +1,56 @@ +Basic Usage +=========== + +*This is the new (0.15+) interface for ``ruamel.yaml``, it is still in +the process of being fleshed out*. Please pin your dependency to +``ruamel.yaml<0.15`` for production software. + +------ + +You load a YAML document using:: + + from ruamel.yaml import YAML + + yaml=YAML(typ='safe') # default if not specfied is round-trip + + yaml.load(doc) + +in this ``doc`` can be a file pointer (i.e. an object that has the +`.read()` method, a string or a ``pathlib.Path()``. `typ='safe'` +accomplishes the same as what ``safe_load()`` did before: loading of a +document without resolving unknow tags. + +Dumping works in the same way:: + + from ruamel.yaml import YAML + + yaml=YAML() + yaml.default_flow_style = False + yaml.dump({a: [1, 2], s) + +in this ``s`` can be a file pointer (i.e. an object that has the +`.write()` method, a ``pathlib.Path()`` or ``None`` (the default, which causes the +YAML documented to be returned as a string. + +*If you have `yaml.dump()` +return the YAML doc as string, do not just ``print`` that returned +value*. In that case use `yaml.dump(data, sys.stdout)`, which is more +efficient (and shows that you know what you are doing). + +More examples +------------- + +Using the C based SafeLoader (at this time is inherited from +libyaml/PyYAML and e.g. loads ``0o52`` as well as ``052`` load as integer ``42``):: + + from ruamel.yaml import YAML + + yaml=YAML(typ="safe") + yaml.load("""a:\n b: 2\n c: 3\n""") + +Using the Python based SafeLoader (YAML 1.2 support, ``052`` loads as ``52``):: + + from ruamel.yaml import YAML + + yaml=YAML(typ="safe", pure=True) + yaml.load("""a:\n b: 2\n c: 3\n""") diff --git a/_doc/detail.rst b/_doc/detail.rst index 3df8fff..272b70c 100644 --- a/_doc/detail.rst +++ b/_doc/detail.rst @@ -72,9 +72,9 @@ for calculating the cumulative indent for deeper levels and specifying ``indent=3`` resp. ``block_seq_indent=2``, migth give correct, but counter intuitive results. -**It is best to always have** `indent >= block_seq_indent + 2` **but - this is not enforced**. Depending on your structure, not following - this advice **might lead to invalid output**. +**It is best to always have** ``indent >= block_seq_indent + 2`` +**but this is not enforced**. Depending on your structure, not following +this advice **might lead to invalid output**. Positioning ':' in top level mappings, prefix in ':' ---------------------------------------------------- diff --git a/_doc/index.rst b/_doc/index.rst index 6bfbdd3..28e3cdc 100644 --- a/_doc/index.rst +++ b/_doc/index.rst @@ -13,11 +13,8 @@ Contents: overview install + basicuse detail example + api pyyaml - - - - - diff --git a/_test/data/duplicate-anchor-1.loader-error b/_test/data/duplicate-anchor-1.loader-warning index 906cf29..906cf29 100644 --- a/_test/data/duplicate-anchor-1.loader-error +++ b/_test/data/duplicate-anchor-1.loader-warning diff --git a/_test/data/duplicate-anchor-2.loader-error b/_test/data/duplicate-anchor-2.loader-warning index 62b4389..62b4389 100644 --- a/_test/data/duplicate-anchor-2.loader-error +++ b/_test/data/duplicate-anchor-2.loader-warning diff --git a/_test/lib/test_errors.py b/_test/lib/test_errors.py index f89392c..9576c90 100644 --- a/_test/lib/test_errors.py +++ b/_test/lib/test_errors.py @@ -3,6 +3,8 @@ from __future__ import print_function import ruamel.yaml as yaml import test_emitter +import warnings +warnings.simplefilter('ignore', yaml.error.UnsafeLoaderWarning) def test_loader_error(error_filename, verbose=False): diff --git a/_test/test_z_data.py b/_test/test_z_data.py index 3b72618..d85ba28 100644 --- a/_test/test_z_data.py +++ b/_test/test_z_data.py @@ -9,8 +9,11 @@ import platform # NOQA sys.path.insert(0, os.path.dirname(__file__) + '/lib') -import ruamel.yaml # NOQA +import ruamel.yaml # NOQA import test_appliance # NOQA +import warnings # NOQA +warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning) + args = [] diff --git a/composer.py b/composer.py index e706145..2d84dd7 100644 --- a/composer.py +++ b/composer.py @@ -30,19 +30,23 @@ class Composer(object): def __init__(self, loader=None): # type: (Any) -> None self.loader = loader - if self.loader is not None: + if self.loader is not None and getattr(self.loader, '_composer', None) is None: self.loader._composer = self self.anchors = {} # type: Dict[Any, Any] @property def parser(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + self.loader.parser return self.loader._parser @property def resolver(self): # type: () -> Any # assert self.loader._resolver is not None + if hasattr(self.loader, 'typ'): + self.loader.resolver return self.loader._resolver def check_node(self): diff --git a/constructor.py b/constructor.py index fd37ddd..4a52b27 100644 --- a/constructor.py +++ b/constructor.py @@ -44,7 +44,7 @@ class BaseConstructor(object): def __init__(self, preserve_quotes=None, loader=None): # type: (bool, Any) -> None self.loader = loader - if self.loader is not None: + if self.loader is not None and getattr(self.loader, '_constructor', None) is None: self.loader._constructor = self self.loader = loader self.constructed_objects = {} # type: Dict[Any, Any] @@ -56,6 +56,8 @@ class BaseConstructor(object): @property def composer(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + return self.loader.composer try: return self.loader._composer except AttributeError: @@ -67,6 +69,8 @@ class BaseConstructor(object): @property def resolver(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + return self.loader.resolver return self.loader._resolver def check_data(self): @@ -5,7 +5,6 @@ from __future__ import absolute_import from _ruamel_yaml import CParser, CEmitter # type: ignore from ruamel.yaml.constructor import Constructor, BaseConstructor, SafeConstructor -from ruamel.yaml.serializer import Serializer from ruamel.yaml.representer import Representer, SafeRepresenter, BaseRepresenter from ruamel.yaml.resolver import Resolver, BaseResolver @@ -98,7 +97,7 @@ class CSafeDumper(CEmitter, SafeRepresenter, Resolver): # type: ignore Resolver.__init__(self) -class CDumper(CEmitter, Serializer, Representer, Resolver): # type: ignore +class CDumper(CEmitter, Representer, Resolver): # type: ignore def __init__(self, stream, default_style=None, default_flow_style=None, canonical=None, indent=None, width=None, @@ -55,18 +55,13 @@ class Emitter(object): top_level_colon_align=None, prefix_colon=None, dumper=None): # type: (StreamType, Any, int, int, bool, Any, int, bool, Any, Any) -> None self.dumper = dumper - if self.dumper is not None: + if self.dumper is not None and getattr(self.dumper, '_emitter', None) is None: self.dumper._emitter = self - # The stream should have the methods `write` and possibly `flush`. - if not hasattr(stream, 'write') and hasattr(stream, 'open'): - self.stream = stream.open('w') # pathlib.Path() instance - else: - if not hasattr(stream, 'write'): - raise YAMLStreamError('stream argument needs to have a write() method') - self.stream = stream + self.stream = stream # Encoding can be overriden by STREAM-START. self.encoding = None # type: Union[None, Text] + self.allow_space_break = None # Emitter is a state machine with a stack of states to handle nested # structures. @@ -136,6 +131,23 @@ class Emitter(object): self.analysis = None # type: Any self.style = None # type: Any + @property + def stream(self): + # type: () -> Any + try: + return self._stream + except AttributeError: + raise YAMLStreamError('output stream needs to specified') + + @stream.setter + def stream(self, val): + # type: (Any) -> None + if val is None: + return + if not hasattr(val, 'write'): + raise YAMLStreamError('stream argument needs to have a write() method') + self._stream = val + def dispose(self): # type: () -> None # Reset the state attributes (to clear self-references) @@ -632,6 +644,7 @@ class Emitter(object): (self.flow_level and self.analysis.allow_flow_plain or (not self.flow_level and self.analysis.allow_block_plain))): return '' + self.analysis.allow_block = True if self.event.style and self.event.style in '|>': if (not self.flow_level and not self.simple_key_context and self.analysis.allow_block): @@ -902,9 +915,12 @@ class Emitter(object): # Spaces followed by breaks, as well as special character are only # allowed for double quoted scalars. - if space_break or special_characters: - allow_flow_plain = allow_block_plain = \ - allow_single_quoted = allow_block = False + if special_characters: + allow_flow_plain = allow_block_plain = allow_single_quoted = allow_block = False + elif space_break: + allow_flow_plain = allow_block_plain = allow_single_quoted = False + if not self.allow_space_break: + allow_block = False # Although the plain scalar writer supports breaks, we never emit # multiline plain scalars. @@ -974,7 +990,7 @@ class Emitter(object): self.column = indent if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) def write_line_break(self, data=None): # type: (Any) -> None @@ -993,7 +1009,7 @@ class Emitter(object): data = u'%%YAML %s' % version_text if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) self.write_line_break() def write_tag_directive(self, handle_text, prefix_text): @@ -1001,7 +1017,7 @@ class Emitter(object): data = u'%%TAG %s %s' % (handle_text, prefix_text) if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) self.write_line_break() # Scalar streams. @@ -1247,7 +1263,7 @@ class Emitter(object): self.column += len(data) if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) self.whitespace = False self.indention = False spaces = False @@ -1269,7 +1285,7 @@ class Emitter(object): self.column += len(data) if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) start = end elif breaks: if ch not in u'\n\x85\u2028\u2029': @@ -1290,7 +1306,7 @@ class Emitter(object): self.column += len(data) if self.encoding: # type: ignore data = data.encode(self.encoding) - self.stream.write(data) # type: ignore + self.stream.write(data) start = end if ch is not None: spaces = (ch == u' ') @@ -129,6 +129,7 @@ class MarkedYAMLError(YAMLError): class YAMLStreamError(Exception): pass + class ReusedAnchorWarning(Warning): pass @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals +import sys import warnings import ruamel.yaml @@ -25,8 +26,387 @@ if False: # MYPY from typing import List, Set, Dict, Union, Any # NOQA from ruamel.yaml.compat import StreamType, StreamTextType, VersionType # NOQA +try: + from _ruamel_yaml import CParser, CEmitter # type: ignore +except: + CParser = CEmitter = None + # import io +enforce = object() + + +# YAML is an acronym, i.e. spoken: rhymes with "camel". And thus a +# subset of abbreviations, which should all caps according to PEP8 + +class YAML(object): + def __init__(self, _kw=enforce, typ=None, pure=False): + # type: (Any, Any, Any) -> None + """ + _kw: not used, forces keyword arguments in 2.7 (in 3 you can do (*, safe_load=..) + typ: 'rt'/None -> RoundTripLoader/RoundTripDumper, (default) + 'safe' -> SafeLoader/SafeDumper, + 'unsafe' -> normal/unsafe Loader/Dumper + 'base' -> baseloader + pure: if True only use Python modules + """ + if _kw is not enforce: + raise TypeError("{}.__init__() takes no positional argument but at least " + "one was given ({!r})".format(self.__class__.__name__, _kw)) + + self.typ = 'rt' if typ is None else typ + self.Resolver = ruamel.yaml.resolver.VersionedResolver + self.allow_unicode = True + self.Reader = None # type: Any + self.Scanner = None # type: Any + self.Serializer = None # type: Any + if self.typ == 'rt': + # no optimized rt-dumper yet + self.Emitter = ruamel.yaml.emitter.Emitter # type: Any + self.Serializer = ruamel.yaml.serializer.Serializer # type: Any + self.Representer = ruamel.yaml.representer.RoundTripRepresenter # type: Any + self.Scanner = ruamel.yaml.scanner.RoundTripScanner # type: Any + # no optimized rt-parser yet + self.Parser = ruamel.yaml.parser.RoundTripParser # type: Any + self.Composer = ruamel.yaml.composer.Composer # type: Any + self.Constructor = ruamel.yaml.constructor.RoundTripConstructor # type: Any + elif self.typ == 'safe': + self.Emitter = ruamel.yaml.emitter.Emitter if pure or CEmitter is None \ + else CEmitter + self.Representer = ruamel.yaml.representer.SafeRepresenter + self.Parser = ruamel.yaml.parser.Parser if pure or CParser is None else CParser + self.Composer = ruamel.yaml.composer.Composer + self.Constructor = ruamel.yaml.constructor.SafeConstructor + elif self.typ == 'base': + self.Emitter = ruamel.yaml.emitter.Emitter + self.Representer = ruamel.yaml.representer.BaseRepresenter + self.Parser = ruamel.yaml.parser.Parser if pure or CParser is None else CParser + self.Composer = ruamel.yaml.composer.Composer + self.Constructor = ruamel.yaml.constructor.BaseConstructor + elif self.typ == 'unsafe': + self.Emitter = ruamel.yaml.emitter.Emitter + self.Representer = ruamel.yaml.representer.Representer + self.Parser = ruamel.yaml.parser.Parser if pure or CParser is None else CParser + self.Composer = ruamel.yaml.composer.Composer + self.Constructor = ruamel.yaml.constructor.Constructor + self.stream = None + self.canonical = None + self.indent = None + self.width = None + self.line_break = None + self.block_seq_indent = None + self.top_level_colon_align = None + self.prefix_colon = None + self.version = None + self.preserve_quotes = None + self.encoding = None + self.explicit_start = None + self.explicit_end = None + self.tags = None + self.default_style = None + self.default_flow_style = None + + @property + def reader(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Reader(None, loader=self)) + return getattr(self, attr) + + @property + def scanner(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Scanner(loader=self)) + return getattr(self, attr) + + @property + def parser(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + if self.Parser is not CParser: + setattr(self, attr, self.Parser(loader=self)) + else: + if getattr(self, '_stream', None) is None: + # wait for the stream + return None + else: + # if not hasattr(self._stream, 'read') and hasattr(self._stream, 'open'): + # # pathlib.Path() instance + # setattr(self, attr, CParser(self._stream)) + # else: + setattr(self, attr, CParser(self._stream)) # type: ignore + # self._parser = self._composer = self + # print('scanner', self.loader.scanner) + + return getattr(self, attr) + + @property + def composer(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Composer(loader=self)) + return getattr(self, attr) + + @property + def constructor(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Constructor( + preserve_quotes=self.preserve_quotes, loader=self)) + return getattr(self, attr) + + @property + def resolver(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Resolver( + version=self.version, loader=self)) + return getattr(self, attr) + + @property + def emitter(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + if self.Emitter is not CEmitter: + setattr(self, attr, self.Emitter( + None, canonical=self.canonical, + indent=self.indent, width=self.width, + allow_unicode=self.allow_unicode, line_break=self.line_break, + block_seq_indent=self.block_seq_indent, + dumper=self)) + else: + if getattr(self, '_stream', None) is None: + # wait for the stream + return None + return None + return getattr(self, attr) + + @property + def serializer(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Serializer( + encoding=self.encoding, + explicit_start=self.explicit_start, explicit_end=self.explicit_end, + version=self.version, tags=self.tags, dumper=self)) + return getattr(self, attr) + + @property + def representer(self): + # type: () -> Any + attr = '_' + sys._getframe().f_code.co_name + if not hasattr(self, attr): + setattr(self, attr, self.Representer( + default_style=self.default_style, + default_flow_style=self.default_flow_style, + dumper=self)) + return getattr(self, attr) + + # separate output resolver? + + def load(self, stream): + # type: (StreamTextType) -> Any + """ + at this point you either have the non-pure Parser (which has its own reader and + scanner) or you have the pure Parser. + If the pure Parser is set, then set the Reader and Scanner, if not already set. + If either the Scanner or Reader are set, you cannot use the non-pure Parser, + so reset it to the pure parser and set the Reader resp. Scanner if necessary + """ + if not hasattr(stream, 'read') and hasattr(stream, 'open'): + # pathlib.Path() instance + with stream.open('r') as fp: # type: ignore + return self.load(fp) + constructor, parser = self.get_constructor_parser(stream) + try: + return constructor.get_single_data() + finally: + parser.dispose() + + def load_all(self, stream, _kw=enforce): # , skip=None): + # type: (StreamTextType, Any) -> Any + if _kw is not enforce: + raise TypeError("{}.__init__() takes no positional argument but at least " + "one was given ({!r})".format(self.__class__.__name__, _kw)) + if not hasattr(stream, 'read') and hasattr(stream, 'open'): + # pathlib.Path() instance + with stream.open('r') as fp: # type: ignore + yield self.load_all(fp, _kw=enforce) + # if skip is None: + # skip = [] + # elif isinstance(skip, int): + # skip = [skip] + constructor, parser = self.get_constructor_parser(stream) + try: + while constructor.check_data(): + yield constructor.get_data() + finally: + parser.dispose() + + def get_constructor_parser(self, stream): + # type: (StreamTextType) -> Any + """ + the old cyaml needs special setup, and therefore the stream + """ + if self.Parser is not CParser: + if self.Reader is None: + self.Reader = ruamel.yaml.reader.Reader + if self.Scanner is None: + self.Scanner = ruamel.yaml.scanner.Scanner + self.reader.stream = stream + else: + if self.Reader is not None: + if self.Scanner is None: + self.Scanner = ruamel.yaml.scanner.Scanner + self.Parser = ruamel.yaml.parser.Parser + self.reader.stream = stream + elif self.Scanner is not None: + if self.Reader is None: + self.Reader = ruamel.yaml.reader.Reader + self.Parser = ruamel.yaml.parser.Parser + self.reader.stream = stream + else: + # combined C level reader>scanner>parser + # does some calls to the resolver, e.g. BaseResolver.descend_resolver + # if you just initialise the CParser, to much of resolver.py + # is actually used + rslvr = self.Resolver + if rslvr is ruamel.yaml.resolver.VersionedResolver: + rslvr = ruamel.yaml.resolver.Resolver + + class XLoader(self.Parser, self.Constructor, rslvr): # type: ignore + def __init__(selfx, stream, version=None, preserve_quotes=None): + # type: (StreamTextType, VersionType, bool) -> None + CParser.__init__(selfx, stream) + selfx._parser = selfx._composer = selfx + self.Constructor.__init__(selfx, loader=selfx) + rslvr.__init__(selfx, loadumper=selfx) + self._stream = stream + loader = XLoader(stream) + return loader, loader + return self.constructor, self.parser + + def dump(self, data, stream=None): + # type: (Any, StreamType) -> Any + return self.dump_all([data], stream) + + def dump_all(self, documents, stream=None): + # type: (Any, StreamType) -> Any + """ + Serialize a sequence of Python objects into a YAML stream. + If stream is None, return the produced string instead. + """ + # The stream should have the methods `write` and possibly `flush`. + if not hasattr(stream, 'write') and hasattr(stream, 'open'): + # pathlib.Path() instance + with stream.open('w') as fp: # type: ignore + return self.dump_all(documents, fp) + getvalue = None + if self.top_level_colon_align is True: + tlca = max([len(str(x)) for x in documents[0]]) # type: Any + else: + tlca = self.top_level_colon_align + if stream is None: + if self.encoding is None: + stream = StringIO() + else: + stream = BytesIO() + getvalue = stream.getvalue + serializer, representer, emitter = \ + self.get_serializer_representer_emitter(stream, tlca) + try: + self.serializer.open() + for data in documents: + try: + self.representer.represent(data) + except AttributeError: + # print(dir(dumper._representer)) + raise + self.serializer.close() + finally: + try: + self.emitter.dispose() + except AttributeError: + raise + # self.dumper.dispose() # cyaml + delattr(self, "_serializer") + delattr(self, "_emitter") + if getvalue is not None: + return getvalue() + return None + + def get_serializer_representer_emitter(self, stream, tlca): + # type: (StreamType, Any) -> Any + # we have only .Serializer to deal with (vs .Reader & .Scanner), much simpler + if self.Emitter is not CEmitter: + if self.Serializer is None: + self.Serializer = ruamel.yaml.serializer.Serializer + self.emitter.stream = stream + self.emitter.top_level_colon_align = tlca + return self.serializer, self.representer, self.emitter + if self.Serializer is not None: + # cannot set serializer with CEmitter + self.Emitter = ruamel.yaml.emitter.Emitter + self.emitter.stream = stream + self.emitter.top_level_colon_align = tlca + return self.serializer, self.representer, self.emitter + # C routines + + rslvr = ruamel.yaml.resolver.BaseResolver if self.typ == 'base' \ + else ruamel.yaml.resolver.Resolver + + class XDumper(CEmitter, self.Representer, rslvr): # type: ignore + def __init__(selfx, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, block_seq_indent=None, + top_level_colon_align=None, prefix_colon=None): + # type: (StreamType, Any, Any, Any, bool, Union[None, int], Union[None, int], bool, Any, Any, Union[None, bool], Union[None, bool], Any, Any, Any, Any, Any) -> None # NOQA + CEmitter.__init__(selfx, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, + explicit_end=explicit_end, + version=version, tags=tags) + selfx._emitter = selfx._serializer = selfx._representer = selfx + Representer.__init__(selfx, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(selfx) + self._stream = stream + dumper = XDumper(stream) + self._emitter = self._serializer = dumper + return dumper, dumper, dumper + + # basic types + def map(self, **kw): + # type: (Any) -> Any + if self.typ == 'rt': + from ruamel.yaml.comments import CommentedMap + return CommentedMap(**kw) + else: + return dict(**kw) + + def seq(self, *args): + # type: (Any) -> Any + if self.typ == 'rt': + from ruamel.yaml.comments import CommentedSeq + return CommentedSeq(*args) + else: + return list(*args) + + +######################################################################################## def scan(stream, Loader=Loader): # type: (StreamTextType, Any) -> Any @@ -104,7 +104,7 @@ class Parser(object): def __init__(self, loader): # type: (Any) -> None self.loader = loader - if self.loader is not None: + if self.loader is not None and getattr(self.loader, '_parser', None) is None: self.loader._parser = self self.current_event = None self.yaml_version = None @@ -116,11 +116,15 @@ class Parser(object): @property def scanner(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + self.loader.scanner return self.loader._scanner @property def resolver(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + self.loader.resolver return self.loader._resolver def dispose(self): @@ -74,10 +74,9 @@ class Reader(object): def __init__(self, stream, loader=None): # type: (StreamTextType, Any) -> None self.loader = loader - if self.loader is not None: + if self.loader is not None and getattr(self.loader, '_reader', None) is None: self.loader._reader = self - self.name = None - self.stream = None # type: Any # as .read is called + self.name = None # type: Any self.stream_pointer = 0 self.eof = True self.buffer = u'' @@ -88,22 +87,35 @@ class Reader(object): self.index = 0 self.line = 0 self.column = 0 - if isinstance(stream, text_type): + self.stream = stream # type: Any # as .read is called + + @property + def stream(self): + # type: () -> Any + try: + return self._stream + except AttributeError: + raise YAMLStreamError('input stream needs to specified') + + @stream.setter + def stream(self, val): + # type: (Any) -> None + if val is None: + return + self._stream = None + if isinstance(val, text_type): self.name = "<unicode string>" - self.check_printable(stream) - self.buffer = stream+u'\0' - elif isinstance(stream, binary_type): + self.check_printable(val) + self.buffer = val + u'\0' + elif isinstance(val, binary_type): self.name = "<byte string>" - self.raw_buffer = stream + self.raw_buffer = val self.determine_encoding() else: - if not hasattr(stream, 'read') and hasattr(stream, 'open'): - self.stream = stream.open('r') - else: - if not hasattr(stream, 'read'): - raise YAMLStreamError('stream argument needs to have a read() method') - self.stream = stream - self.name = getattr(stream, 'name', "<file>") + if not hasattr(val, 'read'): + raise YAMLStreamError('stream argument needs to have a read() method') + self._stream = val + self.name = getattr(self.stream, 'name', "<file>") self.eof = False self.raw_buffer = None self.determine_encoding() diff --git a/representer.py b/representer.py index a9e229c..f6eef1b 100644 --- a/representer.py +++ b/representer.py @@ -60,6 +60,8 @@ class BaseRepresenter(object): def serializer(self): # type: () -> Any try: + if hasattr(self.dumper, 'typ'): + return self.dumper.serializer return self.dumper._serializer except AttributeError: return self # cyaml diff --git a/resolver.py b/resolver.py index 15fb55f..83656d1 100644 --- a/resolver.py +++ b/resolver.py @@ -104,7 +104,7 @@ class BaseResolver(object): def __init__(self, loadumper=None): # type: (Any, Any) -> None self.loadumper = loadumper - if self.loadumper is not None: + if self.loadumper is not None and getattr(self.loadumper, '_resolver', None) is None: self.loadumper._resolver = self.loadumper self._loader_version = None # type: Any self.resolver_exact_paths = [] # type: List[Any] @@ -114,6 +114,8 @@ class BaseResolver(object): def parser(self): # type: () -> Any if self.loadumper is not None: + if hasattr(self.loadumper, 'typ'): + return self.loadumper.parser return self.loadumper._parser return None @@ -204,6 +206,8 @@ class BaseResolver(object): def descend_resolver(self, current_node, current_index): # type: (Any, Any) -> None + # print('xx2', self) + # sys.exit(0) if not self.yaml_path_resolvers: return exact_paths = {} @@ -429,7 +433,10 @@ class VersionedResolver(BaseResolver): try: version = self.parser.yaml_version except AttributeError: - version = self.loadumper._serializer.use_version # dumping + if hasattr(self.loadumper, 'typ'): + version = self.loadumper.version + else: + version = self.loadumper._serializer.use_version # dumping if version is None: version = self._loader_version if version is None: @@ -72,7 +72,7 @@ class Scanner(object): # self.forward(l=1) # read the next l characters and move the pointer self.loader = loader - if self.loader is not None: + if self.loader is not None and getattr(self.loader, '_scanner', None) is None: self.loader._scanner = self # Had we reached the end of the stream? @@ -130,6 +130,8 @@ class Scanner(object): @property def reader(self): # type: () -> Any + if hasattr(self.loader, 'typ'): + self.loader.reader return self.loader._reader # Public methods. diff --git a/serializer.py b/serializer.py index bda778f..f224e96 100644 --- a/serializer.py +++ b/serializer.py @@ -56,11 +56,15 @@ class Serializer(object): @property def emitter(self): # type: () -> Any + if hasattr(self.dumper, 'typ'): + return self.dumper.emitter return self.dumper._emitter @property def resolver(self): # type: () -> Any + if hasattr(self.dumper, 'typ'): + self.dumper.resolver return self.dumper._resolver def open(self): |