Repository: dynaconf/dynaconf

Issue:
[bug] Unable to load configuration files using the `*` wildcard
**Describe the bug**
Unable to load yaml configuration file using the `*` wildcard in `setting_files` argument

**To Reproduce**
Steps to reproduce the behavior:

1. Having the following folder structure

<!-- Describe or use the command `$ tree -v` and paste below -->

<details>
<summary> Project structure </summary>

```bash
# project.py
# .config/data_sources.yaml
# settings.toml
```
</details>

2. Having the following config files:

<!-- Please adjust if you are using different files and formats! -->

<details>
<summary> Config files </summary>

**.config/data_sources.yaml**
```yaml
DEFAULT:
  DATA_SOURCES:
    NAME: 1
```

and

**settings.toml**
```toml
[default]
LOG_LEVEL="INFO"
```

</details>

3. Having the following app code:

<details>
<summary> Code </summary>

**project.py**
```python
from dynaconf import Dynaconf

settings = Dynaconf(
  environments=True,
  lowercase_read=False,
  load_dotenv=True,
  settings_files=["settings.toml", ".config/*.yaml"]
)

print(settings.LOG_LEVEL)
print(settings.DATA_SOURCES)
```

</details>

4. Executing under the following environment

<details>
<summary> Execution </summary>

```bash
$ python project.py
```

</details>

**Expected behavior**
When running the script it should first print "INFO" (value for LOG_LEVEL) and then print {"name": 1} (value for DATA_SOURCES as specified in `.config/data_sources.yaml`)
This works as expected when I specify `settings_files=["settings.toml", ".config/data_sources.yaml"]` (without the * wildcard).

**Environment (please complete the following information):**
 - OS: CentOS Linux Release 7.9.2009 (Core)
 - Dynaconf Version: 3.2.0
 - Python version: 3.10.8



Discussion:

Resolving patch:
diff --git a/dynaconf/base.py b/dynaconf/base.py
--- a/dynaconf/base.py
+++ b/dynaconf/base.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import copy
-import glob
 import importlib
 import inspect
 import os
@@ -33,6 +32,7 @@
 from dynaconf.utils import upperfy
 from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.files import find_file
+from dynaconf.utils.files import glob
 from dynaconf.utils.functional import empty
 from dynaconf.utils.functional import LazyObject
 from dynaconf.utils.parse_conf import apply_converter
