Skip to content

Commit

Permalink
bumping strutils version, making a utils for decorator utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
mahmoud committed Feb 3, 2016
1 parent 4cfa95e commit 2afcedc
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 158 deletions.
157 changes: 1 addition & 156 deletions lithoxyl/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import sys

from utils import wraps
from record import Record
from common import DEBUG, INFO, CRITICAL, get_level

Expand Down Expand Up @@ -199,159 +200,3 @@ def __repr__(self):
return '<%s name=%r sinks=%r>' % (cn, self.name, self.sinks)
except:
return object.__repr__(self)


def wraps(func, injected=None, **kw):
# TODO: docstring
# TODO: py3 for this and FunctionBuilder
if injected is None:
injected = []
elif isinstance(injected, basestring):
injected = [injected]

update_dict = kw.pop('update_dict', True)
if kw:
raise TypeError('unexpected kwargs: %r' % kw.keys())

fb = FunctionBuilder.from_func(func)
for arg in injected:
fb.remove_arg(arg)

fb.body = 'return _call(%s)' % fb.get_sig_str()

def wrapper_wrapper(wrapper_func):
execdict = dict(_call=wrapper_func, _func=func)
fully_wrapped = fb.get_func(execdict, with_dict=update_dict)

return fully_wrapped

return wrapper_wrapper


import inspect
import itertools

from strutils import iter_splitlines


def indent(text, margin, newline='\n', key=bool):
indented_lines = [(margin + line if key(line) else line)
for line in iter_splitlines(text)]
return newline.join(indented_lines)


class FunctionBuilder(object):

_defaults = {'args': [],
'varargs': None,
'keywords': None,
'defaults': (),
'doc': '',
'dict': {},
'module': None,
'body': 'pass',
'indent': 4}

_compile_count = itertools.count()

def __init__(self, name, **kw):
self.name = name
for a in self._defaults.keys():
val = kw.pop(a, None)
if val is None:
val = self._defaults[a]
setattr(self, a, val)

if kw:
raise TypeError('unexpected kwargs: %r' % kw.keys())
return

# def get_argspec(self): # TODO

def get_sig_str(self):
return inspect.formatargspec(self.args, self.varargs,
self.keywords, [])[1:-1]

@classmethod
def from_func(cls, func):
# TODO: copy_body? gonna need a good signature regex.
argspec = inspect.getargspec(func)
kwargs = {'name': func.__name__,
'doc': func.__doc__,
'defaults': func.__defaults__,
'module': func.__module__,
'dict': getattr(func, '__dict__', {})}

for a in ('args', 'varargs', 'keywords', 'defaults'):
kwargs[a] = getattr(argspec, a)

return cls(**kwargs)

def get_func(self, execdict=None, add_source=True, with_dict=True):
execdict = execdict or {}
body = self.body or self._default_body

tmpl = 'def {name}({sig_str}):'
if self.doc:
tmpl += '\n """{doc}"""'
tmpl += '\n{body}'

body = indent(self.body, ' ' * self.indent)

name = self.name.replace('<', '_').replace('>', '_') # lambdas
src = tmpl.format(name=name, sig_str=self.get_sig_str(),
doc=self.doc, body=body)

self._compile(src, execdict)
func = execdict[name]

func.__name__ = self.name
func.__doc__ = self.doc
func.__defaults__ = self.defaults
if with_dict:
func.__dict__.update(self.dict)
func.__module__ = self.module
# TODO: caller module fallback?

if add_source:
func.__source__ = src

return func

def get_defaults_dict(self):
ret = dict(reversed(zip(reversed(self.args),
reversed(self.defaults or []))))
return ret

def remove_arg(self, arg_name):
d_dict = self.get_defaults_dict()
try:
self.args.remove(arg_name)
except ValueError:
raise ValueError('arg %r not found in %s argument list: %r'
% (arg_name, self.name, self.args))
d_dict.pop(arg_name)
self.defaults = tuple([d_dict[a] for a in self.args if a in d_dict])
return

def _compile(self, src, execdict):
filename = ('<boltons.FunctionBuilder-%d>'
% (next(self._compile_count),))
try:
code = compile(src, filename, 'single')
exec(code, execdict)
except Exception:
raise
return execdict


"""decorator.py is bad because it excessively changes your decorator
API to be reliant on decorator.py's strange aesthetic. A pre-existing
decorator can't easily be migrated, and a decorator.py decorator is
not compatible with functools.wraps.
Function signature propagation is orthogonal to closure usage. The
author of decorator.py seems to find a problem with having a function
inside of a function and/or relying on closures and/or functools.wraps
interface.
"""
18 changes: 17 additions & 1 deletion lithoxyl/strutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
__all__ = ['camel2under', 'under2camel', 'slugify', 'split_punct_ws',
'unit_len', 'ordinalize', 'cardinalize', 'pluralize', 'singularize',
'asciify', 'html2text', 'strip_ansi', 'bytes2human', 'find_hashtags',
'a10n', 'gunzip_bytes', 'iter_splitlines'] # 'StringBuffer']
'a10n', 'gunzip_bytes', 'iter_splitlines', 'indent'] # 'StringBuffer']


_punct_ws_str = string.punctuation + string.whitespace
Expand Down Expand Up @@ -627,3 +627,19 @@ def iter_splitlines(text):
if tail:
yield tail
return


