From d056d404ab9f508e6034236f49d5858b60b7e6ff Mon Sep 17 00:00:00 2001 From: adamghill Date: Tue, 9 Sep 2025 08:49:07 -0500 Subject: [PATCH 1/4] Move from `toml` to `tomli`. Fixes #3. --- pyproject.toml | 2 +- src/dj_toml_settings/toml_parser.py | 12 +++++++++--- uv.lock | 13 ++----------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 31b9823..af7ae54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] dependencies = [ "python-dateutil>=2.9.0.post0", - "toml>=0.10.2", + "tomli>=1.1.0; python_version < '3.11'", "typeguard>=2", ] diff --git a/src/dj_toml_settings/toml_parser.py b/src/dj_toml_settings/toml_parser.py index fc9bd1c..8a29b5f 100644 --- a/src/dj_toml_settings/toml_parser.py +++ b/src/dj_toml_settings/toml_parser.py @@ -1,13 +1,18 @@ import logging import os +import sys from datetime import datetime from pathlib import Path from typing import Any -import toml from dateutil import parser as dateparser from typeguard import typechecked +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + from dj_toml_settings.value_parsers.dict_parsers import ( EnvParser, InsertParser, @@ -76,10 +81,11 @@ def get_data(self) -> dict: data = {} try: - data = toml.load(self.path) + with open(self.path, "rb") as f: + data = tomllib.load(f) except FileNotFoundError: logger.warning(f"Cannot find file at: {self.path}") - except toml.TomlDecodeError: + except tomllib.TOMLDecodeError: logger.error(f"Cannot parse TOML at: {self.path}") return data.get("tool", {}).get("django", {}) or {} diff --git a/uv.lock b/uv.lock index af57486..98bf306 100644 --- a/uv.lock +++ b/uv.lock @@ -107,7 +107,7 @@ version = "0.4.0" source = { editable = "." } dependencies = [ { name = "python-dateutil" }, - { name = "toml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typeguard" }, ] @@ -124,7 +124,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "python-dateutil", specifier = ">=2.9.0.post0" }, - { name = "toml", specifier = ">=0.10.2" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1.1.0" }, { name = "typeguard", specifier = ">=2" }, ] @@ -314,15 +314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, -] - [[package]] name = "tomli" version = "2.2.1" From f39023ba5297fb7fa1e228494ac24c8b2c60b1cc Mon Sep 17 00:00:00 2001 From: adamghill Date: Tue, 9 Sep 2025 08:49:23 -0500 Subject: [PATCH 2/4] Fix tests for `tomli`. --- tests/test_toml_parser/test_parse_file.py | 92 +++++------------------ 1 file changed, 17 insertions(+), 75 deletions(-) diff --git a/tests/test_toml_parser/test_parse_file.py b/tests/test_toml_parser/test_parse_file.py index 336835f..e57b2e8 100644 --- a/tests/test_toml_parser/test_parse_file.py +++ b/tests/test_toml_parser/test_parse_file.py @@ -31,7 +31,7 @@ def test_type_bool_true(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -DEBUG = { $value = "True", $type = "bool" } +DEBUG = { "$value" = "True", "$type" = "bool" } """) actual = Parser(path).parse_file() @@ -45,7 +45,7 @@ def test_type_float(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -FLOAT = { $value = "1.5", $type = "float" } +FLOAT = { "$value" = "1.5", "$type" = "float" } """) actual = Parser(path).parse_file() @@ -188,7 +188,7 @@ def test_env(tmp_path, monkeypatch): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $env = "SOME_VAR" } +SOMETHING = { "$env" = "SOME_VAR" } """) actual = Parser(path).parse_file() @@ -204,7 +204,7 @@ def test_env_in_nested_dict(tmp_path, monkeypatch): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { MORE = { $env = "SOME_VAR" } } +SOMETHING = { MORE = { "$env" = "SOME_VAR" } } """) actual = Parser(path).parse_file() @@ -265,7 +265,7 @@ def test_env_missing(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $env = "SOME_VAR" } +SOMETHING = { "$env" = "SOME_VAR" } """) actual = Parser(path).parse_file() @@ -279,7 +279,7 @@ def test_env_default(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $env = "SOME_VAR", $default = "default" } +SOMETHING = { "$env" = "SOME_VAR", "$default" = "default" } """) actual = Parser(path).parse_file() @@ -293,7 +293,7 @@ def test_path(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $path = "test-file" } +SOMETHING = { "$path" = "test-file" } """) actual = Parser(path).parse_file() @@ -307,7 +307,7 @@ def test_relative_path(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $path = "./test-file" } +SOMETHING = { "$path" = "./test-file" } """) actual = Parser(path).parse_file() @@ -321,7 +321,7 @@ def test_parent_path(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $path = "../test-file" } +SOMETHING = { "$path" = "../test-file" } """) actual = Parser(path).parse_file() @@ -335,7 +335,7 @@ def test_parent_path_2(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $path = "./../test-file" } +SOMETHING = { "$path" = "./../test-file" } """) actual = Parser(path).parse_file() @@ -352,7 +352,7 @@ def test_insert(tmp_path): SOMETHING = [1] [tool.django.apps.something] -SOMETHING = { $insert = 2 } +SOMETHING = { "$insert" = 2 } """) actual = Parser(path).parse_file() @@ -366,7 +366,7 @@ def test_insert_missing(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -SOMETHING = { $insert = 1 } +SOMETHING = { "$insert" = 1 } """) actual = Parser(path).parse_file() @@ -383,7 +383,7 @@ def test_insert_invalid(tmp_path): SOMETHING = "hello" [tool.django.apps.something] -SOMETHING = { $insert = 1 } +SOMETHING = { "$insert" = 1 } """) with pytest.raises(InvalidActionError) as e: @@ -406,7 +406,7 @@ def test_insert_index(tmp_path): SOMETHING = [1] [tool.django.apps.something] -SOMETHING = { $insert = 2, $index = 0 } +SOMETHING = { "$insert" = 2, "$index" = 0 } """) actual = Parser(path).parse_file() @@ -665,65 +665,7 @@ def test_none(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -TEST = { $none = 1 } -""") - - actual = Parser(path).parse_file() - - assert expected == actual - - -def test_special_prefix(tmp_path): - expected = { - "TOML_SETTINGS_SPECIAL_PREFIX": "&", - "TEST": None, - } - - path = tmp_path / "pyproject.toml" - path.write_text(""" -[tool.django] -TOML_SETTINGS_SPECIAL_PREFIX = "&" -TEST = { &none = 1 } -""") - - actual = Parser(path).parse_file() - - assert expected == actual - - -def test_special_suffix(tmp_path): - expected = { - "TOML_SETTINGS_SPECIAL_PREFIX": "", - "TOML_SETTINGS_SPECIAL_SUFFIX": "*", - "TEST": None, - } - - path = tmp_path / "pyproject.toml" - path.write_text(""" -[tool.django] -TOML_SETTINGS_SPECIAL_PREFIX = "" -TOML_SETTINGS_SPECIAL_SUFFIX = "*" -TEST = { none* = 1 } -""") - - actual = Parser(path).parse_file() - - assert expected == actual - - -def test_special_prefix_and_suffix(tmp_path): - expected = { - "TOML_SETTINGS_SPECIAL_PREFIX": "&", - "TOML_SETTINGS_SPECIAL_SUFFIX": "*", - "TEST": None, - } - - path = tmp_path / "pyproject.toml" - path.write_text(""" -[tool.django] -TOML_SETTINGS_SPECIAL_PREFIX = "&" -TOML_SETTINGS_SPECIAL_SUFFIX = "*" -TEST = { &none* = 1 } +TEST = { "$none" = 1 } """) actual = Parser(path).parse_file() @@ -785,7 +727,7 @@ def test_variable_start_path(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -BASE_DIR = { $path = "." } +BASE_DIR = { "$path" = "." } STATIC_ROOT = "${BASE_DIR}/staticfiles" """) @@ -800,7 +742,7 @@ def test_variable_end_path(tmp_path): path = tmp_path / "pyproject.toml" path.write_text(""" [tool.django] -BASE_DIR = { $path = "/something" } +BASE_DIR = { "$path" = "/something" } STATIC_ROOT = "/blob${BASE_DIR}" """) From 3b49c9c04a6126e7f580ce03330b27e826620a14 Mon Sep 17 00:00:00 2001 From: adamghill Date: Tue, 9 Sep 2025 08:49:51 -0500 Subject: [PATCH 3/4] Remove support for custom prefix or suffix. --- README.md | 9 -------- .../value_parsers/dict_parsers.py | 22 +++++-------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 98240cc..a14e803 100644 --- a/README.md +++ b/README.md @@ -86,15 +86,6 @@ ALLOWED_HOSTS = ["example.com"] By default, special operations are denoted by an [`inline table`](https://toml.io/en/v1.0.0#inline-table), (aka a `dictionary`) with a key that starts with a `$`, e.g. `{ "$value" = "1" }`. -The prefix and suffix that denotes a special operation can be configured with `TOML_SETTINGS_SPECIAL_PREFIX` or `TOML_SETTINGS_SPECIAL_SUFFIX` in `[tool.django]`. - -```toml -[tool.django] -TOML_SETTINGS_SPECIAL_PREFIX = "&" -TOML_SETTINGS_SPECIAL_SUFFIX = "*" -BASE_DIR = { "&path*" = "." } -``` - ### Path Converts a string to a `Path` object by using a `$path` key. Handles relative paths based on the location of the parsed TOML file. diff --git a/src/dj_toml_settings/value_parsers/dict_parsers.py b/src/dj_toml_settings/value_parsers/dict_parsers.py index aa71606..d2f36e8 100644 --- a/src/dj_toml_settings/value_parsers/dict_parsers.py +++ b/src/dj_toml_settings/value_parsers/dict_parsers.py @@ -27,26 +27,16 @@ def __init__(self, data: dict, value: dict): if not hasattr(self, "key"): raise NotImplementedError("Missing key attribute") - self.key = self.add_prefix_and_suffix_to_key(self.key) + self.key = self.add_prefix_to_key(self.key) def match(self) -> bool: return self.key in self.value @typechecked - def add_prefix_and_suffix_to_key(self, key: str) -> str: - """Gets the key for the special operator. Defaults to "$" as the prefix, and "" as the suffix. - - To change in the included TOML settings, set: - ``` - TOML_SETTINGS_SPECIAL_PREFIX = "" - TOML_SETTINGS_SPECIAL_SUFFIX = "" - ``` - """ - - prefix = self.data.get("TOML_SETTINGS_SPECIAL_PREFIX", "$") - suffix = self.data.get("TOML_SETTINGS_SPECIAL_SUFFIX", "") + def add_prefix_to_key(self, key: str) -> str: + """Gets the key for the special operator.""" - return f"{prefix}{key}{suffix}" + return f"${key}" def parse(self, *args, **kwargs): raise NotImplementedError("parse() not implemented") @@ -56,7 +46,7 @@ class EnvParser(DictParser): key: str = "env" def parse(self) -> Any: - default_special_key = self.add_prefix_and_suffix_to_key("default") + default_special_key = self.add_prefix_to_key("default") default_value = self.value.get(default_special_key) env_value = self.value[self.key] @@ -114,7 +104,7 @@ def parse(self) -> Any: raise InvalidActionError(f"`insert` cannot be used for value of type: {type(self.data[self.data_key])}") # Insert the data - index_key = self.add_prefix_and_suffix_to_key("index") + index_key = self.add_prefix_to_key("index") index = self.value.get(index_key, len(insert_data)) insert_data.insert(index, self.value[self.key]) From 0a2059edfe382bda25bf8b05cf54598dde63766c Mon Sep 17 00:00:00 2001 From: adamghill Date: Tue, 9 Sep 2025 08:53:52 -0500 Subject: [PATCH 4/4] Add changelog. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5a0eb..172bb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.5.0-dev + +- Move from `toml` to `tomli` for TOML parsing to allow using `tomllib` in standard library for Python > 3.11. + +### Breaking changes + +- Remove custom prefix or suffixes for special operators. Everything should start with "$" to reduce code and unnecessary complications. + ## 0.4.0 - Add `$value` operator.