Repository: dynaconf/dynaconf (python)

Issue:
[bug] Deleting entry raises an error
**Describe the bug**

As part of my tests, I want to temporarily remove a section of my config to ensure my application reacts properly to this section being missing.

I try `del config.some_section` which produces the following error:

```
In [1]: config
Out[1]: <dynaconf.base.LazySettings object at 0x7fe6597e4c50>

In [2]: config.database
Out[2]: <Box: {'host': 'some_host', 'port': 5431, 'user': 'postgres', 'password': 'secret', 'name': 'the_data'}>

In [3]: del config.database
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 del config.database

File ~/dev/prospect/dataworker/.venv/lib/python3.11/site-packages/dynaconf/utils/functional.py:64, in LazyObject.__delattr__(self, name)
     62 if self._wrapped is empty:
     63     self._setup()
---> 64 delattr(self._wrapped, name)

File ~/dev/prospect/dataworker/.venv/lib/python3.11/site-packages/dynaconf/base.py:300, in Settings.__delattr__(self, name)
    298 self._deleted.add(name)
    299 if hasattr(self, name):
--> 300     super().__delattr__(name)

AttributeError: 'Settings' object has no attribute 'database'

In [4]: config.database
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 config.database

File ~/dev/prospect/dataworker/.venv/lib/python3.11/site-packages/dynaconf/base.py:124, in LazySettings.__getattr__(self, name)
    122     self._setup()
    123 if name in self._wrapped._deleted:  # noqa
--> 124     raise AttributeError(
    125         f"Attribute {name} was deleted, " "or belongs to different env"
    126     )
    128 if name not in RESERVED_ATTRS:
    129     lowercase_mode = self._kwargs.get(
    130         "LOWERCASE_READ_FOR_DYNACONF",
    131         default_settings.LOWERCASE_READ_FOR_DYNACONF,
    132     )

AttributeError: Attribute database was deleted, or belongs to different env
```



**To Reproduce**

Try to delete a section of a dynaconf config object. My config object is not special, I create it using the standard method. It's an instance of `dynaconf.base.LazySettings`. The section in question do exist before I try to delete it.

**Expected behavior**

I expect the command to be successful, not throw an error, and subsequent attempts to access this section to fail as if it had never been configured in the first place.

**Environment (please complete the following information):**
 - Dynaconf Version: 3.2.4

**Comments**

It looks that there is some code written to handle deleting a section of the config but

Pull request:
fix: del attr wouldnt work with lowercase
Closes #1042 

Notes:
* The first level is always uppercase internally and calling del with different case attr-name would fail.
* Currently we only support deleting a nested level item with exact matching (not sure if needs "fixing").

Patch:
diff --git a/dynaconf/utils/functional.py b/dynaconf/utils/functional.py
index f633279..ac084cf 100644
--- a/dynaconf/utils/functional.py
+++ b/dynaconf/utils/functional.py
@@ -63,7 +63,7 @@ class LazyObject:
             raise TypeError(f"can't delete {name}.")
         if self._wrapped is empty:
             self._setup()