def indent(text, margin, newline='\n', key=bool):
"""The missing counterpart to the built-in :func:`textwrap.dedent`.
Args:
text (str): The text to indent.
margin (str): The string to prepend to each line.
newline (str): The newline used to rejoin the lines (default: \\n)
key (callable): Called on each line to determine whether to
indent it. Default: :class:`bool`, to ensure that empty lines do
not get whitespace added.
"""
indented_lines = [(margin + line if key(line) else line)
for line in iter_splitlines(text)]
return newline.join(indented_lines)
2 changes: 1 addition & 1 deletion lithoxyl/tests/test_fb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import inspect
from lithoxyl.logger import FunctionBuilder
from lithoxyl.utils import FunctionBuilder


def example_func(a, b, c=1, d=1):
Expand Down
161 changes: 161 additions & 0 deletions lithoxyl/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-

import inspect
import itertools

from strutils import indent


def wraps(func, injected=None, **kw):
"""Modeled after the built-in :func:`functools.wraps`, this version of
`wraps` enables a decorator to be more informative and transparent
than ever. Use `wraps` to make your wrapper functions have the
same name, documentation, and signature information as the inner
function that is being wrapped.
By default, this version of `wraps` copies the inner function's
signature exactly, allowing seamless introspection with the
built-in :mod:`inspect` module. In addition, the outer signature
can be modified. By passing a list of *injected* argument names,
those arguments will be removed from the wrapper's signature.
"""
# TODO: py3 for this and FunctionBuilder
if injected is None:
injected = []
elif isinstance(injected, basestring):
injected = [injected]

update_dict = kw.pop('update_dict', True)
if kw:
raise TypeError('unexpected kwargs: %r' % kw.keys())

fb = FunctionBuilder.from_func(func)
for arg in injected:
fb.remove_arg(arg)

fb.body = 'return _call(%s)' % fb.get_sig_str()

def wrapper_wrapper(wrapper_func):
execdict = dict(_call=wrapper_func, _func=func)
fully_wrapped = fb.get_func(execdict, with_dict=update_dict)

return fully_wrapped

return wrapper_wrapper


class FunctionBuilder(object):

_defaults = {'args': [],
'varargs': None,
'keywords': None,
'defaults': (),
'doc': '',
'dict': {},
'module': None,
'body': 'pass',
'indent': 4}

_compile_count = itertools.count()

def __init__(self, name, **kw):
self.name = name
for a in self._defaults.keys():
val = kw.pop(a, None)
if val is None:
val = self._defaults[a]
setattr(self, a, val)

if kw:
raise TypeError('unexpected kwargs: %r' % kw.keys())
return

# def get_argspec(self): # TODO

def get_sig_str(self):
return inspect.formatargspec(self.args, self.varargs,
self.keywords, [])[1:-1]

@classmethod
def from_func(cls, func):
# TODO: copy_body? gonna need a good signature regex.
# TODO: might worry about __closure__?
argspec = inspect.getargspec(func)
kwargs = {'name': func.__name__,
'doc': func.__doc__,
'module': func.__module__,
'dict': getattr(func, '__dict__', {})}

for a in ('args', 'varargs', 'keywords', 'defaults'):
kwargs[a] = getattr(argspec, a)

return cls(**kwargs)

def get_func(self, execdict=None, add_source=True, with_dict=True):
execdict = execdict or {}
body = self.body or self._default_body

tmpl = 'def {name}({sig_str}):'
if self.doc:
tmpl += '\n """{doc}"""'
tmpl += '\n{body}'

body = indent(self.body, ' ' * self.indent)

name = self.name.replace('<', '_').replace('>', '_') # lambdas
src = tmpl.format(name=name, sig_str=self.get_sig_str(),
doc=self.doc, body=body)

self._compile(src, execdict)
func = execdict[name]

func.__name__ = self.name
func.__doc__ = self.doc
func.__defaults__ = self.defaults
if with_dict:
func.__dict__.update(self.dict)
func.__module__ = self.module
# TODO: caller module fallback?

if add_source:
func.__source__ = src

return func

def get_defaults_dict(self):
ret = dict(reversed(zip(reversed(self.args),
reversed(self.defaults or []))))
return ret

def remove_arg(self, arg_name):
d_dict = self.get_defaults_dict()
try:
self.args.remove(arg_name)
except ValueError:
raise ValueError('arg %r not found in %s argument list: %r'
% (arg_name, self.name, self.args))
d_dict.pop(arg_name)
self.defaults = tuple([d_dict[a] for a in self.args if a in d_dict])
return

def _compile(self, src, execdict):
filename = ('<boltons.FunctionBuilder-%d>'
% (next(self._compile_count),))
try:
code = compile(src, filename, 'single')
exec(code, execdict)
except Exception:
raise
return execdict


"""decorator.py is bad because it excessively changes your decorator
API to be reliant on decorator.py's strange aesthetic. A pre-existing
decorator can't easily be migrated, and a decorator.py decorator is
not compatible with functools.wraps.
Function signature propagation is orthogonal to closure usage. The
author of decorator.py seems to find a problem with having a function
inside of a function and/or relying on closures and/or functools.wraps
interface.
"""

0 comments on commit 2afcedc

Please sign in to comment.