Repository: rochacbruno/dynaconf (python)

Issue:
Regression detected [was] Case insensitive access of structures inside lists
**Describe the bug**

When I access a Box that is stored in a BoxList the access becomes case sensitive. I know about DynaBox, but for some reason  the list access returns a vanilla Box and not a DynaBox.

Background: I need to parse more or less complex data from the config (routing stuff) and enrich the data structures with defaults after parsing. Therefore I want to set change the settings from within code.

If something like this is out of scope for Dynaconf, could someone recommend an alternative approach? Maybe only store user provided routing settings and all the other general simple configs like logging level in Dynaconf and manage the routing config elsewhere?

**To Reproduce**
Steps to reproduce the behavior:

1. Run the following code placed in `tmp.py` with pytest `pytest tmp.py`:

```python
from dynaconf.vendor.box import BoxList, Box
from dynaconf.utils.boxing import DynaBox

def test_accessing_dynabox_inside_boxlist_inside_dynabox():
    data = DynaBox({"nested": [{"deeper": "nest"}]})
    assert data.nested[0].deeper == "nest"
    assert data.NESTED[0].deeper == "nest"
    with pytest.raises(BoxKeyError):
        assert data.NESTED[0].DEEPER == "nest"

    data = DynaBox({"nested": [DynaBox({"deeper": "nest"})]})
    assert data.nested[0].deeper == "nest"
    assert data.NESTED[0].deeper == "nest"
    with pytest.raises(BoxKeyError):
        assert data.NESTED[0].DEEPER == "nest"
```

Even though I am passing in a DynaBox it gets changed to a Box

Dynaconf 3.1.2

Pull request:
Fix #462 make DynaBox nested List to use DynaBox as default class
Fix #462

Patch:
diff --git a/dynaconf/vendor/box/box_list.py b/dynaconf/vendor/box/box_list.py
index b3778f8..9c03a67 100644
--- a/dynaconf/vendor/box/box_list.py
+++ b/dynaconf/vendor/box/box_list.py
@@ -7,14 +7,19 @@ _C='strict'
 _B='utf-8'
 _A=None
 import copy,re
-from typing import Iterable
+from typing import Iterable,Optional
 from dynaconf.vendor import box
 from .converters import _to_yaml,_from_yaml,_to_json,_from_json,_to_toml,_from_toml,_to_csv,_from_csv,BOX_PARAMETERS
 from .exceptions import BoxError,BoxTypeError,BoxKeyError
 _list_pos_re=re.compile('\\[(\\d+)\\]')
+DYNABOX_CLASS=_A
+def get_dynabox_class_avoiding_circular_import():
+	global DYNABOX_CLASS
+	if DYNABOX_CLASS is _A:from dynaconf.utils.boxing import DynaBox as A;DYNABOX_CLASS=A
+	return DYNABOX_CLASS
 class BoxList(list):
-	def __init__(A,iterable=_A,box_class=box.Box,**C):
-		B=iterable;A.box_class=box_class;A.box_options=C;A.box_org_ref=A.box_org_ref=id(B)if B else 0
+	def __init__(A,iterable=_A,box_class=_A,**C):
+		B=iterable;A.box_class=box_class or get_dynabox_class_avoiding_circular_import();A.box_options=C;A.box_org_ref=A.box_org_ref=id(B)if B else 0
 		if B:
 			for D in B:A.append(D)
 		if C.get(_E):
diff --git a/dynaconf/vendor/ruamel/yaml/compat.py b/dynaconf/vendor/ruamel/yaml/compat.py
index 661b8d5..0512ad7 100644
--- a/dynaconf/vendor/ruamel/yaml/compat.py
+++ b/dynaconf/vendor/ruamel/yaml/compat.py
@@ -34,7 +34,7 @@ else:
 	def to_unicode(s):return unicode(s)
 if PY3:string_types=str;integer_types=int;class_types=type;text_type=str;binary_type=bytes;MAXSIZE=sys.maxsize;unichr=chr;import io;StringIO=io.StringIO;BytesIO=io.BytesIO;no_limit_int=int;from collections.abc import Hashable,MutableSequence,MutableMapping,Mapping
 else:string_types=basestring;integer_types=int,long;class_types=type,types.ClassType;text_type=unicode;binary_type=str;unichr=unichr;from StringIO import StringIO as _StringIO;StringIO=_StringIO;import cStringIO;BytesIO=cStringIO.StringIO;no_limit_int=long;from collections import Hashable,MutableSequence,MutableMapping,Mapping
-if _B:StreamType=Any;StreamTextType=StreamType;VersionType=Union[(List[int],str,Tuple[(int,int)])]
+if _B:StreamType=Any;StreamTextType=StreamType;VersionType=Union[List[int],str,Tuple[int,int]]
 if PY3:builtins_module='builtins'
 else:builtins_module='__builtin__'
 UNICODE_SIZE=4 if sys.maxunicode>65535 else 2
