From 70b3567a0910e9f570750885eacba232bba7e701 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Wed, 24 Jun 2026 10:21:20 +0200 Subject: [PATCH] fix: case-insensitive ConfigBox key lookup for __getitem__, __contains__, __delitem__, __setitem__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConfigBox docstring advertises "loosey goosey" case-insensitive access but __getitem__ (dict-style), __contains__, __delitem__ and __setitem__ had no case-insensitive support. __getattr__ only retried with lowered input, missing MixedCase→lowercase resolution. Add _case_insensitive_key() helper and override the four dunder methods to resolve keys case-insensitively. __getattr__ now uses the same key-scan fallback instead of the broken .lower() retry. --- box/config_box.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/box/config_box.py b/box/config_box.py index 5f8ad55..3715085 100644 --- a/box/config_box.py +++ b/box/config_box.py @@ -20,6 +20,15 @@ class ConfigBox(Box): _protected_keys = dir(Box) + ["bool", "int", "float", "list", "getboolean", "getfloat", "getint"] + @staticmethod + def _case_insensitive_key(keys, item): + """Return a matching key (case-insensitive) from *keys*, or *item* if no match.""" + lower = item.lower() + for k in keys: + if k.lower() == lower: + return k + return item + def __getattr__(self, item): """ Config file keys are stored in lower case, be a little more @@ -28,7 +37,35 @@ def __getattr__(self, item): try: return super().__getattr__(item) except AttributeError: - return super().__getattr__(item.lower()) + # Try case-insensitive match across all keys, not just lowering input + matched = self._case_insensitive_key(self.keys(), item) + if matched != item: + return super().__getattr__(matched) + raise + + def __getitem__(self, item, _ignore_default=False): + try: + return super().__getitem__(item, _ignore_default=_ignore_default) + except KeyError: + matched = self._case_insensitive_key(self.keys(), item) + if matched != item: + return super().__getitem__(matched, _ignore_default=_ignore_default) + raise + + def __contains__(self, item): + if super().__contains__(item): + return True + return self._case_insensitive_key(self.keys(), item) != item + + def __setitem__(self, key, value): + matched = self._case_insensitive_key(self.keys(), key) + if matched != key: + key = matched + super().__setitem__(key, value) + + def __delitem__(self, key): + matched = self._case_insensitive_key(self.keys(), key) + super().__delitem__(matched) def __dir__(self) -> list[str]: return super().__dir__() + ["bool", "int", "float", "list", "getboolean", "getfloat", "getint"]