-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: dump_settings management command
- Loading branch information
1 parent
2a07080
commit 6c7900c
Showing
2 changed files
with
119 additions
and
0 deletions.
There are no files selected for viewing
105 changes: 105 additions & 0 deletions
105
openedx/core/djangoapps/util/management/commands/dump_settings.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
""" | ||
Defines the dump_settings management command. | ||
""" | ||
import inspect | ||
import json | ||
import re | ||
import sys | ||
from datetime import timedelta | ||
from importlib.resources import files | ||
from path import Path | ||
|
||
from django.conf import settings | ||
from django.core.management.base import BaseCommand, CommandError | ||
from django.test import TestCase | ||
|
||
|
||
SETTING_NAME_REGEX = re.compile(r'^[A-Z][A-Z0-9_]*$') | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Dump current Django settings to JSON for debugging/diagnostics. | ||
BEWARE: OUTPUT IS NOT SUITABLE FOR CONSUMPTION BY PRODUCTION SYSTEMS. | ||
The purpose of this output is to be *helpful* for a human operator to understand how their settings are being | ||
rendered and how they differ between different settings files. The serialization format is NOT perfect: there are | ||
certain situations where two different settings will output identical JSON. For example, this command does NOT: | ||
disambiguate between strings and dotted paths to Python objects: | ||
* some.module.some_function # <-- an actual function object | ||
* "some.module.some_function" # <-- a string that is a dotted path to said function object | ||
disambiguate between lists and tuples: | ||
* (1, 2, 3) # <-- the tuple will b printed out as [1, 2, 3] | ||
* [1, 2, 3] | ||
disambiguate between sets and sorted lists: | ||
* {2, 1, 3} # <-- the set will be printed out as [1, 2, 3] | ||
* [1, 2, 3] | ||
disambiguate between internationalized and non-internationalized strings: | ||
* _("hello") | ||
* "hello" | ||
""" | ||
|
||
def handle(self, *args, **kwargs): | ||
""" | ||
Handle the command. | ||
""" | ||
settings_json = { | ||
name: _to_json_friendly_repr(getattr(settings, name)) | ||
for name in dir(settings) | ||
if SETTING_NAME_REGEX.match(name) | ||
} | ||
print(json.dumps(settings_json, indent=4)) | ||
|
||
|
||
def _to_json_friendly_repr(value: object) -> object: | ||
""" | ||
Turn the value into something that we can print to a JSON file (that is: str, bool, None, int, float, list, dict). | ||
See the docstring of `Command` for warnings about this function's behavior. | ||
""" | ||
if isinstance(value, (type(None), bool, int, float, str)): | ||
# All these types can be printed directly | ||
return value | ||
if isinstance(value, (list, tuple)): | ||
# Print both lists and tuples as JSON arrays | ||
return [_to_json_friendly_repr(element) for element in value] | ||
if isinstance(value, set): | ||
# Print sets by sorting them (so that order doesn't matter) and printing the result as a JSON array | ||
return [sorted(_to_json_friendly_repr(element) for element in value)] | ||
if isinstance(value, dict): | ||
# Print dicts as JSON objects | ||
for subkey in value.keys(): | ||
if not isinstance(subkey, (str, int)): | ||
raise ValueError(f"Unexpected dict key {subkey} of type {type(subkey)}") | ||
return {subkey: _to_json_friendly_repr(subval) for subkey, subval in value.items()} | ||
if isinstance(value, Path): | ||
# Print path objects as the string `Path('path/to/something')`. | ||
return repr(value) | ||
if isinstance(value, timedelta): | ||
# Print timedelta objects as the string `datetime.timedelta(days=1, ...)` | ||
return repr(value) | ||
if proxy_args := getattr(value, "_proxy____args", None): | ||
# Print gettext_lazy as simply the wrapped string | ||
if len(proxy_args) == 1: | ||
if isinstance(proxy_args[0], str): | ||
return proxy_args[0] | ||
raise ValueError(f"Not sure how to dump value {value!r} with proxy args {proxy_args!r}") | ||
if value is sys.stderr: | ||
# Print the stderr object as simply "sys.stderr" | ||
return "sys.stderr" | ||
try: | ||
# For anything else, assume it's a function or a class, and try to print its dotted path. | ||
module = value.__module__ | ||
qualname = value.__qualname__ | ||
except AttributeError: | ||
# If that doesn't work, then give up--we don't know how to print this value. | ||
raise ValueError(f"Not sure how to dump value {value!r} of type {type(value)}") | ||
if qualname == "<lambda>": | ||
# Handle lambdas by printing the source lines | ||
return inspect.getsource(value).strip() | ||
return f"{module}.{qualname}" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
Basic test for dump_settings management command | ||
""" | ||
import io | ||
import json | ||
|
||
from django.core.management import CommandError, call_command | ||
|
||
|
||
def test_with_setting_args(): | ||
with io.StringIO() as output: | ||
call_command('dump_settings', stdout=output) | ||
result = json.load(output) | ||
assert "xmodule.x_module.XModuleMixin" in result['XBLOCK_MIXINS'] |