From 381331f917dad2adaa94f3c34f257562fc2b1c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 24 Jan 2025 12:45:21 +0000 Subject: [PATCH 1/6] Drop python 3.7 support, add 3.13 --- .github/workflows/python-test.yml | 2 +- README.md | 2 +- setup.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 32b1adf..2007e5c 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 5b25e70..1c7c197 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ Python 3+ compatible port of the [configobj](https://pypi.python.org/pypi/configobj/) library. The Github CI/CD Pipeline runs tests on python versions: -- 3.7 - 3.8 - 3.9 - 3.10 - 3.11 - 3.12 +- 3.13 ## Documentation diff --git a/setup.py b/setup.py index dbb87e9..33a7546 100644 --- a/setup.py +++ b/setup.py @@ -81,12 +81,12 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -109,7 +109,7 @@ py_modules=MODULES, package_dir={'': 'src'}, packages=PACKAGES, - python_requires='>=3.7', + python_requires='>=3.8', classifiers=CLASSIFIERS, keywords=KEYWORDS, license='BSD-3-Clause', From 10214112cb4ef904b2fb38e1e61dda14baeaf44f Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Mon, 2 Jun 2025 23:34:04 +0000 Subject: [PATCH 2/6] Update documentation to match current behaviour - fallback to ascii when decoding. This is probably not the correct behaviour, but I'll leave fixing that (and thinking through the consequences of changing the encoding) for later. Fixes #256 --- docs/configobj.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configobj.rst b/docs/configobj.rst index ef6fe28..4456ab4 100644 --- a/docs/configobj.rst +++ b/docs/configobj.rst @@ -417,8 +417,7 @@ ConfigObj takes the following arguments (with the default values shown) : ``default_encoding``, if specified, is the encoding used to decode byte strings in the **ConfigObj** before writing. If this is ``None``, then - the Python default encoding (``sys.defaultencoding`` - usually ASCII) is - used. + a default encoding (currently ASCII) is used. For most Western European users, a value of ``latin-1`` is sensible. @@ -869,7 +868,8 @@ members) will first be decoded to Unicode using the encoding specified by the ``default_encoding`` attribute. This ensures that the output is in the encoding specified. -If this value is ``None`` then ``sys.defaultencoding`` is used instead. +If this value is ``None`` then a default encoding (currently ASCII) is used to decode +any byte-strings in your ConfigObj instance. unrepr From c2480dc8caa1914b7a6b1ea47097d2da516c2865 Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Thu, 5 Jun 2025 10:07:57 -0700 Subject: [PATCH 3/6] When writing an inline comment with a leading `#` put a space between the value and the `#` Fixes #261 --- src/configobj/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/configobj/__init__.py b/src/configobj/__init__.py index 3f91127..0d76ffc 100644 --- a/src/configobj/__init__.py +++ b/src/configobj/__init__.py @@ -2000,6 +2000,8 @@ def _handle_comment(self, comment): start = self.indent_type if not comment.startswith('#'): start += self._a_to_u(' # ') + else: + start += self._a_to_u(' ') return (start + comment) From eea828703defcd52b80671c4606e935a40362ca7 Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Thu, 5 Jun 2025 10:45:54 -0700 Subject: [PATCH 4/6] Add test and update tests for inline comment with preceding space --- src/tests/test_configobj.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/tests/test_configobj.py b/src/tests/test_configobj.py index 017428a..eeb0a1c 100644 --- a/src/tests/test_configobj.py +++ b/src/tests/test_configobj.py @@ -542,13 +542,13 @@ def test_validate(self, val): 'key1 = Hello', '', '# section comment', - '[section]# inline comment', + '[section] # inline comment', '# key1 comment', 'key1 = 6', '# key2 comment', 'key2 = True', '# subsection comment', - '[[sub-section]]# inline comment', + '[[sub-section]] # inline comment', '# another key1 comment', 'key1 = 3.0' ] @@ -560,9 +560,9 @@ def test_writing_empty_values(self): 'key2 =# a comment', ] cfg = ConfigObj(config_with_empty_values) - assert cfg.write() == ['', 'key1 = ""', 'key2 = ""# a comment'] + assert cfg.write() == ['', 'key1 = ""', 'key2 = "" # a comment'] cfg.write_empty_values = True - assert cfg.write() == ['', 'key1 = ', 'key2 = # a comment'] + assert cfg.write() == ['', 'key1 = ', 'key2 = # a comment'] class TestUnrepr(object): @@ -1164,6 +1164,12 @@ def test_inline_comments(self): c.inline_comments['foo'] = 'Nice bar' assert c.write() == ['foo = bar # Nice bar'] + def test_inline_comments_with_leading_number_sign(self): + c = ConfigObj() + c['foo'] = 'bar' + 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'}} From 288d98456395614038fc29321480b1e6c25e774e Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Thu, 5 Jun 2025 20:49:04 -0700 Subject: [PATCH 5/6] Add support for infile being a pathlib.Path object Also add a test when create_empty is True and a string filename is passed in for infile Fixes #235 --- docs/configobj.rst | 7 ++++--- src/configobj/__init__.py | 18 ++++++++++++++++-- src/tests/test_configobj.py | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/configobj.rst b/docs/configobj.rst index 4456ab4..9fd6bde 100644 --- a/docs/configobj.rst +++ b/docs/configobj.rst @@ -281,9 +281,10 @@ ConfigObj takes the following arguments (with the default values shown) : * Nothing. In which case the ``filename`` attribute of your ConfigObj will be ``None``. You can set a filename at any time. - * A filename. What happens if the file doesn't already exist is determined by - the options ``file_error`` and ``create_empty``. The filename will be - preserved as the ``filename`` attribute. This can be changed at any time. + * A filename or pathlib.Path object. What happens if the file doesn't already + exist is determined by the options ``file_error`` and ``create_empty``. The + filename will be preserved as the ``filename`` attribute. This can be + changed at any time. * A list of lines. Any trailing newlines will be removed from the lines. The ``filename`` attribute of your ConfigObj will be ``None``. diff --git a/src/configobj/__init__.py b/src/configobj/__init__.py index 0d76ffc..0ae986a 100644 --- a/src/configobj/__init__.py +++ b/src/configobj/__init__.py @@ -18,6 +18,7 @@ import sys from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE +from pathlib import Path from ._version import __version__ @@ -1241,7 +1242,20 @@ def _load(self, infile, configspec): with open(infile, 'w') as h: h.write('') content = [] - + + elif isinstance(infile, Path): + self.filename = str(infile) + if infile.is_file(): + with infile.open('rb') as h: + content = h.readlines() or [] + elif self.file_error: + raise IOError('Config file not found: "%s".' % self.filename) + else: + if self.create_empty: + with infile.open('w') as h: + h.write('') + content = [] + elif isinstance(infile, (list, tuple)): content = list(infile) @@ -1275,7 +1289,7 @@ def set_section(in_section, this_section): # needs splitting into lines - but needs doing *after* decoding # in case it's not an 8 bit encoding else: - raise TypeError('infile must be a filename, file like object, or list of lines.') + raise TypeError('infile must be a filename, file like object, pathlib.Path or list of lines.') if content: # don't do it for the empty ConfigObj diff --git a/src/tests/test_configobj.py b/src/tests/test_configobj.py index eeb0a1c..37ddbf0 100644 --- a/src/tests/test_configobj.py +++ b/src/tests/test_configobj.py @@ -4,6 +4,7 @@ import re from codecs import BOM_UTF8 +from pathlib import Path from warnings import catch_warnings from tempfile import NamedTemporaryFile @@ -1104,6 +1105,24 @@ def test_creating_with_a_dictionary(): assert dictionary_cfg_content == cfg.dict() assert dictionary_cfg_content is not cfg.dict() +def test_reading_a_pathlib_path(cfg_contents): + cfg = cfg_contents(""" +[section] +foo = bar""") + c = ConfigObj(Path(cfg)) + assert 'foo' in c['section'] + +def test_creating_a_file_from_pathlib_path(tmp_path): + infile = tmp_path / 'config.ini' + assert not Path(tmp_path / 'config.ini').is_file() + c = ConfigObj(Path(infile), create_empty=True) + assert Path(tmp_path / 'config.ini').is_file() + +def test_creating_a_file_from_string(tmp_path): + infile = str(tmp_path / 'config.ini') + assert not Path(infile).is_file() + c = ConfigObj(infile, create_empty=True) + assert Path(infile).is_file() class TestComments(object): @pytest.fixture From e55ac37422e89af3581a79b8c62ddd839656c052 Mon Sep 17 00:00:00 2001 From: Gene Wood Date: Fri, 6 Jun 2025 08:22:06 -0700 Subject: [PATCH 6/6] Document release and master branches in README Fixes #263 --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 1c7c197..3f6bafe 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,24 @@ You can find a full manual on how to use ConfigObj at [readthedocs](http://confi This is a mature project that is not actively maintained at this time. +## Branches + +The default branch of this repository is the [`release` branch](https://github.com/DiffSK/configobj/tree/release), +rather than the [`master` branch](https://github.com/DiffSK/configobj/tree/master). +This decision dates back to when this project moved from being actively +maintained to it's current state as a mature project. +At that time (in 2023), [changes introduced](https://github.com/DiffSK/configobj/compare/v5.0.6...master) +into the `master` branch had not been fully integrated, and the tooling had +shifted in ways that made [continuing from `master` unclear](https://github.com/DiffSK/configobj/pull/237#issuecomment-1925401612). + +Instead, [we chose to continue development](https://github.com/DiffSK/configobj/issues/213#issuecomment-1377686121) +from the `5.0.6` release branch. Any changes made to master after that release +were effectively abandoned, and the `release` branch has served as the main +line of development since. + +As a result, going forward, new pull requests should be made against the +`release` branch as the `master` branch is frozen in time. + ## Past Contributors: - [Michael Foord](https://agileabstractions.com/)