-        delattr(self._wrapped, name)
+        delattr(self._wrapped, name.upper())
 
     def _setup(self):
         """
diff --git a/dynaconf/vendor/box/box.py b/dynaconf/vendor/box/box.py
index e94aaa6..81ac513 100644
--- a/dynaconf/vendor/box/box.py
+++ b/dynaconf/vendor/box/box.py
@@ -15,6 +15,7 @@ from pathlib import Path
 from typing import Any, Union, Tuple, List, Dict
 
 from dynaconf.vendor import box
+from dynaconf.utils import find_the_correct_casing
 from .converters import (_to_json, _from_json, _from_toml, _to_toml, _from_yaml, _to_yaml, BOX_PARAMETERS)
 from .exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning
 
@@ -402,7 +403,7 @@ class Box(dict):
                     if _camel_killer(key) == each_key:
                         key = each_key
                         break
-        super().__delitem__(key)
+        super().__delitem__(find_the_correct_casing(key, self))
 
     def __delattr__(self, item):
         if self._box_config['frozen_box']:
diff --git a/requirements_dev.txt b/requirements_dev.txt
index e38b505..9c186a7 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -22,7 +22,7 @@ pytest
 pytest-cov
 pytest-mock
 commentjson
-lovely-pytest-docker
+pytest-docker
 tox
 
 # style check


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index d03d12e..1895f65 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -25,6 +25,26 @@ def test_deleted_raise(settings):
     assert settings.exists("TODELETE") is False
     assert settings.get("TODELETE") is None
 
+    # case: del using lowercase when original was mixed-case
+    settings.ToDelete3 = True
+    assert "ToDelete3" in settings
+    assert settings.TODELETE3 is True
+    del settings.todelete3
+    with pytest.raises(AttributeError):
+        assert settings.TODELETE3 is True
+    assert settings.exists("TODELETE3") is False
+    assert settings.get("TODELETE3") is None
+
+    # case: del nested item
+    settings.set("Abc.Xyz.Uv", 123)
+    assert settings.abc.xyz.uv == 123
+    del settings.abc.xyz.uv
+    with pytest.raises(AttributeError):
+        assert settings.abc.xyz.uv == 123
+    assert settings.exists("abc.xyz") is True
+    assert settings.exists("abc.xyz.uv") is False
+    assert settings.get("abc.xyz.uv") is None
+
 
 def test_delete_and_set_again(settings):
     """asserts variable can be deleted and set again"""
diff --git a/tests/test_redis.py b/tests/test_redis.py
index 644b688..56ba2a4 100644
--- a/tests/test_redis.py
+++ b/tests/test_redis.py
@@ -1,6 +1,8 @@
 from __future__ import annotations
 
 import os
+from collections.abc import Iterator
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -10,8 +12,11 @@ from dynaconf.loaders.redis_loader import load
 from dynaconf.loaders.redis_loader import write
 from dynaconf.utils.inspect import get_history
 
+if TYPE_CHECKING:
+    from pytest_docker.plugin import Service
 
-def custom_checker(ip_address, port):
+
+def is_responsive(url):
     # This function should be check if the redis server is online and ready
     # write(settings, {"SECRET": "redis_works"})
     # return load(settings, key="SECRET")
@@ -28,12 +33,12 @@ if DYNACONF_TEST_REDIS_URL:
 else:
 
     @pytest.fixture(scope="module")
-    def docker_redis(docker_services):
-        docker_services.start("redis")
-        public_port = docker_services.wait_for_service(
-            "redis", 6379, check_server=custom_checker
+    def docker_redis(docker_ip: str, docker_services: Iterator[Service]):
+        port = docker_services.port_for("redis", 6379)
+        url = f"http://{docker_ip}:{port}"
+        docker_services.wait_until_responsive(
+            timeout=15.0, pause=0.1, check=lambda: is_responsive(url)
         )
-        url = f"http://{docker_services.docker_ip}:{public_port}"
         return url
 
 
diff --git a/tests/test_vault.py b/tests/test_vault.py
index f764330..6e4beeb 100644
--- a/tests/test_vault.py
+++ b/tests/test_vault.py
@@ -1,7 +1,8 @@
 from __future__ import annotations
 
 import os
-from time import sleep
+from collections.abc import Iterator
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -11,8 +12,11 @@ from dynaconf.loaders.vault_loader import load
 from dynaconf.loaders.vault_loader import write
 from dynaconf.utils.inspect import get_history
 
+if TYPE_CHECKING:
+    from pytest_docker.plugin import Service
 
-def custom_checker(ip_address, port):
+
+def is_responsive(url):
     # This function should be check if the redis server is online and ready
     # write(settings, {"SECRET": "redis_works"})
     # return load(settings, key="SECRET")
@@ -20,14 +24,12 @@ def custom_checker(ip_address, port):
 
 
 @pytest.fixture(scope="module")
-def docker_vault(docker_services):
-    docker_services.start("vault")
-    public_port = docker_services.wait_for_service(
-        "vault", 8200, check_server=custom_checker
+def docker_vault(docker_ip: str, docker_services: Iterator[Service]):
+    public_port = docker_services.port_for("vault", 8200)
+    url = f"http://{docker_ip}:{public_port}"
+    docker_services.wait_until_responsive(
+        timeout=15.0, pause=3, check=lambda: is_responsive(url)
     )
-    url = f"http://{docker_services.docker_ip}:{public_port}"
-
-    sleep(3)
     return url