Run update-copyright.py
[rss2email.git] / rss2email / util.py
1 # Copyright (C) 2012-2013 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of rss2email.
4 #
5 # rss2email is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 2 of the License, or (at your option) version 3 of
8 # the License.
9 #
10 # rss2email is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # rss2email.  If not, see <http://www.gnu.org/licenses/>.
16
17 """Odds and ends
18 """
19
20 import importlib as _importlib
21 import pickle as _pickle
22 import pickletools as _pickletools
23 import sys as _sys
24 import threading as _threading
25
26 from . import error as _error
27
28
29 class TimeLimitedFunction (_threading.Thread):
30     """Run `function` with a time limit of `timeout` seconds.
31
32     >>> import time
33     >>> def sleeping_return(sleep, x):
34     ...     time.sleep(sleep)
35     ...     return x
36     >>> TimeLimitedFunction(0.5, sleeping_return)(0.1, 'x')
37     'x'
38     >>> TimeLimitedFunction(0.5, sleeping_return)(10, 'y')
39     Traceback (most recent call last):
40       ...
41     rss2email.error.TimeoutError: 0.5 second timeout exceeded
42     >>> TimeLimitedFunction(0.5, time.sleep)('x')
43     Traceback (most recent call last):
44       ...
45     rss2email.error.TimeoutError: error while running time limited function: a float is required
46     """
47     def __init__(self, timeout, target, **kwargs):
48         super(TimeLimitedFunction, self).__init__(target=target, **kwargs)
49         self.setDaemon(True)  # daemon kwarg only added in Python 3.3.
50         self.timeout = timeout
51         self.result = None
52         self.error = None
53
54     def run(self):
55         """Based on Thread.run().
56
57         We add handling for self.result and self.error.
58         """
59         try:
60             if self._target:
61                 self.result = self._target(*self._args, **self._kwargs)
62         except:
63             self.error = _sys.exc_info()
64         finally:
65             # Avoid a refcycle if the thread is running a function with
66             # an argument that has a member that points to the thread.
67             del self._target, self._args, self._kwargs
68
69     def __call__(self, *args, **kwargs):
70         self._args = args
71         self._kwargs = kwargs
72         self.start()
73         self.join(self.timeout)
74         if self.error:
75             raise _error.TimeoutError(
76                 time_limited_function=self) from self.error[1]
77         elif self.isAlive():
78             raise _error.TimeoutError(time_limited_function=self)
79         return self.result
80
81
82 def import_name(obj):
83     """Return the full import name for a Python object
84
85     Note that this does not always exist (e.g. for dynamically
86     generated functions).  This function does it's best, using Pickle
87     for the heavy lifting.  For example:
88
89     >>> import_name(import_name)
90     'rss2email.util import_name'
91
92     Note the space between the module (``rss2email.util``) and the
93     function within the module (``import_name``).
94
95     Some objects can't be pickled:
96
97     >>> import_name(lambda x: 'muahaha')
98     Traceback (most recent call last):
99       ...
100     _pickle.PicklingError: Can't pickle <class 'function'>: attribute lookup builtins.function failed
101
102     Some objects don't have a global scope:
103
104     >>> import_name('abc')
105     Traceback (most recent call last):
106       ...
107     ValueError: abc
108     """
109     pickle = _pickle.dumps(obj)
110     for opcode,arg,pos in _pickletools.genops(pickle):
111         if opcode.name == 'GLOBAL':
112             return arg
113     raise ValueError(obj)
114
115 def import_function(name):
116     """Import a function using the full import name
117
118     >>> import_function('rss2email.util import_function')  # doctest: +ELLIPSIS
119     <function import_function at 0x...>
120     >>> import_function(import_name(import_function))  # doctest: +ELLIPSIS
121     <function import_function at 0x...>
122
123     >>> import_function('rss2email.util does_not_exist')
124     Traceback (most recent call last):
125       ...
126     AttributeError: 'module' object has no attribute 'does_not_exist'
127     >>> import_function('rss2email.util has invalid syntax')
128     Traceback (most recent call last):
129       ...
130     AttributeError: 'module' object has no attribute 'has invalid syntax'
131     >>> import_function('rss2email.util.no_space')
132     Traceback (most recent call last):
133       ...
134     ValueError: rss2email.util.no_space
135     """
136     try:
137         module_name,function_name = name.split(' ', 1)
138     except ValueError as e:
139         raise ValueError(name) from e
140     module = _importlib.import_module(module_name)
141     return getattr(module, function_name)