diff --git a/configurator/_toml.py b/configurator/_toml.py new file mode 100644 index 0000000..4e74a4e --- /dev/null +++ b/configurator/_toml.py @@ -0,0 +1,16 @@ +import io + +try: + import tomllib +except ImportError: + import tomli as tomllib + + +def load(f, *, parse_float=float): + # wrapper around tomllib.load to be more forgiving of streams opened in text mode + if isinstance(f, io.TextIOWrapper): + return tomllib.load(f.buffer, parse_float=parse_float) + elif isinstance(f, io.StringIO): + return tomllib.loads(f.getvalue(), parse_float=parse_float) + else: + return tomllib.load(f, parse_float=parse_float) diff --git a/configurator/parsers.py b/configurator/parsers.py index 93bc3b3..48c1434 100644 --- a/configurator/parsers.py +++ b/configurator/parsers.py @@ -1,4 +1,5 @@ from collections import defaultdict +from importlib import import_module class ParseError(Exception): @@ -10,9 +11,10 @@ class ParseError(Exception): class Parsers(defaultdict): + # file extension: module name, method name supported = { 'json': ('json', 'load'), - 'toml': ('toml', 'load'), + 'toml': ('configurator._toml', 'load'), 'yml': ('yaml', 'safe_load'), 'yaml': ('yaml', 'safe_load'), } @@ -23,5 +25,5 @@ def __missing__(self, extension): except KeyError: raise ParseError('No parser found for {!r}'.format(extension)) else: - module = __import__(module_name) + module = import_module(module_name) return getattr(module, parser_name) diff --git a/setup.py b/setup.py index 6e0b155..15cbbb4 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ python_requires=">=3.6", extras_require=dict( yaml=['pyyaml'], - toml=['toml'], + toml=['tomli; python_version < "3.11"'], test=[ 'jinja2', 'mock', diff --git a/tests/test_toml_loading.py b/tests/test_toml_loading.py new file mode 100644 index 0000000..151eeb0 --- /dev/null +++ b/tests/test_toml_loading.py @@ -0,0 +1,65 @@ +import pytest +from testfixtures import compare + +from configurator import Config + + +def test_load_toml_from_path_implicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + config = Config.from_path(path) + compare(config.k, "v") + + +def test_load_toml_from_path_explicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + parser = Config.parsers['toml'] + config = Config.from_path(path, parser) + compare(config.k, "v") + + +def test_load_toml_from_byte_stream_implicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + with path.open(mode="rb") as stream: + config = Config.from_stream(stream) + compare(config.k, "v") + + +def test_load_toml_from_byte_stream_explicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + parser = Config.parsers['toml'] + with path.open(mode="rb") as stream: + config = Config.from_stream(stream, parser) + compare(config.k, "v") + + +def test_load_toml_from_text_stream_implicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + with path.open(mode="rt") as stream: + config = Config.from_stream(stream) + compare(config.k, "v") + + +def test_load_toml_from_text_stream_explicit_parser(tmp_path): + path = tmp_path / 'test.toml' + path.write_bytes(b'k = "v"') + parser = Config.parsers['toml'] + with path.open(mode="rt") as stream: + config = Config.from_stream(stream, parser) + compare(config.k, "v") + + +def test_load_toml_from_text(): + parser = Config.parsers['toml'] + config = Config.from_text('k = "v"', parser) + compare(config.k, "v") + + +def test_load_toml_from_bytes(): + parser = Config.parsers['toml'] + config = Config.from_text(b'k = "v"', parser) + compare(config.k, "v")