Skip to content

Commit

Permalink
Merge pull request #117 from mahmoud/better-builtins-roundtripping
Browse files Browse the repository at this point in the history
Better builtin roundtripping
  • Loading branch information
mahmoud authored Nov 13, 2019
2 parents 6907167 + 2ee3cda commit 12e0555
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 94 deletions.
130 changes: 80 additions & 50 deletions glom/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from boltons.typeutils import make_sentinel
from boltons.iterutils import is_iterable
from boltons.funcutils import format_invocation
#from boltons.funcutils import format_invocation

PY2 = (sys.version_info[0] == 2)
if PY2:
Expand Down Expand Up @@ -248,6 +248,57 @@ def __str__(self):
return msg


if getattr(__builtins__, '__dict__', None) is not None:
# pypy's __builtins__ is a module, as is CPython's REPL, but at
# normal execution time it's a dict?
__builtins__ = __builtins__.__dict__


_BUILTIN_ID_NAME_MAP = dict([(id(v), k)
for k, v in __builtins__.items()])

def bbrepr(obj):
"""A better repr for builtins, when the built-in repr isn't
roundtrippable.
"""
ret = repr(obj)
if not ret.startswith('<'):
return ret
return _BUILTIN_ID_NAME_MAP.get(id(obj), ret)


# TODO: push this back up to boltons with repr kwarg
def format_invocation(name='', args=(), kwargs=None, **kw):
"""Given a name, positional arguments, and keyword arguments, format
a basic Python-style function call.
>>> print(format_invocation('func', args=(1, 2), kwargs={'c': 3}))
func(1, 2, c=3)
>>> print(format_invocation('a_func', args=(1,)))
a_func(1)
>>> print(format_invocation('kw_func', kwargs=[('a', 1), ('b', 2)]))
kw_func(a=1, b=2)
"""
_repr = kw.pop('repr', repr)
if kw:
raise TypeError('unexpected keyword args: %r' % ', '.join(kw.keys()))
kwargs = kwargs or {}
a_text = ', '.join([_repr(a) for a in args])
if isinstance(kwargs, dict):
kwarg_items = [(k, kwargs[k]) for k in sorted(kwargs)]
else:
kwarg_items = kwargs
kw_text = ', '.join(['%s=%s' % (k, _repr(v)) for k, v in kwarg_items])

all_args_text = a_text
if all_args_text and kw_text:
all_args_text += ', '
all_args_text += kw_text

return '%s(%s)' % (name, all_args_text)


class Path(object):
"""Path objects specify explicit paths when the default
``'a.b.c'``-style general access syntax won't work or isn't
Expand Down Expand Up @@ -454,7 +505,7 @@ def glomit(self, target, scope):

def __repr__(self):
cn = self.__class__.__name__
return '%s(%r)' % (cn, self.value)
return '%s(%s)' % (cn, bbrepr(self.value))


class Spec(object):
Expand Down Expand Up @@ -498,8 +549,8 @@ def glomit(self, target, scope):
def __repr__(self):
cn = self.__class__.__name__
if self.scope:
return '%s(%r, scope=%r)' % (cn, self.spec, self.scope)
return '%s(%r)' % (cn, self.spec)
return '%s(%s, scope=%r)' % (cn, bbrepr(self.spec), self.scope)
return '%s(%s)' % (cn, bbrepr(self.spec))


class Coalesce(object):
Expand Down Expand Up @@ -607,7 +658,7 @@ def glomit(self, target, scope):

def __repr__(self):
cn = self.__class__.__name__
return format_invocation(cn, self.subspecs, self._orig_kwargs)
return format_invocation(cn, self.subspecs, self._orig_kwargs, repr=bbrepr)


class Inspect(object):
Expand Down Expand Up @@ -772,7 +823,7 @@ def _eval(t):

def __repr__(self):
cn = self.__class__.__name__
return '%s(%r, args=%r, kwargs=%r)' % (cn, self.func, self.args, self.kwargs)
return '%s(%s, args=%r, kwargs=%r)' % (cn, bbrepr(self.func), self.args, self.kwargs)


def _is_spec(obj, strict=False):
Expand Down Expand Up @@ -907,6 +958,7 @@ def specs(self, *a, **kw):
:meth:`~Invoke.specs()` and other :class:`Invoke`
methods may be called multiple times, just remember that every
call returns a new spec.
"""
ret = self.__class__(self.func)
ret._args = self._args + ('S', a, kw)
Expand Down Expand Up @@ -944,29 +996,31 @@ def star(self, args=None, kwargs=None):
return ret

def __repr__(self):
chunks = [self.__class__.__name__]
base_fname = self.__class__.__name__
fname_map = {'C': 'constants', 'S': 'specs', '*': 'star'}
if type(self.func) is Spec:
chunks.append('.specfunc({!r})'.format(self.func.spec))
base_fname += '.specfunc'
args = (self.func.spec,)
else:
chunks.append('({!r})'.format(self.func))
args = (self.func,)
chunks = [format_invocation(base_fname, args, repr=bbrepr)]

