Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added Format #131

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/snippets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions glom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

from glom.core import (glom,
Fill,
Format,
Auto,
register,
Glommer,
Expand All @@ -17,6 +18,7 @@
Check,
Path,
Literal,
Let,
Coalesce,
Inspect,
GlomError,
Expand Down
42 changes: 42 additions & 0 deletions glom/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ")"
9 changes: 8 additions & 1 deletion glom/test/test_fill.py
Original file line number Diff line number Diff line change
@@ -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'})
Expand All @@ -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)"