Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/)
Expand Down
13 changes: 7 additions & 6 deletions docs/configobj.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -417,8 +418,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.

Expand Down Expand Up @@ -869,7 +869,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
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
20 changes: 18 additions & 2 deletions src/configobj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2000,6 +2014,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)


Expand Down
33 changes: 29 additions & 4 deletions src/tests/test_configobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re

from codecs import BOM_UTF8
from pathlib import Path
from warnings import catch_warnings
from tempfile import NamedTemporaryFile

Expand Down Expand Up @@ -542,13 +543,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'
]
Expand All @@ -560,9 +561,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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1164,6 +1183,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'}}
Expand Down