summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst52
-rw-r--r--__init__.py2
-rw-r--r--_test/test_version.py105
-rw-r--r--constructor.py16
-rw-r--r--cyaml.py6
-rw-r--r--emitter.py13
-rw-r--r--loader.py18
-rw-r--r--main.py33
-rw-r--r--resolver.py154
9 files changed, 368 insertions, 31 deletions
diff --git a/README.rst b/README.rst
index 582e451..4daf5bd 100644
--- a/README.rst
+++ b/README.rst
@@ -2,6 +2,13 @@
ruamel.yaml
===========
+
+**Starting with 0.11.0 the RoundTripLoader differentiates
+between YAML 1.2 and YAML 1.1. This may cause compatibility problems,
+see the "Document version support" section below.**
+
+----
+
Starting with 0.10.7 the package has been reorganised and the
command line utility is in its own package ``ruamel.yaml.cmd`` (so
installing ``ruamel.yaml`` doesn't pull in possibly irrelevant modules
@@ -45,6 +52,42 @@ Major differences with PyYAML 3.11:
(``lc.key('a')``, ``lc.value('a')`` resp. ``lc.item(3)``)
- preservation of whitelines after block scalars. Contributed by Sam Thursfield.
+Document version support.
+=========================
+
+In YAML a document version can be explicitly set by using::
+
+ %YAML 1.x
+
+before the document start (at the top or before a
+``---``). For ``ruamel.yaml`` x has to be 1 or 2. If no explicit
+version is set `version 1.2 <http://www.yaml.org/spec/1.2/spec.html>`_
+is assumed (which has been released in 2009).
+
+The 1.2 version does **not** support:
+
+- sexagesimals like ``12:34:56``
+- octals that start with 0 only: like ``012`` for number 10 (``0o12`` **is**
+ supported by YAML 1.2)
+- Unquoted Yes and On as alternatives for True and No and Off for False.
+
+If you cannot change your YAML files and you need them to load as 1.1
+you can load with:
+
+ ruamel.yaml.load(some_str, Loader=ruamel.yaml.RoundTripLoader, version=(1, 1))
+
+or the equivalent (version can be a tuple, list or string):
+
+ ruamel.yaml.round_trip_load(some_str, version="1.1")
+
+this also works for ``load_all``/``round_trip_load_all``.
+
+*If you cannot change your code, stick with ruamel.yaml==0.10.23 and let
+me know if it would help to be able to set an environment variable.*
+
+This does not affect dump as ruamel.yaml never emitted sexagesimals, nor
+octal numbers, and emitted booleans always as true resp. false
+
Round trip including comments
=============================
@@ -272,3 +315,12 @@ Testing
Testing is done using `tox <https://pypi.python.org/pypi/tox>`_, which
uses `virtualenv <https://pypi.python.org/pypi/virtualenv>`_ and
`pytest <http://pytest.org/latest/>`_.
+
+ChangeLog
+=========
+
+::
+
+ 0.11.0 (2016-02-18):
+ - RoundTripLoader loads 1.2 by default (no sexagesimals, 012 octals nor
+ yes/no/on/off booleans
diff --git a/__init__.py b/__init__.py
index f9193ef..7ff4a96 100644
--- a/__init__.py
+++ b/__init__.py
@@ -9,7 +9,7 @@ from __future__ import absolute_import
_package_data = dict(
full_package_name="ruamel.yaml",
- version_info=(0, 10, 23),
+ version_info=(0, 11, 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/_test/test_version.py b/_test/test_version.py
new file mode 100644
index 0000000..55d80c3
--- /dev/null
+++ b/_test/test_version.py
@@ -0,0 +1,105 @@
+# coding: utf-8
+
+import pytest # NOQA
+
+import ruamel.yaml
+from roundtrip import dedent
+
+def load(s, version=None):
+ return ruamel.yaml.round_trip_load(dedent(s), version)
+
+
+
+class TestVersions:
+ def test_explicit_1_2(self):
+ l = load("""\
+ %YAML 1.2
+ ---
+ - 12:34:56
+ - 012
+ - 012345678
+ - 0o12
+ - on
+ - off
+ - yes
+ - no
+ - true
+ """)
+ assert l[0] == '12:34:56'
+ assert l[1] == 12
+ assert l[2] == '012345678'
+ assert l[3] == 10
+ assert l[4] == 'on'
+ assert l[5] == 'off'
+ assert l[6] == 'yes'
+ assert l[7] == 'no'
+ assert l[8] is True
+
+ def test_explicit_1_1(self):
+ l = load("""\
+ %YAML 1.1
+ ---
+ - 12:34:56
+ - 012
+ - 012345678
+ - 0o12
+ - on
+ - off
+ - yes
+ - no
+ - true
+ """)
+ assert l[0] == 45296
+ assert l[1] == 10
+ assert l[2] == '012345678'
+ assert l[3] == 10
+ assert l[4] is True
+ assert l[5] is False
+ assert l[6] is True
+ assert l[7] is False
+ assert l[8] is True
+
+ def test_implicit_1_2(self):
+ l = load("""\
+ - 12:34:56
+ - 012
+ - 012345678
+ - 0o12
+ - on
+ - off
+ - yes
+ - no
+ - true
+ """)
+ assert l[0] == '12:34:56'
+ assert l[1] == 12
+ assert l[2] == '012345678'
+ assert l[3] == 10
+ assert l[4] == 'on'
+ assert l[5] == 'off'
+ assert l[6] == 'yes'
+ assert l[7] == 'no'
+ assert l[8] is True
+
+ def test_load_version_1_1(self):
+ l = load("""\
+ - 12:34:56
+ - 012
+ - 012345678
+ - 0o12
+ - on
+ - off
+ - yes
+ - no
+ - true
+ """, version="1.1")
+ assert l[0] == 45296
+ assert l[1] == 10
+ assert l[2] == '012345678'
+ assert l[3] == 10
+ assert l[4] is True
+ assert l[5] is False
+ assert l[6] is True
+ assert l[7] is False
+ assert l[8] is True
+
diff --git a/constructor.py b/constructor.py
index c3b5a59..9ae2e23 100644
--- a/constructor.py
+++ b/constructor.py
@@ -73,6 +73,9 @@ class BaseConstructor(object):
return data
def construct_object(self, node, deep=False):
+ """deep is True when creating an object/mapping recursively,
+ in that case want the underlying elements available during construction
+ """
if node in self.constructed_objects:
return self.constructed_objects[node]
if deep:
@@ -132,6 +135,9 @@ class BaseConstructor(object):
return node.value
def construct_sequence(self, node, deep=False):
+ """deep is True when creating an object/mapping recursively,
+ in that case want the underlying elements available during construction
+ """
if not isinstance(node, SequenceNode):
raise ConstructorError(
None, None,
@@ -141,6 +147,9 @@ class BaseConstructor(object):
for child in node.value]
def construct_mapping(self, node, deep=False):
+ """deep is True when creating an object/mapping recursively,
+ in that case want the underlying elements available during construction
+ """
if not isinstance(node, MappingNode):
raise ConstructorError(
None, None,
@@ -250,6 +259,9 @@ class SafeConstructor(BaseConstructor):
node.value = merge + node.value
def construct_mapping(self, node, deep=False):
+ """deep is True when creating an object/mapping recursively,
+ in that case want the underlying elements available during construction
+ """
if isinstance(node, MappingNode):
self.flatten_mapping(node)
return BaseConstructor.construct_mapping(self, node, deep=deep)
@@ -288,9 +300,9 @@ class SafeConstructor(BaseConstructor):
return sign*int(value[2:], 16)
elif value.startswith('0o'):
return sign*int(value[2:], 8)
- elif value[0] == '0':
+ elif self.processing_version != (1, 2) and value[0] == '0':
return sign*int(value, 8)
- elif ':' in value:
+ elif self.processing_version != (1, 2) and ':' in value:
digits = [int(part) for part in value.split(':')]
digits.reverse()
base = 1
diff --git a/cyaml.py b/cyaml.py
index 22be7dd..93e2dd7 100644
--- a/cyaml.py
+++ b/cyaml.py
@@ -18,21 +18,21 @@ except (ImportError, ValueError): # for Jython
class CBaseLoader(CParser, BaseConstructor, BaseResolver):
- def __init__(self, stream):
+ def __init__(self, stream, version=None):
CParser.__init__(self, stream)
BaseConstructor.__init__(self)
BaseResolver.__init__(self)
class CSafeLoader(CParser, SafeConstructor, Resolver):
- def __init__(self, stream):
+ def __init__(self, stream, version=None):
CParser.__init__(self, stream)
SafeConstructor.__init__(self)
Resolver.__init__(self)
class CLoader(CParser, Constructor, Resolver):
- def __init__(self, stream):
+ def __init__(self, stream, version=None):
CParser.__init__(self, stream)
Constructor.__init__(self)
Resolver.__init__(self)
diff --git a/emitter.py b/emitter.py
index adfe5c2..9523664 100644
--- a/emitter.py
+++ b/emitter.py
@@ -1218,17 +1218,18 @@ class Emitter(object):
col = self.column + 1
# print('post_comment', self.line, self.column, value)
try:
- if PY2:
- value = value.encode('utf-8')
- except UnicodeDecodeError:
- pass
- try:
# at least one space if the current column >= the start column of the comment
# but not at the start of a line
nr_spaces = col - self.column
if self.column and value.strip() and nr_spaces < 1:
nr_spaces = 1
- self.stream.write(' ' * nr_spaces + value)
+ value = ' ' * nr_spaces + value
+ try:
+ if PY2:
+ value = value.encode('utf-8')
+ except UnicodeDecodeError:
+ pass
+ self.stream.write(value)
except TypeError:
print('TypeError while trying to write', repr(value), type(value))
raise
diff --git a/loader.py b/loader.py
index 8bc96ae..676f01d 100644
--- a/loader.py
+++ b/loader.py
@@ -18,9 +18,8 @@ except (ImportError, ValueError): # for Jython
from ruamel.yaml.resolver import * # NOQA
-class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor,
- BaseResolver):
- def __init__(self, stream):
+class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver):
+ def __init__(self, stream, version=None):
Reader.__init__(self, stream)
Scanner.__init__(self)
Parser.__init__(self)
@@ -30,7 +29,7 @@ class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor,
class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver):
- def __init__(self, stream):
+ def __init__(self, stream, version=None):
Reader.__init__(self, stream)
Scanner.__init__(self)
Parser.__init__(self)
@@ -40,8 +39,7 @@ class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver):
class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver):
-
- def __init__(self, stream):
+ def __init__(self, stream, version=None):
Reader.__init__(self, stream)
Scanner.__init__(self)
Parser.__init__(self)
@@ -50,12 +48,12 @@ class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver):
Resolver.__init__(self)
-class RoundTripLoader(Reader, RoundTripScanner, Parser,
- Composer, RoundTripConstructor, Resolver):
- def __init__(self, stream):
+class RoundTripLoader(Reader, RoundTripScanner, Parser, Composer,
+ RoundTripConstructor, VersionedResolver):
+ def __init__(self, stream, version=None):
Reader.__init__(self, stream)
RoundTripScanner.__init__(self)
Parser.__init__(self)
Composer.__init__(self)
RoundTripConstructor.__init__(self)
- Resolver.__init__(self)
+ VersionedResolver.__init__(self, version)
diff --git a/main.py b/main.py
index d701005..ed4c039 100644
--- a/main.py
+++ b/main.py
@@ -65,24 +65,24 @@ def compose_all(stream, Loader=Loader):
loader.dispose()
-def load(stream, Loader=Loader):
+def load(stream, Loader=Loader, version=None):
"""
Parse the first YAML document in a stream
and produce the corresponding Python object.
"""
- loader = Loader(stream)
+ loader = Loader(stream, version)
try:
return loader.get_single_data()
finally:
loader.dispose()
-def load_all(stream, Loader=Loader):
+def load_all(stream, Loader=Loader, version=None):
"""
Parse all YAML documents in a stream
and produce corresponding Python objects.
"""
- loader = Loader(stream)
+ loader = Loader(stream, version)
try:
while loader.check_data():
yield loader.get_data()
@@ -90,22 +90,39 @@ def load_all(stream, Loader=Loader):
loader.dispose()
-def safe_load(stream):
+def safe_load(stream, version=None):
+ """
+ Parse the first YAML document in a stream
+ and produce the corresponding Python object.
+ Resolve only basic YAML tags.
+ """
+ return load(stream, SafeLoader, version)
+
+
+def safe_load_all(stream, version=None):
+ """
+ Parse all YAML documents in a stream
+ and produce corresponding Python objects.
+ Resolve only basic YAML tags.
+ """
+ return load_all(stream, SafeLoader, version)
+
+def round_trip_load(stream, version=None):
"""
Parse the first YAML document in a stream
and produce the corresponding Python object.
Resolve only basic YAML tags.
"""
- return load(stream, SafeLoader)
+ return load(stream, RoundTripLoader, version)
-def safe_load_all(stream):
+def round_trip_load_all(stream, version=None):
"""
Parse all YAML documents in a stream
and produce corresponding Python objects.
Resolve only basic YAML tags.
"""
- return load_all(stream, SafeLoader)
+ return load_all(stream, RoundTripLoader, version)
def emit(events, stream=None, Dumper=Dumper,
diff --git a/resolver.py b/resolver.py
index 04dfcf4..44b7b32 100644
--- a/resolver.py
+++ b/resolver.py
@@ -1,6 +1,6 @@
from __future__ import absolute_import
-__all__ = ['BaseResolver', 'Resolver']
+__all__ = ['BaseResolver', 'Resolver', 'VersionedResolver']
try:
from .error import * # NOQA
@@ -15,6 +15,8 @@ except (ImportError, ValueError): # for Jython
import re
+_DEFAULT_VERSION = (1, 2)
+
class ResolverError(YAMLError):
pass
@@ -29,6 +31,7 @@ class BaseResolver(object):
yaml_path_resolvers = {}
def __init__(self):
+ self._loader_version = None
self.resolver_exact_paths = []
self.resolver_prefix_paths = []
@@ -174,6 +177,9 @@ class BaseResolver(object):
elif kind is MappingNode:
return self.DEFAULT_MAPPING_TAG
+ @property
+ def processing_version(self):
+ return None
class Resolver(BaseResolver):
pass
@@ -237,3 +243,149 @@ Resolver.add_implicit_resolver(
u'tag:yaml.org,2002:yaml',
re.compile(u'^(?:!|&|\\*)$'),
list(u'!&*'))
+
+# resolvers consist of
+# - a list of applicable version
+# - a tag
+# - a regexp
+# - a list of first characters to match
+implicit_resolvers = [
+ ([(1, 2)],
+ u'tag:yaml.org,2002:bool',
+ re.compile(u'''^(?:true|True|TRUE|false|False|FALSE)$''', re.X),
+ list(u'tTfF')),
+ ([(1, 1)],
+ u'tag:yaml.org,2002:bool',
+ re.compile(u'''^(?:yes|Yes|YES|no|No|NO
+ |true|True|TRUE|false|False|FALSE
+ |on|On|ON|off|Off|OFF)$''', re.X),
+ list(u'yYnNtTfFoO')),
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:float',
+ re.compile(u'''^(?:
+ [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)?
+ |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
+ |\\.[0-9_]+(?:[eE][-+][0-9]+)?
+ |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*
+ |[-+]?\\.(?:inf|Inf|INF)
+ |\\.(?:nan|NaN|NAN))$''', re.X),
+ list(u'-+0123456789.')),
+ ([(1, 2)],
+ u'tag:yaml.org,2002:int',
+ re.compile(u'''^(?:[-+]?0b[0-1_]+
+ |[-+]?0o?[0-7_]+
+ |[-+]?(?:0|[1-9][0-9_]*)
+ |[-+]?0x[0-9a-fA-F_]+)$''', re.X),
+ list(u'-+0123456789')),
+ ([(1, 1)],
+ u'tag:yaml.org,2002:int',
+ re.compile(u'''^(?:[-+]?0b[0-1_]+
+ |[-+]?0o?[0-7_]+
+ |[-+]?(?:0|[1-9][0-9_]*)
+ |[-+]?0x[0-9a-fA-F_]+
+ |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X),
+ list(u'-+0123456789')),
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:merge',
+ re.compile(u'^(?:<<)$'),
+ [u'<']),
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:null',
+ re.compile(u'''^(?: ~
+ |null|Null|NULL
+ | )$''', re.X),
+ [u'~', u'n', u'N', u'']),
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:timestamp',
+ re.compile(u'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
+ |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?
+ (?:[Tt]|[ \\t]+)[0-9][0-9]?
+ :[0-9][0-9] :[0-9][0-9] (?:\\.[0-9]*)?
+ (?:[ \\t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X),
+ list(u'0123456789')),
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:value',
+ re.compile(u'^(?:=)$'),
+ [u'=']),
+ # The following resolver is only for documentation purposes. It cannot work
+ # because plain scalars cannot start with '!', '&', or '*'.
+ ([(1, 2), (1, 1)],
+ u'tag:yaml.org,2002:yaml',
+ re.compile(u'^(?:!|&|\\*)$'),
+ list(u'!&*')),
+]
+
+
+class VersionedResolver(BaseResolver):
+ """
+ contrary to the "normal" resolver, the smart resolver delays loading
+ the pattern matching rules. That way it can decide to load 1.1 rules
+ or the (default) 1.2 that no longer support octal without 0o, sexagesimals
+ and Yes/No/On/Off booleans.
+ """
+
+ def __init__(self, version=None):
+ BaseResolver.__init__(self)
+ self._loader_version = self.get_loader_version(version)
+ self._version_implicit_resolver = {}
+
+ def add_version_implicit_resolver(self, version, tag, regexp, first):
+ if first is None:
+ first = [None]
+ impl_resolver = self._version_implicit_resolver.setdefault(version, {})
+ for ch in first:
+ impl_resolver.setdefault(ch, []).append((tag, regexp))
+
+ def get_loader_version(self, version):
+ if version is None or isinstance(version, tuple):
+ return version
+ if isinstance(version, list):
+ return tuple(version)
+ # assume string
+ return tuple(map(int, version.split(u'.')))
+
+ @property
+ def resolver(self):
+ """
+ select the resolver based on the version we are parsing
+ """
+ version = self.processing_version
+ if version not in self._version_implicit_resolver:
+ print('>>> version', self.yaml_version, version)
+ for x in implicit_resolvers:
+ if version in x[0]:
+ self.add_version_implicit_resolver(version, x[1], x[2], x[3])
+ return self._version_implicit_resolver[version]
+
+ def resolve(self, kind, value, implicit):
+ if kind is ScalarNode and implicit[0]:
+ if value == u'':
+ resolvers = self.resolver.get(u'', [])
+ else:
+ resolvers = self.resolver.get(value[0], [])
+ resolvers += self.resolver.get(None, [])
+ for tag, regexp in resolvers:
+ if regexp.match(value):
+ return tag
+ implicit = implicit[1]
+ if self.yaml_path_resolvers:
+ exact_paths = self.resolver_exact_paths[-1]
+ if kind in exact_paths:
+ return exact_paths[kind]
+ if None in exact_paths:
+ return exact_paths[None]
+ if kind is ScalarNode:
+ return self.DEFAULT_SCALAR_TAG
+ elif kind is SequenceNode:
+ return self.DEFAULT_SEQUENCE_TAG
+ elif kind is MappingNode:
+ return self.DEFAULT_MAPPING_TAG
+
+ @property
+ def processing_version(self):
+ version = self.yaml_version
+ if version is None:
+ version = self._loader_version
+ if version is None:
+ version = _DEFAULT_VERSION
+ return version