In python I can use a template with
from string import Template
templ = Template('hello ${name}')
print templ.substitute(name='world')
how can I define a default value in the template?
And call the template without any value.
print templ.substitute()
Edit
And when I call without parameters get the default value, example
print templ.substitute()
>> hello name
The Template.substitute method takes a mapping argument in addition to keyword arguments. The keyword arguments override the arguments provided by the mapping positional argument, which makes mapping a natural way to implement defaults without needing to subclass:
from string import Template
defaults = { "name": "default" }
templ = Template('hello ${name}')
print templ.substitute(defaults) # prints hello default
print templ.substitute(defaults, name="world") # prints hello world
This will also work for safe_substitute:
print templ.safe_substitute() # prints hello ${name}
print templ.safe_substitute(defaults) # prints hello default
print templ.safe_substitute(defaults, name="world") # prints hello world
If you are absolutely insistent on passing no arguments to substitute you could subclass Template:
class DefaultTemplate(Template):
def __init__(self, template, default):
self.default = default
super(DefaultTemplate, self).__init__(template)
def mapping(self, mapping):
default_mapping = self.default.copy()
default_mapping.update(mapping)
return default_mapping
def substitute(self, mapping=None, **kws):
return super(DefaultTemplate, self).substitute(self.mapping(mapping or {}), **kws)
def substitute(self, mapping=None, **kws):
return super(DefaultTemplate, self).safe_substitute(self.mapping(mapping or {}), **kws)
And then use it like this:
DefaultTemplate({ "name": "default" }).substitute()
Although I find this to be less explicit and less readable than just passing a mapping with defaults to substitute.
You can create your proxy to Template class and store substitutes there.
from string import Template
from copy import copy
class TemplateWithDefaults(Template):
def __init__(self, template, **defaults):
self.defaults = defaults or {}
super(TemplateWithDefaults, self).__init__(template)
def build_mapping(self, *args, **kwargs):
mapping = copy(self.defaults)
if len(args) == 1:
mapping.update(args[0])
mapping.update(kwargs)
return mapping
def substitute(*args, **kwargs):
self, args = args[0], args[1:]
mapping = self.build_mapping(*args, **kwargs)
return super(TemplateWithDefaults, self).substitute(mapping, **kwargs)
def safe_substitute(*args, **kwargs):
self, args = args[0], args[1:]
mapping = self.build_mapping(*args, **kwargs)
return super(TemplateWithDefaults, self).safe_substitute(mapping, **kwargs)
template = TemplateWithDefaults("$wow", wow=1)
print template.substitute() # outputs 1
print template.substitute(wow=2) # outputs 2
print template.substitute({"wow": 2}) # outputs 2
print template.substitute() # outputs 1 (means no side effects)
UPD: edited code to handle dict as first argument. Original api compatibility.
If the default value is the variable name (like in the question), the missing data could be added automatically:
class MyTemplate(Template):
def substitute(self, *args, **kwds):
try:
return super().substitute(*args, **kwds)
except KeyError as err:
key = str(err.args[0])
kwds[key] = key
return self.substitute(*args, **kwds)
Related
I have a class based decorator. The problem is that I need to be able to have access to wraped function args and kwargs but now I can't and I don't understand why. It should be easy but it's not for me.
class limit:
def __call__(self, fn):
#wraps(fn)
# the idea to use signature like (request, *args, **kwargs) is bad. I must accept *args and **kwargs only
def wrapper(*args, **kwargs):
# pdb breakpoint is here
user = kwargs.get('user') or kwargs.get('request').user // ERROR
return fn(*args, **kwargs)
return wrapper
Let's have a look at pdb. That's ****CENSORED**** crazy.
(Pdb) args
args = (<User: dua>,)
kwargs = {}
(Pdb) kwargs
{}
(Pdb) args.args
args = (<User: dua>,)
kwargs = {}
(Pdb) args.args.args.args
args = (<User: dua>,)
kwargs = {}
(Pdb) args.get('user')
args = (<User: dua>,)
kwargs = {}
(Pdb) type(args)
<class 'tuple'>
(Pdb)
The question is how can I get access to args, kwargs and treat args as list and kwargs as dict.
P.S. I don't know why it args and kwargs looks like that. Why they do look like that?
As mentioned at the answer args is command of pdb. User repr(args) to see args.
Next question is possible signatures of function:
1) def fn(request, ...)
2) def fn(self, a, b, c, etc, user)
3) def fn(user)
Are there a way to deal with all of them with one decorator?
Your pdb output is not really relevant, args here acts like interactive pdb command.
For the sake of simplicity, either use repr(args) or temporary rename *args argument
I ended up with this solution:
def wrapper(*args, **kwargs):
user = None
fn_signature_args = inspect.getfullargspec(fn).args
# looking for a user in kwargs
if 'user' in kwargs:
user = kwargs['user']
# looking for a user as positional argument
elif 'user' in fn_signature_args:
index = fn_signature_args.index('user')
user = args[index]
# looking for a user as self or request attribute
elif fn_signature_args[0] in ('request', 'self'):
user = args[0].user
I am new to python and decorators and am stumped in writing a decorator which reports not only passed args and kwargs but ALSO the unchanged default kwargs.
This is what I have so far.
def document_call(fn):
def wrapper(*args, **kwargs):
print 'function %s called with positional args %s and keyword args %s' % (fn.__name__, args, kwargs)
return fn(*args, **kwargs)
return wrapper
#document_call
def square(n, trial=True, output=False):
# kwargs are a bit of nonsense to test function
if not output:
print 'no output'
if trial:
print n*n
square(6) # with this call syntax, the default kwargs are not reported
# function square called with positional args (6,) and keyword args {}
# no output
36
square(7,output=True) # only if a kwarg is changed from default is it reported
# function square called with positional args (7,) and keyword args {'output': True}
49
The 'problem' is that this decorator reports the args that are passed in the call to square but does not report the default kwargs defined in the square definition. The only way kwargs are reported is if they're changed from their default i.e. passed to the square call.
Any recommendations for how I get the kwargs in the square definition reported too?
Edit after following up on the inspect suggestions, which helped me to the solution below. I changed the output of positional params to include their names because I thought it made the output easier to understand.
import inspect
def document_call(fn):
def wrapper(*args, **kwargs):
argspec = inspect.getargspec(fn)
n_postnl_args = len(argspec.args) - len(argspec.defaults)
# get kwargs passed positionally
passed = {k:v for k,v in zip(argspec.args[n_postnl_args:], args[n_postnl_args:])}
# update with kwargs
passed.update({k:v for k,v in kwargs.iteritems()})
print 'function %s called with \n positional args %s\n passed kwargs %s\n default kwargs %s' % (
fn.__name__, {k:v for k,v in zip(argspec.args, args[:n_postnl_args])},
passed,
{k:v for k,v in zip(argspec.args[n_postnl_args:], argspec.defaults) if k not in passed})
return fn(*args, **kwargs)
return wrapper
That was a good learning experience. It's neat to see three different solutions to the same problem. Thanks to the Answerers!
You'll have to introspect the function that you wrapped, to read the defaults. You can do this with the inspect.getargspec() function.
The function returns a tuple with, among others, a sequence of all argument names, and a sequence of default values. The last of the argument names pair up with the defaults to form name-default pairs; you can use this to create a dictionary and extract unused defaults from there:
import inspect
argspec = inspect.getargspec(fn)
positional_count = len(argspec.args) - len(argspec.defaults)
defaults = dict(zip(argspec.args[positional_count:], argspec.defaults))
You'll need to take into account that positional arguments can specify default arguments too, so the dance to figure out keyword arguments is a little more involved but looks like this:
def document_call(fn):
argspec = inspect.getargspec(fn)
positional_count = len(argspec.args) - len(argspec.defaults)
defaults = dict(zip(argspec.args[positional_count:], argspec.defaults))
def wrapper(*args, **kwargs):
used_kwargs = kwargs.copy()
used_kwargs.update(zip(argspec.args[positional_count:], args[positional_count:]))
print 'function %s called with positional args %s and keyword args %s' % (
fn.__name__, args[:positional_count],
{k: used_kwargs.get(k, d) for k, d in defaults.items()})
return fn(*args, **kwargs)
return wrapper
This determines what keyword paramaters were actually used from both the positional arguments passed in, and the keyword arguments, then pulls out default values for those not used.
Demo:
>>> square(39)
function square called with positional args (39,) and keyword args {'trial': True, 'output': False}
no output
1521
>>> square(39, False)
function square called with positional args (39,) and keyword args {'trial': False, 'output': False}
no output
>>> square(39, False, True)
function square called with positional args (39,) and keyword args {'trial': False, 'output': True}
>>> square(39, False, output=True)
function square called with positional args (39,) and keyword args {'trial': False, 'output': True}
Starting with Python 3.5 you can use BoundArguments.apply_defaults to fill in missing arguments with their default value:
import inspect
def document_call(fn):
def wrapper(*args, **kwargs):
bound = inspect.signature(fn).bind(*args, **kwargs)
bound.apply_defaults()
print(f'{fn.__name__} called with {bound}')
return fn(*args, **kwargs)
return wrapper
Since the decorator function wrapper takes any argument and just passes everything on, of course it does not know anything about the parameters of the wrapped function and its default values.
So without actually looking at the decorated function, you will not get this information. Fortunately, you can use the inspect module to figure out the default arguments of the wrapped function.
You can use the inspect.getargspec function to get the information about the default argument values in the function signature. You just need to match them up properly with the parameter names:
def document_call(fn):
argspec = inspect.getargspec(fn)
defaultArguments = list(reversed(zip(reversed(argspec.args), reversed(argspec.defaults))))
def wrapper(*args, **kwargs):
all_kwargs = kwargs.copy()
for arg, value in defaultArguments:
if arg not in kwargs:
all_kwargs[arg] = value
print 'function %s called with positional args %s and keyword args %s' % (fn.__name__, args, all_kwargs)
# still make the call using kwargs, to let the function handle its default values
return fn(*args, **kwargs)
return wrapper
Note that you could still improve this as right now you are handling positional and named arguments separately. For example, in your square function, you could also set trial by passing it as a positional argument after n. This will make it not appear in the kwargs. So you’d have to match the positional arguments with your kwargs to get the full information. You can get all the information about the positions from the argspec.
In python 3.6 I did it using inspect.getfullargspec:
def document_call(func):
#wraps(func)
def decorator(*args, **kwargs):
fullargspec = getfullargspec(func)
default_kwargs = fullargspec.kwonlydefaults
print('Default kwargs', default_kwargs)
print('Passed kwargs', kwargs)
return func(*args, **kwargs)
return decorator
Be aware about using the * separator when defining the decorated function for this to work
#document_call
def square(n, *, trial=True, output=False):
# kwargs are a bit of nonsense to test function
if not output:
print 'no output'
if trial:
print n*n
Here is the code modified to work with python3
import inspect
import decorator
#decorator.decorator
def log_call(fn,*args, **kwargs):
sign = inspect.signature(fn)
arg_names = list(sign.parameters.keys())
passed = {k:v for k,v in zip(arg_names[:len(args)], args)}
passed.update({k:v for k,v in kwargs.items()})
params_str = ", ".join([f"{k}={passed.get(k, '??')}" for k in arg_names])
print (f"{fn.__name__}({params_str})")
return fn(*args, **kwargs)
Note I'm using additional library "decorator" as it preserves the function signature.
I need to be able to dynamically invoke a method on a class that accepts various parameters based on the string name and a dictionary of variables. I know how to find the signature with the inspect module, and I can get the method with the getattr, but I do not know how to assign the parameters in the correct order to invoke it in a purely dynamic way.
class MyClass():
def call_me(a, b, *args, foo='bar', **kwargs):
print('Hey, I got called!')
command = {
'action':'call_me',
'parameters':{
'a': 'Apple',
'b': 'Banana',
'args':['one','two','three','four'],
'foo':'spam',
'clowns':'bad',
'chickens':'good'
}
}
me = MyClass()
action = getattr(me,command['action'])
... now what?
I need to be able to dynamically call this function as if this code were used, without any foreknowledge of the actual parameters for the method:
a = command['parameters']['a']
b = command['parameters']['b']
args = command['parameters']['args']
foo = command['parameters']['foo']
kwargs = {
'clowns': command['parameters']['clowns'],
'chickens':command['parameters']['chickens']
}
value = action(a, b, *args, foo=foo, **kwargs)
Surely there is a good pythonic way to do this.
Edit: Fixed getattr to call instance of MyClass instead of MyClass directly.
This is the best way I have found so far to capture every possible combination of normal args, *args, keyword args and **kwargs without getting any errors:
import inspect
class MyClass():
def a(self):
pass
def b(self,foo):
pass
def c(self,foo,*extras):
pass
def d(self,foo,food='spam'):
pass
def e(self,foo,**kwargs):
pass
def f(self,foo,*extras,food='spam'):
pass
def g(self,foo,*extras,**kwargs):
pass
def h(self,foo,*extras,food='spam',**kwargs):
pass
def i(self,*extras):
pass
def j(self,*extras,food='spam'):
pass
def k(self,*extras,**kwargs):
pass
def l(self,*extras,food='spam',**kwargs):
pass
def m(self,food='spam'):
pass
def n(self,food='spam',**kwargs):
pass
def o(self,**kwargs):
pass
def dynamic_invoke(obj,name,parameters):
action = getattr(obj,name)
spec = inspect.getfullargspec(action)
used = []
args = ()
kwargs = {}
for a in spec.args[1:]:
# skip the "self" argument since we are bound to a class
args += (parameters[a], )
used.append(a)
if spec.varargs:
args += tuple(parameters[spec.varargs])
used.append(spec.varargs)
for kw in spec.kwonlyargs:
try:
kwargs[kw] = parameters[kw]
used.append(kw)
except KeyError:
pass
# pass remaining parameters to kwargs, if allowed
if spec.varkw:
for k,v in parameters.items():
if k not in used:
kwargs[k] = v
return action(*args,**kwargs)
me = MyClass()
params = {
'foo':'bar',
'extras':['one','two','three','four'],
'food':'eggs',
'parrot':'blue'
}
dynamic_invoke(me,'a',params)
dynamic_invoke(me,'b',params)
dynamic_invoke(me,'c',params)
dynamic_invoke(me,'d',params)
dynamic_invoke(me,'e',params)
dynamic_invoke(me,'f',params)
dynamic_invoke(me,'g',params)
dynamic_invoke(me,'h',params)
dynamic_invoke(me,'i',params)
dynamic_invoke(me,'j',params)
dynamic_invoke(me,'k',params)
dynamic_invoke(me,'l',params)
dynamic_invoke(me,'m',params)
dynamic_invoke(me,'n',params)
dynamic_invoke(me,'o',params)
print('done!')
Try like this:
action = getattr(me,command['action'])
action(**{'a': 'Apple',
'b': 'Banana',
'args':['one','two','three','four'],
'foo':'spam',
'clowns':'bad',
'chickens':'good'
})
I want to give user API for my library with easier way to distinguish different types of parameters which I pass to function. All groups of arguments are defined earlier (for now I have 3 groups), but attributes of them need to be constructed on run. I can do this in Django ORM style, where double underscore separates 2 parts of parameter. But it is very unreadable. Example:
def api_function(**kwargs):
""" Separate passed arguments """
api_function(post__arg1='foo', api__arg1='bar', post_arg2='foo2')
Better way do this SQLAlchemy, but only to compare attributes and all args are defined earlier. Example:
class API(object):
arg1 = Arg()
arg2 = Arg()
class Post(object): #...
def api_function(*args):
""" Separate passed arguments """
api_function(POST.arg1=='foo', API.arg1=='bar', POST.arg2=='foo2')
What I would like to achive is behaviour like this:
class API(object): # Magic
class POST(object): # Magic
def api_function(*args):
""" Separate passed arguments """
api_function(POST.arg1='foo', API.arg1='bar', POST.arg2='foo2')
What have I tried:
declare metamodel with defined __setattr__, but it rise on evaluation SyntaxError: keyword can't be an expression
declare __set__, but it is designed for known attributes
My questions are:
Is it even possible in Python to work like in third snippet?
If not, is there any really close solution to look like in third snippet? The best way should use assignment operator API.arg1='foo', the worst API(arg1='foo')
Requirements -- should work at least at Python 2.7. Good to work on Python 3.2.
EDIT1
My first test, which is using equality operator (but it NEVER should be use in this way):
class APIMeta(type):
def __getattr__(cls, item):
return ApiData(item, None)
class API(object):
__metaclass__ = APIMeta
def __init__(self, key, value):
self.key = key
self.value = value
def __str__(self):
return "{0}={1}".format(self.key, self.value)
def __eq__(self, other):
self.value = other
return self
def print_api(*api_data):
for a in api_data:
print(str(a))
print_api(API.page=='3', API=='bar')
It is working right, but using == is suggesting that I want to compare something and I want to assign value.
NOTE: I don't know how much I like this schema you want. But I know one annoying thing will be all the imports to call api_function. E.G. from api import POST, API, api_function
As I said in the comments, the first way is not possible. This is because assignment (=) is a statement not an expression, so it can't return a value. Source
But the other way you asked for certainly is:
class POST(object):
def __init__(self, **kwargs):
self.args = kwargs
# You'll also probably want to make this function a little safer.
def __getattr__(self, name):
return self.args[name]
def api_function(*args):
# Update this to how complicated the handling needs to be
# but you get the general idea...
post_data = None
for a in args:
if isinstance(a, POST):
post_data = a.args
if post_data is None:
raise Exception('This function needs a POST object passed.')
print post_data
Using it:
>>> api_function('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in api_function
Exception: This function needs a POST object passed.
>>> api_function(POST(arg1='foo'))
{'arg1': 'foo'}
>>> api_function(POST(arg1='foo',
... arg2='bar'
... )
... )
{'arg1': 'foo', 'arg2': 'bar'}
Here's my solution. It's not the best in design, as the structure of the argument grouper is nested quite deep, so I'd appreciate feedback on it:
class ArgumentGrouper(object):
"""Transforms a function so that you can apply arguments in named groups.
This system isn't tested as thoroughly as something with so many moving
parts should be. Use at own risk.
Usage:
#ArgumentGrouper("foo", "bar")
def method(regular_arg, foo__arg1, bar__arg2):
print(regular_arg + foo__arg1 + bar__arg2)
method.foo(", ").bar("world!")("Hello")() # Prints "Hello, world!"
"""
def __call__(self, func):
"""Decorate the function."""
return self.Wrapper(func, self.argument_values)
def __init__(self, *argument_groups):
"""Constructor.
argument_groups -- The names of argument groups in the function.
"""
self.argument_values = {i: {} for i in argument_groups}
class Wrapper(object):
"""This is the result of decorating the function. You can call group
names as function to supply their keyword arguments.
"""
def __call__(self, *args):
"""Execute the decorated function by passing any given arguments
and predefined group arguments.
"""
kwargs = {}
for group, values in self.argument_values.items():
for name, value in values.items():
# Add a new argument in the form foo__arg1 to kwargs, as
# per the supplied arguments.
new_name = "{}__{}".format(
group,
name
)
kwargs[new_name] = value
# Invoke the function with the determined arguments.
return self.func(*args, **kwargs)
def __init__(self, func, argument_values):
"""Constructor.
func -- The decorated function.
argument_values -- A dict with the current values for group
arguments. Must be a reference to the actual dict, since each
WrappedMethod uses it.
"""
self.func = func
self.argument_values = argument_values
def __getattr__(self, name):
"""When trying to call `func.foo(arg1="bar")`, provide `foo`. TODO:
This would be better handled at initialization time.
"""
if name in self.argument_values:
return self.WrappedMethod(name, self, self.argument_values)
else:
return self.__dict__[name]
class WrappedMethod(object):
"""For `func.foo(arg1="bar")`, this is `foo`. Pretends to be a
function that takes the keyword arguments to be supplied to the
decorated function.
"""
def __call__(self, **kwargs):
"""`foo` has been called, record the arguments passed."""
for k, v in kwargs.items():
self.argument_values[self.name][k] = v
return self.wrapper
def __init__(self, name, wrapper, argument_values):
"""Constructor.
name -- The name of the argument group. (This is the string
"foo".)
wrapper -- The decorator. We need this so that we can return it
to chain calls.
argument_values -- A dict with the current values for group
arguments. Must be a reference to the actual dict, since
each WrappedMethod uses it.
"""
self.name = name
self.wrapper = wrapper
self.argument_values = argument_values
# Usage:
#ArgumentGrouper("post", "api")
def api_function(regular_arg, post__arg1, post__arg2, api__arg3):
print("Got regular args {}".format(regular_arg))
print("Got API args {}, {}, {}".format(post__arg1, post__arg2, api__arg3))
api_function.post(
arg1="foo", arg2="bar"
).api(
arg3="baz"
)
api_function("foo")
Then, usage:
#ArgumentGrouper("post", "api")
def api_function(regular_arg, post__arg1, post__arg2, api__arg3):
print("Got regular args {}".format(regular_arg))
print("Got API args {}, {}, {}".format(post__arg1, post__arg2, api__arg3))
api_function.post(
arg1="foo", arg2="bar"
).api(
arg3="baz"
)
api_function("foo")
Output:
Got regular args foo
Got API args foo, bar, baz
It should be simple to scrape argument group names by introspection.
You'll notice the argument naming convention is hardcoded into the WrappedMethod, so you'll have to make sure you're okay with that.
You can also invoke it in one statement:
api_function.post(
arg1="foo", arg2="bar"
).api(
arg3="baz"
)("foo")
Or you could add a dedicated run method which would invoke it, which would just take the place of Wrapper.__call__.
Python don't allow to use assignment operator inside any other code, so:
(a=1)
func((a=1))
will rise SyntaxError. This means that it is not possible to use it in this way. Moreover:
func(API.arg1=3)
Will be treated that left side of assignment is argument API.arg1 which is not valid name in Python for variables. Only solution is to make this in SQLAlchemy style:
func({
API.arg1: 'foo',
API.arg2: 'bar',
DATA.arg1: 'foo1',
})
or
func(**{
API.arg1: 'foo',
API.arg2: 'bar',
DATA.arg1: 'foo1',
})
or just only:
func( API(arg1='foo', arg2='bar'), POST(arg1='foo1'), POST(arg2='bar1'))
Thank you for your interest and answers.
I'm having a lot of trouble getting a good grasp on decorators despite having read many an article on the subject (including [this][1] very popular one on SO). I'm suspecting I must be stupid, but with all the stubbornness that comes with being stupid, I've decided to try to figure this out.
That, and I suspect I have a good use case...
Below is some code from a project of mine that extracts text from PDF files. Processing involves three steps:
Set up PDFMiner objects needed for processing of PDF file (boilerplate initializations).
Apply a processing function to the PDF file.
No matter what happens, close the file.
I recently learned about context managers and the with statement, and this seemed like a good use case for them. As such, I started by defining the PDFMinerWrapper class:
class PDFMinerWrapper(object):
'''
Usage:
with PDFWrapper('/path/to/file.pdf') as doc:
doc.dosomething()
'''
def __init__(self, pdf_doc, pdf_pwd=''):
self.pdf_doc = pdf_doc
self.pdf_pwd = pdf_pwd
def __enter__(self):
self.pdf = open(self.pdf_doc, 'rb')
parser = PDFParser(self.pdf) # create a parser object associated with the file object
doc = PDFDocument() # create a PDFDocument object that stores the document structure
parser.set_document(doc) # connect the parser and document objects
doc.set_parser(parser)
doc.initialize(self.pdf_pwd) # pass '' if no password required
return doc
def __exit__(self, type, value, traceback):
self.pdf.close()
# if we have an error, catch it, log it, and return the info
if isinstance(value, Exception):
self.logError()
print traceback
return value
Now I can easily work with a PDF file and be sure that it will handle errors gracefully. In theory, all I need to do is something like this:
with PDFMinerWrapper('/path/to/pdf') as doc:
foo(doc)
This is great, except that I need to check that the PDF document is extractable before applying a function to the object returned by PDFMinerWrapper. My current solution involves an intermediate step.
I'm working with a class I call Pamplemousse which serves as an interface to work with the PDFs. It, in turn, uses PDFMinerWrapper each time an operation must be performed on the file to which the object has been linked.
Here is some (abridged) code that demonstrates its use:
class Pamplemousse(object):
def __init__(self, inputfile, passwd='', enc='utf-8'):
self.pdf_doc = inputfile
self.passwd = passwd
self.enc = enc
def with_pdf(self, fn, *args):
result = None
with PDFMinerWrapper(self.pdf_doc, self.passwd) as doc:
if doc.is_extractable: # This is the test I need to perform
# apply function and return result
result = fn(doc, *args)
return result
def _parse_toc(self, doc):
toc = []
try:
toc = [(level, title) for level, title, dest, a, se in doc.get_outlines()]
except PDFNoOutlines:
pass
return toc
def get_toc(self):
return self.with_pdf(self._parse_toc)
Any time I wish to perform an operation on the PDF file, I pass the relevant function to the with_pdf method along with its arguments. The with_pdf method, in turn, uses the with statement to exploit the context manager of PDFMinerWrapper (thus ensuring graceful handling of exceptions) and executes the check before actually applying the function it has been passed.
My question is as follows:
I would like to simplify this code such that I do not have to explicitly call Pamplemousse.with_pdf. My understanding is that decorators could be of help here, so:
How would I implement a decorator whose job would be to call the with statement and execute the extractability check?
Is it possible for a decorator to be a class method, or must my decorator be a free-form function or class?
The way I interpreted you goal, was to be able to define multiple methods on your Pamplemousse class, and not constantly have to wrap them in that call. Here is a really simplified version of what it might be:
def if_extractable(fn):
# this expects to be wrapping a Pamplemousse object
def wrapped(self, *args):
print "wrapper(): Calling %s with" % fn, args
result = None
with PDFMinerWrapper(self.pdf_doc) as doc:
if doc.is_extractable:
result = fn(self, doc, *args)
return result
return wrapped
class Pamplemousse(object):
def __init__(self, inputfile):
self.pdf_doc = inputfile
# get_toc will only get called if the wrapper check
# passes the extractable test
#if_extractable
def get_toc(self, doc, *args):
print "get_toc():", self, doc, args
The decorator if_extractable is defined is just a function, but it expects to be used on instance methods of your class.
The decorated get_toc, which used to delegate to a private method, simply will expect to receive a doc object and the args, if it passed the check. Otherwise it doesn't get called and the wrapper returns None.
With this, you can keep defining your operation functions to expect a doc
You could even add some type checking to make sure its wrapping the expected class:
def if_extractable(fn):
def wrapped(self, *args):
if not hasattr(self, 'pdf_doc'):
raise TypeError('if_extractable() is wrapping '\
'a non-Pamplemousse object')
...
A decorator is just a function that takes a function and returns another. You can do anything you like:
def my_func():
return 'banana'
def my_decorator(f): # see it takes a function as an argument
def wrapped():
res = None
with PDFMineWrapper(pdf_doc, passwd) as doc:
res = f()
return res
return wrapper # see, I return a function that also calls f
Now if you apply the decorator:
#my_decorator
def my_func():
return 'banana'
The wrapped function will replace my_func, so the extra code will be called.
You might want to try along the lines of this:
def with_pdf(self, fn, *args):
def wrappedfunc(*args):
result = None
with PDFMinerWrapper(self.pdf_doc, self.passwd) as doc:
if doc.is_extractable: # This is the test I need to perform
# apply function and return result
result = fn(doc, *args)
return result
return wrappedfunc
and when you need to wrap the function, just do this:
#pamplemousseinstance.with_pdf
def foo(doc, *args):
print 'I am doing stuff with', doc
print 'I also got some good args. Take a look!', args
Here is some demonstration code:
#! /usr/bin/python
class Doc(object):
"""Dummy PDFParser Object"""
is_extractable = True
text = ''
class PDFMinerWrapper(object):
'''
Usage:
with PDFWrapper('/path/to/file.pdf') as doc:
doc.dosomething()
'''
def __init__(self, pdf_doc, pdf_pwd=''):
self.pdf_doc = pdf_doc
self.pdf_pwd = pdf_pwd
def __enter__(self):
return self.pdf_doc
def __exit__(self, type, value, traceback):
pass
def safe_with_pdf(fn):
"""
This is the decorator, it gets passed the fn we want
to decorate.
However as it is also a class method it also get passed
the class. This appears as the first argument and the
function as the second argument.
"""
print "---- Decorator ----"
print "safe_with_pdf: First arg (fn):", fn
def wrapper(self, *args, **kargs):
"""
This will get passed the functions arguments and kargs,
which means that we can intercept them here.
"""
print "--- We are now in the wrapper ---"
print "wrapper: First arg (self):", self
print "wrapper: Other args (*args):", args
print "wrapper: Other kargs (**kargs):", kargs
# This function is accessible because this function is
# a closure, thus still has access to the decorators
# ivars.
print "wrapper: The function we run (fn):", fn
# This wrapper is now pretending to be the original function
# Perform all the checks and stuff
with PDFMinerWrapper(self.pdf, self.passwd) as doc:
if doc.is_extractable:
# Now call the orininal function with its
# argument and pass it the doc
result = fn(doc, *args, **kargs)
else:
result = None
print "--- End of the Wrapper ---"
return result
# Decorators are expected to return a function, this
# function is then run instead of the decorated function.
# So instead of returning the original function we return the
# wrapper. The wrapper will be run with the original functions
# argument.
# Now by using closures we can still access the original
# functions by looking up fn (the argument that was passed
# to this function) inside of the wrapper.
print "--- Decorator ---"
return wrapper
class SomeKlass(object):
#safe_with_pdf
def pdf_thing(doc, some_argument):
print ''
print "-- The Function --"
# This function is now passed the doc from the wrapper.
print 'The contents of the pdf:', doc.text
print 'some_argument', some_argument
print "-- End of the Function --"
print ''
doc = Doc()
doc.text = 'PDF contents'
klass = SomeKlass()
klass.pdf = doc
klass.passwd = ''
klass.pdf_thing('arg')
I recommend running that code to see how it works. Some of the interesting points to look out for tho:
First you will notice that we only pass a single argument to pdf_thing() but if you look at the method it takes two arguments:
#safe_with_pdf
def pdf_thing(doc, some_argument):
print ''
print "-- The Function --"
This is because if you look at the wrapper where we all the function:
with PDFMinerWrapper(self.pdf, self.passwd) as doc:
if doc.is_extractable:
# Now call the orininal function with its
# argument and pass it the doc
result = fn(doc, *args, **kargs)
We generate the doc argument and pass it in, along with the original arguments (*args, **kargs). This means that every method or function that is wrapped with this decorator receives an addition doc argument in addition to the arguments listed in its declaration (def pdf_thing(doc, some_argument):).
Another thing to note is that the wrapper:
def wrapper(self, *args, **kargs):
"""
This will get passed the functions arguments and kargs,
which means that we can intercept them here.
"""
Also captures the self argument and does not pass it to the method being called. You could change this behaviour my modifying the function call from:
result = fn(doc, *args, **kargs)
else:
result = None
To:
result = fn(self, doc, *args, **kargs)
else:
result = None
and then changing the method itself to:
def pdf_thing(self, doc, some_argument):
Hope that helps, feel free to ask for more clarification.
EDIT:
To answer the second part of your question.
Yes is can be a class method. Just place safe_with_pdf inside of SomeKlass above and calls to it e.g. The first method in the class.
Also here is a reduced version of the above code, with the decorator in the class.
class SomeKlass(object):
def safe_with_pdf(fn):
"""The decorator which will wrap the method"""
def wrapper(self, *args, **kargs):
"""The wrapper which will call the method is a doc"""
with PDFMinerWrapper(self.pdf, self.passwd) as doc:
if doc.is_extractable:
result = fn(doc, *args, **kargs)
else:
result = None
return result
return wrapper
#safe_with_pdf
def pdf_thing(doc, some_argument):
"""The method to decorate"""
print 'The contents of the pdf:', doc.text
print 'some_argument', some_argument
return '%s - Result' % doc.text
print klass.pdf_thing('arg')