diff --git a/docs/snippets.rst b/docs/snippets.rst index 43bfe3b0..3d94566c 100644 --- a/docs/snippets.rst +++ b/docs/snippets.rst @@ -276,3 +276,76 @@ Will translate to the following tuples: >>> etree = ElementTree.fromstring(html_text) >>> glom(etree, etree2tuples) ('html', [('head', [('title', [])]), ('body', [('p', [])])]) + + +Building Configs +---------------- + +:class:`~glom.Fill` is very good for parameterized configs. +For example, consider a logging `dictconfig`_ + +.. code-block:: python + + CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': 'debug.log', + }, + }, + 'root': { + 'handlers': ['console', 'file'], + 'level': 'WARNING', + }, + } + + def build_config(log_path, level): + new_config = copy.deepcopy(CONFIG) + new_config['handlers']['file']['filename'] = log_path + '/debug.log' + new_config['root']['level'] = level + return new_config + + +This configuration is already a valid :class:`~glom.Fill` spec +which returns itself -- so, we can simply replace the constants +that should be dynamic with specs. + +.. code-block:: python + + + CONFIGSPEC = Fill({ + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': Format('{log_path}/debug.log'), + }, + }, + 'root': { + 'handlers': ['console', 'file'], + 'level': T['level'], + }, + }) + + def build_config(log_path, level): + return glom(dict(log_path=log_path, level=level), CONFIGSPEC) + + +The `glom` version is much more direct -- reading it doesn't require +jumping back and forth between imperative paths and a data structure. +An entire class of bugs where the paths in `build_config()` and the +global data structure get out of sync is eliminated. + + +.. _dictconfig: https://docs.python.org/3/library/logging.config.html#logging-config-dictschema diff --git a/glom/__init__.py b/glom/__init__.py index 8bf37611..ad2b0361 100644 --- a/glom/__init__.py +++ b/glom/__init__.py @@ -1,6 +1,7 @@ from glom.core import (glom, Fill, + Format, Auto, register, Glommer, @@ -17,6 +18,7 @@ Check, Path, Literal, + Let, Coalesce, Inspect, GlomError, diff --git a/glom/core.py b/glom/core.py index fe03e285..c57ccc41 100644 --- a/glom/core.py +++ b/glom/core.py @@ -2015,3 +2015,45 @@ def _fill(target, spec, scope): if callable(spec): return spec(target) return spec + + +class Format(object): + """ + Format(fmt_str) is a deferred call to fmt_str.format(). + + That is, Format(string) is equivalent to lambda t: string.format(**t) + + An optional second parameter can be used to pass something other + than the current target to fmr_str.format() + + For example, the subspec can be used to build a dictionary: + + >>> from glom import glom, Format + >>> target = {'data': [0, 1]} + >>> glom(target, Format("first {data0} second {data1}", {'data0': 'data.0', 'data1': 'data.1'})) + "first 0 second 1" + + As another example, :class:`~glom.Let` and :attr:`~glom.S` can be used: + + >>> from glom import Let, S + >>> glom(target, (Let(data0='data.0', data1='data.1'), Format("first {data0} second {data1}", S)) + "first 0 second 1" + """ + def __init__(self, fmt_str, spec=T): + self.fmt_str, self.spec = fmt_str, spec + + def glomit(self, target, scope): + if self.spec is T: + fmt_args = target + else: + fmt_args = scope[glom](target, self.spec, scope) + # according to spec, only strings are allowed as ** args; + # filter out non-str keys as a convenience + return self.fmt_str.format( + **{k: v for k, v in fmt_args.items() if isinstance(k, str)}) + + def __repr__(self): + ret = "Format(" + repr(self.fmt_str) + if self.spec is not T: + ret += ", " + repr(self.spec) + return ret + ")" diff --git a/glom/test/test_fill.py b/glom/test/test_fill.py index 1fcd47af..3c69141a 100644 --- a/glom/test/test_fill.py +++ b/glom/test/test_fill.py @@ -1,4 +1,4 @@ -from glom import Auto, Fill, T, glom +from glom import Auto, Fill, T, S, glom, Format, Let def test(): assert glom('abc', Fill((T[0], {T[1]: T[2]}))) == ('a', {'b': 'c'}) @@ -18,3 +18,10 @@ def test(): assert repr(Fill(T)) == 'Fill(T)' assert repr(Fill(len)) == 'Fill(len)' + + +def test_format(): + assert glom(dict(a=1, b=2), Format("{a}{b}")) == "12" + assert glom(dict(a=1, b=2), (Let(a='a', b='b'), Format("{a}{b}", S))) == "12" + assert repr(Format('')) == "Format('')" + assert repr(Format('', S)) == "Format('', S)"