for i in range(len(self._args) // 3):
op, args, kwargs = self._args[i * 3: i * 3 + 3]
op, args, _kwargs = self._args[i * 3: i * 3 + 3]
fname = fname_map[op]
chunks.append('.{}('.format(fname))
if op in ('C', 'S'):
chunks.append(', '.join(
[repr(a) for a in args] +
['{}={!r}'.format(k, v) for k, v in kwargs.items()
if self._cur_kwargs[k] is kwargs]))
kwargs = [(k, v) for k, v in _kwargs.items()
if self._cur_kwargs[k] is _kwargs]
else:
kwargs = {}
if args:
chunks.append('args=' + repr(args))
if args and kwargs:
chunks.append(", ")
if kwargs:
chunks.append('kwargs=' + repr(kwargs))
chunks.append(')')
kwargs['args'] = args
if _kwargs:
kwargs['kwargs'] = _kwargs
args = ()

chunks.append('.' + format_invocation(fname, args, kwargs, repr=bbrepr))

return ''.join(chunks)

def glomit(self, target, scope):
Expand Down Expand Up @@ -1169,24 +1223,6 @@ def _t_eval(target, _t, scope):
ROOT = make_sentinel('ROOT')


def _format_invocation(name='', args=(), kwargs=None): # pragma: no cover
# TODO: add to boltons
kwargs = kwargs or {}
a_text = ', '.join([repr(a) for a in args])
if isinstance(kwargs, dict):
kwarg_items = kwargs.items()
else:
kwarg_items = kwargs
kw_text = ', '.join(['%s=%r' % (k, v) for k, v in kwarg_items])

star_args_text = a_text
if star_args_text and kw_text:
star_args_text += ', '
star_args_text += kw_text

return '%s(%s)' % (name, star_args_text)


class Let(object):
"""
This specifier type assigns variables to the scope.
Expand All @@ -1208,27 +1244,21 @@ def glomit(self, target, scope):

def __repr__(self):
cn = self.__class__.__name__
return _format_invocation(cn, kwargs=self._binding)
return format_invocation(cn, kwargs=self._binding, repr=bbrepr)


def _format_t(path, root=T):
def kwarg_fmt(kw):
if isinstance(kw, str):
return kw
return repr(kw)
prepr = ['T' if root is T else 'S']
i = 0
while i < len(path):
op, arg = path[i], path[i + 1]
if op == '.':
prepr.append('.' + arg)
elif op == '[':
prepr.append("[%r]" % (arg,))
prepr.append("[%s]" % (bbrepr(arg),))
elif op == '(':
args, kwargs = arg
prepr.append("(%s)" % ", ".join([repr(a) for a in args] +
["%s=%r" % (kwarg_fmt(k), v)
for k, v in kwargs.items()]))
prepr.append(format_invocation(args=args, kwargs=kwargs, repr=bbrepr))
elif op == 'P':
return _format_path(path)
i += 2
Expand Down Expand Up @@ -1435,7 +1465,7 @@ def glomit(self, target, scope):
def __repr__(self):
cn = self.__class__.__name__
posargs = (self.spec,) if self.spec is not T else ()
return format_invocation(cn, posargs, self._orig_kwargs)
return format_invocation(cn, posargs, self._orig_kwargs, repr=bbrepr)


class Auto(object):
Expand Down Expand Up @@ -1966,7 +1996,7 @@ def fill(self, target):

def __repr__(self):
cn = self.__class__.__name__
rpr = '' if self.spec is None else repr(self.spec)
rpr = '' if self.spec is None else bbrepr(self.spec)
return '%s(%s)' % (cn, rpr)


Expand Down
6 changes: 3 additions & 3 deletions glom/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
from pprint import pprint

from .core import Path, T, S, Spec, glom, UnregisteredTarget, GlomError, PathAccessError, UP
from .core import TType, register_op, TargetRegistry
from .core import TType, register_op, TargetRegistry, bbrepr

try:
basestring
except NameError:
basestring = str


if getattr(__builtins__, '__dict__', None):
if getattr(__builtins__, '__dict__', None) is not None:
# pypy's __builtins__ is a module, as is CPython's REPL, but at
# normal execution time it's a dict?
__builtins__ = __builtins__.__dict__
Expand Down Expand Up @@ -206,7 +206,7 @@ def __repr__(self):
cn = self.__class__.__name__
if self.missing is None:
return '%s(%r, %r)' % (cn, self._orig_path, self.val)
return '%s(%r, %r, missing=%r)' % (cn, self._orig_path, self.val, self.missing)
return '%s(%r, %r, missing=%s)' % (cn, self._orig_path, self.val, bbrepr(self.missing))


def assign(obj, path, val, missing=None):
Expand Down
19 changes: 14 additions & 5 deletions glom/reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from boltons.typeutils import make_sentinel

from .core import TargetRegistry, Path, T, glom, GlomError, UnregisteredTarget
from .core import TargetRegistry, Path, T, glom, GlomError, UnregisteredTarget, format_invocation, bbrepr

_MISSING = make_sentinel('_MISSING')

Expand Down Expand Up @@ -96,7 +96,10 @@ def _fold(self, iterator):

def __repr__(self):
cn = self.__class__.__name__
return '%s(%r, init=%r, op=%r)' % (cn, self.subspec, self.init, self.op)
kwargs = {'init': self.init}
if self.op is not operator.iadd:
kwargs['op'] = self.op
return format_invocation(cn, (self.subspec,), kwargs, repr=bbrepr)


class Sum(Fold):
Expand All @@ -122,7 +125,9 @@ def __init__(self, subspec=T, init=int):

def __repr__(self):
cn = self.__class__.__name__
return '%s(%r, init=%r)' % (cn, self.subspec, self.init)
args = () if self.subspec is T else (self.subspec,)
kwargs = {'init': self.init} if self.init is not int else {}
return format_invocation(cn, args, kwargs, repr=bbrepr)


class Flatten(Fold):
Expand Down Expand Up @@ -153,9 +158,13 @@ def _fold(self, iterator):

def __repr__(self):
cn = self.__class__.__name__
args = () if self.subspec is T else (self.subspec,)
kwargs = {}
if self.lazy:
return '%s(%r, init="lazy")' % (cn, self.subspec)
return '%s(%r, init=%r)' % (cn, self.subspec, self.init)
kwargs['init'] = 'lazy'
elif self.init is not list:
kwargs['init'] = self.init
return format_invocation(cn, args, kwargs, repr=bbrepr)


def flatten(target, **kwargs):
Expand Down
30 changes: 11 additions & 19 deletions glom/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from boltons.iterutils import split_iter, chunked_iter, windowed_iter, unique_iter, first
from boltons.funcutils import FunctionBuilder

from .core import glom, T, STOP, SKIP, Check, _MISSING, Path, TargetRegistry, Call, Spec, S
from .core import glom, T, STOP, SKIP, Check, _MISSING, Path, TargetRegistry, Call, Spec, S, bbrepr, format_invocation


class Iter(object):
Expand Down Expand Up @@ -62,29 +62,21 @@ def __init__(self, subspec=T, **kwargs):
return

def __repr__(self):
chunks = [self.__class__.__name__]
base_args = ()
if self.subspec != T:
chunks.append('({!r})'.format(self.subspec))
else:
chunks.append('()')
base_args = (self.subspec,)
base = format_invocation(self.__class__.__name__, base_args, repr=bbrepr)
chunks = [base]
for fname, args, _ in reversed(self._iter_stack):
meth = getattr(self, fname)
fb = FunctionBuilder.from_func(meth)
fb.args = fb.args[1:] # drop self
arg_names = fb.get_arg_names()
# TODO: something fancier with defaults:
chunks.append("." + fname)
if len(args) == 0:
chunks.append("()")
elif len(arg_names) == 1:
assert len(args) == 1
chunks.append('({!r})'.format(args[0]))
elif arg_names:
chunks.append('({})'.format(", ".join([
'{}={!r}'.format(name, val) for name, val in zip(arg_names, args)])))
else:
# p much just slice bc no kwargs
chunks.append('({})'.format(", ".join(['%s' % a for a in args])))
kwargs = []
if len(args) > 1 and arg_names:
args, kwargs = (), zip(arg_names, args)
chunks.append('.' + format_invocation(fname, args, kwargs, repr=bbrepr))
return ''.join(chunks)

def glomit(self, target, scope):
Expand Down Expand Up @@ -386,5 +378,5 @@ def glomit(self, target, scope):
def __repr__(self):
cn = self.__class__.__name__
if self._default is None:
return '%s(%r)' % (cn, self._spec)
return '%s(%r, default=%r)' % (cn, self._spec, self._default)
return '%s(%s)' % (cn, bbrepr(self._spec))
return '%s(%s, default=%s)' % (cn, bbrepr(self._spec), bbrepr(self._default))
9 changes: 7 additions & 2 deletions glom/test/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,10 @@ def test_abstract_iterable():
class MyIterable(object):
def __iter__(self):
return iter([1, 2, 3])
mi = MyIterable()
assert list(mi) == [1, 2, 3]

assert isinstance(MyIterable(), glom_core._AbstractIterable)
assert isinstance(mi, glom_core._AbstractIterable)


def test_call_and_target():
Expand Down Expand Up @@ -231,6 +233,9 @@ def test(*a, **kw):
).constants(3, b='b').specs(c='c'
).star(args='args2', kwargs='kwargs')
repr(spec) # no exceptions
assert repr(Invoke(len).specs(T)) == 'Invoke(len).specs(T)'
assert (repr(Invoke.specfunc(next).constants(len).constants(1))
== 'Invoke.specfunc(next).constants(len).constants(1)')
assert glom(data, spec) == 'test'
assert args == [
(1, 2, 3, 4, 5),
Expand Down Expand Up @@ -441,6 +446,6 @@ def test_api_repr():
if not callable(getattr(v, 'glomit', None)):
continue
if v.__repr__ is object.__repr__:
spec_types_wo_reprs.append(k)
spec_types_wo_reprs.append(k) # pragma: no cover

assert set(spec_types_wo_reprs) == set([])
Loading

0 comments on commit 12e0555

Please sign in to comment.