diff --git a/.travis.yml b/.travis.yml index c4e028b..30082a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,19 +4,10 @@ python: - "2.7" - "3.6" -install: - - pip install -r requirements_dev.txt - script: - - bin/py.test -vvs tests - -notifications: - irc: - channels: - - "chat.freenode.net#aboutcode" - on_success: change - on_failure: always - use_notice: true - skip_join: true - template: - - "%{repository_slug}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} : %{build_url}" + - pip install pyyaml==3.13 + - pip install -r requirements_dev.txt + - py.test -vvs tests + - pip uninstall -y pyyaml + - pip install pyyaml + - py.test -vvs tests diff --git a/appveyor.yml b/appveyor.yml index ceb7ffe..6f77fec 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,16 +1,12 @@ version: '{build}' -install: - - configure etc/conf/dev - build: off test_script: - - set + - pip install pyyaml==3.13 + - pip install -r requirements_dev.txt +# test also on latest version + - py.test -vvs tests + - pip uninstall -y pyyaml + - pip install pyyaml - py.test -vvs tests - -on_success: - - "python etc/scripts/irc-notify.py aboutcode [{project_name}:{branch}] {short_commit}: \"{message}\" ({author}) {color_green}Succeeded,Details: {build_url},Commit: {commit_url}" - -on_failure: - - "python etc/scripts/irc-notify.py aboutcode [{project_name}:{branch}] {short_commit}: \"{message}\" ({author}) {color_red}Failed,Details: {build_url},Commit: {commit_url}" diff --git a/setup.py b/setup.py index 40b6668..a25a34a 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='saneyaml', - version='0.3', + version='0.4', license='Apache-2.0', description='Dump readable YAML and load safely any YAML preserving ' 'ordering and avoiding surprises of type conversions. ' @@ -50,6 +50,6 @@ 'yaml', 'block', 'flow', 'readable', ], install_requires=[ - 'PyYAML >= 3.11, <= 3.13', + 'PyYAML', ], ) diff --git a/src/saneyaml.py b/src/saneyaml.py index 9286215..4176e47 100644 --- a/src/saneyaml.py +++ b/src/saneyaml.py @@ -20,6 +20,7 @@ from collections import OrderedDict from functools import partial +import re import sys import yaml @@ -127,7 +128,8 @@ def ordered_loader(self, node, check_dupe=False): # This avoid unwanted type conversions for unquoted strings and the resulting # content damaging. This overrides the implicit resolvers. Callers must handle # type conversion explicitly from unicode to other types in the loaded objects. - +# NOTE: we are still using the built-in loader for booleans. It will recognize +# yes/no as a boolean. BaseSaneLoader.add_constructor('tag:yaml.org,2002:str', BaseSaneLoader.string_loader) BaseSaneLoader.add_constructor('tag:yaml.org,2002:null', BaseSaneLoader.string_loader) BaseSaneLoader.add_constructor('tag:yaml.org,2002:boolean', BaseSaneLoader.string_loader) @@ -166,6 +168,8 @@ class DupeKeySaneLoader(BaseSaneLoader): # Dumping ############################################################################### +WIDTH = 90 + def dump(obj, indent=2, encoding=None): """ Return a safe and sane YAML string representation from `obj`. @@ -187,7 +191,7 @@ def dump(obj, indent=2, encoding=None): # anything above 2 will yield weird vertical indents on lists and maps indent=indent, # make this 80ish - width=90, + width=WIDTH, # posix LF line_break='\n', # no --- and ... @@ -212,7 +216,7 @@ def __init__(self, stream, canonical=None, indent=None, width=None, allow_unicode=None, line_break=None, encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): + version=None, tags=None, sort_keys=False, **kwargs): IndentingEmitter.__init__(self, stream, canonical=canonical, indent=indent, width=width, allow_unicode=allow_unicode, line_break=line_break) @@ -245,7 +249,7 @@ def null_dumper(self, value): # NOQA """ Always dump nulls as empty string. """ - return self.represent_scalar('tag:yaml.org,2002:null', '') + return self.represent_scalar('tag:yaml.org,2002:str', '', style=None) def string_dumper(self, value): """ @@ -254,20 +258,50 @@ def string_dumper(self, value): """ tag = 'tag:yaml.org,2002:str' style = None + + if value is None: + return '' + + if isinstance(value, bool): + value = 'yes' if value else 'no' + style = '' + if isinstance(value, float): style = "'" + if isinstance(value, int): + value = unicode(value) + style = '' + if isinstance(value, bytes): value = value.decode('utf-8') + elif isinstance(value, int): + value = unicode(value) elif not isinstance(value, unicode): value = repr(value) # do not quote integer strings - if value.isdigit() and unicode(int(value)) == value: - style = None - tag = 'tag:yaml.org,2002:int' + if value.isdigit(): + if value.lstrip('0') == value: + style = '' + else: + # things such as 012 needs to be quoted + style = "'" + + # quote things that could be mistakenly loaded as date + if is_iso_date(value): + style = "'" + + # quote things that could be mistakenly loaded as float such as version numbers + if value !='.' and len(value.split('.')) == 2 and all(c in '0123456789.' for c in value): + style = "'" - if '\n' in value: + elif value == 'null': + style = "'" + + # if '\n' in value or len(value) > WIDTH: + # literal_style for multilines or long + elif '\n' in value: # literal_style for multilines style = '|' @@ -282,12 +316,30 @@ def boolean_dumper(self, value): return self.represent_scalar('tag:yaml.org,2002:bool', value, style=None) +def is_float(s): + """ + Return True if this is a float with trailing zeroes such as `1.20` + """ + try: + float(s) + return s.startswith('0') or s.endswith('0') + except: + return False + + +# Return True if s is an iso date such as `2019-12-12` +is_iso_date = re.compile(r'19|20[0-9]{2}-[0-1][0-9]-[0-3]?[1-9]').match + + SaneDumper.add_representer(int, SaneDumper.string_dumper) SaneDumper.add_representer(odict, SaneDumper.ordered_dumper) SaneDumper.add_representer(OrderedDict, SaneDumper.ordered_dumper) SaneDumper.add_representer(type(None), SaneDumper.null_dumper) -SaneDumper.add_representer(bool, SaneDumper.boolean_dumper) +SaneDumper.add_representer(bool, SaneDumper.string_dumper) SaneDumper.add_representer(bytes, SaneDumper.string_dumper) SaneDumper.add_representer(str, SaneDumper.string_dumper) SaneDumper.add_representer(unicode, SaneDumper.string_dumper) SaneDumper.add_representer(float, SaneDumper.string_dumper) + +SaneDumper.yaml_implicit_resolvers = {} +SaneDumper.yaml_path_resolvers = {} diff --git a/tests/data/yamls/corner-cases.yml.expected.yaml.dump b/tests/data/yamls/corner-cases.yml.expected.yaml.dump index 2b1278e..ac4194d 100644 --- a/tests/data/yamls/corner-cases.yml.expected.yaml.dump +++ b/tests/data/yamls/corner-cases.yml.expected.yaml.dump @@ -1,8 +1,8 @@ about_resource: 'null' name: '123.34' about_resource_path: '012' -? '' +? : - this - 'null' - '2012-03-12' -that: '' +that: