Repository: dynaconf/dynaconf (python)

Issue:
feat: populate_obj must convert to dict before setting.
This must be implemented on master and backported to 3.2.8

Current behavior

```py
settings = Dynaconf()
class Anything: ...
obj = Anything()


settings.set("data", {"key: "value"})
settings.populate_obj(obj)

# now when accessing the type of data wil be Box
print(type(obj.DATA))
dynaconf.utils.boxing.DynaBox
```

Expected feature

```py
settings = Dynaconf()
class Anything: ...
obj = Anything()


settings.set("data", {"key: "value"})
settings.populate_obj(obj, raw_dict=True)

# THEN
# populate_obj with `raw_dict` will call `self.to_dict` that 
# will take data out of the box, before doing the assignments.
# 

# now when accessing the type of data wil be Box
print(type(obj.DATA))
dict
```

This is specially needed in the case of interior mutability

Pull request description:
feat: populate_obj takes convert_to_dict [port from 3.2.8]
Defaults to false, when set to true makes the
populated keys to not be Box values but Python types.

port of #1237
closes #1227

Patch:
diff --git a/docs/advanced.md b/docs/advanced.md
index e90e132..8192f8e 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -593,6 +593,15 @@ assert obj.VALUE == 42.1  # AttributeError
 
 ```
 
+**new in 3.2.7**
+- `populate_obj` will take `internals` boolean to enable or disable populating internal settings.
+
+
+**new in 3.2.8**
+
+- `populate_obj` will take `convert_to_dict` boolean to enable or disable converting the settings object to a dict before populating the object, this makes the data structures like Box and BoxList to be converted to the raw dict/list before populating the object.
+
+
 ## Exporting
 
 You can generate a file with current configs by calling `dynaconf list -o /path/to/file.ext` see more in [cli](cli.md)
@@ -605,7 +614,7 @@ from dynaconf import settings
 from dynaconf.utils.boxing import DynaBox
 
 # generates a dict with all the keys for `development` env
-data = settings.as_dict(env='development')
+data = settings.to_dict(env='development')
 
 # writes to a file, the format is inferred by extension
 # can be .yaml, .toml, .ini, .json, .py
diff --git a/dynaconf/base.py b/dynaconf/base.py
index 759cf42..fa8260a 100644
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -1469,24 +1469,30 @@ class Settings:
         merge=False,
         merge_unique=False,
         internal=False,
+        convert_to_dict=False,
     ):
         """Given the `obj` populate it using self.store items.
 
         :param obj: An object to be populated, a class instance.
         :param keys: A list of keys to be included.
         :param ignore: A list of keys to be excluded.
+        :param merge: Merge values with values existing in object.
+        :param merge_unique: Merge without duplication.
+        :param internal: Include internal keys.
+        :param convert_to_dict: Convert the settings to a pure dict (no Box)
+         before populating.
         """
         merge = merge or self.get("MERGE_ENABLED_FOR_DYNACONF")
+        data = self.to_dict(internal=internal) if convert_to_dict else self
         keys = keys or self.keys()
         for key in keys:
+            key = upperfy(key)
             if not internal:
                 if key in UPPER_DEFAULT_SETTINGS:
                     continue
-            key = upperfy(key)
             if ignore and key in ignore:
                 continue
-            value = self.get(key, empty)
-
+            value = data.get(key, empty)
             if isinstance(value, (list, dict)):
                 if isinstance(value, dict):
                     if "dynaconf_merge" in value:
@@ -1500,7 +1506,6 @@ class Settings:
                         merge = merge_unique = True
                 if merge and (existing := getattr(obj, key, None)):
                     value = object_merge(existing, value, merge_unique)
-
             if value is not empty:
                 setattr(obj, key, value)
 


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 7fa263b..726ae8a 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -11,6 +11,7 @@ from dynaconf import Validator
 from dynaconf.loaders import toml_loader
 from dynaconf.loaders import yaml_loader
 from dynaconf.strategies.filtering import PrefixFilter
+from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.parse_conf import true_values
 from dynaconf.vendor.box.box_list import BoxList
 
@@ -140,6 +141,26 @@ def test_populate_obj_with_ignore(settings):
         assert obj.VALUE == 42.1
 
 
+def test_populate_obj_convert_to_dict(settings):
+    class Obj:
+        pass
+
+    # first make sure regular populate brings in Box and BoxList
+    obj = Obj()
+    settings.populate_obj(obj)
+    assert isinstance(obj.ADICT, DynaBox)
+    assert isinstance(obj.ALIST, BoxList)
+    assert isinstance(obj.ADICT.to_yaml(), str)
+
+    # now make sure convert_to_dict=True brings in dict and list
+    obj = Obj()
+    settings.populate_obj(obj, convert_to_dict=True)
+    assert isinstance(obj.ADICT, dict)
+    assert isinstance(obj.ALIST, list)
+    with pytest.raises(AttributeError):
+        assert isinstance(obj.ADICT.to_yaml(), str)
+
+
 def test_call_works_as_get(settings):
     """settings.get('name') is the same as settings('name')"""