Repository: dynaconf/dynaconf
Pull request #1251 (2025-02-13 19:49:11)

feat: Add CLI command `debug-info`
To be ported to main

Related #1234 

### dynaconf debug-info

Prints debug information about the current settings instance

```console
$ dynaconf debug-info --help
Usage: dynaconf debug-info [OPTIONS]

  Prints debug information about the settings instance.

Options:
  -k, --key TEXT                  Filters result by key.
  -v, --verbose                   Increase verbosity of the output.
  -f, --format [yaml|json|json-compact]
                                  The output format.
  --help                          Show this message and exit.
```

The default format is JSON, and the verbosity levels are

- 0 (default): Include only the count of keys loaded by each loader
- 1: Include the keys loaded by each loader
- 2: Include the keys loaded by each loader and the values of each key


Sample output in YAML format:

**TIP**: adding `-v` will list the keys and `-vv` will list the keys and values. 

```yaml
# dynaconf debug-info --format=yaml
versions:
  dynaconf: 3.2.8
  django: 4.2.16
root_path: /app/settings
validators: ["Validator("FOO", must_exist=True, eq="bar")"]
core_loaders: ["dynaconf.loaders.env_loader"]
loaded_files:
- /app/settings/defaults.py
- /app/settings/development_defaults.py
- /etc/app/settings.yaml
history:
- loader: set_method
  identifier: init_kwargs
  data: 13
- loader: set_method
  identifier: default_settings
  data: 56
- loader: settings_loader
  identifier: /app/settings/defaults
  data: 256
- loader: py
  identifier: /app/settings/development_defaults
  data: 16
- loader: set_method
  identifier: lambda_1933577857047901425@instance
  data: 2
- loader: standard_settings_loader
  identifier: /etc/app/settings.yaml
  data: 1
post_hooks:
- <function <lambda> at 0x716a7b89c900>
loaded_hooks:
- hook: lambda_1933577857047901425@instance
  data: 1
environments:
- development
- production
loaded_envs: ['development']
```


