Skip to content

Commit 4ed2350

Browse files
authored
fix(set): non-str key raising type error #1005 (#1008)
fix bug reported on #1005
1 parent 9170beb commit 4ed2350

6 files changed

Lines changed: 115 additions & 35 deletions

File tree

docs/index.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,22 +396,30 @@ mixed settings formats across your application.
396396

397397
#### Supported formats
398398

399+
Create your settings in the desired format and specify it on `settings_files`
400+
argument on your dynaconf instance or pass it in `-f <format>` if using `dynaconf init` command.
401+
402+
The following are the currently supported formats:
403+
399404
- **.toml** - Default and **recommended** file format.
400405
- **.yaml|.yml** - Recommended for Django applications.
401406
- **.json** - Useful to reuse existing or exported settings.
402407
- **.ini** - Useful to reuse legacy settings.
403408
- **.py** - **Not Recommended** but supported for backwards compatibility.
404409
- **.env** - Useful to automate the loading of environment variables.
405410

406-
!!! info
407-
Create your settings in the desired format and specify it on `settings_files`
408-
argument on your dynaconf instance or pass it in `-f <format>` if using `dynaconf init` command.
409-
410411
!!! tip
411412
Can't find the file format you need for your settings?
412413
You can create your custom loader and read any data source.
413414
read more on [extending dynaconf](/advanced/)
414415

416+
#### Key types
417+
418+
Dynaconf will try to preserve non-string integers such as `1: foo` in yaml,
419+
or arbitrary types defined within python, like `settings.set("a", {1: "b", (1,2): "c"})`.
420+
421+
This is intended for special cases only, as envvars and most file loaders won't support non-string key types.
422+
415423
#### Reading settings from files
416424

417425
On files by default dynaconf loads all the existing keys and sections