@@ -1241,11 +1241,13 @@ def load_file(
 
                 paths = [
                     p
-                    for p in sorted(glob.glob(filepath))
+                    for p in sorted(glob(filepath, root_dir=self._root_path))
                     if ".local." not in p
                 ]
                 local_paths = [
-                    p for p in sorted(glob.glob(filepath)) if ".local." in p
+                    p
+                    for p in sorted(glob(filepath, root_dir=self._root_path))
+                    if ".local." in p
                 ]
 
                 # Handle possible *.globs sorted alphanumeric
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -16,6 +16,8 @@
 from dynaconf.utils import ensure_a_list
 from dynaconf.utils.boxing import DynaBox
 from dynaconf.utils.files import get_local_filename
+from dynaconf.utils.files import glob
+from dynaconf.utils.files import has_magic
 from dynaconf.utils.parse_conf import false_values
 
 if TYPE_CHECKING:
@@ -230,10 +232,19 @@ def settings_loader(
     modules_names = []
     for item in files:
         item = str(item)  # Ensure str in case of LocalPath/Path is passed.
-        if item.endswith(ct.ALL_EXTENSIONS + (".py",)):
-            p_root = obj._root_path or (
-                os.path.dirname(found_files[0]) if found_files else None
-            )
+        p_root = obj._root_path or (
+            os.path.dirname(found_files[0]) if found_files else None
+        )
+        if has_magic(item):
+            # handle possible globs inside files list
+            # like ["path/*.yaml", "path/ABC?.yaml"]
+            globedfiles = glob(item, root_dir=p_root)
+            for globedfile in globedfiles:
+                # use object.find_file logic to handle skip files
+                found = obj.find_file(globedfile, project_root=p_root)
+                if found:
+                    found_files.append(found)
+        elif item.endswith(ct.ALL_EXTENSIONS + (".py",)):
             found = obj.find_file(item, project_root=p_root)
             if found:
                 found_files.append(found)
diff --git a/dynaconf/utils/files.py b/dynaconf/utils/files.py
--- a/dynaconf/utils/files.py
+++ b/dynaconf/utils/files.py
@@ -2,6 +2,9 @@
 
 import inspect
 import os
+import re
+import sys
+from glob import glob as python_glob
 
 from dynaconf.utils import deduplicate
 
@@ -109,3 +112,35 @@ def get_local_filename(filename):
     return os.path.join(
         os.path.dirname(str(filename)), f"{name}.local.{extension}"
     )
+
+
+magic_check = re.compile("([*?[])")
+magic_check_bytes = re.compile(b"([*?[])")
+
+
+def has_magic(s):
+    """Taken from python glob module"""
+    if isinstance(s, bytes):
+        match = magic_check_bytes.search(s)
+    else:
+        match = magic_check.search(s)
+    return match is not None
+
+
+def glob(
+    pathname,
+    *,
+    root_dir=None,
+    dir_fd=None,
+    recursive=True,
+    include_hidden=True,
+):
+    """Redefined std glob assuming some defaults.
+    and fallback for diffente python versions."""
+    glob_args = {"recursive": recursive}
+    if sys.version_info >= (3, 10):
+        glob_args["root_dir"] = root_dir
+        glob_args["dir_fd"] = dir_fd
+    if sys.version_info >= (3, 11):
+        glob_args["include_hidden"] = include_hidden
+    return python_glob(pathname, **glob_args)


Test patch:
diff --git a/tests_functional/issues/982_wildcard/.config/data_sources.yaml b/tests_functional/issues/982_wildcard/.config/data_sources.yaml
new file mode 100644
--- /dev/null
+++ b/tests_functional/issues/982_wildcard/.config/data_sources.yaml
@@ -0,0 +1,3 @@
+DEFAULT:
+  DATA_SOURCES:
+    NAME: 1
diff --git a/tests_functional/issues/982_wildcard/app.py b/tests_functional/issues/982_wildcard/app.py
new file mode 100644
--- /dev/null
+++ b/tests_functional/issues/982_wildcard/app.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+import os
+import sys
+
+# If python version is <  3.10 skip test
+# glob on that version of Python doesnt have root_dir param
+if sys.version_info < (3, 10):
+    print("Skip test")
+    sys.exit(0)
+
+from dynaconf import Dynaconf
+
+settings = Dynaconf(
+    environments=True,
+    lowercase_read=False,
+    load_dotenv=True,
+    root_path=os.path.dirname(os.path.abspath(__file__)),
+    settings_files=["settings.toml", ".config/*.yaml"],
+)
+
+assert settings.LOG_LEVEL == "INFO"
+assert settings.DATA_SOURCES == {"NAME": 1}
+print(settings.LOG_LEVEL)
+print(settings.DATA_SOURCES)
diff --git a/tests_functional/issues/982_wildcard/settings.toml b/tests_functional/issues/982_wildcard/settings.toml
new file mode 100644
--- /dev/null
+++ b/tests_functional/issues/982_wildcard/settings.toml
@@ -0,0 +1,2 @@
+[default]
+LOG_LEVEL = "INFO"
diff --git a/tests_functional/skipfile.toml b/tests_functional/skipfile.toml
--- a/tests_functional/skipfile.toml
+++ b/tests_functional/skipfile.toml
@@ -20,4 +20,5 @@ nt = [
     "741_envvars_ignored",
     "missing_pwd",
     "876_deleted_cwd",
+    "982_wildcard",
 ]