Patch:
diff --git a/docs/advanced.md b/docs/advanced.md
index ee1dd61..0d0b171 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -221,6 +221,44 @@ Kwargs-only:
 
 Returns a list of history-records (the same as in the `history` key of `inspect_settings` records. In fact, `inspect_settings` uses `get_history`, so this is just available if your main goal is to use the data directly. It offer some basic filtering capabilities, but it is assumed that if you choose to use this you'll probably want to process and filter the data by your own.
 
+
+### `get_debug_info`
+
+Returns a dictionary containing debug information about the settings object. This information includes the current environment, the settings file, the loaders, and the history of the settings object.
+
+```python
+from dynaconf.utils.inspect_settings import get_debug_info
+get_debug_info(settings.DYNACONF, verbosity=0, key="data")
+# result
+{'core_loaders': [],
+ 'environments': ['development', 'production'],
+ 'history': [{'data': {}, 'identifier': 'init_kwargs', 'loader': 'set_method'},
+             {'data': {},
+              'identifier': 'default_settings',
+              'loader': 'set_method'},
+             {'data': {},
+              'identifier': 'envvars_first_load',
+              'loader': 'set_method'},
+             {'data': {},
+              'identifier': 'settings_module_method',
+              'loader': 'set_method'},
+             {'data': {'data': [0]},
+              'identifier': '/etc/app/defaults.py',
+              'loader': 'py'},
+             {'data': {'data': '@merge 7,8,9'},
+              'identifier': '/etc/app/settings.yaml',
+              'loader': 'yaml'},
+ 'loaded_envs': ["development"],
+ 'loaded_files': ['/etc/app/defaults.py',
+                  '/etc/app/settings.yaml'],
+ 'loaded_hooks': [],
+ 'post_hooks': [],
+ 'root_path': '/run/app',
+ 'validators': [],
+ 'versions': {'django': '4.2.16', 'dynaconf': '3.2.8.dev0'}}
+```
+
+
 ## Update dynaconf settings using command-line arguments (cli)
 
 Dynaconf is designed to load the settings file and, when applicable, prioritize overrides from envvar.
diff --git a/docs/cli.md b/docs/cli.md
index 8cdcefe..83d77bc 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -124,16 +124,26 @@ Usage: dynaconf inspect [OPTIONS]
 
 Options:
   -k, --key TEXT                  Filters result by key.
-  -e, --env TEXT                  Filters result by environment.
+  -e, --env TEXT                  Filters result by environment on --report-
+                                  mode=inspect.
+
   -f, --format [yaml|json|json-compact]
                                   The output format.
-  -s, --old-first                 Invert history sorting to 'old-first'
-  -n, --limit INTEGER             Limits how many history entries are shown.
+  -s, --old-first                 Invert history sorting to 'old-first' on
+                                  --report-mode=inspect.
+
+  -n, --limit INTEGER             Limits how many history entries are shown on
+                                  --report-mode=inspect.
+
   -a, --all                       Show dynaconf internal settings?
+  -m, --report-mode [inspect|debug]
+  -v, --verbose                   Increase verbosity of the output on
+                                  --report-mode=debug.
+
   --help                          Show this message and exit.
+                    Show this message and exit.
 ```
 
-
 Sample usage:
 
 ```yaml
@@ -167,6 +177,72 @@ To save to a file, use regular stream redirect methods:
 $ dynaconf -i app.settings inspect -k foo -f yaml > dump.yaml
 ```
 
+#### Inspect Debug Info
+
+> **NEW** in version 3.2.8
+
+Prints debug information about the current settings instance
+
+```bash
+dynaconf inspect -m debug -vv
+```
+
+Verbosity levels are
+
+- 0 (default): Include only the count of keys loaded by each loader
+- 1: Include the keys loaded by each loader
+- 2: Include the keys loaded by each loader and the values of each key
+
+
+Debug mode also accepts the `-k key` to filter the output by a specific key.
+
+
+Sample output in YAML format:
+
+**TIP**: adding `-v` will list the keys and `-vv` will list the keys and values. 
+
+```yaml
+# dynaconf inspect -m debug --format=yaml
+versions:
+  dynaconf: 3.2.8
+  django: 4.2.16
+root_path: /app/settings
+validators: ["Validator("FOO", must_exist=True, eq="bar")"]
+core_loaders: ["dynaconf.loaders.env_loader"]
+loaded_files:
+- /app/settings/defaults.py
+- /app/settings/development_defaults.py
+- /etc/app/settings.yaml
+history:
+- loader: set_method
+  identifier: init_kwargs
+  data: 13
+- loader: set_method
+  identifier: default_settings
+  data: 56
+- loader: settings_loader
+  identifier: /app/settings/defaults
+  data: 256
+- loader: py
+  identifier: /app/settings/development_defaults
+  data: 16
+- loader: set_method
+  identifier: lambda_1933577857047901425@instance
+  data: 2
+- loader: standard_settings_loader
+  identifier: /etc/app/settings.yaml
+  data: 1
+post_hooks:
+- <function <lambda> at 0x716a7b89c900>
+loaded_hooks:
+- hook: lambda_1933577857047901425@instance
+  data: 1
+environments:
+- development
+- production
+loaded_envs: ['development']
+```
+
 ### Dynaconf get
 
 Get raw value for a single key
diff --git a/dynaconf/cli.py b/dynaconf/cli.py
index feaf853..df38299 100644
--- a/dynaconf/cli.py
+++ b/dynaconf/cli.py
@@ -28,6 +28,7 @@ from dynaconf.utils.inspect import EnvNotFoundError
 from dynaconf.utils.inspect import inspect_settings
 from dynaconf.utils.inspect import KeyNotFoundError
 from dynaconf.utils.inspect import OutputFormatError
+from dynaconf.utils.inspect import print_debug_info
 from dynaconf.utils.parse_conf import parse_conf_data
 from dynaconf.utils.parse_conf import unparse_conf_data
 from dynaconf.validator import ValidationError
@@ -54,7 +55,12 @@ def set_settings(ctx, instance=None):
 
     settings = None
 
-    _echo_enabled = ctx.invoked_subcommand not in ["get", "inspect", None]
+    _echo_enabled = ctx.invoked_subcommand not in [
+        "get",
+        "inspect",
+        "debug-info",
+        None,
+    ]
     if "--json" in click.get_os_args():
         _echo_enabled = False
 
@@ -857,7 +863,10 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
 @main.command()
 @click.option("--key", "-k", help="Filters result by key.")
 @click.option(
-    "--env", "-e", help="Filters result by environment.", default=None
+    "--env",
+    "-e",
+    help="Filters result by environment on --report-mode=inspect.",
+    default=None,
 )
 @click.option(
     "--format",
@@ -870,7 +879,7 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
     "--old-first",
     "new_first",
     "-s",
-    help="Invert history sorting to 'old-first'",
+    help="Invert history sorting to 'old-first' on --report-mode=inspect.",
     default=True,
     is_flag=True,
 )
@@ -880,7 +889,7 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
     "-n",
     default=None,
     type=int,
-    help="Limits how many history entries are shown.",
+    help="Limits how many history entries are shown on --report-mode=inspect.",
 )
 @click.option(
     "--all",
@@ -890,28 +899,54 @@ INSPECT_FORMATS = list(builtin_dumpers.keys())
     is_flag=True,
     help="Show dynaconf internal settings?",
 )
+@click.option(
+    "--report-mode",
+    "report_mode",
+    "-m",
+    default="inspect",
+    type=click.Choice(["inspect", "debug"]),
+)
+@click.option(
+    "-v",
+    "--verbose",
+    count=True,
+    help="Increase verbosity of the output on --report-mode=debug.",
+)
 def inspect(
-    key, env, format, new_first, history_limit, _all
+    key, env, format, new_first, history_limit, _all, report_mode, verbose
 ):  # pragma: no cover
     """
     Inspect the loading history of the given settings instance.
 
     Filters by key and environment, otherwise shows all.
     """
-    try:
-        inspect_settings(
+
+    if report_mode == "debug":
+        print_debug_info(
             settings,
-            key=key,
-            env=env or None,
             dumper=format,
-            new_first=new_first,
-            include_internal=_all,
-            history_limit=history_limit,
-            print_report=True,
+            verbosity=verbose,
+            key=key,
         )
         click.echo()
-    except (KeyNotFoundError, EnvNotFoundError, OutputFormatError) as err:
-        click.echo(err)
+    elif report_mode == "inspect":
+        try:
+            inspect_settings(
+                settings,
+                key=key,
+                env=env or None,
+                dumper=format,
+                new_first=new_first,
+                include_internal=_all,
+                history_limit=history_limit,
+                print_report=True,
+            )
+            click.echo()
+        except (KeyNotFoundError, EnvNotFoundError, OutputFormatError) as err:
+            click.echo(err)
+            sys.exit(1)
+    else:
+        click.echo("Invalid report mode")
         sys.exit(1)
 
 
diff --git a/dynaconf/loaders/__init__.py b/dynaconf/loaders/__init__.py
index 9b53fe6..1879cf8 100644
--- a/dynaconf/loaders/__init__.py
+++ b/dynaconf/loaders/__init__.py
@@ -147,6 +147,22 @@ def execute_module_hooks(
 execute_hooks = execute_module_hooks
 
 
+def _get_unique_hook_id(hook_func, hook_source):
+    """get unique identifier for a hook function.
+    in most of cases this will be the function name@source_file
+    however, if the function is a lambda, it will be a hash of the code object.
+    because lambda functions are not hashable itself and we can't rely on its id.
+    """
+    hook_unique_id = hook_func.__name__
+    if hook_unique_id == "<lambda>":
+        frame_info = getattr(hook_func, "__code__", None)
+        if frame_info:
+            hook_unique_id = f"lambda_{hash(frame_info.co_code)}"
+        else:
+            hook_unique_id = f"lambda_{id(hook_func)}"
+    return f"{hook_unique_id}@{hook_source}"
+
+
 def _run_hook_module(hook_type, hook_module, obj, key=""):
     """
     Run a hook function from hook_module.
@@ -154,11 +170,6 @@ def _run_hook_module(hook_type, hook_module, obj, key=""):
     Given a @hook_type, a @hook_module and a settings @obj, load the function
     and execute it if found.
     """
-    hook_source = hook_module.__file__
-
-    # check if already loaded
-    if hook_type in obj._loaded_hooks.get(hook_source, {}):
-        return
 
     # check errors
     if hook_module and getattr(hook_module, "_error", False):
@@ -166,9 +177,12 @@ def _run_hook_module(hook_type, hook_module, obj, key=""):
             raise hook_module._error
 
     # execute hook
+    hook_source = hook_module.__file__
     hook_func = getattr(hook_module, hook_type, None)
     if hook_func:
-        _run_hook_function(obj, hook_type, hook_func, hook_source, key)
+        identifier = _get_unique_hook_id(hook_func, hook_source)
+        if hook_type not in obj._loaded_hooks.get(identifier, {}):
+            _run_hook_function(obj, hook_type, hook_func, hook_source, key)
 
 
 def _run_hook_function(
@@ -201,9 +215,10 @@ def _run_hook_function(
         # and documentation warns about this behavior.
         hook_func._called = True
 
-    # update obj settings
+    identifier = _get_unique_hook_id(hook_func, hook_source)
+
     if hook_dict:
-        identifier = f"{hook_func.__name__}@{hook_source}"
+        # update obj settings
         merge = hook_dict.pop(
             "dynaconf_merge", hook_dict.pop("DYNACONF_MERGE", False)
         )
@@ -224,7 +239,7 @@ def _run_hook_function(
             )
 
     # add to registry
-    obj._loaded_hooks[hook_source][hook_type] = hook_dict
+    obj._loaded_hooks[identifier][hook_type] = hook_dict
 
 
 def settings_loader(
diff --git a/dynaconf/utils/inspect.py b/dynaconf/utils/inspect.py
index fdf77f7..18cc5dd 100644
--- a/dynaconf/utils/inspect.py
+++ b/dynaconf/utils/inspect.py
@@ -4,7 +4,10 @@ from __future__ import annotations
 
 import json
 import sys
+from contextlib import suppress
 from functools import partial
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version
 from pathlib import PosixPath
 from typing import Any
 from typing import Callable
@@ -28,11 +31,21 @@ if TYPE_CHECKING:  # pragma: no cover
 
 # Dumpers config
 
-json_pretty = partial(json.dump, indent=2)
-json_compact = json.dump
+json_pretty = partial(json.dump, indent=2, default=str)
+json_compact = partial(json.dump, default=str)
+
+
+def yaml_dumper_with_defaults(data: dict, text_stream: TextIO) -> None:
+    """Easier way to get YAML dumper to handle unseralizable types"""
+    yaml = YAML()
+    # let JSON handle unserializable types and then load it back
+    data = json.loads(json.dumps(data, default=str))
+    yaml.default_flow_style = False
+    yaml.dump(data, text_stream)
+
 
 builtin_dumpers = {
-    "yaml": YAML().dump,
+    "yaml": yaml_dumper_with_defaults,
     "json": json_pretty,
     "json-compact": json_compact,
 }
@@ -312,7 +325,7 @@ def _get_data_by_key(
     key_dotted_path: str,
     default: Any = None,
     sep="__",
-):
+) -> Any:
     """
     Returns value found in data[key] using dot-path str (e.g, "path.to.key").
     Raises KeyError if not found
@@ -344,3 +357,127 @@ def _get_data_by_key(
         if not default:
             raise KeyError(f"Path not found in data: {key_dotted_path!r}")
         return default
+
+
+def get_debug_info(
+    settings: Settings | LazySettings,
+    verbosity: int = 0,
+    key: str | None = None,
+) -> dict:
+    """Returns a dict with debug info about the settings object"""
+
+    if key:
+        verbosity = 2
+
+    def filter_by_key(data: dict) -> dict:
+        """If key is not None, filter dict keeping only the key"""
+        if key:
+            try:
+                return {key: _get_data_by_key(data, key)}
+            except KeyError:
+                return {}
+        return data
+
+    def build_loading_history() -> list[dict]:
+        _data = []
+        for (
+            source_metadata,
+            source_data,
+        ) in settings._loaded_by_loaders.items():
+            _real_data = filter_by_key(source_data)
+            if verbosity == 0:
+                _data.append(
+                    {
+                        "loader": source_metadata.loader,
+                        "identifier": source_metadata.identifier,
+                        "data": len(_real_data),
+                    }
+                )
+            elif verbosity == 1:
+                _data.append(
+                    {
+                        "loader": source_metadata.loader,
+                        "identifier": source_metadata.identifier,
+                        "data": list(_real_data.keys()),
+                    }
+                )
+            else:
+                _data.append(
+                    {
+                        "loader": source_metadata.loader,
+                        "identifier": source_metadata.identifier,
+                        "data": _real_data,
+                    }
+                )
+        return _data
+
+    def build_loaded_hooks():
+        _data = []
+        for hook, hook_data in settings._loaded_hooks.items():
+            _real_data = filter_by_key(hook_data.get("post", {}))
+            if verbosity == 0:
+                _data.append(
+                    {
+                        "hook": str(hook),
+                        "data": len(_real_data),
+                    }
+                )
+            elif verbosity == 1:
+                _data.append(
+                    {
+                        "hook": str(hook),
+                        "data": list(_real_data.keys()),
+                    }
+                )
+            else:
+                _data.append(
+                    {
+                        "hook": str(hook),
+                        "data": _real_data,
+                    }
+                )
+        return _data
+
+    data = {
+        "versions": {
+            "dynaconf": version("dynaconf"),
+        },
+        "root_path": settings._root_path,
+        "validators": [str(v) for v in settings.validators],
+        "core_loaders": settings._loaders,
+        "loaded_files": settings._loaded_files,
+        "history": build_loading_history(),
+        "post_hooks": [str(h) for h in settings._post_hooks],
+        "loaded_hooks": build_loaded_hooks(),
+    }
+    for name in ["django", "flask", "fastapi", "starlette"]:
+        with suppress(PackageNotFoundError):
+            data["versions"][name] = version(name)
+
+    if settings.get("ENVIRONMENTS_FOR_DYNACONF"):
+        data["environments"] = list(settings.get("ENVIRONMENTS_FOR_DYNACONF"))
+        data["loaded_envs"] = settings._loaded_envs
+
+    if key:
+        data["current"] = {key: settings.get(key)}
+    return data
+
+
+def print_debug_info(
+    settings: Settings | LazySettings,
+    *,
+    dumper: DumperPreset | DumperType | None = None,
+    verbosity: int = 0,
+    key: str | None = None,
+):
+    """Calls dumper with settings debug info"""
+    dumper = dumper or "yaml"
+    if isinstance(dumper, str):
+        dumper = builtin_dumpers.get(dumper)
+        if dumper is None:
+            raise OutputFormatError(
+                f"The desired format is not available: {dumper!r}"
+            )
+
+    data = get_debug_info(settings, verbosity, key)
+    dumper(data, sys.stdout)


Test patch:
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
index 6b9abe8..6b9c01a 100644
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -14,6 +14,7 @@ from dynaconf import Dynaconf
 from dynaconf.utils.inspect import _ensure_serializable
 from dynaconf.utils.inspect import _get_data_by_key
 from dynaconf.utils.inspect import EnvNotFoundError
+from dynaconf.utils.inspect import get_debug_info
 from dynaconf.utils.inspect import get_history
 from dynaconf.utils.inspect import inspect_settings
 from dynaconf.utils.inspect import KeyNotFoundError
@@ -853,3 +854,156 @@ def test_inspect_exception_invalid_format():
     settings = Dynaconf()
     with pytest.raises(OutputFormatError):
         inspect_settings(settings, dumper="invalid_format")
+
+
+def test_get_debug_info(tmp_path):
+    file_a = tmp_path / "a.yml"
+    file_b = tmp_path / "b.yml"
+    file_c = tmp_path / "c.py"
+    create_file(
+        file_a,
+        """\
+        dicty:
+          a: A
+          b:
+            - 1
+            - c: C
+              d: D
+        """,
+    )
+    create_file(
+        file_b,
+        """\
+        listy:
+          - 1
+          - a: A
+            b: B
+            c:
+              - 1
+              - 2
+        """,
+    )
+    create_file(
+        file_c,
+        """\
+        from dynaconf import post_hook
+
+        FRUIT = "tomato"
+
+        @post_hook
+        def set_name_to_foo2(settings):
+            return {"name": "foo2"}
+        """,
+    )
+
+    def set_name_to_foo(settings):
+        return {"name": "foo"}
+
+    settings = Dynaconf(
+        settings_file=[file_a, file_b],
+        post_hooks=[
+            set_name_to_foo,
+            lambda settings: {"bar": "baz"},
+        ],
+        hello="world",
+        validators=[Validator("name", required=True)],
+    )
+
+    settings.load_file(file_c)
+
+    debug_info = get_debug_info(settings)
+    assert "dynaconf" in debug_info["versions"]
+    assert debug_info["root_path"] == str(tmp_path)
+    assert len(debug_info["validators"]) == 1
+    assert len(debug_info["post_hooks"]) == 3
+    assert len(debug_info["loaded_hooks"]) == 3
+    assert len(debug_info["loaded_files"]) == 3
+    assert len(debug_info["history"]) == 11
+
+    # Now including keys
+    debug_info = get_debug_info(settings, verbosity=1)
+    assert debug_info["history"][0]["data"] == [
+        "POST_HOOKS",
+        "HELLO",
+        "SETTINGS_FILE_FOR_DYNACONF",
+    ]
+    assert debug_info["history"][0]["identifier"] == "init_kwargs"
+    assert len(debug_info["history"][1]["data"]) > 1
+    assert debug_info["history"][1]["identifier"] == "default_settings"
+    assert debug_info["history"][2]["data"] == ["SETTINGS_MODULE"]
+    assert debug_info["history"][2]["identifier"] == "settings_module_method"
+    assert debug_info["history"][3]["data"] == ["DICTY"]
+    assert debug_info["history"][3]["identifier"] == str(file_a)
+    assert debug_info["history"][4]["data"] == ["LISTY"]
+    assert debug_info["history"][4]["identifier"] == str(file_b)
+    assert debug_info["history"][5]["data"] == ["NAME"]
+    assert debug_info["history"][5]["identifier"] == "set_name_to_foo@instance"
+    assert debug_info["history"][6]["data"] == ["BAR"]
+    assert "lambda_" in debug_info["history"][6]["identifier"]
+    assert debug_info["history"][9]["data"] == ["FRUIT"]
+
+    c_identifier = str(file_c).rstrip(".py")
+
+    assert debug_info["history"][9]["identifier"] == c_identifier
+    assert debug_info["history"][10]["data"] == ["NAME"]
+    assert (
+        debug_info["history"][10]["identifier"]
+        == f"set_name_to_foo2@{c_identifier}"
+    )
+    assert debug_info["loaded_files"] == [
+        str(file_a),
+        str(file_b),
+        str(file_c),
+    ]
+    assert debug_info["loaded_hooks"][0]["data"] == ["name"]
+    assert debug_info["loaded_hooks"][0]["hook"] == "set_name_to_foo@instance"
+    assert debug_info["loaded_hooks"][1]["data"] == ["bar"]
+    assert "lambda_" in debug_info["loaded_hooks"][1]["hook"]
+    assert debug_info["loaded_hooks"][2]["data"] == ["name"]
+    assert (
+        debug_info["loaded_hooks"][2]["hook"]
+        == f"set_name_to_foo2@{c_identifier}"
+    )
+    assert "set_name_to_foo" in debug_info["post_hooks"][0]
+    assert "lambda" in debug_info["post_hooks"][1]
+    assert "set_name_to_foo2" in debug_info["post_hooks"][2]
+
+    # Now including keys and values
+    debug_info = get_debug_info(settings, verbosity=2)
+    assert debug_info["history"][0]["data"]["HELLO"] == "world"
+    assert debug_info["history"][3]["data"]["DICTY"] == {
+        "a": "A",
+        "b": [1, {"c": "C", "d": "D"}],
+    }
+    assert debug_info["history"][4]["data"]["LISTY"] == [
+        1,
+        {"a": "A", "b": "B", "c": [1, 2]},
+    ]
+    assert debug_info["history"][5]["data"]["NAME"] == "foo"
+    assert debug_info["history"][6]["data"]["BAR"] == "baz"
+    assert debug_info["history"][9]["data"]["FRUIT"] == "tomato"
+    assert debug_info["history"][10]["data"]["NAME"] == "foo2"
+    assert debug_info["loaded_hooks"][0]["data"]["name"] == "foo"
+    assert debug_info["loaded_hooks"][1]["data"]["bar"] == "baz"
+    assert debug_info["loaded_hooks"][2]["data"]["name"] == "foo2"
+
+    # Now passing a specific key
+    debug_info = get_debug_info(settings, key="DICTY")
+    # assert the passed key is present on its own entry
+    # assert other keys are not present
+    assert debug_info["history"][0]["data"] == {}
+    assert debug_info["history"][1]["data"] == {}
+    assert debug_info["history"][2]["data"] == {}
+    assert debug_info["history"][3]["data"]["DICTY"] == {
+        "a": "A",
+        "b": [1, {"c": "C", "d": "D"}],
+    }
+    assert debug_info["history"][4]["data"] == {}
+    assert debug_info["history"][5]["data"] == {}
+    assert debug_info["history"][6]["data"] == {}
+    assert debug_info["history"][7]["data"] == {}
+    assert debug_info["history"][8]["data"] == {}
+    assert debug_info["history"][9]["data"] == {}
+    assert debug_info["history"][10]["data"] == {}
+    assert debug_info["loaded_hooks"][0]["data"] == {}
+    assert debug_info["loaded_hooks"][1]["data"] == {}
diff --git a/tests/test_py_loader.py b/tests/test_py_loader.py
index 82a6b3b..4778d3a 100644
--- a/tests/test_py_loader.py
+++ b/tests/test_py_loader.py
@@ -256,7 +256,9 @@ def test_post_load_hooks(clean_env, tmpdir):
     assert settings.DATABASES.default.VERSION == 42
     assert settings.DATABASES.default.FORCED_INT == 12
     assert settings.BANDS == ["Rush", "Yes", "Anathema"]
-    assert settings._loaded_hooks[str(plugin_hook)] == {
+
+    plugin_hook, settings_hook = list(settings._loaded_hooks.keys())
+    assert settings._loaded_hooks[plugin_hook] == {
         "post": {
             "PLUGIN_NAME": "dummyplugin",
             "COLORS": "@merge blue",
@@ -266,7 +268,7 @@ def test_post_load_hooks(clean_env, tmpdir):
             "BANDS": ["Anathema", "dynaconf_merge"],
         }
     }
-    assert settings._loaded_hooks[str(settings_hook)] == {
+    assert settings._loaded_hooks[settings_hook] == {
         "post": {
             "INSTALLED_APPS": ["dummyplugin"],
         }