summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES5
-rw-r--r--README.rst5
-rw-r--r--__init__.py12
-rw-r--r--_test/data/duplicate-key.former-loader-error.code1
-rw-r--r--_test/data/duplicate-key.former-loader-error.data3
-rw-r--r--_test/data/duplicate-mapping-key.former-loader-error.code1
-rw-r--r--_test/data/duplicate-mapping-key.former-loader-error.data6
-rw-r--r--_test/data/duplicate-value-key.former-loader-error.code1
-rw-r--r--_test/data/duplicate-value-key.former-loader-error.data4
-rw-r--r--_test/test_anchor.py26
-rw-r--r--_test/test_api_change.py28
-rw-r--r--compat.py10
-rw-r--r--constructor.py100
-rw-r--r--error.py94
-rw-r--r--main.py7
-rw-r--r--nodes.py2
-rw-r--r--tox.ini2
17 files changed, 247 insertions, 60 deletions
diff --git a/CHANGES b/CHANGES
index d6d49d4..778ac5e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,8 @@
+[0, 15, 1]: 2017-06-07
+ - `duplicate keys <http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys>`_
+ in mappings generate an error (in the old API this change generates a warning until 0.16)
+ - dependecy on ruamel.ordereddict for 2.7 now via extras_require
+
[0, 15, 0]: 2017-06-04
- it is no allowed to pass in a ``pathlib.Path`` as "stream" parameter to all
load/dump functions
diff --git a/README.rst b/README.rst
index a5aa8d9..838989e 100644
--- a/README.rst
+++ b/README.rst
@@ -32,6 +32,11 @@ ChangeLog
.. should insert NEXT: at the beginning of line for next key
+0.15.1 (2017-06-07):
+ - `duplicate keys <http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys>`_
+ in mappings generate an error (in the old API this change generates a warning until 0.16)
+ - dependecy on ruamel.ordereddict for 2.7 now via extras_require
+
0.15.0 (2017-06-04):
- it is no allowed to pass in a ``pathlib.Path`` as "stream" parameter to all
load/dump functions
diff --git a/__init__.py b/__init__.py
index 64f9a9f..bcec717 100644
--- a/__init__.py
+++ b/__init__.py
@@ -11,16 +11,15 @@ if False: # MYPY
_package_data = dict(
full_package_name='ruamel.yaml',
- version_info=(0, 15, 1, 'dev'),
- __version__='0.15.1.dev',
+ version_info=(0, 15, 1),
+ __version__='0.15.1',
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
entry_points=None,
- install_requires=dict(
- any=[],
- py27=['ruamel.ordereddict'],
- ),
+ extras_require={':platform_python_implementation=="CPython" and python_version<="2.7"': [
+ 'ruamel.ordereddict',
+ ]},
ext_modules=[dict(
name='_ruamel_yaml',
src=['ext/_ruamel_yaml.c', 'ext/api.c', 'ext/writer.c', 'ext/dumper.c',
@@ -45,6 +44,7 @@ _package_data = dict(
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Text Processing :: Markup',
],
+ keywords='yaml 1.2 parser round-trip preserve quotes order config',
windows_wheels=True,
read_the_docs='yaml',
many_linux='libyaml-devel',
diff --git a/_test/data/duplicate-key.former-loader-error.code b/_test/data/duplicate-key.former-loader-error.code
deleted file mode 100644
index cb73906..0000000
--- a/_test/data/duplicate-key.former-loader-error.code
+++ /dev/null
@@ -1 +0,0 @@
-{ 'foo': 'baz' }
diff --git a/_test/data/duplicate-key.former-loader-error.data b/_test/data/duplicate-key.former-loader-error.data
deleted file mode 100644
index 84deb8f..0000000
--- a/_test/data/duplicate-key.former-loader-error.data
+++ /dev/null
@@ -1,3 +0,0 @@
----
-foo: bar
-foo: baz
diff --git a/_test/data/duplicate-mapping-key.former-loader-error.code b/_test/data/duplicate-mapping-key.former-loader-error.code
deleted file mode 100644
index 17a6285..0000000
--- a/_test/data/duplicate-mapping-key.former-loader-error.code
+++ /dev/null
@@ -1 +0,0 @@
-{ 'foo': { 'baz': 'bat', 'foo': 'duplicate key' } }
diff --git a/_test/data/duplicate-mapping-key.former-loader-error.data b/_test/data/duplicate-mapping-key.former-loader-error.data
deleted file mode 100644
index 7e7b4d1..0000000
--- a/_test/data/duplicate-mapping-key.former-loader-error.data
+++ /dev/null
@@ -1,6 +0,0 @@
----
-&anchor foo:
- foo: bar
- *anchor: duplicate key
- baz: bat
- *anchor: duplicate key
diff --git a/_test/data/duplicate-value-key.former-loader-error.code b/_test/data/duplicate-value-key.former-loader-error.code
deleted file mode 100644
index 12f48c1..0000000
--- a/_test/data/duplicate-value-key.former-loader-error.code
+++ /dev/null
@@ -1 +0,0 @@
-{ 'foo': 'bar', '=': 2 }
diff --git a/_test/data/duplicate-value-key.former-loader-error.data b/_test/data/duplicate-value-key.former-loader-error.data
deleted file mode 100644
index b34a1d6..0000000
--- a/_test/data/duplicate-value-key.former-loader-error.data
+++ /dev/null
@@ -1,4 +0,0 @@
----
-=: 1
-foo: bar
-=: 2
diff --git a/_test/test_anchor.py b/_test/test_anchor.py
index 05c8693..fc4a881 100644
--- a/_test/test_anchor.py
+++ b/_test/test_anchor.py
@@ -11,6 +11,7 @@ import platform
from roundtrip import round_trip, dedent, round_trip_load, round_trip_dump # NOQA
from ruamel.yaml.compat import PY3
from ruamel.yaml.error import ReusedAnchorWarning
+from ruamel.yaml import version_info
def load(s):
@@ -325,6 +326,31 @@ class TestMergeKeysValues:
assert len(x) == ref
+class TestDuplicateKeyThroughAnchor:
+ def test_duplicate_key_00(self):
+ from ruamel.yaml import safe_load, round_trip_load
+ from ruamel.yaml.constructor import DuplicateKeyFutureWarning, DuplicateKeyError
+ s = dedent("""\
+ &anchor foo:
+ foo: bar
+ *anchor : duplicate key
+ baz: bat
+ *anchor : duplicate key
+ """)
+ if version_info < (0, 15, 1):
+ pass
+ elif version_info < (0, 16, 0):
+ with pytest.warns(DuplicateKeyFutureWarning):
+ safe_load(s)
+ with pytest.warns(DuplicateKeyFutureWarning):
+ round_trip_load(s)
+ else:
+ with pytest.raises(DuplicateKeyError):
+ safe_load(s)
+ with pytest.raises(DuplicateKeyError):
+ round_trip_load(s)
+
+
class TestFullCharSetAnchors:
def test_master_of_orion(self):
# https://bitbucket.org/ruamel/yaml/issues/72/not-allowed-in-anchor-names
diff --git a/_test/test_api_change.py b/_test/test_api_change.py
new file mode 100644
index 0000000..10997f4
--- /dev/null
+++ b/_test/test_api_change.py
@@ -0,0 +1,28 @@
+# coding: utf-8
+
+"""
+testing of anchors and the aliases referring to them
+"""
+
+import pytest
+from ruamel.yaml import YAML
+from ruamel.yaml.constructor import DuplicateKeyError
+
+
+class TestNewAPI:
+ def test_duplicate_keys_00(self):
+ from ruamel.yaml.constructor import DuplicateKeyError
+ yaml = YAML()
+ with pytest.raises(DuplicateKeyError):
+ yaml.load('{a: 1, a: 2}')
+
+ def test_duplicate_keys_01(self):
+ yaml = YAML(typ='safe', pure=True)
+ with pytest.raises(DuplicateKeyError):
+ yaml.load('{a: 1, a: 2}')
+
+ # @pytest.mark.xfail(strict=True)
+ def test_duplicate_keys_02(self):
+ yaml = YAML(typ='safe')
+ with pytest.raises(DuplicateKeyError):
+ yaml.load('{a: 1, a: 2}')
diff --git a/compat.py b/compat.py
index c9f6373..7f3434f 100644
--- a/compat.py
+++ b/compat.py
@@ -180,3 +180,13 @@ def check_anchorname_char(ch):
if ch in u',[]{}':
return False
return check_namespace_char(ch)
+
+
+def version_tnf(t1, t2=None):
+ # type: (Any, Any) -> Any
+ from ruamel.yaml import version_info # NOQA
+ if version_info < t1:
+ return True
+ if t2 is not None and version_info < t2:
+ return None
+ return False
diff --git a/constructor.py b/constructor.py
index 4a52b27..37a7a8f 100644
--- a/constructor.py
+++ b/constructor.py
@@ -9,12 +9,13 @@ import binascii
import re
import sys
import types
+import warnings
-from ruamel.yaml.error import (MarkedYAMLError)
+from ruamel.yaml.error import (MarkedYAMLError, MarkedYAMLFutureWarning)
from ruamel.yaml.nodes import * # NOQA
from ruamel.yaml.nodes import (SequenceNode, MappingNode, ScalarNode)
from ruamel.yaml.compat import (utf8, builtins_module, to_str, PY2, PY3, # NOQA
- ordereddict, text_type, nprint)
+ ordereddict, text_type, nprint, version_tnf)
from ruamel.yaml.comments import * # NOQA
from ruamel.yaml.comments import (CommentedMap, CommentedOrderedMap, CommentedSet,
CommentedKeySeq, CommentedSeq)
@@ -36,6 +37,14 @@ class ConstructorError(MarkedYAMLError):
pass
+class DuplicateKeyFutureWarning(MarkedYAMLFutureWarning):
+ pass
+
+
+class DuplicateKeyError(MarkedYAMLFutureWarning):
+ pass
+
+
class BaseConstructor(object):
yaml_constructors = {} # type: Dict[Any, Any]
@@ -52,6 +61,7 @@ class BaseConstructor(object):
self.state_generators = [] # type: List[Any]
self.deep_construct = False
self._preserve_quotes = preserve_quotes
+ self.allow_duplicate_keys = version_tnf((0, 15, 1), (0, 16))
@property
def composer(self):
@@ -193,31 +203,62 @@ class BaseConstructor(object):
None, None,
"expected a mapping node, but found %s" % node.id,
node.start_mark)
- mapping = {}
- for key_node, value_node in node.value:
- # keys can be list -> deep
- key = self.construct_object(key_node, deep=True)
- # lists are not hashable, but tuples are
- if not isinstance(key, collections.Hashable): # type: ignore
- if isinstance(key, list):
- key = tuple(key)
- if PY2:
- try:
- hash(key)
- except TypeError as exc:
- raise ConstructorError(
- "while constructing a mapping", node.start_mark,
- "found unacceptable key (%s)" %
- exc, key_node.start_mark)
- else:
- if not isinstance(key, collections.Hashable):
- raise ConstructorError(
- "while constructing a mapping", node.start_mark,
- "found unhashable key", key_node.start_mark)
-
- value = self.construct_object(value_node, deep=deep)
- mapping[key] = value
- return mapping
+ total_mapping = {}
+ if hasattr(node, 'merge'):
+ todo = [(node.merge, False), (node.value, True)]
+ else:
+ todo = [(node.value, True)]
+ for values, check in todo:
+ mapping = {}
+ for key_node, value_node in values:
+ # keys can be list -> deep
+ key = self.construct_object(key_node, deep=True)
+ # lists are not hashable, but tuples are
+ if not isinstance(key, collections.Hashable): # type: ignore
+ if isinstance(key, list):
+ key = tuple(key)
+ if PY2:
+ try:
+ hash(key)
+ except TypeError as exc:
+ raise ConstructorError(
+ "while constructing a mapping", node.start_mark,
+ "found unacceptable key (%s)" %
+ exc, key_node.start_mark)
+ else:
+ if not isinstance(key, collections.Hashable):
+ raise ConstructorError(
+ "while constructing a mapping", node.start_mark,
+ "found unhashable key", key_node.start_mark)
+
+ value = self.construct_object(value_node, deep=deep)
+ if check:
+ self.check_mapping_key(node, key_node, mapping, key, value)
+ mapping[key] = value
+ total_mapping.update(mapping)
+ return total_mapping
+
+ def check_mapping_key(self, node, key_node, mapping, key, value):
+ if key in mapping:
+ if not self.allow_duplicate_keys:
+ args = [
+ "while constructing a mapping", node.start_mark,
+ 'found duplicate key "{}" with value "{}" '
+ '(original value: "{}")'.format(
+ key, value, mapping[key]), key_node.start_mark,
+ """
+ To suppress this check see:
+ http://yaml.readthedocs.io/en/latest/api.html#duplicate-keys
+ """,
+ """\
+ Duplicate keys will become and error in future releases, and are errors
+ by default when using the new API.
+ """,
+ ]
+ if self.allow_duplicate_keys is None:
+ warnings.warn(DuplicateKeyFutureWarning(*args))
+ else:
+ raise DuplicateKeyError(*args)
def construct_pairs(self, node, deep=False):
# type: (Any, bool) -> Any
@@ -299,7 +340,8 @@ class SafeConstructor(BaseConstructor):
else:
index += 1
if bool(merge):
- node.value = merge + node.value
+ node.merge = merge # separate merge keys to be able to update without duplicate
+ # node.value = merge + node.value
def construct_mapping(self, node, deep=False):
# type: (Any, bool) -> Any
@@ -1140,6 +1182,8 @@ class RoundTripConstructor(SafeConstructor):
"while constructing a mapping", node.start_mark,
"found unhashable key", key_node.start_mark)
value = self.construct_object(value_node, deep=deep)
+ self.check_mapping_key(node, key_node, maptyp, key, value)
+
if key_node.comment and len(key_node.comment) > 4 and \
key_node.comment[4]:
if last_value is None:
diff --git a/error.py b/error.py
index 96872cc..6276ac2 100644
--- a/error.py
+++ b/error.py
@@ -3,6 +3,7 @@
from __future__ import absolute_import
import warnings
+import textwrap
from ruamel.yaml.compat import utf8
@@ -12,7 +13,8 @@ if False: # MYPY
__all__ = [
'FileMark', 'StringMark', 'CommentMark', 'YAMLError', 'MarkedYAMLError',
- 'ReusedAnchorWarning', 'UnsafeLoaderWarning',
+ 'ReusedAnchorWarning', 'UnsafeLoaderWarning', 'MarkedYAMLWarning',
+ 'MarkedYAMLFutureWarning',
]
@@ -98,13 +100,14 @@ class YAMLError(Exception):
class MarkedYAMLError(YAMLError):
def __init__(self, context=None, context_mark=None,
- problem=None, problem_mark=None, note=None):
+ problem=None, problem_mark=None, note=None, warn=None):
# type: (Any, Any, Any, Any, Any) -> None
self.context = context
self.context_mark = context_mark
self.problem = problem
self.problem_mark = problem_mark
self.note = note
+ # warn is ignored
def __str__(self):
# type: () -> Any
@@ -121,8 +124,9 @@ class MarkedYAMLError(YAMLError):
lines.append(self.problem)
if self.problem_mark is not None:
lines.append(str(self.problem_mark))
- if self.note is not None:
- lines.append(self.note)
+ if self.note is not None and self.note:
+ note = textwrap.dedent(self.note)
+ lines.append(note)
return '\n'.join(lines)
@@ -130,11 +134,49 @@ class YAMLStreamError(Exception):
pass
-class ReusedAnchorWarning(Warning):
+class YAMLWarning(Warning):
pass
-class UnsafeLoaderWarning(Warning):
+class MarkedYAMLWarning(YAMLWarning):
+ def __init__(self, context=None, context_mark=None,
+ problem=None, problem_mark=None, note=None, warn=None):
+ # type: (Any, Any, Any, Any, Any) -> None
+ self.context = context
+ self.context_mark = context_mark
+ self.problem = problem
+ self.problem_mark = problem_mark
+ self.warn = warn
+
+ def __str__(self):
+ # type: () -> Any
+ lines = [] # type: List[str]
+ if self.context is not None:
+ lines.append(self.context)
+ if self.context_mark is not None \
+ and (self.problem is None or self.problem_mark is None or
+ self.context_mark.name != self.problem_mark.name or
+ self.context_mark.line != self.problem_mark.line or
+ self.context_mark.column != self.problem_mark.column):
+ lines.append(str(self.context_mark))
+ if self.problem is not None:
+ lines.append(self.problem)
+ if self.problem_mark is not None:
+ lines.append(str(self.problem_mark))
+ if self.note is not None and self.note:
+ note = textwrap.dedent(self.note)
+ lines.append(note)
+ if self.warn is not None and self.warn:
+ warn = textwrap.dedent(self.warn)
+ lines.append(warn)
+ return '\n'.join(lines)
+
+
+class ReusedAnchorWarning(YAMLWarning):
+ pass
+
+
+class UnsafeLoaderWarning(YAMLWarning):
text = """
The default 'Loader' for 'load(stream)' without further arguments can be unsafe.
Use 'load(stream, Loader=ruamel.yaml.Loader)' explicitly if that is OK.
@@ -147,3 +189,43 @@ In most other cases you should consider using 'safe_load(stream)'"""
pass
warnings.simplefilter('once', UnsafeLoaderWarning)
+
+
+class YAMLFutureWarning(Warning):
+ pass
+
+
+class MarkedYAMLFutureWarning(YAMLFutureWarning):
+ def __init__(self, context=None, context_mark=None,
+ problem=None, problem_mark=None, note=None, warn=None):
+ # type: (Any, Any, Any, Any, Any) -> None
+ self.context = context
+ self.context_mark = context_mark
+ self.problem = problem
+ self.problem_mark = problem_mark
+ self.note = note
+ self.warn = warn
+
+ def __str__(self):
+ # type: () -> Any
+ lines = [] # type: List[str]
+ if self.context is not None:
+ lines.append(self.context)
+
+ if self.context_mark is not None \
+ and (self.problem is None or self.problem_mark is None or
+ self.context_mark.name != self.problem_mark.name or
+ self.context_mark.line != self.problem_mark.line or
+ self.context_mark.column != self.problem_mark.column):
+ lines.append(str(self.context_mark))
+ if self.problem is not None:
+ lines.append(self.problem)
+ if self.problem_mark is not None:
+ lines.append(str(self.problem_mark))
+ if self.note is not None and self.note:
+ note = textwrap.dedent(self.note)
+ lines.append(note)
+ if self.warn is not None and self.warn:
+ warn = textwrap.dedent(self.warn)
+ lines.append(warn)
+ return '\n'.join(lines)
diff --git a/main.py b/main.py
index 65f933a..52ce601 100644
--- a/main.py
+++ b/main.py
@@ -99,6 +99,7 @@ class YAML(object):
self.prefix_colon = None
self.version = None
self.preserve_quotes = None
+ self.allow_duplicate_keys = False # duplicate keys in map, set
self.encoding = None
self.explicit_start = None
self.explicit_end = None
@@ -157,8 +158,9 @@ class YAML(object):
# 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))
+ cnst = self.Constructor(preserve_quotes=self.preserve_quotes, loader=self)
+ cnst.allow_duplicate_keys = self.allow_duplicate_keys
+ setattr(self, attr, cnst)
return getattr(self, attr)
@property
@@ -289,6 +291,7 @@ class YAML(object):
CParser.__init__(selfx, stream)
selfx._parser = selfx._composer = selfx
self.Constructor.__init__(selfx, loader=selfx)
+ selfx.allow_duplicate_keys = self.allow_duplicate_keys
rslvr.__init__(selfx, loadumper=selfx)
self._stream = stream
loader = XLoader(stream)
diff --git a/nodes.py b/nodes.py
index 75cefb6..96f3190 100644
--- a/nodes.py
+++ b/nodes.py
@@ -97,5 +97,5 @@ class SequenceNode(CollectionNode):
class MappingNode(CollectionNode):
- __slots__ = ()
+ __slots__ = ('merge', )
id = 'mapping'
diff --git a/tox.ini b/tox.ini
index 2524272..7fe738a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
[tox]
# envlist = pep8,py35,py27,py34,py33,py26,pypy,jython
#envlist = py35,py27,py34,py33,py26,pypy,jython
-envlist = pep8,py35,py36,py27,py34,pypy
+envlist = pep8,py36,py27,py35,py34,pypy
[testenv]
commands =