diff options
author | Rob Dennis <rdennis@gmail.com> | 2014-04-11 00:23:32 -0400 |
---|---|---|
committer | Rob Dennis <rdennis@gmail.com> | 2014-04-11 00:23:32 -0400 |
commit | 6ae299fdabcf177e7f6176af34f43dbb067ddf1a (patch) | |
tree | c22a8481535bb5bfeb7b5c83dd10ce7d17031a44 | |
parent | 54a2572fb9d6c9b1f63585938ad246ddada49982 (diff) | |
parent | a7d62f8e08bf107ad378801134db8942ecaf5d49 (diff) | |
download | configobj-git-6ae299fdabcf177e7f6176af34f43dbb067ddf1a.tar.gz |
Merge pull request #54 from robdennis/release
Release 5.0.4
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | _version.py | 2 | ||||
-rw-r--r-- | configobj.py | 2 | ||||
-rw-r--r-- | docs/conf.py | 4 | ||||
-rw-r--r-- | docs/configobj.rst | 11 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-rw-r--r-- | test_configobj.py | 30 | ||||
-rw-r--r-- | tests/test_configobj.py | 326 |
8 files changed, 249 insertions, 132 deletions
@@ -9,7 +9,7 @@ Found at [readthedocs](http://configobj.readthedocs.org/) Status ========= -This project has is now maintained by [Eli Courtwright](https://github.com/EliAndrewC) and [Rob Dennis](https://github.com/robdennis) with the blessing of original creator [Michael Foord](http://www.voidspace.org.uk/) and the most recent release is version *5.0.3* (view [changelog](http://configobj.readthedocs.org/en/v5.0.3/configobj.html#version-5-0-3)). +This project has is now maintained by [Eli Courtwright](https://github.com/EliAndrewC) and [Rob Dennis](https://github.com/robdennis) with the blessing of original creator [Michael Foord](http://www.voidspace.org.uk/) and the most recent release is version *5.0.4* (view [changelog](http://configobj.readthedocs.org/en/latest/configobj.html#version-5-0-4)). For long time ConfigObj users, the biggest change is in the officially supported python versions: - 2.6 diff --git a/_version.py b/_version.py index 245a3aa..742c20e 100644 --- a/_version.py +++ b/_version.py @@ -1 +1 @@ -__version__ = '5.0.3'
\ No newline at end of file +__version__ = '5.0.4'
\ No newline at end of file diff --git a/configobj.py b/configobj.py index 8ad7c8f..d730a13 100644 --- a/configobj.py +++ b/configobj.py @@ -1234,7 +1234,7 @@ class ConfigObj(Section): self.filename = infile if os.path.isfile(infile): with open(infile, 'rb') as h: - content = h.read() or [] + content = h.readlines() or [] elif self.file_error: # raise an error if the file doesn't exist raise IOError('Config file not found: "%s".' % self.filename) diff --git a/docs/conf.py b/docs/conf.py index b6f83e1..1e4f78c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,8 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from _version import __version__ - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -52,7 +50,7 @@ copyright = u'2014, Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright' # built documents. # # The full version, including alpha/beta/rc tags. -release = __version__ +release = '5.0.4' # The short X.Y version. version = '.'.join(release.split('.')[:2]) diff --git a/docs/configobj.rst b/docs/configobj.rst index 763fee2..445d694 100644 --- a/docs/configobj.rst +++ b/docs/configobj.rst @@ -8,7 +8,7 @@ ---------------------------------------- :Authors: Michael Foord, Nicola Larosa, Rob Dennis, Eli Courtwright -:Version: ConfigObj 5.0.3 +:Version: ConfigObj 5.0.4 :Date: 2014/02/08 :PyPI Entry: `ConfigObj on PyPI <http://pypi.python.org/pypi/configobj/>`_ :Homepage: `Github Page`_ @@ -64,7 +64,7 @@ For support and bug reports please use the ConfigObj `Github Page`_. Downloading =========== -The current version is **5.0.3**, dated 4th April 2014. ConfigObj 5 is +The current version is **5.0.4**, dated 11th April 2014. ConfigObj 5 is stable and mature. We still expect to pick up a few bugs along the way though, particularly with respect to Python 3 compatibility [#]_. We recommend downloading and installing using pip: @@ -895,7 +895,7 @@ newlines When a config file is read, ConfigObj records the type of newline separators in the file and uses this separator when writing. It defaults to ``None``, and ConfigObj -uses the system default (``os.sep``) if write is called without newlines having +uses the system default (``os.linesep``) if write is called without newlines having been set. @@ -2383,6 +2383,11 @@ CHANGELOG This is an abbreviated changelog showing the major releases up to version 4. From version 4 it lists all releases and changes. +2014/04/11 - Version 5.0.4 +-------------------------- +* BUGFIX: correcting that the code path fixed in 5.0.3 didn't cover reading in + config files + 2014/04/04 - Version 5.0.3 -------------------------- * BUGFIX: not handling unicode encoding well, especially with respect to writing out files @@ -51,6 +51,8 @@ It has lots of other features though : * The order of keys/sections is preserved * Powerful ``unrepr`` mode for storing/retrieving Python data-types +| Release 5.0.4 corrects a unicode-bug that still existed in reading files after +| fixing lists of string in 5.0.3 | Release 5.0.3 corrects errors related to the incorrectly handling unicode | encoding and writing out files | Release 5.0.2 adds a specific error message when trying to install on @@ -90,7 +92,7 @@ KEYWORDS = "config, ini, dictionary, application, admin, sysadmin, configuration setup(name=NAME, version=VERSION, - install_requires=['six==1.5.2'], + install_requires=['six'], description=DESCRIPTION, long_description=LONG_DESCRIPTION, author=AUTHOR, diff --git a/test_configobj.py b/test_configobj.py index 49ebd78..2133a7d 100644 --- a/test_configobj.py +++ b/test_configobj.py @@ -487,36 +487,6 @@ def _test_errors(): """ -def _test_unrepr_comments(): - """ - >>> config = ''' - ... # initial comments - ... # with two lines - ... key = "value" - ... # section comment - ... [section] # inline section comment - ... # key comment - ... key = "value" - ... # final comment - ... # with two lines - ... '''.splitlines() - >>> c = ConfigObj(config, unrepr=True) - >>> c == { 'key': 'value', - ... 'section': { 'key': 'value'}} - 1 - >>> c.initial_comment == ['', '# initial comments', '# with two lines'] - 1 - >>> c.comments == {'section': ['# section comment'], 'key': []} - 1 - >>> c.inline_comments == {'section': '# inline section comment', 'key': ''} - 1 - >>> c['section'].comments == { 'key': ['# key comment']} - 1 - >>> c.final_comment == ['# final comment', '# with two lines'] - 1 - """ - - def _test_validate_with_copy_and_many(): """ >>> spec = ''' diff --git a/tests/test_configobj.py b/tests/test_configobj.py index 98210f1..c37bd73 100644 --- a/tests/test_configobj.py +++ b/tests/test_configobj.py @@ -1,5 +1,8 @@ # coding=utf-8 +from __future__ import unicode_literals import os +import re + from codecs import BOM_UTF8 from warnings import catch_warnings from tempfile import NamedTemporaryFile @@ -12,6 +15,72 @@ from configobj import ConfigObj, flatten_errors, ReloadError, DuplicateError, Mi from validate import Validator, VdtValueTooSmallError +def cfg_lines(config_string_representation): + """ + :param config_string_representation: string representation of a config + file (typically a triple-quoted string) + :type config_string_representation: str or unicode + :return: a list of lines of that config. Whitespace on the left will be + trimmed based on the indentation level to make it a bit saner to assert + content of a particular line + :rtype: str or unicode + """ + lines = config_string_representation.splitlines() + + for idx, line in enumerate(lines): + if line.strip(): + line_no_with_content = idx + break + else: + raise ValueError('no content in provided config file: ' + '{!r}'.format(config_string_representation)) + + first_content = lines[line_no_with_content] + if isinstance(first_content, six.binary_type): + first_content = first_content.decode('utf-8') + ws_chars = len(re.search('^(\s*)', first_content).group(1)) + + def yield_stringified_line(): + for line in lines: + if isinstance(line, six.binary_type): + yield line.decode('utf-8') + else: + yield line + + + return [re.sub('^\s{0,%s}' % ws_chars, '', line).encode('utf-8') + for line in yield_stringified_line()] + + +@pytest.fixture +def cfg_contents(request): + + def make_file_with_contents_and_return_name(config_string_representation): + """ + :param config_string_representation: string representation of a config + file (typically a triple-quoted string) + :type config_string_representation: str or unicode + :return: a list of lines of that config. Whitespace on the left will be + trimmed based on the indentation level to make it a bit saner to assert + content of a particular line + :rtype: basestring + """ + + lines = cfg_lines(config_string_representation) + + with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: + for line in lines: + if isinstance(line, six.binary_type): + cfg_file.write(line + os.linesep.encode('utf-8')) + else: + cfg_file.write((line + os.linesep).encode('utf-8')) + request.addfinalizer(lambda : os.unlink(cfg_file.name)) + + return cfg_file.name + + return make_file_with_contents_and_return_name + + def test_order_preserved(): c = ConfigObj() c['a'] = 1 @@ -75,8 +144,8 @@ def test_with_default(): c.pop('c') -def test_interpolation_with_section_names(): - cfg = """ +def test_interpolation_with_section_names(cfg_contents): + cfg = cfg_contents(""" item1 = 1234 [section] [[item1]] @@ -85,7 +154,7 @@ item1 = 1234 [[[item1]]] why = would you do this? [[other-subsection]] - item2 = '$item1'""".splitlines() + item2 = '$item1'""") c = ConfigObj(cfg, interpolation='Template') # This raises an exception in 4.7.1 and earlier due to the section @@ -103,11 +172,17 @@ def test_interoplation_repr(): class TestEncoding(object): + @pytest.fixture + def ant_cfg(self): + return """ + [tags] + [[bug]] + translated = \U0001f41c + """ + #issue #18 - def test_unicode_conversion_when_encoding_is_set(self): - cfg = """ - test = some string - """.splitlines() + def test_unicode_conversion_when_encoding_is_set(self, cfg_contents): + cfg = cfg_contents(b"test = some string") c = ConfigObj(cfg, encoding='utf8') @@ -119,10 +194,8 @@ class TestEncoding(object): #issue #18 - def test_no_unicode_conversion_when_encoding_is_omitted(self): - cfg = """ - test = some string - """.splitlines() + def test_no_unicode_conversion_when_encoding_is_omitted(self, cfg_contents): + cfg = cfg_contents(b"test = some string") c = ConfigObj(cfg) if six.PY2: @@ -132,15 +205,36 @@ class TestEncoding(object): assert isinstance(c['test'], str) #issue #44 - def test_that_encoding_argument_is_used_to_decode(self): + def test_that_encoding_using_list_of_strings(self): cfg = [b'test = \xf0\x9f\x90\x9c'] c = ConfigObj(cfg, encoding='utf8') - assert isinstance(c['test'], six.text_type) + if six.PY2: + assert isinstance(c['test'], unicode) + assert not isinstance(c['test'], str) + else: + assert isinstance(c['test'], str) + #TODO: this can be made more explicit if we switch to unicode_literals assert c['test'] == b'\xf0\x9f\x90\x9c'.decode('utf8') + #issue #44 + def test_encoding_in_subsections(self, ant_cfg, cfg_contents): + c = cfg_contents(ant_cfg) + cfg = ConfigObj(c, encoding='utf-8') + + assert isinstance(cfg['tags']['bug']['translated'], six.text_type) + + #issue #44 + def test_encoding_in_config_files(self, request, ant_cfg): + # the cfg_contents fixture is doing this too, but be explicit + with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: + cfg_file.write(ant_cfg.encode('utf-8')) + request.addfinalizer(lambda : os.unlink(cfg_file.name)) + + cfg = ConfigObj(cfg_file.name, encoding='utf-8') + assert isinstance(cfg['tags']['bug']['translated'], six.text_type) @pytest.fixture def testconfig1(): @@ -196,10 +290,10 @@ def testconfig2(): @pytest.fixture def testconfig6(): return b''' - name1 = """ a single line value """ # comment - name2 = \''' another single line value \''' # comment - name3 = """ a single line value """ - name4 = \''' another single line value \''' + name1 = """ a single line value """ # comment + name2 = \''' another single line value \''' # comment + name3 = """ a single line value """ + name4 = \''' another single line value \''' [ "multi section" ] name1 = """ Well, this is a @@ -221,30 +315,30 @@ def testconfig6(): @pytest.fixture -def a(testconfig1): +def a(testconfig1, cfg_contents): """ also copied from main doc tests """ - return ConfigObj(testconfig1.splitlines(), raise_errors=True) + return ConfigObj(cfg_contents(testconfig1), raise_errors=True) @pytest.fixture -def b(testconfig2): +def b(testconfig2, cfg_contents): """ also copied from main doc tests """ - return ConfigObj(testconfig2.splitlines(), raise_errors=True) + return ConfigObj(cfg_contents(testconfig2), raise_errors=True) @pytest.fixture -def i(testconfig6): +def i(testconfig6, cfg_contents): """ also copied from main doc tests """ - return ConfigObj(testconfig6.splitlines(), raise_errors=True) + return ConfigObj(cfg_contents(testconfig6), raise_errors=True) -def test_configobj_dict_representation(a, b): +def test_configobj_dict_representation(a, b, cfg_contents): assert a.depth == 0 assert a == { @@ -293,10 +387,10 @@ def test_configobj_dict_representation(a, b): }, } - t = ''' + t = cfg_lines(""" 'a' = b # !"$%^&*(),::;'@~#= 33 "b" = b #= 6, 33 -''' .split('\n') + """) t2 = ConfigObj(t) assert t2 == {'a': 'b', 'b': 'b'} t2.inline_comments['b'] = '' @@ -311,7 +405,7 @@ def test_behavior_when_list_values_is_false(): key3 = "double quotes" key4 = "list", 'with', several, "quotes" ''' - cfg = ConfigObj(c.splitlines(), list_values=False) + cfg = ConfigObj(cfg_lines(c), list_values=False) assert cfg == { 'key1': 'no quotes', 'key2': "'single quotes'", @@ -334,8 +428,8 @@ def test_behavior_when_list_values_is_false(): ] -def test_flatten_errors(val): - config = ''' +def test_flatten_errors(val, cfg_contents): + config = cfg_contents(""" test1=40 test2=hello test3=3 @@ -350,8 +444,8 @@ def test_flatten_errors(val): test2=hello test3=3 test4=5.0 - '''.split('\n') - configspec = ''' + """) + configspec = cfg_contents(""" test1= integer(30,50) test2= string test3=integer @@ -366,7 +460,7 @@ def test_flatten_errors(val): test2=string test3=integer test4=float(6.0) - '''.split('\n') + """) c1 = ConfigObj(config, configspec=configspec) res = c1.validate(val) assert flatten_errors(c1, res) == [([], 'test4', False), (['section'], 'test4', False), (['section', 'sub section'], 'test4', False)] @@ -395,6 +489,8 @@ def test_unicode_handling(): # final comment2 ''' + # needing to keep line endings means this isn't a good candidate + # for the cfg_lines utility method u = u_base.encode('utf_8').splitlines(True) u[0] = BOM_UTF8 + u[0] uc = ConfigObj(u) @@ -483,12 +579,12 @@ class TestWritingConfigs(object): class TestUnrepr(object): def test_in_reading(self): - config_to_be_unreprd = ''' + config_to_be_unreprd = cfg_lines(""" key1 = (1, 2, 3) # comment key2 = True key3 = 'a string' key4 = [1, 2, 3, 'a mixed list'] - '''.splitlines() + """) cfg = ConfigObj(config_to_be_unreprd, unrepr=True) assert cfg == { 'key1': (1, 2, 3), @@ -499,11 +595,12 @@ class TestUnrepr(object): assert cfg == ConfigObj(cfg.write(), unrepr=True) - def test_in_multiline_values(self): - config_with_multiline_value = '''k = \"""{ -'k1': 3, -'k2': 6.0}\""" -'''.splitlines() + def test_in_multiline_values(self, cfg_contents): + config_with_multiline_value = cfg_contents(''' + k = \"""{ + 'k1': 3, + 'k2': 6.0}\""" + ''') cfg = ConfigObj(config_with_multiline_value, unrepr=True) assert cfg == {'k': {'k1': 3, 'k2': 6.0}} @@ -596,24 +693,30 @@ class TestSectionBehavior(object): assert n == a assert n is not a - def test_merging(self): - config_with_subsection = '''[section1] - option1 = True - [[subsection]] - more_options = False - # end of file'''.splitlines() - config_that_overwrites_parameter = '''# File is user.ini - [section1] - option1 = False - # end of file'''.splitlines() + def test_merging(self, cfg_contents): + config_with_subsection = cfg_contents(""" + [section1] + option1 = True + [[subsection]] + more_options = False + # end of file + """) + config_that_overwrites_parameter = cfg_contents(""" + # File is user.ini + [section1] + option1 = False + # end of file + """) c1 = ConfigObj(config_that_overwrites_parameter) c2 = ConfigObj(config_with_subsection) c2.merge(c1) assert c2.dict() == {'section1': {'option1': 'False', 'subsection': {'more_options': 'False'}}} - def test_walking_with_in_place_updates(self): - config = '''[XXXXsection] - XXXXkey = XXXXvalue'''.splitlines() + def test_walking_with_in_place_updates(self, cfg_contents): + config = cfg_contents(""" + [XXXXsection] + XXXXkey = XXXXvalue + """) cfg = ConfigObj(config) assert cfg.dict() == {'XXXXsection': {'XXXXkey': 'XXXXvalue'}} def transform(section, key): @@ -715,14 +818,14 @@ class TestReloading(object): assert str(excinfo.value) == 'reload failed, filename is not set.' def test_reloading_with_an_actual_file(self, request, - reloadable_cfg_content): + reloadable_cfg_content, + cfg_contents): - # with open('temp', 'w') as cfg_file: - with NamedTemporaryFile(delete=False, mode='w') as cfg_file: - cfg_file.write(reloadable_cfg_content) + with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: + cfg_file.write(reloadable_cfg_content.encode('utf-8')) request.addfinalizer(lambda : os.unlink(cfg_file.name)) - configspec = ''' + configspec = cfg_contents(""" test1= integer(30,50) test2= string test3=integer @@ -742,7 +845,7 @@ class TestReloading(object): test2=string test3=integer test4=float(4.5) - '''.splitlines() + """) cfg = ConfigObj(cfg_file.name, configspec=configspec) cfg.configspec['test1'] = 'integer(50,60)' @@ -813,7 +916,7 @@ class TestInterpolation(object): return cfg @pytest.fixture - def template_cfg(self): + def template_cfg(self, cfg_contents): interp_cfg = ''' [DEFAULT] keyword1 = value1 @@ -841,7 +944,7 @@ class TestInterpolation(object): [[[ sub-sub-section ]]] convoluted = "$bar + $baz + $quux + $bar" ''' - return ConfigObj(interp_cfg.splitlines(), interpolation='Template') + return ConfigObj(cfg_contents(interp_cfg), interpolation='Template') def test_interpolation(self, config_parser_cfg): test_section = config_parser_cfg['section'] @@ -932,23 +1035,23 @@ class TestValues(object): Tests specifics about behaviors with types of values """ @pytest.fixture - def testconfig3(self): - return ''' - a = , - b = test, - c = test1, test2 , test3 - d = test1, test2, test3, - '''.splitlines() - - def test_empty_values(self): - cfg_with_empty = ''' + def testconfig3(self, cfg_contents): + return cfg_contents(""" + a = , + b = test, + c = test1, test2 , test3 + d = test1, test2, test3, + """) + + def test_empty_values(self, cfg_contents): + cfg_with_empty = cfg_contents(""" k = k2 =# comment test val = test val2 = , val3 = 1, val4 = 1, 2 - val5 = 1, 2, '''.splitlines() + val5 = 1, 2, """) cwe = ConfigObj(cfg_with_empty) # see a comma? it's a list assert cwe == {'k': '', 'k2': '', 'val': 'test', 'val2': [], @@ -1014,27 +1117,39 @@ def test_creating_with_a_dictionary(): assert dictionary_cfg_content is not cfg.dict() -def test_multiline_comments(i): - assert i == { - 'name4': ' another single line value ', - 'multi section': { - 'name4': '\n Well, this is a\n multiline ' - 'value\n ', - 'name2': '\n Well, this is a\n multiline ' - 'value\n ', - 'name3': '\n Well, this is a\n multiline ' - 'value\n ', - 'name1': '\n Well, this is a\n multiline ' - 'value\n ', - }, - 'name2': ' another single line value ', - 'name3': ' a single line value ', - 'name1': ' a single line value ', - } - - class TestComments(object): - def test_starting_and_ending_comments(self, a, testconfig1): + @pytest.fixture + def comment_filled_cfg(self, cfg_contents): + return cfg_contents(""" + # initial comments + # with two lines + key = "value" + # section comment + [section] # inline section comment + # key comment + key = "value" + + # final comment + # with two lines""" + ) + + def test_multiline_comments(self, i): + + expected_multiline_value = '\nWell, this is a\nmultiline value\n' + assert i == { + 'name4': ' another single line value ', + 'multi section': { + 'name4': expected_multiline_value, + 'name2': expected_multiline_value, + 'name3': expected_multiline_value, + 'name1': expected_multiline_value, + }, + 'name2': ' another single line value ', + 'name3': ' a single line value ', + 'name1': ' a single line value ', + } + + def test_starting_and_ending_comments(self, a, testconfig1, cfg_contents): filename = a.filename a.filename = None @@ -1061,6 +1176,33 @@ class TestComments(object): c.inline_comments['foo'] = 'Nice bar' assert c.write() == ['foo = bar # Nice bar'] + def test_unrepr_comments(self, comment_filled_cfg): + c = ConfigObj(comment_filled_cfg, unrepr=True) + assert c == { 'key': 'value', 'section': { 'key': 'value'}} + assert c.initial_comment == [ + '', '# initial comments', '# with two lines' + ] + assert c.comments == {'section': ['# section comment'], 'key': []} + assert c.inline_comments == { + 'section': '# inline section comment', 'key': '' + } + assert c['section'].comments == { 'key': ['# key comment']} + assert c.final_comment == ['', '# final comment', '# with two lines'] + + def test_comments(self, comment_filled_cfg): + c = ConfigObj(comment_filled_cfg) + assert c == { 'key': 'value', 'section': { 'key': 'value'}} + assert c.initial_comment == [ + '', '# initial comments', '# with two lines' + ] + assert c.comments == {'section': ['# section comment'], 'key': []} + assert c.inline_comments == { + 'section': '# inline section comment', 'key': None + } + assert c['section'].comments == { 'key': ['# key comment']} + assert c.final_comment == ['', '# final comment', '# with two lines'] + + def test_overwriting_filenames(a, b, i): #TODO: I'm not entirely sure what this test is actually asserting @@ -1150,7 +1292,7 @@ class TestEdgeCasesWhenWritingOut(object): def test_writing_out_dict_value_with_unrepr(self): # issue #42 - cfg = ['thing = {"a": 1}'] + cfg = [str('thing = {"a": 1}')] c = ConfigObj(cfg, unrepr=True) assert repr(c) == "ConfigObj({'thing': {'a': 1}})" assert c.write() == ["thing = {'a': 1}"] |