diff --git a/dynaconf/vendor/ruamel/yaml/resolver.py b/dynaconf/vendor/ruamel/yaml/resolver.py
index 92b1836..7377ca5 100644
--- a/dynaconf/vendor/ruamel/yaml/resolver.py
+++ b/dynaconf/vendor/ruamel/yaml/resolver.py
@@ -61,7 +61,7 @@ class BaseResolver:
 		elif kind is list:kind=SequenceNode
 		elif kind is dict:kind=MappingNode
 		elif kind not in[ScalarNode,SequenceNode,MappingNode]and kind is not _A:raise ResolverError('Invalid node kind: %s'%(kind,))
-		cls.yaml_path_resolvers[(tuple(new_path),kind)]=tag
+		cls.yaml_path_resolvers[tuple(new_path),kind]=tag
 	def descend_resolver(self,current_node,current_index):
 		if not self.yaml_path_resolvers:return
 		exact_paths={};prefix_paths=[]
@@ -70,10 +70,10 @@ class BaseResolver:
 			for (path,kind) in self.resolver_prefix_paths[-1]:
 				if self.check_resolver_prefix(depth,path,kind,current_node,current_index):
 					if len(path)>depth:prefix_paths.append((path,kind))
-					else:exact_paths[kind]=self.yaml_path_resolvers[(path,kind)]
+					else:exact_paths[kind]=self.yaml_path_resolvers[path,kind]
 		else:
 			for (path,kind) in self.yaml_path_resolvers:
-				if not path:exact_paths[kind]=self.yaml_path_resolvers[(path,kind)]
+				if not path:exact_paths[kind]=self.yaml_path_resolvers[path,kind]
 				else:prefix_paths.append((path,kind))
 		self.resolver_exact_paths.append(exact_paths);self.resolver_prefix_paths.append(prefix_paths)
 	def ascend_resolver(self):
diff --git a/dynaconf/vendor_src/box/box_list.py b/dynaconf/vendor_src/box/box_list.py
index 1af182a..8687c40 100644
--- a/dynaconf/vendor_src/box/box_list.py
+++ b/dynaconf/vendor_src/box/box_list.py
@@ -4,7 +4,7 @@
 # Copyright (c) 2017-2020 - Chris Griffith - MIT License
 import copy
 import re
-from typing import Iterable
+from typing import Iterable, Optional
 
 
 from dynaconf.vendor import box
@@ -15,14 +15,28 @@ from .exceptions import BoxError, BoxTypeError, BoxKeyError
 _list_pos_re = re.compile(r'\[(\d+)\]')
 
 
+DYNABOX_CLASS = None  # a cache constant to avoid multiple imports
+
+
+def get_dynabox_class_avoiding_circular_import():
+    """
+    See dynaconf issue #462
+    """
+    global DYNABOX_CLASS
+    if DYNABOX_CLASS is None:
+        from dynaconf.utils.boxing import DynaBox
+        DYNABOX_CLASS = DynaBox
+    return DYNABOX_CLASS
+
+
 class BoxList(list):
     """
     Drop in replacement of list, that converts added objects to Box or BoxList
     objects as necessary.
     """
 
-    def __init__(self, iterable: Iterable = None, box_class: box.Box = box.Box, **box_options):
-        self.box_class = box_class
+    def __init__(self, iterable: Iterable = None, box_class : Optional[box.Box] = None, **box_options):
+        self.box_class = box_class or get_dynabox_class_avoiding_circular_import()
         self.box_options = box_options
         self.box_org_ref = self.box_org_ref = id(iterable) if iterable else 0
         if iterable:


Test patch:
diff --git a/tests/test_dynabox.py b/tests/test_dynabox.py
index 6d8ae1c..7b183ce 100644
--- a/tests/test_dynabox.py
+++ b/tests/test_dynabox.py
@@ -1,4 +1,9 @@
+import pytest
+
 from dynaconf.utils.boxing import DynaBox
+from dynaconf.vendor.box import Box
+from dynaconf.vendor.box import BoxKeyError
+from dynaconf.vendor.box import BoxList
 
 
 box = DynaBox(
@@ -73,3 +78,17 @@ def test_get():
 def test_copy_no_cause_inf_recursion():
     box.__copy__()
     box.copy()
+
+
+def test_accessing_dynabox_inside_boxlist_inside_dynabox():
+    data = DynaBox({"nested": [{"deeper": "nest"}]})
+    assert data.nested[0].deeper == "nest"
+    assert data.NESTED[0].deeper == "nest"
+    assert data.NESTED[0].DEEPER == "nest"
+
+    data = DynaBox({"nested": BoxList([DynaBox({"deeper": "nest"})])})
+    assert data.nested[0].deeper == "nest"
+    assert data.NESTED[0].deeper == "nest"
+    assert isinstance(data.NESTED, BoxList)
+    assert isinstance(data.NESTED[0], DynaBox)
+    assert data.NESTED[0].DEEPER == "nest"