dynaconf/base.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -476,23 +476,24 @@ def get(
476476
sysenv_fallback = self._store.get("SYSENV_FALLBACK_FOR_DYNACONF")
477477

478478
nested_sep = self._store.get("NESTED_SEPARATOR_FOR_DYNACONF")
479-
if nested_sep and nested_sep in key:
480-
# turn FOO__bar__ZAZ in `FOO.bar.ZAZ`
481-
key = key.replace(nested_sep, ".")
482-
483-
if dotted_lookup is empty:
484-
dotted_lookup = self._store.get("DOTTED_LOOKUP_FOR_DYNACONF")
485-
486-
if "." in key and dotted_lookup:
487-
return self._dotted_get(
488-
dotted_key=key,
489-
default=default,
490-
cast=cast,
491-
fresh=fresh,
492-
parent=parent,
493-
)
479+
if isinstance(key, str):
480+
if nested_sep and nested_sep in key:
481+
# turn FOO__bar__ZAZ in `FOO.bar.ZAZ`
482+
key = key.replace(nested_sep, ".")
483+
484+
if dotted_lookup is empty:
485+
dotted_lookup = self._store.get("DOTTED_LOOKUP_FOR_DYNACONF")
486+
487+
if "." in key and dotted_lookup:
488+
return self._dotted_get(
489+
dotted_key=key,
490+
default=default,
491+
cast=cast,
492+
fresh=fresh,
493+
parent=parent,
494+
)
494495

495-
key = upperfy(key)
496+
key = upperfy(key)
496497

497498
# handles system environment fallback
498499
if default is None:
@@ -913,7 +914,7 @@ def set(
913914
):
914915
"""Set a value storing references for the loader
915916
916-
:param key: The key to store
917+
:param key: The key to store. Can be of any type.
917918
:param value: The raw value to parse and store
918919
:param loader_identifier: Optional loader name e.g: toml, yaml etc.
919920
Or isntance of SourceMetadata
@@ -938,20 +939,21 @@ def set(
938939
if dotted_lookup is empty:
939940
dotted_lookup = self.get("DOTTED_LOOKUP_FOR_DYNACONF")
940941
nested_sep = self.get("NESTED_SEPARATOR_FOR_DYNACONF")
941-
if nested_sep and nested_sep in key:
942-
key = key.replace(nested_sep, ".") # FOO__bar -> FOO.bar
943-
944-
if "." in key and dotted_lookup is True:
945-
return self._dotted_set(
946-
key,
947-
value,
948-
loader_identifier=source_metadata,
949-
tomlfy=tomlfy,
950-
validate=validate,
951-
)
942+
if isinstance(key, str):
943+
if nested_sep and nested_sep in key:
944+
key = key.replace(nested_sep, ".") # FOO__bar -> FOO.bar
945+
946+
if "." in key and dotted_lookup is True:
947+
return self._dotted_set(
948+
key,
949+
value,
950+
loader_identifier=source_metadata,
951+
tomlfy=tomlfy,
952+
validate=validate,
953+
)
954+
key = upperfy(key.strip())
952955

953956
parsed = parse_conf_data(value, tomlfy=tomlfy, box_settings=self)
954-
key = upperfy(key.strip())
955957

956958
# Fix for #869 - The call to getattr trigger early evaluation
957959
existing = (
@@ -1001,7 +1003,12 @@ def set(
10011003
# Set the parsed value
10021004
self.store[key] = parsed
10031005
self._deleted.discard(key)
1004-
super().__setattr__(key, parsed)
1006+
1007+
# check if str because we can't directly set/get non-str with obj. e.g.
1008+
# setting.1
1009+
# settings.(1,2)
1010+
if isinstance(key, str):
1011+
super().__setattr__(key, parsed)
10051012

10061013
# Track history for inspect, store the raw_value
10071014
if source_metadata in self._loaded_by_loaders:

dynaconf/utils/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,9 @@ def isnamedtupleinstance(value):
461461

462462

463463
def find_the_correct_casing(key: str, data: dict[str, Any]) -> str | None:
464-
"""Given a key, find the proper casing in data
464+
"""Given a key, find the proper casing in data.
465+
466+
Return 'None' for non-str key types.
465467
466468
Arguments:
467469
key {str} -- A key to be searched in data
@@ -473,6 +475,8 @@ def find_the_correct_casing(key: str, data: dict[str, Any]) -> str | None:
473475
if not isinstance(key, str) or key in data:
474476
return key
475477
for k in data.keys():
478+
if not isinstance(k, str):
479+
return None
476480
if k.lower() == key.lower():
477481
return k
478482
if k.replace(" ", "_").lower() == key.lower():

tests/test_base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,26 @@ def test_set_new_merge_issue_241_5(tmpdir):
513513
}
514514

515515

516+
def test_set_with_non_str_types():
517+
"""This replicates issue #1005 in a simplified setup."""
518+
settings = Dynaconf(merge_enabled=True)
519+
settings.set("a", {"b": {1: "foo"}})
520+
settings.set("a", {"b": {"c": "bar"}})
521+
assert settings["a"]["b"][1] == "foo"
522+
assert settings["a"]["b"]["c"] == "bar"
523+
524+
525+
def test_set_with_non_str_types_on_first_level():
526+
"""Non-str key types on first level."""
527+
settings = Dynaconf(merge_enabled=True)
528+
settings.set(1, {"b": {1: "foo"}})
529+
settings.set("a", {"1": {1: "foo"}})
530+
assert settings[1]["b"][1] == "foo"
531+
assert settings[1].b[1] == "foo"
532+
assert settings.get(1).b[1] == "foo"
533+
assert settings["a"]["1"][1] == "foo"
534+
535+
516536
def test_exists(settings):
517537
settings.set("BOOK", "TAOCP")
518538
assert settings.exists("BOOK") is True
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.PHONY: test
2+
3+
test:
4+
pytest -v app_test.py
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
https://github.com/dynaconf/dynaconf/issues/1005
3+
"""
4+
from __future__ import annotations
5+
6+
from textwrap import dedent
7+
8+
from dynaconf import Dynaconf
9+
10+
11+
def create_file(fn: str, data: str):
12+
with open(fn, "w") as file:
13+
file.write(dedent(data))
14+
return fn
15+
16+
17+
def test_issue_1005(tmp_path):
18+
file_a = create_file(
19+
tmp_path / "t.yaml",
20+
"""\
21+
default:
22+
a_mapping:
23+
1: ["a_string"]
24+
""",
25+
)
26+
create_file(
27+
tmp_path / "t.local.yaml",
28+
"""\
29+
default:
30+
a_value: .inf
31+
""",
32+
)
33+
34+
settings = Dynaconf(settings_file=file_a, merge_enabled=True)
35+
36+
assert settings.as_dict()["DEFAULT"]["a_mapping"]
37+
assert settings.as_dict()["DEFAULT"]["a_value"]

0 commit comments

Comments
 (0)