")
111 | summary_start += 19
112 | summary_end = html.index("", summary_start)
113 | summary_text = html[summary_start:summary_end]
114 | summary_text = (
115 | summary_text.replace(" ", " ")
116 | .replace("\n", "")
117 | .replace('\\n', "")
118 | .replace('\t', "")
119 | .replace('\\t', "")
120 | .replace('\\\'', '\'')
121 | .replace("
", "")
127 | .replace(" ", " ")
128 | )
129 | assert "NameError: name \'bar\' is not defined" in summary_text
130 | assert "while handling path /4" in summary_text
131 |
132 |
133 | def test_inherited_exception_handler():
134 | request, response = test_manager.test_client.get('/5')
135 | assert response.status == 200
136 |
137 |
138 | def test_chained_exception_handler():
139 | request, response = test_manager.test_client.get('/6/0', debug=True)
140 | assert response.status == 500
141 |
142 | html = str(response.body)
143 |
144 | assert ('response = handler(request, *args, **kwargs)' in html) or (
145 | 'response = handler(request, **kwargs)' in html
146 | )
147 | assert 'handler_6' in html
148 | assert 'foo = 1 / arg' in html
149 | assert 'ValueError' in html
150 | assert 'The above exception was the direct cause' in html
151 |
152 | try:
153 | summary_start = html.index("
", summary_start)
156 | except ValueError:
157 | # Sanic 20.3 and later uses a cut down HTML5 spec,
158 | # see here: https://stackoverflow.com/a/25749523
159 | summary_start = html.index("
")
160 | summary_start += 19
161 | summary_end = html.index("", summary_start)
162 | summary_text = html[summary_start:summary_end]
163 | summary_text = (
164 | summary_text.replace(" ", " ")
165 | .replace("\n", "")
166 | .replace('\\n', "")
167 | .replace('\t', "")
168 | .replace('\\t', "")
169 | .replace('\\\'', '\'')
170 | .replace("
", "")
171 | .replace("", "")
172 | .replace("
", "")
173 | .replace("
", "")
174 | .replace("
", "")
175 | .replace("", "")
176 | .replace(" ", " ")
177 | )
178 | assert "ZeroDivisionError: division by zero " in summary_text
179 | assert "while handling path /6/0" in summary_text
180 |
181 |
182 | def test_exception_handler_lookup():
183 | class CustomError(Exception):
184 | pass
185 |
186 | class CustomServerError(ServerError):
187 | pass
188 |
189 | def custom_error_handler():
190 | pass
191 |
192 | def server_error_handler():
193 | pass
194 |
195 | def import_error_handler():
196 | pass
197 |
198 | try:
199 | ModuleNotFoundError
200 | except:
201 |
202 | class ModuleNotFoundError(ImportError):
203 | pass
204 |
205 | try:
206 | handler = ErrorHandler()
207 | except TypeError:
208 | handler = ErrorHandler("auto")
209 | handler.add(ImportError, import_error_handler)
210 | handler.add(CustomError, custom_error_handler)
211 | handler.add(ServerError, server_error_handler)
212 |
213 | assert handler.lookup(ImportError()) == import_error_handler
214 | assert handler.lookup(ModuleNotFoundError()) == import_error_handler
215 | assert handler.lookup(CustomError()) == custom_error_handler
216 | assert handler.lookup(ServerError('Error')) == server_error_handler
217 | assert handler.lookup(CustomServerError('Error')) == server_error_handler
218 |
219 | # once again to ensure there is no caching bug
220 | assert handler.lookup(ImportError()) == import_error_handler
221 | assert handler.lookup(ModuleNotFoundError()) == import_error_handler
222 | assert handler.lookup(CustomError()) == custom_error_handler
223 | assert handler.lookup(ServerError('Error')) == server_error_handler
224 | assert handler.lookup(CustomServerError('Error')) == server_error_handler
225 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Sanic Plugin Toolkit
2 | ====================
3 |
4 | |Build Status| |Latest Version| |Supported Python versions| |License|
5 |
6 | Welcome to the Sanic Plugin Toolkit.
7 |
8 | The Sanic Plugin Toolkit (SPTK) is a lightweight python library aimed at making it as simple as possible to build
9 | plugins for the Sanic Async HTTP Server.
10 |
11 | The SPTK provides a `SanicPlugin` python base object that your plugin can build upon. It is set up with all of the basic
12 | functionality that the majority of Sanic Plugins will need.
13 |
14 | A SPTK Sanic Plugin is implemented in a similar way to Sanic Blueprints. You can use convenience decorators to set up all
15 | of the routes, middleware, exception handlers, and listeners your plugin uses in the same way you would a blueprint,
16 | and any Application developer can import your plugin and register it into their application.
17 |
18 | The Sanic Plugin Toolkit is more than just a Blueprints-like system for Plugins. It provides an enhanced middleware
19 | system, and manages Context objects.
20 |
21 | **Notice:** Please update to SPTK v0.90.1 if you need compatibility with Sanic v21.03+.
22 |
23 | The Enhanced Middleware System
24 | ------------------------------
25 |
26 | The Middleware system in the Sanic Plugin Toolkit both builds upon and extends the native Sanic middleware system.
27 | Rather than simply having two middleware queues ('request', and 'response'), the middleware system in SPF uses five
28 | additional queues.
29 |
30 | - Request-Pre: These middleware run *before* the application's own request middleware.
31 | - Request-Post: These middleware run *after* the application's own request middleware.
32 | - Response-Pre: These middleware run *before* the application's own response middleware.
33 | - Response-Post: These middleware run *after* the application's own response middleware.
34 | - Cleanup: These middleware run *after* all of the above middleware, and are run after a response is sent, and are run even if response is None.
35 |
36 | So as a plugin developer you can choose whether you need your middleware to be executed before or after the
37 | application's own middleware.
38 |
39 | You can also assign a priority to each of your plugin's middleware so you can more precisely control the order in which
40 | your middleware is executed, especially when the application is using multiple plugins.
41 |
42 | The Context Object Manager
43 | --------------------------
44 |
45 | One feature that many find missing from Sanic is a context object. SPF provides multiple context objects that can be
46 | used for different purposes.
47 |
48 | - A shared context: All plugins registered in the SPF have access to a shared, persistent context object, which anyone can read and write to.
49 | - A per-request context: All plugins get access to a shared temporary context object anyone can read and write to that is created at the start of a request, and deleted when a request is completed.
50 | - A per-plugin context: All plugins get their own private persistent context object that only that plugin can read and write to.
51 | - A per-plugin per-request context: All plugins get a temporary private context object that is created at the start of a request, and deleted when a request is completed.
52 |
53 |
54 | Installation
55 | ------------
56 |
57 | Install the extension with using pip, or easy\_install.
58 |
59 | .. code:: bash
60 |
61 | $ pip install -U sanic-plugin-toolkit
62 |
63 | Usage
64 | -----
65 |
66 | A simple plugin written using the Sanic Plugin Toolkit will look like this:
67 |
68 | .. code:: python
69 |
70 | # Source: my_plugin.py
71 | from sanic_plugin_toolkit import SanicPlugin
72 | from sanic.response import text
73 |
74 | class MyPlugin(SanicPlugin):
75 | def __init__(self, *args, **kwargs):
76 | super(MyPlugin, self).__init__(*args, **kwargs)
77 | # do pre-registration plugin init here.
78 | # Note, context objects are not accessible here.
79 |
80 | def on_registered(self, context, reg, *args, **kwargs):
81 | # do post-registration plugin init here
82 | # We have access to our context and the shared context now.
83 | context.my_private_var = "Private variable"
84 | shared = context.shared
85 | shared.my_shared_var = "Shared variable"
86 |
87 | my_plugin = MyPlugin()
88 |
89 | # You don't need to add any parameters to @middleware, for default behaviour
90 | # This is completely compatible with native Sanic middleware behaviour
91 | @my_plugin.middleware
92 | def my_middleware(request)
93 | h = request.headers
94 | #Do request middleware things here
95 |
96 | #You can tune the middleware priority, and add a context param like this
97 | #Priority must be between 0 and 9 (inclusive). 0 is highest priority, 9 the lowest.
98 | #If you don't specify an 'attach_to' parameter, it is a 'request' middleware
99 | @my_plugin.middleware(priority=6, with_context=True)
100 | def my_middleware2(request, context):
101 | context['test1'] = "test"
102 | print("Hello world")
103 |
104 | #Add attach_to='response' to make this a response middleware
105 | @my_plugin.middleware(attach_to='response', with_context=True)
106 | def my_middleware3(request, response, context):
107 | # Do response middleware here
108 | return response
109 |
110 | #Add relative='pre' to make this a response middleware run _before_ the
111 | #application's own response middleware
112 | @my_plugin.middleware(attach_to='response', relative='pre', with_context=True)
113 | def my_middleware4(request, response, context):
114 | # Do response middleware here
115 | return response
116 |
117 | #Add attach_to='cleanup' to make this run even when the Response is None.
118 | #This queue is fired _after_ response is already sent to the client.
119 | @my_plugin.middleware(attach_to='cleanup', with_context=True)
120 | def my_middleware5(request, context):
121 | # Do per-request cleanup here.
122 | return None
123 |
124 | #Add your plugin routes here. You can even choose to have your context passed in to the route.
125 | @my_plugin.route('/test_plugin', with_context=True)
126 | def t1(request, context):
127 | words = context['test1']
128 | return text('from plugin! {}'.format(words))
129 |
130 |
131 | The Application developer can use your plugin in their code like this:
132 |
133 | .. code:: python
134 |
135 | # Source: app.py
136 | from sanic import Sanic
137 | from sanic_plugin_toolkit import SanicPluginRealm
138 | from sanic.response import text
139 | import my_plugin
140 |
141 | app = Sanic(__name__)
142 | realm = SanicPluginRealm(app)
143 | assoc = realm.register_plugin(my_plugin)
144 |
145 | # ... rest of user app here
146 |
147 |
148 | There is support for using a config file to define the list of plugins to load when SPF is added to an App.
149 |
150 | .. code:: ini
151 |
152 | # Source: sptk.ini
153 | [plugins]
154 | MyPlugin
155 | AnotherPlugin=ExampleArg,False,KWArg1=True,KWArg2=33.3
156 |
157 | .. code:: python
158 |
159 | # Source: app.py
160 | app = Sanic(__name__)
161 | app.config['SPTK_LOAD_INI'] = True
162 | app.config['SPTK_INI_FILE'] = 'sptk.ini'
163 | realm = SanicPluginRealm(app)
164 |
165 | # We can get the assoc object from SPF, it is already registered
166 | assoc = spf.get_plugin_assoc('MyPlugin')
167 |
168 | Or if the developer prefers to do it the old way, (like the Flask way), they can still do it like this:
169 |
170 | .. code:: python
171 |
172 | # Source: app.py
173 | from sanic import Sanic
174 | from sanic.response import text
175 | from my_plugin import MyPlugin
176 |
177 | app = Sanic(__name__)
178 | # this magically returns your previously initialized instance
179 | # from your plugin module, if it is named `my_plugin` or `instance`.
180 | assoc = MyPlugin(app)
181 |
182 | # ... rest of user app here
183 |
184 | Contributing
185 | ------------
186 |
187 | Questions, comments or improvements? Please create an issue on
188 | `Github
`__
189 |
190 | Credits
191 | -------
192 |
193 | Ashley Sommer `ashleysommer@gmail.com `__
194 |
195 |
196 | .. |Build Status| image:: https://api.travis-ci.org/ashleysommer/sanic-plugin-toolkit.svg?branch=master
197 | :target: https://travis-ci.org/ashleysommer/sanic-plugin-toolkit
198 |
199 | .. |Latest Version| image:: https://img.shields.io/pypi/v/sanic-plugin-toolkit.svg
200 | :target: https://pypi.python.org/pypi/sanic-plugin-toolkit/
201 |
202 | .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/sanic-plugin-toolkit.svg
203 | :target: https://img.shields.io/pypi/pyversions/sanic-plugin-toolkit.svg
204 |
205 | .. |License| image:: http://img.shields.io/:license-mit-blue.svg
206 | :target: https://pypi.python.org/pypi/sanic-plugin-toolkit/
207 |
--------------------------------------------------------------------------------
/sanic_plugin_toolkit/context.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | This is the specialised dictionary that is used by Sanic Plugin Toolkit
4 | to manage Context objects. It can be hierarchical, and it searches its
5 | parents if it cannot find an item in its own dictionary. It can create its
6 | own children.
7 | """
8 |
9 |
10 | class HierDict(object):
11 | """
12 | This is the specialised dictionary that is used by the Sanic Plugin Toolkit
13 | to manage Context objects. It can be hierarchical, and it searches its
14 | parents if it cannot find an item in its own dictionary. It can create its
15 | own children.
16 | """
17 |
18 | __slots__ = ('_parent_hd', '_dict', '__weakref__')
19 |
20 | @classmethod
21 | def _iter_slots(cls):
22 | use_cls = cls
23 | bases = cls.__bases__
24 | base_count = 0
25 | while True:
26 | if use_cls.__slots__:
27 | for _s in use_cls.__slots__:
28 | yield _s
29 | if base_count >= len(bases):
30 | break
31 | use_cls = bases[base_count]
32 | base_count += 1
33 | return
34 |
35 | def _inner(self):
36 | """
37 | :return: the internal dictionary
38 | :rtype: dict
39 | """
40 | return object.__getattribute__(self, '_dict')
41 |
42 | def __repr__(self):
43 | _dict_repr = repr(self._inner())
44 | return "HierDict({:s})".format(_dict_repr)
45 |
46 | def __str__(self):
47 | _dict_str = str(self._inner())
48 | return "HierDict({:s})".format(_dict_str)
49 |
50 | def __len__(self):
51 | return len(self._inner())
52 |
53 | def __setitem__(self, key, value):
54 | # TODO: If key is in __slots__, ignore it and return
55 | return self._inner().__setitem__(key, value)
56 |
57 | def __getitem__(self, item):
58 | try:
59 | return self._inner().__getitem__(item)
60 | except KeyError as e1:
61 | parents_searched = [self]
62 | parent = self._parent_hd
63 | while parent:
64 | try:
65 | return parent._inner().__getitem__(item)
66 | except KeyError:
67 | parents_searched.append(parent)
68 | # noinspection PyProtectedMember
69 | next_parent = parent._parent_hd
70 | if next_parent in parents_searched:
71 | raise RuntimeError("Recursive HierDict found!")
72 | parent = next_parent
73 | raise e1
74 |
75 | def __delitem__(self, key):
76 | self._inner().__delitem__(key)
77 |
78 | def __getattr__(self, item):
79 | if item in self._iter_slots():
80 | return object.__getattribute__(self, item)
81 | try:
82 | return self.__getitem__(item)
83 | except KeyError as e:
84 | raise AttributeError(*e.args)
85 |
86 | def __setattr__(self, key, value):
87 | if key in self._iter_slots():
88 | if key == '__weakref__':
89 | if value is None:
90 | return
91 | else:
92 | raise ValueError("Cannot set weakrefs on Context")
93 | return object.__setattr__(self, key, value)
94 | try:
95 | return self.__setitem__(key, value)
96 | except Exception as e: # pragma: no cover
97 | # what exceptions can occur on setting an item?
98 | raise e
99 |
100 | def __contains__(self, item):
101 | return self._inner().__contains__(item)
102 |
103 | def get(self, key, default=None):
104 | try:
105 | return self.__getattr__(key)
106 | except (AttributeError, KeyError):
107 | return default
108 |
109 | def set(self, key, value):
110 | try:
111 | return self.__setattr__(key, value)
112 | except Exception as e: # pragma: no cover
113 | raise e
114 |
115 | def items(self):
116 | """
117 | A set-like read-only view HierDict's (K,V) tuples
118 | :return:
119 | :rtype: frozenset
120 | """
121 | return self._inner().items()
122 |
123 | def keys(self):
124 | """
125 | An object containing a view on the HierDict's keys
126 | :return:
127 | :rtype: tuple # using tuple to represent an immutable list
128 | """
129 | return self._inner().keys()
130 |
131 | def values(self):
132 | """
133 | An object containing a view on the HierDict's values
134 | :return:
135 | :rtype: tuple # using tuple to represent an immutable list
136 | """
137 | return self._inner().values()
138 |
139 | def replace(self, key, value):
140 | """
141 | If this HierDict doesn't already have this key, it sets
142 | the value on a parent HierDict if that parent has the key,
143 | otherwise sets the value on this HierDict.
144 | :param key:
145 | :param value:
146 | :return: Nothing
147 | :rtype: None
148 | """
149 | if key in self._inner().keys():
150 | return self.__setitem__(key, value)
151 | parents_searched = [self]
152 | parent = self._parent_hd
153 | while parent:
154 | try:
155 | if key in parent.keys():
156 | return parent.__setitem__(key, value)
157 | except (KeyError, AttributeError):
158 | pass
159 | parents_searched.append(parent)
160 | # noinspection PyProtectedMember
161 | next_parent = parent._parent_context
162 | if next_parent in parents_searched:
163 | raise RuntimeError("Recursive HierDict found!")
164 | parent = next_parent
165 | return self.__setitem__(key, value)
166 |
167 | # noinspection PyPep8Naming
168 | def update(self, E=None, **F):
169 | """
170 | Update HierDict from dict/iterable E and F
171 | :return: Nothing
172 | :rtype: None
173 | """
174 | if E is not None:
175 | if hasattr(E, 'keys'):
176 | for K in E:
177 | self.replace(K, E[K])
178 | elif hasattr(E, 'items'):
179 | for K, V in E.items():
180 | self.replace(K, V)
181 | else:
182 | for K, V in E:
183 | self.replace(K, V)
184 | for K in F:
185 | self.replace(K, F[K])
186 |
187 | def __new__(cls, parent, *args, **kwargs):
188 | self = super(HierDict, cls).__new__(cls)
189 | self._dict = dict(*args, **kwargs)
190 | if parent is not None:
191 | assert isinstance(parent, HierDict), "Parent context must be a valid initialised HierDict"
192 | self._parent_hd = parent
193 | else:
194 | self._parent_hd = None
195 | return self
196 |
197 | def __init__(self, *args, **kwargs):
198 | args = list(args)
199 | args.pop(0) # remove parent
200 | super(HierDict, self).__init__()
201 |
202 | def __getstate__(self):
203 | state_dict = {}
204 | for s in HierDict.__slots__:
205 | if s == "__weakref__":
206 | continue
207 | state_dict[s] = object.__getattribute__(self, s)
208 | return state_dict
209 |
210 | def __setstate__(self, state):
211 | for s, v in state.items():
212 | setattr(self, s, v)
213 |
214 | def __reduce__(self):
215 | state_dict = self.__getstate__()
216 | _ = state_dict.pop('_stk_realm', None)
217 | parent_context = state_dict.pop('_parent_hd')
218 | return (HierDict.__new__, (self.__class__, parent_context), state_dict)
219 |
220 |
221 | class SanicContext(HierDict):
222 | __slots__ = ('_stk_realm',)
223 |
224 | def __repr__(self):
225 | _dict_repr = repr(self._inner())
226 | return "SanicContext({:s})".format(_dict_repr)
227 |
228 | def __str__(self):
229 | _dict_str = str(self._inner())
230 | return "SanicContext({:s})".format(_dict_str)
231 |
232 | def create_child_context(self, *args, **kwargs):
233 | return SanicContext(self._stk_realm, self, *args, **kwargs)
234 |
235 | def __new__(cls, stk_realm, parent, *args, **kwargs):
236 | if parent is not None:
237 | assert isinstance(parent, SanicContext), "Parent context must be a valid initialised SanicContext"
238 | self = super(SanicContext, cls).__new__(cls, parent, *args, **kwargs)
239 | self._stk_realm = stk_realm
240 | return self
241 |
242 | def __init__(self, *args, **kwargs):
243 | args = list(args)
244 | # remove realm
245 | _stk_realm = args.pop(0) # noqa: F841
246 | super(SanicContext, self).__init__(*args)
247 |
248 | def __getstate__(self):
249 | state_dict = super(SanicContext, self).__getstate__()
250 | for s in SanicContext.__slots__:
251 | state_dict[s] = object.__getattribute__(self, s)
252 | return state_dict
253 |
254 | def __reduce__(self):
255 | state_dict = self.__getstate__()
256 | realm = state_dict.pop('_stk_realm')
257 | parent_context = state_dict.pop('_parent_hd')
258 | return (SanicContext.__new__, (self.__class__, realm, parent_context), state_dict)
259 |
260 | def for_request(self, req):
261 | # shortcut for context.request[id(req)]
262 | requests_ctx = self.request
263 | return requests_ctx[id(req)] if req else None
264 |
--------------------------------------------------------------------------------
/sanic_plugin_toolkit/plugins/contextualize.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from collections import namedtuple
3 |
4 | from sanic_plugin_toolkit import SanicPlugin
5 | from sanic_plugin_toolkit.plugin import SANIC_21_6_0, SANIC_21_9_0, SANIC_VERSION, FutureMiddleware, FutureRoute
6 |
7 |
8 | ContextualizeAssociatedTuple = namedtuple('ContextualizeAssociatedTuple', ['plugin', 'reg'])
9 |
10 |
11 | class ContextualizeAssociated(ContextualizeAssociatedTuple):
12 | __slots__ = ()
13 |
14 | # Decorator
15 | def middleware(self, *args, **kwargs):
16 | """Decorate and register middleware
17 | :param args: captures all of the positional arguments passed in
18 | :type args: tuple(Any)
19 | :param kwargs: captures the keyword arguments passed in
20 | :type kwargs: dict(Any)
21 | :return: The middleware function to use as the decorator
22 | :rtype: fn
23 | """
24 | kwargs.setdefault('priority', 5)
25 | kwargs.setdefault('relative', None)
26 | kwargs.setdefault('attach_to', None)
27 | kwargs['with_context'] = True # This is the whole point of this plugin
28 | plugin = self.plugin
29 | reg = self.reg
30 |
31 | if len(args) == 1 and callable(args[0]):
32 | middle_f = args[0]
33 | return plugin._add_new_middleware(reg, middle_f, **kwargs)
34 |
35 | def wrapper(middle_f):
36 | nonlocal plugin, reg
37 | nonlocal args, kwargs
38 | return plugin._add_new_middleware(reg, middle_f, *args, **kwargs)
39 |
40 | return wrapper
41 |
42 | def route(self, uri, *args, **kwargs):
43 | """Create a plugin route from a decorated function.
44 | :param uri: endpoint at which the route will be accessible.
45 | :type uri: str
46 | :param args: captures all of the positional arguments passed in
47 | :type args: tuple(Any)
48 | :param kwargs: captures the keyword arguments passed in
49 | :type kwargs: dict(Any)
50 | :return: The exception function to use as the decorator
51 | :rtype: fn
52 | """
53 | if len(args) == 0 and callable(uri):
54 | raise RuntimeError("Cannot use the @route decorator without " "arguments.")
55 | kwargs.setdefault('methods', frozenset({'GET'}))
56 | kwargs.setdefault('host', None)
57 | kwargs.setdefault('strict_slashes', False)
58 | kwargs.setdefault('stream', False)
59 | kwargs.setdefault('name', None)
60 | kwargs.setdefault('version', None)
61 | kwargs.setdefault('ignore_body', False)
62 | kwargs.setdefault('websocket', False)
63 | kwargs.setdefault('subprotocols', None)
64 | kwargs.setdefault('unquote', False)
65 | kwargs.setdefault('static', False)
66 | if SANIC_21_6_0 <= SANIC_VERSION:
67 | kwargs.setdefault('version_prefix', '/v')
68 | if SANIC_21_9_0 <= SANIC_VERSION:
69 | kwargs.setdefault('error_format', None)
70 | kwargs['with_context'] = True # This is the whole point of this plugin
71 | plugin = self.plugin
72 | reg = self.reg
73 |
74 | def wrapper(handler_f):
75 | nonlocal plugin, reg
76 | nonlocal uri, args, kwargs
77 | return plugin._add_new_route(reg, uri, handler_f, *args, **kwargs)
78 |
79 | return wrapper
80 |
81 | def listener(self, event, *args, **kwargs):
82 | """Create a listener from a decorated function.
83 | :param event: Event to listen to.
84 | :type event: str
85 | :param args: captures all of the positional arguments passed in
86 | :type args: tuple(Any)
87 | :param kwargs: captures the keyword arguments passed in
88 | :type kwargs: dict(Any)
89 | :return: The function to use as the listener
90 | :rtype: fn
91 | """
92 | if len(args) == 1 and callable(args[0]):
93 | raise RuntimeError("Cannot use the @listener decorator without " "arguments")
94 | kwargs['with_context'] = True # This is the whole point of this plugin
95 | plugin = self.plugin
96 | reg = self.reg
97 |
98 | def wrapper(listener_f):
99 | nonlocal plugin, reg
100 | nonlocal event, args, kwargs
101 | return plugin._add_new_listener(reg, event, listener_f, *args, **kwargs)
102 |
103 | return wrapper
104 |
105 | def websocket(self, uri, *args, **kwargs):
106 | """Create a websocket route from a decorated function
107 | # Deprecated. Use @contextualize.route("/path", websocket=True)
108 | """
109 |
110 | kwargs["websocket"] = True
111 | kwargs["with_context"] = True # This is the whole point of this plugin
112 |
113 | return self.route(uri, *args, **kwargs)
114 |
115 |
116 | class Contextualize(SanicPlugin):
117 | __slots__ = ()
118 |
119 | AssociatedTuple = ContextualizeAssociated
120 |
121 | def _add_new_middleware(self, reg, middle_f, *args, **kwargs):
122 | # A user should never call this directly.
123 | # it should be called only by the AssociatedTuple
124 | assert reg in self.registrations
125 | (realm, p_name, url_prefix) = reg
126 | context = self.get_context_from_realm(reg)
127 | # This is how we add a new middleware _after_ the plugin is registered
128 | m = FutureMiddleware(middle_f, args, kwargs)
129 | realm._register_middleware_helper(m, realm, self, context)
130 | return middle_f
131 |
132 | def _add_new_route(self, reg, uri, handler_f, *args, **kwargs):
133 | # A user should never call this directly.
134 | # it should be called only by the AssociatedTuple
135 | assert reg in self.registrations
136 | (realm, p_name, url_prefix) = reg
137 | context = self.get_context_from_realm(reg)
138 | # This is how we add a new route _after_ the plugin is registered
139 | r = FutureRoute(handler_f, uri, args, kwargs)
140 | realm._register_route_helper(r, realm, self, context, p_name, url_prefix)
141 | return handler_f
142 |
143 | def _add_new_listener(self, reg, event, listener_f, *args, **kwargs):
144 | # A user should never call this directly.
145 | # it should be called only by the AssociatedTuple
146 | assert reg in self.registrations
147 | (realm, p_name, url_prefix) = reg
148 | context = self.get_context_from_realm(reg)
149 | # This is how we add a new listener _after_ the plugin is registered
150 | realm._plugin_register_listener(event, listener_f, self, context, *args, **kwargs)
151 | return listener_f
152 |
153 | # Decorator
154 | def middleware(self, *args, **kwargs):
155 | """Decorate and register middleware
156 | :param args: captures all of the positional arguments passed in
157 | :type args: tuple(Any)
158 | :param kwargs: captures the keyword arguments passed in
159 | :type kwargs: dict(Any)
160 | :return: The middleware function to use as the decorator
161 | :rtype: fn
162 | """
163 | kwargs.setdefault('priority', 5)
164 | kwargs.setdefault('relative', None)
165 | kwargs.setdefault('attach_to', None)
166 | kwargs['with_context'] = True # This is the whole point of this plugin
167 | if len(args) == 1 and callable(args[0]):
168 | middle_f = args[0]
169 | return super(Contextualize, self).middleware(middle_f, **kwargs)
170 |
171 | def wrapper(middle_f):
172 | nonlocal self, args, kwargs
173 | return super(Contextualize, self).middleware(*args, **kwargs)(middle_f)
174 |
175 | return wrapper
176 |
177 | # Decorator
178 | def route(self, uri, *args, **kwargs):
179 | """Create a plugin route from a decorated function.
180 | :param uri: endpoint at which the route will be accessible.
181 | :type uri: str
182 | :param args: captures all of the positional arguments passed in
183 | :type args: tuple(Any)
184 | :param kwargs: captures the keyword arguments passed in
185 | :type kwargs: dict(Any)
186 | :return: The exception function to use as the decorator
187 | :rtype: fn
188 | """
189 | if len(args) == 0 and callable(uri):
190 | raise RuntimeError("Cannot use the @route decorator without arguments.")
191 | kwargs.setdefault('methods', frozenset({'GET'}))
192 | kwargs.setdefault('host', None)
193 | kwargs.setdefault('strict_slashes', False)
194 | kwargs.setdefault('stream', False)
195 | kwargs.setdefault('name', None)
196 | kwargs.setdefault('version', None)
197 | kwargs.setdefault('ignore_body', False)
198 | kwargs.setdefault('websocket', False)
199 | kwargs.setdefault('subprotocols', None)
200 | kwargs.setdefault('unquote', False)
201 | kwargs.setdefault('static', False)
202 | kwargs['with_context'] = True # This is the whole point of this plugin
203 |
204 | def wrapper(handler_f):
205 | nonlocal self, uri, args, kwargs
206 | return super(Contextualize, self).route(uri, *args, **kwargs)(handler_f)
207 |
208 | return wrapper
209 |
210 | # Decorator
211 | def listener(self, event, *args, **kwargs):
212 | """Create a listener from a decorated function.
213 | :param event: Event to listen to.
214 | :type event: str
215 | :param args: captures all of the positional arguments passed in
216 | :type args: tuple(Any)
217 | :param kwargs: captures the keyword arguments passed in
218 | :type kwargs: dict(Any)
219 | :return: The exception function to use as the listener
220 | :rtype: fn
221 | """
222 | if len(args) == 1 and callable(args[0]):
223 | raise RuntimeError("Cannot use the @listener decorator without arguments")
224 | kwargs['with_context'] = True # This is the whole point of this plugin
225 |
226 | def wrapper(listener_f):
227 | nonlocal self, event, args, kwargs
228 | return super(Contextualize, self).listener(event, *args, **kwargs)(listener_f)
229 |
230 | return wrapper
231 |
232 | def websocket(self, uri, *args, **kwargs):
233 | """Create a websocket route from a decorated function
234 | # Deprecated. Use @contextualize.route("/path",websocket=True)
235 | """
236 |
237 | kwargs["websocket"] = True
238 | kwargs["with_context"] = True # This is the whole point of this plugin
239 |
240 | return self.route(uri, *args, **kwargs)
241 |
242 | def __init__(self, *args, **kwargs):
243 | super(Contextualize, self).__init__(*args, **kwargs)
244 |
245 |
246 | instance = contextualize = Contextualize()
247 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Sanic Plugin Toolkit
2 | ====================
3 |
4 | 1.2.1
5 | ------
6 | - Misc bugfixes for Sanic v21.3, and v21.9
7 | - Fix plugins with static dirs, on sanic v21.9
8 | - Explicit non-compatibility with Sanic v21.12+
9 |
10 | 1.2.0
11 | ------
12 | - Fix compatibility with Sanic v21.9
13 | - Monkey-patch _startup() to inject SPTK into the app _after_ Touchup is run
14 | - Fix tests on sanic v21.9
15 |
16 | 1.1.0
17 | ------
18 | - Fix bugs when using SPTK on Sanic v21.6+
19 | - Fixed project name in pyproject.toml
20 | - Updated to latest poetry, poetry-core, and black
21 | - Fixed some bad typos in the Makefile
22 |
23 | 1.0.1
24 | ------
25 | - Suppress deprecation warnings when injecting SPTK into Sanic App or Sanic Blueprint
26 | - Utilize the app.ctx (and bp.ctx) Namespace in Sanic 21.3+ instead of using the sanic config dict
27 |
28 | 1.0.0
29 | ------
30 | - Fix compatibility with Sanic 21.3
31 | - Minor fixes
32 | - Cleanup for v1.0.0 release
33 | - Release for Sanic 21.3.1+ Only!
34 |
35 | 0.99.1
36 | ------
37 | - Project Renamed to Sanic Plugin Toolkit
38 | - Module is renamed from spf to sanic_plugin_toolkit (fixes #16)
39 | - Changed to PEP517/518 project, with pyproject.toml and Poetry
40 | - removed setup.py, setup.cfg, environment.yml, tox.ini
41 |
42 | 0.9.5
43 | -----------
44 | - Fixed some small bugs that were found during the larger big rewrite
45 | - Pinned this series of SPF to maximum Sanic v20.12.x, this series will not work on Sanic 21.x
46 |
47 | - A new version of SanicPluginsFramework named SanicPluginToolkit in development that will work on Sanic 21.x
48 | - It will have the module renamed to sanic_plugin_toolkit to avoid the conflict with the other `spf` library on Pip.
49 | - It will be a PEP 517/518 project, with pyproject.toml and Poetry orchestration.
50 | - New features in Sanic 21.x have necessitated some big changes in SanicPluginToolkit (this is a good thing!)
51 |
52 | 0.9.4.post2
53 | -----------
54 | - Pinned this series of SPF to maximum Sanic v20.12.x, this series will not work on Sanic 21.x
55 |
56 | - A new version of SanicPluginsFramework is in development that will work on Sanic 21.x
57 | - It will have a new module name to avoid the conflict with the other `spf` library on Pip.
58 | - It will be a PEP 517/518 project, with pyproject.toml and Poetry orchestration.
59 | - New features in Sanic 21.x will necessitate some big changes in SanicPluginsFramework (this is a good thing!)
60 |
61 |
62 | 0.9.4.post1
63 | -----------
64 | - Add ``setuptools`` as a specific requirement to this project.
65 |
66 | - It is needed for the entrypoints-based plugin auto-discovery feature
67 | - ``setuptools`` is not always included in a python distribution, so we cannot assume it will be there
68 | - Pinned to ``>40.0`` for now, but will likely change when we migrate to a Poetry/PEP517-based project
69 |
70 |
71 | 0.9.4
72 | -----------
73 | - If the Sanic server emits a "before_server_start" event, use this to initialize SPF, instead of the
74 | "after_server_start" event.
75 |
76 | - This solves a potential race-condition introduced in SPF v0.8.2, when this was reversed.
77 | - Changed the RuntimeError thrown in that circumstance to a Sanic ``ServerError``
78 |
79 | - This may make the error easier to catch and filter. Also may change what the end-user sees when this occurs.
80 |
81 |
82 | 0.9.3
83 | -----------
84 | - Fixed calling routes on a SPF-enabled Sanic app using asgi_client before the app is started.
85 | - Clarified error message generated when a SPF-enabled route is called before the Sanic server is booted.
86 | - Fixed test breakages for Sanic 20.3 and 20.6 releases
87 | - Updated testing packages in requirements-dev
88 | - Updated Travis and TOX to include python 3.8 tests
89 |
90 |
91 | 0.9.2
92 | -----------
93 | - Added a convenience feature on SanicContext class to get the request-context for a given request
94 | - Added correct licence file to LICENSE.txt
95 |
96 | - Existing one was accidentally a copy of the old Sanic-CORS licence file
97 | - Renamed from LICENSE to LICENSE.txt
98 |
99 |
100 | 0.9.1
101 | -----------
102 | - Fixed a problem with error reporting when a plugin is not yet registered on the SPF
103 |
104 |
105 | 0.9.0
106 | -----------
107 | - Released 0.9.0 with Sanic 19.12LTS compatibility
108 | - Minimum supported sanic version is 18.12LTS
109 |
110 |
111 | 0.9.0.b1
112 | -----------
113 | - New minimum supported sanic version is 18.12LTS
114 | - Fixed bugs with Sanic 19.12LTS
115 | - Fixed registering plugin routes on blueprints
116 | - Tested more on blueprints
117 | - Added python3.7 tests to tox, and travis
118 | - Max supported sanic version for this release series is unknown for now.
119 |
120 |
121 | 0.8.2.post1
122 | -----------
123 | - Explicitly set max Sanic version supported to 19.6.3
124 | - This is the last SPF version to support Sanic v0.8.3
125 |
126 | - (please update to 18.12 or greater if you are still on 0.8.3)
127 |
128 |
129 | 0.8.2
130 | -----
131 | - Change all usages of "before_server_start" to "after_server_start"
132 |
133 | - The logic is basically the same, and this ensures compatibility with external servers, like ASGI mode, and using gunicorn runner, etc.
134 |
135 |
136 | 0.8.1
137 | -----
138 | - Plugin names in the config file are now case insensitive
139 | - Plugin names exported using entrypoints are now case insensitive
140 |
141 | 0.8.0
142 | -----
143 | - Added support for a spf config file
144 |
145 | - This is in the python configparser format, it is like an INI file.
146 | - See the config file example in /examples/ for how to use it.
147 |
148 | - Added ability to get a plugin assoc object from SPF, simply by asking for the plugin name.
149 |
150 | - This is to facilitate pulling the assoc object from when a plugin was registered via the config file
151 |
152 | - A new way of advertising sanic plugins using setup.py entrypoints is defined.
153 |
154 | - We use it in this project to advertise the 'Contextualize' plugin.
155 |
156 | - Fixed some example files.
157 |
158 | 0.7.0
159 | -----
160 | - Added a new type of middleware called "cleanup" middleware
161 |
162 | - It Runs after response middleware, whether response is generated or not, and even if there was errors.
163 | - Moved the request-context removal process to run in the "cleanup" middleware step, because sometimes Response middleware is not run, eg. if Response is None (like in the case of a Websocket route), then Response Middleware will never fire.
164 | - Cleanup middleware can be used to do per-request cleanup to prevent memory leaks.
165 |
166 | 0.6.7
167 | -----
168 | - A critical fix for plugin-private-request contexts. They were always overwriting the shared request context when they were created.
169 | - Added new 'id' field inside the private request context container and the shared request context container, to tell them apart when they are used.
170 | - Added a new test for this exact issue.
171 |
172 | 0.6.6
173 | -----
174 | - No 1.0 yet, there are more features planed before we call SPF ready for 1.0.
175 | - Add more tests, and start filling in some missing test coverage
176 | - Fix a couple of bugs already uncovered by filling in coverage.
177 |
178 | - Notably, fix an issue that was preventing the plugin static file helper from working.
179 |
180 |
181 | 0.6.5
182 | -----
183 | - Changed the versioning scheme to not include ".devN" suffixes. This was preventing SPF from being installed using ``pipenv``
184 |
185 | - This is in preparation for a 1.0.0 release, to coincide with the Sanic 2018.12 release.
186 |
187 |
188 | 0.6.4.dev20181101
189 | -----------------
190 | - Made changes in order for SPF, and Sanic Plugins to be pickled
191 | - This fixes the ability for SPF-enabled Sanic Apps to use ``workers=`` on Windows, to allow multiprocessing.
192 |
193 | - Added ``__setstate__``, ``__getstate__``, and ``__reduce__`` methods to all SPF classes
194 | - Change usages of PriorityQueue to collections.deque (PriorityQueue cannot be pickled because it is a synchronous class)
195 | - Changed the "name" part of all namedtuples to be the same name as the attribute key on the module they are declared in. This is necessary in order to be able to de-pickle a namedtuple object.
196 |
197 | - This *may* be a breaking change?
198 |
199 | - No longer store our own logger, because they cannot be picked. Just use the global logger provided by ``sanic.log.logger``
200 |
201 |
202 | 0.6.3.dev20180717
203 | -----------------
204 | - Added listener functions to contextualize plugin,
205 | - added a new example for using sqlalchemy with contextualize plugin
206 | - Misc fixes
207 |
208 |
209 | 0.6.2.dev20180617
210 | -----------------
211 | - SanicPluginsFramework now comes with its own built-in plugin (one of possibly more to come)
212 | - The Contextualize plugin offers the shared context and enhanced middleware functions of SanicPluginsFramework, to regular Sanic users.
213 | - You no longer need to be writing a plugin in order to access features provided by SPF.
214 | - Bump version
215 |
216 |
217 | 0.6.1.dev20180616
218 | -----------------
219 | - Fix flake problem inhibiting tox tests on travis from passing.
220 |
221 |
222 | 0.6.0.dev20180616
223 | -----------------
224 | - Added long-awaited feature:
225 |
226 | - add Plugin Websocket routes
227 | - and add Plugin Static routes
228 |
229 | - This more-or-less completes the feature line-up for SanicPluginsFramework.
230 | - Testing is not in place for these features yet.
231 | - Bump version to 0.6.0.dev20180616
232 |
233 |
234 | 0.5.2.dev20180201
235 | -----------------
236 | - Changed tox runner os env from ``precise`` to ``trusty``.
237 | - Pin pytest to 3.3.2 due to a major release bug in 3.4.0.
238 |
239 |
240 | 0.5.1.dev20180201
241 | -----------------
242 | - Removed uvloop and ujson from requirements. These break on Windows.
243 | - Sanic requires these, but deals with the incompatibility on windows itself.
244 | - Also ensure requirements.txt is included in the wheel package.
245 | - Added python 3.7 to supported python versions.
246 |
247 |
248 | 0.5.0.dev20171225
249 | -----------------
250 | - Merry Christmas!
251 | - Sanic version 0.7.0 has been out for a couple of weeks now. It is now our minimum required version.
252 | - Fixed a bug related to deleting shared context when app is a Blueprint. Thanks @huangxinping!
253 |
254 |
255 | 0.4.5.dev20171113
256 | -----------------
257 | - Fixed error in plugin.log helper. It now calls the correct context .log function.
258 |
259 |
260 | 0.4.4.dev20171107
261 | -----------------
262 | - Bump to version 0.4.4 because 0.4.3 broke, and PyPI wouldn't let me re-upload it with the same version.
263 |
264 |
265 | 0.4.3.dev20171107
266 | -----------------
267 | - Fixed ContextDict to no longer be derived from ``dict``, while at the same time act more like a dictionary.
268 | - Added ability for the request context to hold more than one request at once. Use ``id(request)`` to get the correct request context from the request-specific context dict.
269 |
270 |
271 | 0.4.2.dev20171106
272 | -----------------
273 | - Added a new namedtuple that represents a plugin registration association.
274 | - It is simply a tuple of the plugin instance, and a matching PluginRegistration.
275 |
276 | - This is needed in the Sanic-Restplus port.
277 |
278 | - Allow plugins to choose their own PluginAssociated class.
279 |
280 |
281 | 0.4.1.dev20171103
282 | -----------------
283 | - Ensure each SPF registers only one 'before_server_start' listener, no matter how many time the SPF is used, and how many plugins are registered on the SPF.
284 | - Added a test to ensure logging works, when got the function from the context object.
285 |
286 |
287 | 0.4.0.dev20171103
288 | -----------------
289 | Some big architecture changes.
290 |
291 | Split plugin and framework into separate files.
292 |
293 | We no longer assume the plugin is going to be registered onto only one app/blueprint.
294 |
295 | The plugin can be registered many times, onto many different SPF instances, on different apps.
296 |
297 | This means we can no longer easily get a known context object directly from the plugin instance, now the context object
298 | must be provided by the SPF that is registered on the given app. We also need to pass around the context object a bit
299 | more than we did before. While this change makes the whole framework more complicated, it now actually feels cleaner.
300 |
301 | This _should_ be enough to get Sanic-Cors ported over to SPF.
302 |
303 | Added some tests.
304 |
305 | Fixed some tests.
306 |
307 |
308 | 0.3.3.dev20171102
309 | -----------------
310 | Fixed bug in getting the plugin context object, when using the view/route decorator feature.
311 |
312 | Got decorator-level middleware working. It runs the middleware on a per-view basis if the Plugin is not registered
313 | on the app or blueprint, when decorating a view with a plugin.
314 |
315 |
316 | 0.3.2.dev20171102
317 | -----------------
318 | First pass cut at implementing a view-specific plugin, using a view decorator.
319 |
320 | This is very handy for when you don't want to register a plugin on the whole application (or blueprint),
321 | rather you just want the plugin to run on specific select views/routes. The main driver for this function is for
322 | porting Sanic-CORS plugin to use sanic-plugins-framework, but it will be useful for may other plugins too.
323 |
324 |
325 | 0.3.1.dev20171102
326 | -----------------
327 | Fixed a bug when getting the spf singleton from a Blueprint
328 |
329 | This fixed Legacy-style plugin registration when using blueprints.
330 |
331 |
332 | 0.3.0.dev20171102
333 | -----------------
334 | Plugins can now be applied to Blueprints! This is a game changer!
335 |
336 | A new url_for function for the plugin! This is a handy thing when you need it.
337 |
338 | Added a new section in the examples in the readme.
339 |
340 | Bug fixes.
341 |
342 |
343 | 0.2.0.dev20171102
344 | -----------------
345 | Added a on_before_register hook for plugins, this is called when the plugin gets registered, but _before_ all of
346 | the Plugin's routes, middleware, tasks, and exception handlers are evaluated. This allows the Plugin Author to
347 | dynamically build routes and middleware at runtime based on the passed in configuration.
348 |
349 | Added changelog.
350 |
351 |
352 | 0.1.0.dev20171101
353 | -----------------
354 | More features!
355 |
356 | SPF can only be instantiated once per App now. If you try to create a new SPF for a given app, it will give you back the existing one.
357 |
358 | Plugins can now be registered into SPF by using the plugin's module, and also by passing in the Class name of the plugin. Its very smart.
359 |
360 | Plugins can use the legacy method to register themselves on an app. Like ``sample_plugin = SamplePlugin(app)`` it will work correctly.
361 |
362 | More tests!
363 |
364 | FLAKE8 now runs on build, and _passes_!
365 |
366 | Misc Bug fixes.
367 |
368 |
369 | 0.1.0.20171018-1 (.post1)
370 | -------------------------
371 | Fix readme, add shields to readme
372 |
373 |
374 | 0.1.0.20171018
375 | --------------
376 | Bump version to trigger travis tests, and initial pypi build
377 |
378 |
379 | 0.1.0.dev1
380 | ----------
381 | Initial release, pre-alpha.
382 | Got TOX build working with Python 3.5 and Python 3.6, with pytest tests and flake8
383 |
--------------------------------------------------------------------------------
/tests/test_plugin_static.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import os
3 |
4 | from time import gmtime, strftime
5 |
6 | import pytest
7 |
8 | from sanic_plugin_toolkit import SanicPlugin
9 |
10 |
11 | class TestPlugin(SanicPlugin):
12 | pass
13 |
14 |
15 | # The following tests are taken directly from Sanic source @ v0.8.2
16 | # and modified to test the SanicPlugin, rather than Sanic
17 |
18 | # ------------------------------------------------------------ #
19 | # GET
20 | # ------------------------------------------------------------ #
21 |
22 |
23 | @pytest.fixture(scope="module")
24 | def static_file_directory():
25 | """The static directory to serve"""
26 | current_file = inspect.getfile(inspect.currentframe())
27 | current_directory = os.path.dirname(os.path.abspath(current_file))
28 | static_directory = os.path.join(current_directory, "static")
29 | return static_directory
30 |
31 |
32 | def get_file_path(static_file_directory, file_name):
33 | return os.path.join(static_file_directory, file_name)
34 |
35 |
36 | def get_file_content(static_file_directory, file_name):
37 | """The content of the static file to check"""
38 | with open(get_file_path(static_file_directory, file_name), "rb") as file:
39 | return file.read()
40 |
41 |
42 | @pytest.fixture(scope="module")
43 | def large_file(static_file_directory):
44 | large_file_path = os.path.join(static_file_directory, "large.file")
45 |
46 | size = 2 * 1024 * 1024
47 | with open(large_file_path, "w") as f:
48 | f.write("a" * size)
49 |
50 | yield large_file_path
51 |
52 | os.remove(large_file_path)
53 |
54 |
55 | @pytest.fixture(autouse=True, scope="module")
56 | def symlink(static_file_directory):
57 | src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), "conftest.py"))
58 | symlink = "symlink"
59 | dist = os.path.join(static_file_directory, symlink)
60 | try:
61 | os.remove(dist)
62 | except FileNotFoundError:
63 | pass
64 | os.symlink(src, dist)
65 | yield symlink
66 | os.remove(dist)
67 |
68 |
69 | @pytest.fixture(autouse=True, scope="module")
70 | def hard_link(static_file_directory):
71 | src = os.path.abspath(os.path.join(os.path.dirname(static_file_directory), "conftest.py"))
72 | hard_link = "hard_link"
73 | dist = os.path.join(static_file_directory, hard_link)
74 | try:
75 | os.remove(dist)
76 | except FileNotFoundError:
77 | pass
78 | os.link(src, dist)
79 | yield hard_link
80 | os.remove(dist)
81 |
82 |
83 | @pytest.mark.parametrize(
84 | "file_name",
85 | ["test.file", "decode me.txt", "python.png", "symlink", "hard_link"],
86 | )
87 | def test_static_file(realm, static_file_directory, file_name):
88 | app = realm._app
89 | plugin = TestPlugin()
90 | plugin.static("/testing.file", get_file_path(static_file_directory, file_name))
91 | realm.register_plugin(plugin)
92 | request, response = app._test_manager.test_client.get("/testing.file")
93 | assert response.status == 200
94 | assert response.body == get_file_content(static_file_directory, file_name)
95 |
96 |
97 | @pytest.mark.parametrize("file_name", ["test.html"])
98 | def test_static_file_content_type(realm, static_file_directory, file_name):
99 | app = realm._app
100 | plugin = TestPlugin()
101 | plugin.static(
102 | "/testing.file",
103 | get_file_path(static_file_directory, file_name),
104 | content_type="text/html; charset=utf-8",
105 | )
106 | realm.register_plugin(plugin)
107 | request, response = app._test_manager.test_client.get("/testing.file")
108 | assert response.status == 200
109 | assert response.body == get_file_content(static_file_directory, file_name)
110 | assert response.headers["Content-Type"] == "text/html; charset=utf-8"
111 |
112 |
113 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "symlink", "hard_link"])
114 | @pytest.mark.parametrize("base_uri", ["/static", "", "/dir"])
115 | def test_static_directory(realm, file_name, base_uri, static_file_directory):
116 | app = realm._app
117 | plugin = TestPlugin()
118 | plugin.static(base_uri, static_file_directory)
119 | realm.register_plugin(plugin)
120 | request, response = app._test_manager.test_client.get(uri="{}/{}".format(base_uri, file_name))
121 | assert response.status == 200
122 | assert response.body == get_file_content(static_file_directory, file_name)
123 |
124 |
125 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
126 | def test_static_head_request(realm, file_name, static_file_directory):
127 | app = realm._app
128 | plugin = TestPlugin()
129 | plugin.static(
130 | "/testing.file",
131 | get_file_path(static_file_directory, file_name),
132 | use_content_range=True,
133 | )
134 | realm.register_plugin(plugin)
135 | request, response = app._test_manager.test_client.head("/testing.file")
136 | assert response.status == 200
137 | assert "Accept-Ranges" in response.headers
138 | assert "Content-Length" in response.headers
139 | assert int(response.headers["Content-Length"]) == len(get_file_content(static_file_directory, file_name))
140 |
141 |
142 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
143 | def test_static_content_range_correct(realm, file_name, static_file_directory):
144 | app = realm._app
145 | plugin = TestPlugin()
146 | plugin.static(
147 | "/testing.file",
148 | get_file_path(static_file_directory, file_name),
149 | use_content_range=True,
150 | )
151 | realm.register_plugin(plugin)
152 | headers = {"Range": "bytes=12-19"}
153 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
154 | assert response.status == 206
155 | assert "Content-Length" in response.headers
156 | assert "Content-Range" in response.headers
157 | static_content = bytes(get_file_content(static_file_directory, file_name))[12:20]
158 | assert int(response.headers["Content-Length"]) == len(static_content)
159 | assert response.body == static_content
160 |
161 |
162 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
163 | def test_static_content_range_front(realm, file_name, static_file_directory):
164 | app = realm._app
165 | plugin = TestPlugin()
166 | plugin.static(
167 | "/testing.file",
168 | get_file_path(static_file_directory, file_name),
169 | use_content_range=True,
170 | )
171 | realm.register_plugin(plugin)
172 | headers = {"Range": "bytes=12-"}
173 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
174 | assert response.status == 206
175 | assert "Content-Length" in response.headers
176 | assert "Content-Range" in response.headers
177 | static_content = bytes(get_file_content(static_file_directory, file_name))[12:]
178 | assert int(response.headers["Content-Length"]) == len(static_content)
179 | assert response.body == static_content
180 |
181 |
182 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
183 | def test_static_content_range_back(realm, file_name, static_file_directory):
184 | app = realm._app
185 | plugin = TestPlugin()
186 | plugin.static(
187 | "/testing.file",
188 | get_file_path(static_file_directory, file_name),
189 | use_content_range=True,
190 | )
191 | realm.register_plugin(plugin)
192 | headers = {"Range": "bytes=-12"}
193 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
194 | assert response.status == 206
195 | assert "Content-Length" in response.headers
196 | assert "Content-Range" in response.headers
197 | static_content = bytes(get_file_content(static_file_directory, file_name))[-12:]
198 | assert int(response.headers["Content-Length"]) == len(static_content)
199 | assert response.body == static_content
200 |
201 |
202 | @pytest.mark.parametrize("use_modified_since", [True, False])
203 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
204 | def test_static_content_range_empty(realm, file_name, static_file_directory, use_modified_since):
205 | app = realm._app
206 | plugin = TestPlugin()
207 | plugin.static(
208 | "/testing.file",
209 | get_file_path(static_file_directory, file_name),
210 | use_content_range=True,
211 | use_modified_since=use_modified_since,
212 | )
213 | realm.register_plugin(plugin)
214 | request, response = app._test_manager.test_client.get("/testing.file")
215 | assert response.status == 200
216 | assert "Content-Length" in response.headers
217 | assert "Content-Range" not in response.headers
218 | assert int(response.headers["Content-Length"]) == len(get_file_content(static_file_directory, file_name))
219 | assert response.body == bytes(get_file_content(static_file_directory, file_name))
220 |
221 |
222 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
223 | def test_static_content_range_error(realm, file_name, static_file_directory):
224 | app = realm._app
225 | plugin = TestPlugin()
226 | plugin.static(
227 | "/testing.file",
228 | get_file_path(static_file_directory, file_name),
229 | use_content_range=True,
230 | )
231 | realm.register_plugin(plugin)
232 | headers = {"Range": "bytes=1-0"}
233 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
234 | assert response.status == 416
235 | assert "Content-Length" in response.headers
236 | assert "Content-Range" in response.headers
237 | assert response.headers["Content-Range"] == "bytes */%s" % (
238 | len(get_file_content(static_file_directory, file_name)),
239 | )
240 |
241 |
242 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
243 | def test_static_content_range_invalid_unit(realm, file_name, static_file_directory):
244 | app = realm._app
245 | plugin = TestPlugin()
246 | plugin.static(
247 | "/testing.file",
248 | get_file_path(static_file_directory, file_name),
249 | use_content_range=True,
250 | )
251 | realm.register_plugin(plugin)
252 | unit = "bit"
253 | headers = {"Range": "{}=1-0".format(unit)}
254 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
255 |
256 | assert response.status == 416
257 | assert "{} is not a valid Range Type".format(unit) in response.text
258 |
259 |
260 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
261 | def test_static_content_range_invalid_start(realm, file_name, static_file_directory):
262 | app = realm._app
263 | plugin = TestPlugin()
264 | plugin.static(
265 | "/testing.file",
266 | get_file_path(static_file_directory, file_name),
267 | use_content_range=True,
268 | )
269 | realm.register_plugin(plugin)
270 | start = "start"
271 | headers = {"Range": "bytes={}-0".format(start)}
272 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
273 |
274 | assert response.status == 416
275 | assert "'{}' is invalid for Content Range".format(start) in response.text
276 |
277 |
278 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
279 | def test_static_content_range_invalid_end(realm, file_name, static_file_directory):
280 | app = realm._app
281 | plugin = TestPlugin()
282 | plugin.static(
283 | "/testing.file",
284 | get_file_path(static_file_directory, file_name),
285 | use_content_range=True,
286 | )
287 | realm.register_plugin(plugin)
288 | end = "end"
289 | headers = {"Range": "bytes=1-{}".format(end)}
290 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
291 |
292 | assert response.status == 416
293 | assert "'{}' is invalid for Content Range".format(end) in response.text
294 |
295 |
296 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt"])
297 | def test_static_content_range_invalid_parameters(realm, file_name, static_file_directory):
298 | app = realm._app
299 | plugin = TestPlugin()
300 | plugin.static(
301 | "/testing.file",
302 | get_file_path(static_file_directory, file_name),
303 | use_content_range=True,
304 | )
305 | realm.register_plugin(plugin)
306 | headers = {"Range": "bytes=-"}
307 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
308 |
309 | assert response.status == 416
310 | assert "Invalid for Content Range parameters" in response.text
311 |
312 |
313 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"])
314 | def test_static_file_specified_host(realm, static_file_directory, file_name):
315 | app = realm._app
316 | plugin = TestPlugin()
317 | plugin.static(
318 | "/testing.file",
319 | get_file_path(static_file_directory, file_name),
320 | host="www.example.com",
321 | )
322 | realm.register_plugin(plugin)
323 | headers = {"Host": "www.example.com"}
324 | request, response = app._test_manager.test_client.get("/testing.file", headers=headers)
325 | assert response.status == 200
326 | assert response.body == get_file_content(static_file_directory, file_name)
327 | request, response = app._test_manager.test_client.get("/testing.file")
328 | assert response.status == 404
329 |
330 |
331 | @pytest.mark.parametrize("use_modified_since", [True, False])
332 | @pytest.mark.parametrize("stream_large_files", [True, 1024])
333 | @pytest.mark.parametrize("file_name", ["test.file", "large.file"])
334 | def test_static_stream_large_file(
335 | realm,
336 | static_file_directory,
337 | file_name,
338 | use_modified_since,
339 | stream_large_files,
340 | large_file,
341 | ):
342 | app = realm._app
343 | plugin = TestPlugin()
344 | plugin.static(
345 | "/testing.file",
346 | get_file_path(static_file_directory, file_name),
347 | use_modified_since=use_modified_since,
348 | stream_large_files=stream_large_files,
349 | )
350 | realm.register_plugin(plugin)
351 | request, response = app._test_manager.test_client.get("/testing.file")
352 |
353 | assert response.status == 200
354 | assert response.body == get_file_content(static_file_directory, file_name)
355 |
356 |
357 | @pytest.mark.parametrize("file_name", ["test.file", "decode me.txt", "python.png"])
358 | def test_use_modified_since(realm, static_file_directory, file_name):
359 |
360 | file_stat = os.stat(get_file_path(static_file_directory, file_name))
361 | modified_since = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(file_stat.st_mtime))
362 | app = realm._app
363 | plugin = TestPlugin()
364 | plugin.static(
365 | "/testing.file",
366 | get_file_path(static_file_directory, file_name),
367 | use_modified_since=True,
368 | )
369 | realm.register_plugin(plugin)
370 | request, response = app._test_manager.test_client.get(
371 | "/testing.file", headers={"If-Modified-Since": modified_since}
372 | )
373 |
374 | assert response.status == 304
375 |
376 |
377 | def test_file_not_found(realm, static_file_directory):
378 | app = realm._app
379 | plugin = TestPlugin()
380 | plugin.static("/static", static_file_directory)
381 | realm.register_plugin(plugin)
382 | request, response = app._test_manager.test_client.get("/static/not_found")
383 |
384 | assert response.status == 404
385 | assert "File not found" in response.text
386 |
387 |
388 | @pytest.mark.parametrize("static_name", ["_static_name", "static"])
389 | @pytest.mark.parametrize("file_name", ["test.html"])
390 | def test_static_name(realm, static_file_directory, static_name, file_name):
391 | app = realm._app
392 | plugin = TestPlugin()
393 | plugin.static("/static", static_file_directory, name=static_name)
394 | realm.register_plugin(plugin)
395 | request, response = app._test_manager.test_client.get("/static/{}".format(file_name))
396 |
397 | assert response.status == 200
398 |
--------------------------------------------------------------------------------
/sanic_plugin_toolkit/plugin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import importlib
3 |
4 | from collections import defaultdict, deque, namedtuple
5 | from distutils.version import LooseVersion
6 | from functools import update_wrapper
7 | from inspect import isawaitable
8 | from typing import Type
9 |
10 | from sanic import Blueprint, Sanic
11 | from sanic import __version__ as sanic_version
12 |
13 |
14 | SANIC_VERSION = LooseVersion(sanic_version)
15 | SANIC_21_6_0 = LooseVersion("21.6.0")
16 | SANIC_21_9_0 = LooseVersion("21.9.0")
17 |
18 | CRITICAL = 50
19 | ERROR = 40
20 | WARNING = 30
21 | INFO = 20
22 | DEBUG = 10
23 |
24 | FutureMiddleware = namedtuple('FutureMiddleware', ['middleware', 'args', 'kwargs'])
25 | FutureRoute = namedtuple('FutureRoute', ['handler', 'uri', 'args', 'kwargs'])
26 | FutureWebsocket = namedtuple('FutureWebsocket', ['handler', 'uri', 'args', 'kwargs'])
27 | FutureStatic = namedtuple('FutureStatic', ['uri', 'file_or_dir', 'args', 'kwargs'])
28 | FutureException = namedtuple('FutureException', ['handler', 'exceptions', 'kwargs'])
29 | PluginRegistration = namedtuple('PluginRegistration', ['realm', 'plugin_name', 'url_prefix'])
30 | PluginAssociated = namedtuple('PluginAssociated', ['plugin', 'reg'])
31 |
32 |
33 | class SanicPlugin(object):
34 | __slots__ = (
35 | 'registrations',
36 | '_routes',
37 | '_ws',
38 | '_static',
39 | '_middlewares',
40 | '_exceptions',
41 | '_listeners',
42 | '_initialized',
43 | '__weakref__',
44 | )
45 |
46 | AssociatedTuple: Type[object] = PluginAssociated
47 |
48 | # Decorator
49 | def middleware(self, *args, **kwargs):
50 | """Decorate and register middleware
51 | :param args: captures all of the positional arguments passed in
52 | :type args: tuple(Any)
53 | :param kwargs: captures the keyword arguments passed in
54 | :type kwargs: dict(Any)
55 | :return: The middleware function to use as the decorator
56 | :rtype: fn
57 | """
58 | kwargs.setdefault('priority', 5)
59 | kwargs.setdefault('relative', None)
60 | kwargs.setdefault('attach_to', None)
61 | kwargs.setdefault('with_context', False)
62 | if len(args) == 1 and callable(args[0]):
63 | middle_f = args[0]
64 | self._middlewares.append(FutureMiddleware(middle_f, args=tuple(), kwargs=kwargs))
65 | return middle_f
66 |
67 | def wrapper(middleware_f):
68 | self._middlewares.append(FutureMiddleware(middleware_f, args=args, kwargs=kwargs))
69 | return middleware_f
70 |
71 | return wrapper
72 |
73 | def exception(self, *args, **kwargs):
74 | """Decorate and register an exception handler
75 | :param args: captures all of the positional arguments passed in
76 | :type args: tuple(Any)
77 | :param kwargs: captures the keyword arguments passed in
78 | :type kwargs: dict(Any)
79 | :return: The exception function to use as the decorator
80 | :rtype: fn
81 | """
82 | if len(args) == 1 and callable(args[0]):
83 | if isinstance(args[0], type) and issubclass(args[0], Exception):
84 | pass
85 | else: # pragma: no cover
86 | raise RuntimeError("Cannot use the @exception decorator without arguments")
87 |
88 | def wrapper(handler_f):
89 | self._exceptions.append(FutureException(handler_f, exceptions=args, kwargs=kwargs))
90 | return handler_f
91 |
92 | return wrapper
93 |
94 | def listener(self, event, *args, **kwargs):
95 | """Create a listener from a decorated function.
96 | :param event: Event to listen to.
97 | :type event: str
98 | :param args: captures all of the positional arguments passed in
99 | :type args: tuple(Any)
100 | :param kwargs: captures the keyword arguments passed in
101 | :type kwargs: dict(Any)
102 | :return: The function to use as the listener
103 | :rtype: fn
104 | """
105 | if len(args) == 1 and callable(args[0]): # pragma: no cover
106 | raise RuntimeError("Cannot use the @listener decorator without arguments")
107 |
108 | def wrapper(listener_f):
109 | if len(kwargs) > 0:
110 | listener_f = (listener_f, kwargs)
111 | self._listeners[event].append(listener_f)
112 | return listener_f
113 |
114 | return wrapper
115 |
116 | def route(self, uri, *args, **kwargs):
117 | """Create a plugin route from a decorated function.
118 | :param uri: endpoint at which the route will be accessible.
119 | :type uri: str
120 | :param args: captures all of the positional arguments passed in
121 | :type args: tuple(Any)
122 | :param kwargs: captures the keyword arguments passed in
123 | :type kwargs: dict(Any)
124 | :return: The function to use as the decorator
125 | :rtype: fn
126 | """
127 | if len(args) == 0 and callable(uri): # pragma: no cover
128 | raise RuntimeError("Cannot use the @route decorator without arguments.")
129 | kwargs.setdefault('methods', frozenset({'GET'}))
130 | kwargs.setdefault('host', None)
131 | kwargs.setdefault('strict_slashes', False)
132 | kwargs.setdefault('stream', False)
133 | kwargs.setdefault('name', None)
134 | kwargs.setdefault('version', None)
135 | kwargs.setdefault('ignore_body', False)
136 | kwargs.setdefault('websocket', False)
137 | kwargs.setdefault('subprotocols', None)
138 | kwargs.setdefault('unquote', False)
139 | kwargs.setdefault('static', False)
140 | if SANIC_21_6_0 <= SANIC_VERSION:
141 | kwargs.setdefault('version_prefix', '/v')
142 | if SANIC_21_9_0 <= SANIC_VERSION:
143 | kwargs.setdefault('error_format', None)
144 |
145 | def wrapper(handler_f):
146 | self._routes.append(FutureRoute(handler_f, uri, args, kwargs))
147 | return handler_f
148 |
149 | return wrapper
150 |
151 | def websocket(self, uri, *args, **kwargs):
152 | """Create a websocket route from a decorated function
153 | deprecated. now use @route("/path",websocket=True)
154 | """
155 | kwargs["websocket"] = True
156 | return self.route(uri, *args, **kwargs)
157 |
158 | def static(self, uri, file_or_directory, *args, **kwargs):
159 | """Create a websocket route from a decorated function
160 | :param uri: endpoint at which the socket endpoint will be accessible.
161 | :type uri: str
162 | :param args: captures all of the positional arguments passed in
163 | :type args: tuple(Any)
164 | :param kwargs: captures the keyword arguments passed in
165 | :type kwargs: dict(Any)
166 | :return: The function to use as the decorator
167 | :rtype: fn
168 | """
169 |
170 | kwargs.setdefault('pattern', r'/?.+')
171 | kwargs.setdefault('use_modified_since', True)
172 | kwargs.setdefault('use_content_range', False)
173 | kwargs.setdefault('stream_large_files', False)
174 | kwargs.setdefault('name', 'static')
175 | kwargs.setdefault('host', None)
176 | kwargs.setdefault('strict_slashes', None)
177 | kwargs.setdefault('content_type', None)
178 | if SANIC_21_9_0 <= SANIC_VERSION:
179 | kwargs.setdefault('resource_type', None)
180 |
181 | self._static.append(FutureStatic(uri, file_or_directory, args, kwargs))
182 |
183 | def on_before_registered(self, context, *args, **kwargs):
184 | pass
185 |
186 | def on_registered(self, context, reg, *args, **kwargs):
187 | pass
188 |
189 | def find_plugin_registration(self, realm):
190 | if isinstance(realm, PluginRegistration):
191 | return realm
192 | for reg in self.registrations:
193 | (r, n, u) = reg
194 | if r is not None and r == realm:
195 | return reg
196 | raise KeyError("Plugin registration not found")
197 |
198 | def first_plugin_context(self):
199 | """Returns the context is associated with the first app this plugin was
200 | registered on"""
201 | # Note, because registrations are stored in a set, its not _really_
202 | # the first one, but whichever one it sees first in the set.
203 | first_realm_reg = next(iter(self.registrations))
204 | return self.get_context_from_realm(first_realm_reg)
205 |
206 | def get_context_from_realm(self, realm):
207 | rt = RuntimeError("Cannot use the plugin's Context before it is registered.")
208 | if isinstance(realm, PluginRegistration):
209 | reg = realm
210 | else:
211 | try:
212 | reg = self.find_plugin_registration(realm)
213 | except LookupError:
214 | raise rt
215 | (r, n, u) = reg
216 | try:
217 | return r.get_context(n)
218 | except KeyError as k:
219 | raise k
220 | except AttributeError:
221 | raise rt
222 |
223 | def get_app_from_realm_context(self, realm):
224 | rt = RuntimeError("Cannot get the app from Realm before this plugin is registered on the Realm.")
225 | if isinstance(realm, PluginRegistration):
226 | reg = realm
227 | else:
228 | try:
229 | reg = self.find_plugin_registration(realm)
230 | except LookupError:
231 | raise rt
232 | context = self.get_context_from_realm(reg)
233 | try:
234 | app = context.app
235 | except (LookupError, AttributeError):
236 | raise rt
237 | return app
238 |
239 | def resolve_url_for(self, realm, view_name, *args, **kwargs):
240 | try:
241 | reg = self.find_plugin_registration(realm)
242 | except LookupError:
243 | raise RuntimeError("Cannot resolve URL because this plugin is not registered on the PluginRealm.")
244 | (realm, name, url_prefix) = reg
245 | app = self.get_app_from_realm_context(reg)
246 | if app is None:
247 | return None
248 | if isinstance(app, Blueprint):
249 | self.warning("Cannot use url_for when plugin is registered on a Blueprint. Use `app.url_for` instead.")
250 | return None
251 | constructed_name = "{}.{}".format(name, view_name)
252 | return app.url_for(constructed_name, *args, **kwargs)
253 |
254 | def log(self, realm, level, message, *args, **kwargs):
255 | try:
256 | reg = self.find_plugin_registration(realm)
257 | except LookupError:
258 | raise RuntimeError("Cannot log using this plugin, because this plugin is not registered on the Realm.")
259 | context = self.get_context_from_realm(reg)
260 | return context.log(level, message, *args, reg=self, **kwargs)
261 |
262 | def debug(self, message, *args, **kwargs):
263 | return self.log(DEBUG, message, *args, **kwargs)
264 |
265 | def info(self, message, *args, **kwargs):
266 | return self.log(INFO, message, *args, **kwargs)
267 |
268 | def warning(self, message, *args, **kwargs):
269 | return self.log(WARNING, message, *args, **kwargs)
270 |
271 | def error(self, message, *args, **kwargs):
272 | return self.log(ERROR, message, *args, **kwargs)
273 |
274 | def critical(self, message, *args, **kwargs):
275 | return self.log(CRITICAL, message, *args, **kwargs)
276 |
277 | @classmethod
278 | def decorate(cls, app, *args, run_middleware=False, with_context=False, **kwargs):
279 | """
280 | This is a decorator that can be used to apply this plugin to a specific
281 | route/view on your app, rather than the whole app.
282 | :param app:
283 | :type app: Sanic | Blueprint
284 | :param args:
285 | :type args: tuple(Any)
286 | :param run_middleware:
287 | :type run_middleware: bool
288 | :param with_context:
289 | :type with_context: bool
290 | :param kwargs:
291 | :param kwargs: dict(Any)
292 | :return: the decorated route/view
293 | :rtype: fn
294 | """
295 | from sanic_plugin_toolkit.realm import SanicPluginRealm
296 |
297 | realm = SanicPluginRealm(app) # get the singleton from the app
298 | try:
299 | assoc = realm.register_plugin(cls, skip_reg=True)
300 | except ValueError as e:
301 | # this is normal, if this plugin has been registered previously
302 | assert e.args and len(e.args) > 1
303 | assoc = e.args[1]
304 | (plugin, reg) = assoc
305 | # plugin may not actually be registered
306 | inst = realm.get_plugin_inst(plugin)
307 | # registered might be True, False or None at this point
308 | regd = True if inst else None
309 | if regd is True:
310 | # middleware will be run on this route anyway, because the plugin
311 | # is registered on the app. Turn it off on the route-level.
312 | run_middleware = False
313 | req_middleware = deque()
314 | resp_middleware = deque()
315 | if run_middleware:
316 | for i, m in enumerate(plugin._middlewares):
317 | attach_to = m.kwargs.pop('attach_to', 'request')
318 | priority = m.kwargs.pop('priority', 5)
319 | with_context = m.kwargs.pop('with_context', False)
320 | mw_handle_fn = m.middleware
321 | if attach_to == 'response':
322 | relative = m.kwargs.pop('relative', 'post')
323 | if relative == "pre":
324 | mw = (0, 0 - priority, 0 - i, mw_handle_fn, with_context, m.args, m.kwargs)
325 | else: # relative = "post"
326 | mw = (1, 0 - priority, 0 - i, mw_handle_fn, with_context, m.args, m.kwargs)
327 | resp_middleware.append(mw)
328 | else: # attach_to = "request"
329 | relative = m.kwargs.pop('relative', 'pre')
330 | if relative == "post":
331 | mw = (1, priority, i, mw_handle_fn, with_context, m.args, m.kwargs)
332 | else: # relative = "pre"
333 | mw = (0, priority, i, mw_handle_fn, with_context, m.args, m.kwargs)
334 | req_middleware.append(mw)
335 |
336 | req_middleware = tuple(sorted(req_middleware))
337 | resp_middleware = tuple(sorted(resp_middleware))
338 |
339 | def _decorator(f):
340 | nonlocal realm, plugin, regd, run_middleware, with_context
341 | nonlocal req_middleware, resp_middleware, args, kwargs
342 |
343 | async def wrapper(request, *a, **kw):
344 | nonlocal realm, plugin, regd, run_middleware, with_context
345 | nonlocal req_middleware, resp_middleware, f, args, kwargs
346 | # the plugin was not registered on the app, it might be now
347 | if regd is None:
348 | _inst = realm.get_plugin_inst(plugin)
349 | regd = _inst is not None
350 |
351 | context = plugin.get_context_from_realm(realm)
352 | if run_middleware and not regd and len(req_middleware) > 0:
353 | for (_a, _p, _i, handler, with_context, args, kwargs) in req_middleware:
354 | if with_context:
355 | resp = handler(request, *args, context=context, **kwargs)
356 | else:
357 | resp = handler(request, *args, **kwargs)
358 | if isawaitable(resp):
359 | resp = await resp
360 | if resp:
361 | return
362 |
363 | response = await plugin.route_wrapper(
364 | f, request, context, a, kw, *args, with_context=with_context, **kwargs
365 | )
366 | if isawaitable(response):
367 | response = await response
368 | if run_middleware and not regd and len(resp_middleware) > 0:
369 | for (_a, _p, _i, handler, with_context, args, kwargs) in resp_middleware:
370 | if with_context:
371 | _resp = handler(request, response, *args, context=context, **kwargs)
372 | else:
373 | _resp = handler(request, response, *args, **kwargs)
374 | if isawaitable(_resp):
375 | _resp = await _resp
376 | if _resp:
377 | response = _resp
378 | break
379 | return response
380 |
381 | return update_wrapper(wrapper, f)
382 |
383 | return _decorator
384 |
385 | async def route_wrapper(
386 | self, route, request, context, request_args, request_kw, *decorator_args, with_context=None, **decorator_kw
387 | ):
388 | """This is the function that is called when a route is decorated with
389 | your plugin decorator. Context will normally be None, but the user
390 | can pass use_context=True so the route will get the plugin
391 | context
392 | """
393 | # by default, do nothing, just run the wrapped function
394 | if with_context:
395 | resp = route(request, context, *request_args, **request_kw)
396 | else:
397 | resp = route(request, *request_args, **request_kw)
398 | if isawaitable(resp):
399 | resp = await resp
400 | return resp
401 |
402 | def __new__(cls, *args, **kwargs):
403 | # making a bold assumption here.
404 | # Assuming that if a sanic plugin is initialized using
405 | # `MyPlugin(app)`, then the user is attempting to do a legacy plugin
406 | # instantiation, aka Flask-Style plugin instantiation.
407 | if args and len(args) > 0 and (isinstance(args[0], Sanic) or isinstance(args[0], Blueprint)):
408 | app = args[0]
409 | try:
410 | mod_name = cls.__module__
411 | mod = importlib.import_module(mod_name)
412 | assert mod
413 | except (ImportError, AssertionError):
414 | raise RuntimeError(
415 | "Failed attempting a legacy plugin instantiation. "
416 | "Cannot find the module this plugin belongs to."
417 | )
418 | # Get the sanic_plugin_toolkit singleton from this app
419 | from sanic_plugin_toolkit.realm import SanicPluginRealm
420 |
421 | realm = SanicPluginRealm(app)
422 | # catch cases like when the module is "__main__" or
423 | # "__call__" or "__init__"
424 | if mod_name.startswith("__"):
425 | # In this case, we cannot use the module to register the
426 | # plugin. Try to use the class method.
427 | assoc = realm.register_plugin(cls, *args, **kwargs)
428 | else:
429 | assoc = realm.register_plugin(mod, *args, **kwargs)
430 | return assoc
431 | self = super(SanicPlugin, cls).__new__(cls)
432 | try:
433 | self._initialized # initialized may be True or Unknown
434 | except AttributeError:
435 | self._initialized = False
436 | return self
437 |
438 | def is_registered_in_realm(self, check_realm):
439 | for reg in self.registrations:
440 | (realm, name, url) = reg
441 | if realm is not None and realm == check_realm:
442 | return True
443 | return False
444 |
445 | def __init__(self, *args, **kwargs):
446 | # Sometimes __init__ can be called twice.
447 | # Ignore it on subsequent times
448 | if self._initialized:
449 | return
450 | super(SanicPlugin, self).__init__(*args, **kwargs)
451 | self._routes = []
452 | self._ws = []
453 | self._static = []
454 | self._middlewares = []
455 | self._exceptions = []
456 | self._listeners = defaultdict(list)
457 | self.registrations = set()
458 | self._initialized = True
459 |
460 | def __getstate__(self):
461 | state_dict = {}
462 | for s in SanicPlugin.__slots__:
463 | state_dict[s] = getattr(self, s)
464 | return state_dict
465 |
466 | def __setstate__(self, state):
467 | for s, v in state.items():
468 | if s == "__weakref__":
469 | if v is None:
470 | continue
471 | else:
472 | raise NotImplementedError("Setting weakrefs on Plugin")
473 | setattr(self, s, v)
474 |
475 | def __reduce__(self):
476 | state_dict = self.__getstate__()
477 | return SanicPlugin.__new__, (self.__class__,), state_dict
478 |
--------------------------------------------------------------------------------
/sanic_plugin_toolkit/realm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import importlib
3 | import re
4 | import sys
5 |
6 | from asyncio import CancelledError
7 | from collections import deque
8 | from distutils.version import LooseVersion
9 | from functools import partial, update_wrapper
10 | from inspect import isawaitable, ismodule
11 | from typing import Any, Dict
12 | from uuid import uuid1
13 |
14 | from sanic import Blueprint, Sanic
15 | from sanic import __version__ as sanic_version
16 | from sanic.exceptions import ServerError
17 | from sanic.log import logger
18 | from sanic.models.futures import FutureException as SanicFutureException
19 | from sanic.models.futures import FutureListener as SanicFutureListener
20 | from sanic.models.futures import FutureMiddleware as SanicFutureMiddleware
21 | from sanic.models.futures import FutureRoute as SanicFutureRoute
22 | from sanic.models.futures import FutureStatic as SanicFutureStatic
23 |
24 |
25 | try:
26 | from sanic.response import BaseHTTPResponse
27 | except ImportError:
28 | from sanic.response import HTTPResponse as BaseHTTPResponse
29 |
30 | from sanic_plugin_toolkit.config import load_config_file
31 | from sanic_plugin_toolkit.context import SanicContext
32 | from sanic_plugin_toolkit.plugin import PluginRegistration, SanicPlugin
33 |
34 |
35 | module = sys.modules[__name__]
36 | CONSTS: Dict[str, Any] = dict()
37 | CONSTS["APP_CONFIG_INSTANCE_KEY"] = APP_CONFIG_INSTANCE_KEY = "__SPTK_INSTANCE"
38 | CONSTS["SPTK_LOAD_INI_KEY"] = SPTK_LOAD_INI_KEY = "SPTK_LOAD_INI"
39 | CONSTS["SPTK_INI_FILE_KEY"] = SPTK_INI_FILE_KEY = "SPTK_INI_FILE"
40 | CONSTS["SANIC_19_12_0"] = SANIC_19_12_0 = LooseVersion("19.12.0")
41 | CONSTS["SANIC_20_12_1"] = SANIC_20_12_1 = LooseVersion("20.12.1")
42 | CONSTS["SANIC_21_3_0"] = SANIC_21_3_0 = LooseVersion("21.3.0")
43 | CONSTS["SANIC_21_12_0"] = SANIC_21_12_0 = LooseVersion("21.12.0")
44 |
45 | # Currently installed sanic version in this environment
46 | SANIC_VERSION = LooseVersion(sanic_version)
47 |
48 | if SANIC_21_12_0 <= SANIC_VERSION:
49 | raise RuntimeError("Sanic-Plugin-Toolkit v1.2 does not work with Sanic v21.12.0")
50 |
51 | CRITICAL = 50
52 | ERROR = 40
53 | WARNING = 30
54 | INFO = 20
55 | DEBUG = 10
56 |
57 | to_snake_case_first_cap_re = re.compile('(.)([A-Z][a-z]+)')
58 | to_snake_case_all_cap_re = re.compile('([a-z0-9])([A-Z])')
59 |
60 |
61 | def to_snake_case(name):
62 | """
63 | Simple helper function.
64 | Changes PascalCase, camelCase, and CAPS_CASE to snake_case.
65 | :param name: variable name to convert
66 | :type name: str
67 | :return: the name of the variable, converted to snake_case
68 | :rtype: str
69 | """
70 | s1 = to_snake_case_first_cap_re.sub(r'\1_\2', name)
71 | return to_snake_case_all_cap_re.sub(r'\1_\2', s1).lower()
72 |
73 |
74 | class SanicPluginRealm(object):
75 | __slots__ = (
76 | '_running',
77 | '_app',
78 | '_plugin_names',
79 | '_contexts',
80 | '_pre_request_middleware',
81 | '_post_request_middleware',
82 | '_pre_response_middleware',
83 | '_post_response_middleware',
84 | '_cleanup_middleware',
85 | '_loop',
86 | '__weakref__',
87 | )
88 |
89 | def log(self, level, message, reg=None, *args, **kwargs):
90 | if reg is not None:
91 | (_, n, _) = reg
92 | message = "{:s}: {:s}".format(str(n), str(message))
93 | return logger.log(level, message, *args, **kwargs)
94 |
95 | def debug(self, message, reg=None, *args, **kwargs):
96 | return self.log(DEBUG, message=message, reg=reg, *args, **kwargs)
97 |
98 | def info(self, message, reg=None, *args, **kwargs):
99 | return self.log(INFO, message=message, reg=reg, *args, **kwargs)
100 |
101 | def warning(self, message, reg=None, *args, **kwargs):
102 | return self.log(WARNING, message=message, reg=reg, *args, **kwargs)
103 |
104 | def error(self, message, reg=None, *args, **kwargs):
105 | return self.log(ERROR, message=message, reg=reg, *args, **kwargs)
106 |
107 | def critical(self, message, reg=None, *args, **kwargs):
108 | return self.log(CRITICAL, message=message, reg=reg, *args, **kwargs)
109 |
110 | def url_for(self, view_name, *args, reg=None, **kwargs):
111 | if reg is not None:
112 | (_, name, url_prefix) = reg
113 | view_name = "{}.{}".format(name, view_name)
114 | app = self._app
115 | if app is None:
116 | return None
117 | if isinstance(app, Blueprint):
118 | bp = app
119 | view_name = "{}.{}".format(app.name, view_name)
120 | return [a.url_for(view_name, *args, **kwargs) for a in bp.apps]
121 | return app.url_for(view_name, *args, **kwargs)
122 |
123 | def _get_realm_plugin(self, plugin):
124 | if isinstance(plugin, str):
125 | if plugin not in self._plugin_names:
126 | self.warning("Cannot lookup that plugin by its name.")
127 | return None
128 | name = plugin
129 | else:
130 | reg = plugin.find_plugin_registration(self)
131 | (_, name, _) = reg
132 | _p_context = self._plugins_context
133 | try:
134 | _plugin_reg = _p_context[name]
135 | except KeyError as k:
136 | self.warning("Plugin not found!")
137 | raise k
138 | return _plugin_reg
139 |
140 | def get_plugin_inst(self, plugin):
141 | _plugin_reg = self._get_realm_plugin(plugin)
142 | try:
143 | inst = _plugin_reg['instance']
144 | except KeyError:
145 | self.warning("Plugin is not registered properly")
146 | inst = None
147 | return inst
148 |
149 | def get_plugin_assoc(self, plugin):
150 | _plugin_reg = self._get_realm_plugin(plugin)
151 | p = _plugin_reg['instance']
152 | reg = _plugin_reg['reg']
153 | associated_tuple = p.AssociatedTuple
154 | return associated_tuple(p, reg)
155 |
156 | def register_plugin(self, plugin, *args, name=None, skip_reg=False, **kwargs):
157 | assert not self._running, "Cannot add, remove, or change plugins " "after the App has started serving."
158 | assert plugin, "Plugin must be a valid type! Do not pass in `None` " "or `False`"
159 |
160 | if isinstance(plugin, type):
161 | # We got passed in a Class. That's ok, we can handle this!
162 | module_name = getattr(plugin, '__module__')
163 | class_name = getattr(plugin, '__name__')
164 | lower_class = to_snake_case(class_name)
165 | try:
166 | mod = importlib.import_module(module_name)
167 | try:
168 | plugin = getattr(mod, lower_class)
169 | except AttributeError:
170 | plugin = mod # try the module-based resolution next
171 | except ImportError:
172 | raise
173 |
174 | if ismodule(plugin):
175 | # We got passed in a module. That's ok, we can handle this!
176 | try: # look for '.instance' on the module
177 | plugin = getattr(plugin, 'instance')
178 | assert plugin is not None
179 | except (AttributeError, AssertionError):
180 | # now look for the same name,
181 | # like my_module.my_module on the module.
182 | try:
183 | plugin_module_name = getattr(plugin, '__name__')
184 | assert plugin_module_name and len(plugin_module_name) > 0
185 | plugin_module_name = plugin_module_name.split('.')[-1]
186 | plugin = getattr(plugin, plugin_module_name)
187 | assert plugin is not None
188 | except (AttributeError, AssertionError):
189 | raise RuntimeError("Cannot import this module as a Sanic Plugin.")
190 |
191 | assert isinstance(plugin, SanicPlugin), "Plugin must be derived from SanicPlugin"
192 | if name is None:
193 | try:
194 | name = str(plugin.__class__.__name__)
195 | assert name is not None
196 | except (AttributeError, AssertionError, ValueError, KeyError):
197 | logger.warning("Cannot determine a name for {}, using UUID.".format(repr(plugin)))
198 | name = str(uuid1(None, None))
199 | assert isinstance(name, str), "Plugin name must be a python unicode string!"
200 |
201 | associated_tuple = plugin.AssociatedTuple
202 |
203 | if name in self._plugin_names: # we're already registered on this Realm
204 | reg = plugin.find_plugin_registration(self)
205 | assoc = associated_tuple(plugin, reg)
206 | raise ValueError("Plugin {:s} is already registered!".format(name), assoc)
207 | if plugin.is_registered_in_realm(self):
208 | raise RuntimeError(
209 | "Plugin already shows it is registered to this " "sanic_plugin_toolkit, maybe under a different name?"
210 | )
211 | self._plugin_names.add(name)
212 | shared_context = self.shared_context
213 | self._contexts[name] = context = SanicContext(self, shared_context, {'shared': shared_context})
214 | _p_context = self._plugins_context
215 | _plugin_reg = _p_context.get(name, None)
216 | if _plugin_reg is None:
217 | _p_context[name] = _plugin_reg = _p_context.create_child_context()
218 | _plugin_reg['name'] = name
219 | _plugin_reg['context'] = context
220 | if skip_reg:
221 | dummy_reg = PluginRegistration(realm=self, plugin_name=name, url_prefix=None)
222 | context['log'] = partial(self.log, reg=dummy_reg)
223 | context['url_for'] = partial(self.url_for, reg=dummy_reg)
224 | plugin.registrations.add(dummy_reg)
225 | # This indicates the plugin is not registered on the app
226 | _plugin_reg['instance'] = None
227 | _plugin_reg['reg'] = None
228 | return associated_tuple(plugin, dummy_reg)
229 | if _plugin_reg.get('instance', False):
230 | raise RuntimeError("The plugin we are trying to register already " "has a known instance!")
231 | reg = self._register_helper(plugin, context, *args, _realm=self, _plugin_name=name, **kwargs)
232 | _plugin_reg['instance'] = plugin
233 | _plugin_reg['reg'] = reg
234 | return associated_tuple(plugin, reg)
235 |
236 | @staticmethod
237 | def _register_exception_helper(e, _realm, plugin, context):
238 | return (
239 | _realm._plugin_register_bp_exception(e.handler, plugin, context, *e.exceptions, **e.kwargs)
240 | if isinstance(_realm._app, Blueprint)
241 | else _realm._plugin_register_app_exception(e.handler, plugin, context, *e.exceptions, **e.kwargs)
242 | )
243 |
244 | @staticmethod
245 | def _register_listener_helper(event, listener, _realm, plugin, context, **kwargs):
246 | return (
247 | _realm._plugin_register_bp_listener(event, listener, plugin, context, **kwargs)
248 | if isinstance(_realm._app, Blueprint)
249 | else _realm._plugin_register_app_listener(event, listener, plugin, context, **kwargs)
250 | )
251 |
252 | @staticmethod
253 | def _register_middleware_helper(m, _realm, plugin, context):
254 | return _realm._plugin_register_middleware(m.middleware, plugin, context, *m.args, **m.kwargs)
255 |
256 | @staticmethod
257 | def _register_route_helper(r, _realm, plugin, context, _p_name, _url_prefix):
258 | # Prepend the plugin URI prefix if available
259 | uri = _url_prefix + r.uri if _url_prefix else r.uri
260 | uri = uri[1:] if uri.startswith('//') else uri
261 | # attach the plugin name to the handler so that it can be
262 | # prefixed properly in the router
263 | _app = _realm._app
264 | handler_name = str(r.handler.__name__)
265 | plugin_prefix = _p_name + '.'
266 | kwargs = r.kwargs
267 | if isinstance(_app, Blueprint):
268 | # blueprint always handles adding __blueprintname__
269 | # So we identify ourselves here a different way.
270 | # r.handler.__name__ = "{}.{}".format(_p_name, handler_name)
271 | if "name" not in kwargs or kwargs["name"] is None:
272 | kwargs["name"] = plugin_prefix + handler_name
273 | elif not kwargs["name"].startswith(plugin_prefix):
274 | kwargs["name"] = plugin_prefix + kwargs["name"]
275 | _realm._plugin_register_bp_route(r.handler, plugin, context, uri, *r.args, **kwargs)
276 | else:
277 | if "name" not in kwargs or kwargs["name"] is None:
278 | kwargs["name"] = plugin_prefix + handler_name
279 | elif not kwargs["name"].startswith(plugin_prefix):
280 | kwargs["name"] = plugin_prefix + kwargs["name"]
281 | _realm._plugin_register_app_route(r.handler, plugin, context, uri, *r.args, **kwargs)
282 |
283 | @staticmethod
284 | def _register_static_helper(s, _realm, plugin, context, _p_name, _url_prefix):
285 | # attach the plugin name to the static route so that it can be
286 | # prefixed properly in the router
287 | kwargs = s.kwargs
288 | name = kwargs.pop('name', 'static')
289 | plugin_prefix = _p_name + '.'
290 | _app = _realm._app
291 | if not name.startswith(plugin_prefix):
292 | name = plugin_prefix + name
293 | # Prepend the plugin URI prefix if available
294 | uri = _url_prefix + s.uri if _url_prefix else s.uri
295 | uri = uri[1:] if uri.startswith('//') else uri
296 | kwargs["name"] = name
297 | return (
298 | _realm._plugin_register_bp_static(uri, s.file_or_dir, plugin, context, *s.args, **kwargs)
299 | if isinstance(_app, Blueprint)
300 | else _realm._plugin_register_app_static(uri, s.file_or_dir, plugin, context, *s.args, **kwargs)
301 | )
302 |
303 | @staticmethod
304 | def _register_helper(plugin, context, *args, _realm=None, _plugin_name=None, _url_prefix=None, **kwargs):
305 | error_str = "Plugin must be initialised using the " "Sanic Plugin Toolkit PluginRealm."
306 | assert _realm is not None, error_str
307 | assert _plugin_name is not None, error_str
308 | _app = _realm._app
309 | assert _app is not None, error_str
310 |
311 | reg = PluginRegistration(realm=_realm, plugin_name=_plugin_name, url_prefix=_url_prefix)
312 | context['log'] = partial(_realm.log, reg=reg)
313 | context['url_for'] = partial(_realm.url_for, reg=reg)
314 | continue_flag = plugin.on_before_registered(context, *args, **kwargs)
315 | if continue_flag is False:
316 | return plugin
317 |
318 | # Routes
319 | [_realm._register_route_helper(r, _realm, plugin, context, _plugin_name, _url_prefix) for r in plugin._routes]
320 |
321 | # Websocket routes
322 | # These are deprecated and should be handled in the _routes_ list above.
323 | [_realm._register_route_helper(w, _realm, plugin, context, _plugin_name, _url_prefix) for w in plugin._ws]
324 |
325 | # Static routes
326 | [_realm._register_static_helper(s, _realm, plugin, context, _plugin_name, _url_prefix) for s in plugin._static]
327 |
328 | # Middleware
329 | [_realm._register_middleware_helper(m, _realm, plugin, context) for m in plugin._middlewares]
330 |
331 | # Exceptions
332 | [_realm._register_exception_helper(e, _realm, plugin, context) for e in plugin._exceptions]
333 |
334 | # Listeners
335 | for event, listeners in plugin._listeners.items():
336 | for listener in listeners:
337 | if isinstance(listener, tuple):
338 | listener, lkw = listener
339 | else:
340 | lkw = {}
341 | _realm._register_listener_helper(event, listener, _realm, plugin, context, **lkw)
342 |
343 | # # this should only ever run once!
344 | plugin.registrations.add(reg)
345 | plugin.on_registered(context, reg, *args, **kwargs)
346 |
347 | return reg
348 |
349 | def _plugin_register_app_route(
350 | self, r_handler, plugin, context, uri, *args, name=None, with_context=False, **kwargs
351 | ):
352 | if with_context:
353 | r_handler = update_wrapper(partial(r_handler, context=context), r_handler)
354 | fr = SanicFutureRoute(r_handler, uri, name=name, **kwargs)
355 | routes = self._app._apply_route(fr)
356 | return routes
357 |
358 | def _plugin_register_bp_route(
359 | self, r_handler, plugin, context, uri, *args, name=None, with_context=False, **kwargs
360 | ):
361 | bp = self._app
362 | if with_context:
363 | r_handler = update_wrapper(partial(r_handler, context=context), r_handler)
364 | # __blueprintname__ gets added in the register() routine
365 | # When app is a blueprint, it doesn't register right away, it happens
366 | # in the blueprint.register() routine.
367 | r_handler = bp.route(uri, *args, name=name, **kwargs)(r_handler)
368 | return r_handler
369 |
370 | def _plugin_register_app_static(self, uri, file_or_dir, plugin, context, *args, **kwargs):
371 | fs = SanicFutureStatic(uri, file_or_dir, **kwargs)
372 | return self._app._apply_static(fs)
373 |
374 | def _plugin_register_bp_static(self, uri, file_or_dir, plugin, context, *args, **kwargs):
375 | bp = self._app
376 | return bp.static(uri, file_or_dir, *args, **kwargs)
377 |
378 | def _plugin_register_app_exception(self, handler, plugin, context, *exceptions, with_context=False, **kwargs):
379 | if with_context:
380 | handler = update_wrapper(partial(handler, context=context), handler)
381 | fe = SanicFutureException(handler, list(exceptions))
382 | return self._app._apply_exception_handler(fe)
383 |
384 | def _plugin_register_bp_exception(self, handler, plugin, context, *exceptions, with_context=False, **kwargs):
385 | if with_context:
386 | handler = update_wrapper(partial(handler, context=context), handler)
387 | return self._app.exception(*exceptions)(handler)
388 |
389 | def _plugin_register_app_listener(self, event, listener, plugin, context, *args, with_context=False, **kwargs):
390 | if with_context:
391 | listener = update_wrapper(partial(listener, context=context), listener)
392 | fl = SanicFutureListener(listener, event)
393 | return self._app._apply_listener(fl)
394 |
395 | def _plugin_register_bp_listener(self, event, listener, plugin, context, *args, with_context=False, **kwargs):
396 | if with_context:
397 | listener = update_wrapper(partial(listener, context=context), listener)
398 | bp = self._app
399 | return bp.listener(event)(listener)
400 |
401 | def _plugin_register_middleware(
402 | self,
403 | middleware,
404 | plugin,
405 | context,
406 | *args,
407 | priority=5,
408 | relative=None,
409 | attach_to=None,
410 | with_context=False,
411 | **kwargs,
412 | ):
413 | assert isinstance(priority, int), "Priority must be an integer!"
414 | assert 0 <= priority <= 9, (
415 | "Priority must be between 0 and 9 (inclusive), " "0 is highest priority, 9 is lowest."
416 | )
417 | assert isinstance(plugin, SanicPlugin), "Plugin middleware only works with a plugin from SPTK."
418 | if len(args) > 0 and isinstance(args[0], str) and attach_to is None:
419 | # for backwards/sideways compatibility with Sanic,
420 | # the first arg is interpreted as 'attach_to'
421 | attach_to = args[0]
422 | if with_context:
423 | middleware = update_wrapper(partial(middleware, context=context), middleware)
424 | if attach_to is None or attach_to == "request":
425 | insert_order = len(self._pre_request_middleware) + len(self._post_request_middleware)
426 | priority_middleware = (priority, insert_order, middleware)
427 | if relative is None or relative == 'pre':
428 | # plugin request middleware default to pre-app middleware
429 | self._pre_request_middleware.append(priority_middleware)
430 | else: # post
431 | assert relative == "post", "A request middleware must have relative = pre or post"
432 | self._post_request_middleware.append(priority_middleware)
433 | elif attach_to == "cleanup":
434 | insert_order = len(self._cleanup_middleware)
435 | priority_middleware = (priority, insert_order, middleware)
436 | assert relative is None, "A cleanup middleware cannot have relative pre or post"
437 | self._cleanup_middleware.append(priority_middleware)
438 | else: # response
439 | assert attach_to == "response", "A middleware kind must be either request or response."
440 | insert_order = len(self._post_response_middleware) + len(self._pre_response_middleware)
441 | # so they are sorted backwards
442 | priority_middleware = (0 - priority, 0.0 - insert_order, middleware)
443 | if relative is None or relative == 'post':
444 | # plugin response middleware default to post-app middleware
445 | self._post_response_middleware.append(priority_middleware)
446 | else: # pre
447 | assert relative == "pre", "A response middleware must have relative = pre or post"
448 | self._pre_response_middleware.append(priority_middleware)
449 | return middleware
450 |
451 | @property
452 | def _plugins_context(self):
453 | try:
454 | return self._contexts['_plugins']
455 | except (AttributeError, KeyError):
456 | raise RuntimeError("PluginRealm does not have a valid plugins context!")
457 |
458 | @property
459 | def shared_context(self):
460 | try:
461 | return self._contexts['shared']
462 | except (AttributeError, KeyError):
463 | raise RuntimeError("PluginRealm does not have a valid shared context!")
464 |
465 | def get_context(self, context=None):
466 | context = context or 'shared'
467 | try:
468 | _context = self._contexts[context]
469 | except KeyError:
470 | logger.error("Context {:s} does not exist!")
471 | return None
472 | return _context
473 |
474 | def get_from_context(self, item, context=None):
475 | context = context or 'shared'
476 | try:
477 | _context = self._contexts[context]
478 | except KeyError:
479 | logger.warning("Context {:s} does not exist! Falling back to shared context".format(context))
480 | _context = self._contexts['shared']
481 | return _context.__getitem__(item)
482 |
483 | def create_temporary_request_context(self, request):
484 | request_hash = id(request)
485 | shared_context = self.shared_context
486 | shared_requests_dict = shared_context.get('request', False)
487 | if not shared_requests_dict:
488 | new_ctx = SanicContext(self, None, {'id': 'shared request contexts'})
489 | shared_context['request'] = shared_requests_dict = new_ctx
490 | shared_request_ctx = shared_requests_dict.get(request_hash, None)
491 | if shared_request_ctx:
492 | # Somehow, we've already created a temporary context for this request.
493 | return shared_request_ctx
494 | shared_requests_dict[request_hash] = shared_request_ctx = SanicContext(
495 | self, None, {'request': request, 'id': "shared request context for request {}".format(id(request))}
496 | )
497 | for name, _p in self._plugins_context.items():
498 | if not (isinstance(_p, SanicContext) and 'instance' in _p and isinstance(_p['instance'], SanicPlugin)):
499 | continue
500 | if not ('context' in _p and isinstance(_p['context'], SanicContext)):
501 | continue
502 | _p_context = _p['context']
503 | if 'request' not in _p_context:
504 | _p_context['request'] = p_request = SanicContext(self, None, {'id': 'private request contexts'})
505 | else:
506 | p_request = _p_context.request
507 | p_request[request_hash] = SanicContext(
508 | self,
509 | None,
510 | {'request': request, 'id': "private request context for {} on request {}".format(name, id(request))},
511 | )
512 | return shared_request_ctx
513 |
514 | def delete_temporary_request_context(self, request):
515 | request_hash = id(request)
516 | shared_context = self.shared_context
517 | try:
518 | _shared_requests_dict = shared_context['request']
519 | del _shared_requests_dict[request_hash]
520 | except KeyError:
521 | pass
522 | for name, _p in self._plugins_context.items():
523 | if not (isinstance(_p, SanicContext) and 'instance' in _p and isinstance(_p['instance'], SanicPlugin)):
524 | continue
525 | if not ('context' in _p and isinstance(_p['context'], SanicContext)):
526 | continue
527 | _p_context = _p['context']
528 | try:
529 | _p_requests_dict = _p_context['request']
530 | del _p_requests_dict[request_hash]
531 | except KeyError:
532 | pass
533 |
534 | async def _handle_request(self, real_handle, request, write_callback, stream_callback):
535 | cancelled = False
536 | try:
537 | _ = await real_handle(request, write_callback, stream_callback)
538 | except CancelledError as ce:
539 | # We still want to run cleanup middleware, even if cancelled
540 | cancelled = ce
541 | except BaseException as be:
542 | logger.error("SPTK caught an error that should have been caught by Sanic response handler.")
543 | logger.error(str(be))
544 | raise
545 | finally:
546 | # noinspection PyUnusedLocal
547 | _ = await self._run_cleanup_middleware(request) # noqa: F841
548 | if cancelled:
549 | raise cancelled
550 |
551 | async def _handle_request_21_03(self, real_handle, request):
552 | cancelled = False
553 | try:
554 | _ = await real_handle(request)
555 | except CancelledError as ce:
556 | # We still want to run cleanup middleware, even if cancelled
557 | cancelled = ce
558 | except BaseException as be:
559 | logger.error("SPTK caught an error that should have been caught by Sanic response handler.")
560 | logger.error(str(be))
561 | raise
562 | finally:
563 | # noinspection PyUnusedLocal
564 | _ = await self._run_cleanup_middleware(request) # noqa: F841
565 | if cancelled:
566 | raise cancelled
567 |
568 | def wrap_handle_request(self, app, new_handler=None):
569 | if new_handler is None:
570 | new_handler = self._handle_request
571 | orig_handle_request = app.handle_request
572 | return update_wrapper(partial(new_handler, orig_handle_request), new_handler)
573 |
574 | async def _run_request_middleware_18_12(self, request):
575 | if not self._running:
576 | raise ServerError("Toolkit processing a request before App server is started.")
577 | self.create_temporary_request_context(request)
578 | if self._pre_request_middleware:
579 | for (_pri, _ins, middleware) in self._pre_request_middleware:
580 | response = middleware(request)
581 | if isawaitable(response):
582 | response = await response
583 | if response:
584 | return response
585 | if self._app.request_middleware:
586 | for middleware in self._app.request_middleware:
587 | response = middleware(request)
588 | if isawaitable(response):
589 | response = await response
590 | if response:
591 | return response
592 | if self._post_request_middleware:
593 | for (_pri, _ins, middleware) in self._post_request_middleware:
594 | response = middleware(request)
595 | if isawaitable(response):
596 | response = await response
597 | if response:
598 | return response
599 | return None
600 |
601 | async def _run_request_middleware_19_12(self, request, request_name=None):
602 | if not self._running:
603 | # Test_mode is only present on Sanic 20.9+
604 | test_mode = getattr(self._app, "test_mode", False)
605 | if self._app.asgi:
606 | if test_mode:
607 | # We're deliberately in Test Mode, we don't expect
608 | # Server events to have been kicked off yet.
609 | pass
610 | else:
611 | # An ASGI app can receive requests from HTTPX even if
612 | # the app is not booted yet.
613 | self.warning("Unexpected ASGI request. Forcing Toolkit " "into running mode without a server.")
614 | self._on_server_start(request.app, request.transport.loop)
615 | elif test_mode:
616 | self.warning("Unexpected test-mode request. Forcing Toolkit " "into running mode without a server.")
617 | self._on_server_start(request.app, request.transport.loop)
618 | else:
619 | raise RuntimeError("Sanic Plugin Toolkit received a request before Sanic server is started.")
620 | self.create_temporary_request_context(request)
621 | if self._pre_request_middleware:
622 | for (_pri, _ins, middleware) in self._pre_request_middleware:
623 | response = middleware(request)
624 | if isawaitable(response):
625 | response = await response
626 | if response:
627 | return response
628 | app = self._app
629 | named_middleware = app.named_request_middleware.get(request_name, deque())
630 | applicable_middleware = app.request_middleware + named_middleware
631 | if applicable_middleware:
632 | for middleware in applicable_middleware:
633 | response = middleware(request)
634 | if isawaitable(response):
635 | response = await response
636 | if response:
637 | return response
638 | if self._post_request_middleware:
639 | for (_pri, _ins, middleware) in self._post_request_middleware:
640 | response = middleware(request)
641 | if isawaitable(response):
642 | response = await response
643 | if response:
644 | return response
645 | return None
646 |
647 | async def _run_request_middleware_21_03(self, request, request_name=None):
648 | if not self._running:
649 | test_mode = self._app.test_mode
650 | if self._app.asgi:
651 | if test_mode:
652 | # We're deliberately in Test Mode, we don't expect
653 | # Server events to have been kicked off yet.
654 | pass
655 | else:
656 | # An ASGI app can receive requests from HTTPX even if
657 | # the app is not booted yet.
658 | self.warning("Unexpected ASGI request. Forcing Toolkit " "into running mode without a server.")
659 | self._on_server_start(request.app, request.transport.loop)
660 | elif test_mode:
661 | self.warning("Unexpected test-mode request. Forcing Toolkit " "into running mode without a server.")
662 | self._on_server_start(request.app, request.transport.loop)
663 | else:
664 | raise RuntimeError("Sanic Plugin Toolkit received a request before Sanic server is started.")
665 |
666 | shared_req_context = self.create_temporary_request_context(request)
667 | realm_request_middleware_started = shared_req_context.get('realm_request_middleware_started', False)
668 | if realm_request_middleware_started:
669 | return None
670 | shared_req_context['realm_request_middleware_started'] = True
671 | if self._pre_request_middleware:
672 | for (_pri, _ins, middleware) in self._pre_request_middleware:
673 | response = middleware(request)
674 | if isawaitable(response):
675 | response = await response
676 | if response:
677 | return response
678 | app = self._app
679 | named_middleware = app.named_request_middleware.get(request_name, deque())
680 | applicable_middleware = app.request_middleware + named_middleware
681 | # request.request_middleware_started is meant as a stop-gap solution
682 | # until RFC 1630 is adopted
683 | if applicable_middleware and not request.request_middleware_started:
684 | request.request_middleware_started = True
685 | for middleware in applicable_middleware:
686 | response = middleware(request)
687 | if isawaitable(response):
688 | response = await response
689 | if response:
690 | return response
691 | if self._post_request_middleware:
692 | for (_pri, _ins, middleware) in self._post_request_middleware:
693 | response = middleware(request)
694 | if isawaitable(response):
695 | response = await response
696 | if response:
697 | return response
698 | return None
699 |
700 | async def _run_response_middleware_18_12(self, request, response):
701 | if self._pre_response_middleware:
702 | for (_pri, _ins, middleware) in self._pre_response_middleware:
703 | _response = middleware(request, response)
704 | if isawaitable(_response):
705 | _response = await _response
706 | if _response:
707 | response = _response
708 | break
709 | if self._app.response_middleware:
710 | for middleware in self._app.response_middleware:
711 | _response = middleware(request, response)
712 | if isawaitable(_response):
713 | _response = await _response
714 | if _response:
715 | response = _response
716 | break
717 | if self._post_response_middleware:
718 | for (_pri, _ins, middleware) in self._post_response_middleware:
719 | _response = middleware(request, response)
720 | if isawaitable(_response):
721 | _response = await _response
722 | if _response:
723 | response = _response
724 | break
725 | return response
726 |
727 | async def _run_response_middleware_19_12(self, request, response, request_name=None):
728 | if self._pre_response_middleware:
729 | for (_pri, _ins, middleware) in self._pre_response_middleware:
730 | _response = middleware(request, response)
731 | if isawaitable(_response):
732 | _response = await _response
733 | if _response:
734 | response = _response
735 | break
736 | app = self._app
737 | named_middleware = app.named_response_middleware.get(request_name, deque())
738 | applicable_middleware = app.response_middleware + named_middleware
739 | if applicable_middleware:
740 | for middleware in applicable_middleware:
741 | _response = middleware(request, response)
742 | if isawaitable(_response):
743 | _response = await _response
744 | if _response:
745 | response = _response
746 | break
747 | if self._post_response_middleware:
748 | for (_pri, _ins, middleware) in self._post_response_middleware:
749 | _response = middleware(request, response)
750 | if isawaitable(_response):
751 | _response = await _response
752 | if _response:
753 | response = _response
754 | break
755 | return response
756 |
757 | async def _run_response_middleware_21_03(self, request, response, request_name=None):
758 | if self._pre_response_middleware:
759 | for (_pri, _ins, middleware) in self._pre_response_middleware:
760 | _response = middleware(request, response)
761 | if isawaitable(_response):
762 | _response = await _response
763 | if _response:
764 | response = _response
765 | if isinstance(response, BaseHTTPResponse):
766 | response = request.stream.respond(response)
767 | break
768 | app = self._app
769 | named_middleware = app.named_response_middleware.get(request_name, deque())
770 | applicable_middleware = app.response_middleware + named_middleware
771 | if applicable_middleware:
772 | for middleware in applicable_middleware:
773 | _response = middleware(request, response)
774 | if isawaitable(_response):
775 | _response = await _response
776 | if _response:
777 | response = _response
778 | if isinstance(response, BaseHTTPResponse):
779 | response = request.stream.respond(response)
780 | break
781 | if self._post_response_middleware:
782 | for (_pri, _ins, middleware) in self._post_response_middleware:
783 | _response = middleware(request, response)
784 | if isawaitable(_response):
785 | _response = await _response
786 | if _response:
787 | response = _response
788 | if isinstance(response, BaseHTTPResponse):
789 | response = request.stream.respond(response)
790 | break
791 | return response
792 |
793 | async def _run_cleanup_middleware(self, request):
794 | return_this = None
795 | if self._cleanup_middleware:
796 | for (_pri, _ins, middleware) in self._cleanup_middleware:
797 | response = middleware(request)
798 | if isawaitable(response):
799 | response = await response
800 | if response:
801 | return_this = response
802 | break
803 | self.delete_temporary_request_context(request)
804 | return return_this
805 |
806 | def _on_server_start(self, app, loop):
807 | if not isinstance(self._app, Blueprint):
808 | assert self._app == app, "Sanic Plugins Framework is not assigned to the correct " "Sanic App!"
809 | if self._running:
810 | # during testing, this will be called _many_ times.
811 | return # Ignore if this is already called.
812 | self._loop = loop
813 |
814 | # sort and freeze these
815 | self._pre_request_middleware = tuple(sorted(self._pre_request_middleware))
816 | self._post_request_middleware = tuple(sorted(self._post_request_middleware))
817 | self._pre_response_middleware = tuple(sorted(self._pre_response_middleware))
818 | self._post_response_middleware = tuple(sorted(self._post_response_middleware))
819 | self._cleanup_middleware = tuple(sorted(self._cleanup_middleware))
820 | self._running = True
821 |
822 | def _on_after_server_start(self, app, loop):
823 | if not self._running:
824 | # Missed before_server_start event
825 | # Run startup now!
826 | self._on_server_start(app, loop)
827 |
828 | async def _startup(self, app, real_startup):
829 | _ = await real_startup()
830 | # Patch app _after_ Touchup is done.
831 | self._patch_app(app)
832 |
833 | def _patch_app(self, app):
834 | # monkey patch the app!
835 |
836 | if SANIC_21_3_0 <= SANIC_VERSION:
837 | app.handle_request = self.wrap_handle_request(app, self._handle_request_21_03)
838 | app._run_request_middleware = self._run_request_middleware_21_03
839 | app._run_response_middleware = self._run_response_middleware_21_03
840 | else:
841 | if SANIC_19_12_0 <= SANIC_VERSION:
842 | app.handle_request = self.wrap_handle_request(app)
843 | app._run_request_middleware = self._run_request_middleware_19_12
844 | app._run_response_middleware = self._run_response_middleware_19_12
845 | else:
846 | app.handle_request = self.wrap_handle_request(app)
847 | app._run_request_middleware = self._run_request_middleware_18_12
848 | app._run_response_middleware = self._run_response_middleware_18_12
849 |
850 | def _patch_blueprint(self, bp):
851 | # monkey patch the blueprint!
852 | # Caveat! We cannot take over the sanic middleware runner when
853 | # app is a blueprint. We will do this a different way.
854 | _spf = self
855 |
856 | async def run_bp_pre_request_mw(request):
857 | nonlocal _spf
858 | _spf.create_temporary_request_context(request)
859 | if _spf._pre_request_middleware:
860 | for (_pri, _ins, middleware) in _spf._pre_request_middleware:
861 | response = middleware(request)
862 | if isawaitable(response):
863 | response = await response
864 | if response:
865 | return response
866 |
867 | async def run_bp_post_request_mw(request):
868 | nonlocal _spf
869 | if _spf._post_request_middleware:
870 | for (_pri, _ins, middleware) in _spf._post_request_middleware:
871 | response = middleware(request)
872 | if isawaitable(response):
873 | response = await response
874 | if response:
875 | return response
876 |
877 | async def run_bp_pre_response_mw(request, response):
878 | nonlocal _spf
879 | altered = False
880 | if _spf._pre_response_middleware:
881 | for (_pri, _ins, middleware) in _spf._pre_response_middleware:
882 | _response = middleware(request, response)
883 | if isawaitable(_response):
884 | _response = await _response
885 | if _response:
886 | response = _response
887 | altered = True
888 | break
889 | if altered:
890 | return response
891 |
892 | async def run_bp_post_response_mw(request, response):
893 | nonlocal _spf
894 | altered = False
895 | if _spf._post_response_middleware:
896 | for (_pri, _ins, middleware) in _spf._post_response_middleware:
897 | _response = middleware(request, response)
898 | if isawaitable(_response):
899 | _response = await _response
900 | if _response:
901 | response = _response
902 | altered = True
903 | break
904 | if self._cleanup_middleware:
905 | for (_pri, _ins, middleware) in self._cleanup_middleware:
906 | response2 = middleware(request)
907 | if isawaitable(response2):
908 | response2 = await response2
909 | if response2:
910 | break
911 | _spf.delete_temporary_request_context(request)
912 | if altered:
913 | return response
914 |
915 | def bp_register(bp_self, orig_register, app, options):
916 | # from sanic.blueprints import FutureMiddleware as BPFutureMW
917 | pre_request = SanicFutureMiddleware(run_bp_pre_request_mw, 'request')
918 | post_request = SanicFutureMiddleware(run_bp_post_request_mw, 'request')
919 | pre_response = SanicFutureMiddleware(run_bp_pre_response_mw, 'response')
920 | post_response = SanicFutureMiddleware(run_bp_post_response_mw, 'response')
921 | # this order is very important. Don't change it. It is correct.
922 | bp_self._future_middleware.insert(0, post_response)
923 | bp_self._future_middleware.insert(0, pre_request)
924 | bp_self._future_middleware.append(post_request)
925 | bp_self._future_middleware.append(pre_response)
926 |
927 | orig_register(app, options)
928 |
929 | if SANIC_21_3_0 <= SANIC_VERSION:
930 | _slots = list(Blueprint.__fake_slots__)
931 | _slots.extend(["register"])
932 | Blueprint.__fake_slots__ = tuple(_slots)
933 | bp.register = update_wrapper(partial(bp_register, bp, bp.register), bp.register)
934 | setattr(bp.ctx, APP_CONFIG_INSTANCE_KEY, self)
935 | else:
936 | bp.register = update_wrapper(partial(bp_register, bp, bp.register), bp.register)
937 | setattr(bp, APP_CONFIG_INSTANCE_KEY, self)
938 |
939 | @classmethod
940 | def _recreate(cls, app):
941 | self = super(SanicPluginRealm, cls).__new__(cls)
942 | self._running = False
943 | self._app = app
944 | self._loop = None
945 | self._plugin_names = set()
946 | # these deques get replaced with frozen tuples at runtime
947 | self._pre_request_middleware = deque()
948 | self._post_request_middleware = deque()
949 | self._pre_response_middleware = deque()
950 | self._post_response_middleware = deque()
951 | self._cleanup_middleware = deque()
952 | self._contexts = SanicContext(self, None)
953 | self._contexts['shared'] = SanicContext(self, None, {'app': app})
954 | self._contexts['_plugins'] = SanicContext(self, None, {'sanic_plugin_toolkit': self})
955 | return self
956 |
957 | def __new__(cls, app, *args, **kwargs):
958 | if not app:
959 | raise RuntimeError("Plugin Realm must be given a valid Sanic App to work with.")
960 | if not (isinstance(app, Sanic) or isinstance(app, Blueprint)):
961 | raise RuntimeError(
962 | "PluginRealm only works with Sanic Apps or Blueprints. Please pass in an app instance to the Realm constructor."
963 | )
964 | # An app _must_ only have one sanic_plugin_toolkit instance associated with it.
965 | # If there is already one registered on the app, return that one.
966 | try:
967 | instance = getattr(app.ctx, APP_CONFIG_INSTANCE_KEY)
968 | assert isinstance(
969 | instance, cls
970 | ), "This app is already registered to a different type of Sanic Plugin Realm!"
971 | return instance
972 | except (AttributeError, LookupError):
973 | # App doesn't have .ctx or key is not present
974 | try:
975 | instance = app.config[APP_CONFIG_INSTANCE_KEY]
976 | assert isinstance(
977 | instance, cls
978 | ), "This app is already registered to a different type of Sanic Plugin Realm!"
979 | return instance
980 | except AttributeError: # app must then be a blueprint
981 | try:
982 | instance = getattr(app, APP_CONFIG_INSTANCE_KEY)
983 | assert isinstance(
984 | instance, cls
985 | ), "This Blueprint is already registered to a different type of Sanic Plugin Realm!"
986 | return instance
987 | except AttributeError:
988 | pass
989 | except LookupError:
990 | pass
991 | self = cls._recreate(app)
992 | if isinstance(app, Blueprint):
993 | bp = app
994 | self._patch_blueprint(bp)
995 | bp.listener('before_server_start')(self._on_server_start)
996 | bp.listener('after_server_start')(self._on_after_server_start)
997 | else:
998 | if hasattr(Sanic, "__fake_slots__"):
999 | _slots = list(Sanic.__fake_slots__)
1000 | _slots.extend(["_startup", "handle_request", "_run_request_middleware", "_run_response_middleware"])
1001 | Sanic.__fake_slots__ = tuple(_slots)
1002 | if hasattr(app, "_startup"):
1003 | # We can wrap startup, to patch _after_ Touchup is done
1004 | app._startup = update_wrapper(partial(self._startup, app, app._startup), app._startup)
1005 | else:
1006 | self._patch_app(app)
1007 | if SANIC_21_3_0 <= SANIC_VERSION:
1008 | setattr(app.ctx, APP_CONFIG_INSTANCE_KEY, self)
1009 | else:
1010 | app.config[APP_CONFIG_INSTANCE_KEY] = self
1011 | app.listener('before_server_start')(self._on_server_start)
1012 | app.listener('after_server_start')(self._on_after_server_start)
1013 | config = getattr(app, 'config', None)
1014 | if config:
1015 | load_ini = config.get(SPTK_LOAD_INI_KEY, True)
1016 | if load_ini:
1017 | ini_file = config.get(SPTK_INI_FILE_KEY, 'sptk.ini')
1018 | try:
1019 | load_config_file(self, app, ini_file)
1020 | except FileNotFoundError:
1021 | pass
1022 | return self
1023 |
1024 | def __init__(self, *args, **kwargs):
1025 | args = list(args) # tuple is not mutable. Change it to a list.
1026 | if len(args) > 0:
1027 | args.pop(0) # remove 'app' arg
1028 | assert self._app and self._contexts, "Sanic Plugin Realm was not initialized correctly."
1029 | assert len(args) < 1, "Unexpected arguments passed to the Sanic Plugin Realm."
1030 | assert len(kwargs) < 1, "Unexpected keyword arguments passed to the SanicPluginRealm."
1031 | super(SanicPluginRealm, self).__init__(*args, **kwargs)
1032 |
1033 | def __getstate__(self):
1034 | if self._running:
1035 | raise RuntimeError("Cannot call __getstate__ on an SPTK app that is already running.")
1036 | state_dict = {}
1037 | for s in SanicPluginRealm.__slots__:
1038 | if s in ('_running', '_loop'):
1039 | continue
1040 | state_dict[s] = getattr(self, s)
1041 | return state_dict
1042 |
1043 | def __setstate__(self, state):
1044 | running = getattr(self, '_running', False)
1045 | if running:
1046 | raise RuntimeError("Cannot call __setstate__ on an SPTK app that is already running.")
1047 | for s, v in state.items():
1048 | if s in ('_running', '_loop'):
1049 | continue
1050 | if s == "__weakref__":
1051 | if v is None:
1052 | continue
1053 | else:
1054 | raise NotImplementedError("Setting weakrefs on SPTK PluginRealm")
1055 | setattr(self, s, v)
1056 |
1057 | def __reduce__(self):
1058 | if self._running:
1059 | raise RuntimeError("Cannot pickle a SPTK PluginRealm App after it has started running!")
1060 | state_dict = self.__getstate__()
1061 | app = state_dict.pop("_app")
1062 | return SanicPluginRealm._recreate, (app,), state_dict
1063 |
--------------------------------------------------------------------------------