Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/middleware.py: 15%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Face Middleware
2===============
4When using Face's Command framework, Face takes over handling dispatch
5of commands and subcommands. A particular command line string is
6routed to the configured function, in much the same way that popular
7web frameworks route requests based on path.
9In more advanced programs, this basic control flow can be enhanced by
10adding middleware. Middlewares comprise a stack of functions, each
11which calls the next, until finally calling the appropriate
12command-handling function. Middlewares are added to the command, with
13the outermost middleware being added first. Remember: first added,
14first called.
16Middlewares are a great way to handle general setup and logic which is
17common across many subcommands, such as verbosity, logging, and
18formatting. Middlewares can also be used to perform additional
19argument validation, and terminate programs early.
21The interface of middlewares retains the same injection ability of
22Command handler functions. Flags and builtins are automatically
23provided. In addition to having its arguments checked against those
24available injectables, a middleware _must_ take a ``next_`` parameter
25as its first argument. Then, much like a decorator, that ``next_``
26function must be invoked to continue to program execution::
29 import time
31 from face import face_middleware, echo
33 @face_middleware
34 def timing_middleware(next_):
35 start_time = time.time()
36 ret = next_()
37 echo('command executed in:', time.time() - start_time, 'seconds')
38 return ret
40As always, code speaks volumes. It's worth noting that ``next_()`` is
41a normal function. If you return without calling it, your command's
42handler function will not be called, nor will any other downstream
43middleware. Another corollary is that this makes it easy to use
44``try``/``except`` to build error handling.
46While already practical, there are two significant ways it can be
47enhanced. The first would be to provide downstream handlers access to
48the ``start_time`` value. The second would be to make the echo
49functionality optional.
51Providing values from middleware
52--------------------------------
54As mentioned above, the first version of our timing middleware works,
55but what if one or more of our handler functions needs to perform a
56calculation based on ``start_time``?
58Common code is easily folded away by middleware, and we can do so here
59by making the start_time available as an injectable::
61 import time
63 from face import face_middleware, echo
65 @face_middleware(provides=['start_time'])
66 def timing_middleware(next_):
67 start_time = time.time()
68 ret = next_(start_time=start_time)
69 echo('command executed in:', time.time() - start_time, 'seconds')
70 return ret
72``start_time`` is added to the list of provides in the middleware
73decoration, and ``next_()`` is simply invoked with a ``start_time``
74keyword argument. Any command handler function that takes a
75``start_time`` keyword argument will automatically pick up the value.
77That's all well and fine, but what if we don't always want to know the
78duration of the command? Whose responsibility is it to expose that
79optional behavior? Lucky for us, middlewares can take care of themselves.
81Adding flags to middleware
82--------------------------
84Right now our middleware changes command output every time it is
85run. While that's pretty handy behavior, the command line is all about
86options.
88We can make our middleware even more reusable by adding self-contained
89optional behavior, via a flag::
91 import time
93 from face import face_middleware, Flag, echo
95 @face_middleware(provides=['start_time'], flags=[Flag('--echo-time', parse_as=True)])
96 def timing_middleware(next_, echo_time):
97 start_time = time.time()
98 ret = next_(start_time=start_time)
99 if echo_time:
100 echo('command executed in:', time.time() - start_time, 'seconds')
101 return ret
103Now, every :class:`Command` that adds this middleware will
104automatically get a flag, ``--echo-time``. Just like other flags, its
105value will be injected into commands that need it.
107.. note:: **Weak Dependencies** - Middlewares that set defaults for
108 keyword arguments are said to have a "weak" dependency on
109 the associated injectable. If the command handler function,
110 or another downstream middleware, do not accept the
111 argument, the flag will not be parsed, or shown in generated
112 help and error messages. This differs from the command
113 handler function itself, which will accept arguments even
114 when the function signature sets a default.
116Wrapping up
117-----------
119I'd like to say that we were only scratching the surface of
120middlewares, but really there's not much more to them. They are an
121advanced feature of face, and a very powerful organizing tool for your
122code, but like many powerful tools, they are simple. You can use them
123in a wide variety of ways. Other useful middleware ideas:
125 * Verbosity middleware - provides a ``verbose`` flag for downstream
126 commands which can write additional output.
127 * Logging middleware - sets up and provides an associated logger
128 object for downstream commands.
129 * Pipe middleware - Many CLIs are made for streaming. There are some
130 semantics a middleware can help with, like breaking pipes.
131 * KeyboardInterrupt middleware - Ctrl-C is a common way to exit
132 programs, but Python generally spits out an ugly stack trace, even
133 where a keyboard interrupt may have been valid.
134 * Authentication middleware - provides an AuthenticatedUser object
135 after checking environment variables and prompting for a username
136 and password.
137 * Debugging middleware - Because face middlewares are functions in a
138 normal Python stack, it's easy to wrap downstream calls in a
139 ``try``/``except``, and add a flag (or environment variable) that
140 enables a ``pdb.post_mortem()`` to drop you into a debug console.
142The possibilities never end. If you build a middleware of particularly
143broad usefulness, consider contributing it back to the core!
145"""
148from face.parser import Flag
149from face.sinter import make_chain, get_arg_names, get_fb, get_callable_labels
150from face.sinter import inject # transitive import for external use
152INNER_NAME = 'next_'
154_BUILTIN_PROVIDES = [INNER_NAME, 'args_', 'cmd_', 'subcmds_',
155 'flags_', 'posargs_', 'post_posargs_',
156 'command_', 'subcommand_']
159def is_middleware(target):
160 """Mostly for internal use, this function returns True if *target* is
161 a valid face middleware.
163 Middlewares can be functions wrapped with the
164 :func:`face_middleware` decorator, or instances of a user-created
165 type, as long as it's a callable following face's signature
166 convention and has the ``is_face_middleware`` attribute set to
167 True.
168 """
169 if callable(target) and getattr(target, 'is_face_middleware', None):
170 return True
171 return False
174def face_middleware(func=None, **kwargs):
175 """A decorator to mark a function as face middleware, which wraps
176 execution of a subcommand handler function. This decorator can be
177 called with or without arguments:
179 Args:
180 provides (list): An optional list of names, declaring which
181 values be provided by this middleware at execution time.
182 flags (list): An optional list of Flag instances, which will be
183 automatically added to any Command which adds this middleware.
184 optional (bool): Whether this middleware should be skipped if its
185 provides are not required by the command.
187 The first argument of the decorated function must be named
188 "next_". This argument is a function, representing the next
189 function in the execution chain, the last of which is the
190 command's handler function.
191 """
192 provides = kwargs.pop('provides', [])
193 if isinstance(provides, str):
194 provides = [provides]
195 flags = list(kwargs.pop('flags', []))
196 if flags:
197 for flag in flags:
198 if not isinstance(flag, Flag):
199 raise TypeError('expected Flag object, not: %r' % flag)
200 optional = kwargs.pop('optional', False)
201 if kwargs:
202 raise TypeError('unexpected keyword arguments: %r' % kwargs.keys())
204 def decorate_face_middleware(func):
205 check_middleware(func, provides=provides)
206 func.is_face_middleware = True
207 func._face_flags = list(flags)
208 func._face_provides = list(provides)
209 func._face_optional = optional
210 return func
212 if func and callable(func):
213 return decorate_face_middleware(func)
215 return decorate_face_middleware
218def get_middleware_chain(middlewares, innermost, preprovided):
219 """Perform basic validation of innermost function, wrap it in
220 middlewares, and raise a :exc:`NameError` on any unresolved
221 arguments.
223 Args:
224 middlewares (list): A list of middleware functions, prechecked
225 by :func:`check_middleware`.
226 innermost (callable): A function to be called after all the
227 middlewares.
228 preprovided (list): A list of built-in or otherwise preprovided
229 injectables.
231 Returns:
232 A single function representing the whole middleware chain.
234 This function is called automatically by :meth:`Command.prepare()`
235 (and thus, :meth:`Command.run()`), and is more or less for
236 internal use.
237 """
238 _inner_exc_msg = "argument %r reserved for middleware use only (%r)"
239 if INNER_NAME in get_arg_names(innermost):
240 raise NameError(_inner_exc_msg % (INNER_NAME, innermost))
242 mw_builtins = set(preprovided) - set([INNER_NAME])
243 mw_provides = [list(mw._face_provides) for mw in middlewares]
245 mw_chain, mw_chain_args, mw_unres = make_chain(middlewares, mw_provides, innermost, mw_builtins, INNER_NAME)
247 if mw_unres:
248 msg = "unresolved middleware or handler arguments: %r" % sorted(mw_unres)
249 avail_unres = mw_unres & (mw_builtins | set(sum(mw_provides, [])))
250 if avail_unres:
251 msg += (' (%r provided but not resolvable, check middleware order.)'
252 % sorted(avail_unres))
253 raise NameError(msg)
254 return mw_chain
257def check_middleware(func, provides=None):
258 """Check that a middleware callable adheres to function signature
259 requirements. Called automatically by
260 :class:`Command.add_middleware()` and elsewhere, this function
261 raises :exc:`TypeError` if any issues are found.
262 """
263 if not callable(func):
264 raise TypeError('expected middleware %r to be a function' % func)
265 fb = get_fb(func)
266 # TODO: this currently gives __main__abc instead of __main__.abc
267 func_label = ''.join(get_callable_labels(func))
268 arg_names = fb.args
269 if not arg_names:
270 raise TypeError('middleware function %r must take at least one'
271 ' argument "%s" as its first parameter'
272 % (func_label, INNER_NAME))
273 if arg_names[0] != INNER_NAME:
274 raise TypeError('middleware function %r must take argument'
275 ' "%s" as the first parameter, not "%s"'
276 % (func_label, INNER_NAME, arg_names[0]))
277 if fb.varargs:
278 raise TypeError('middleware function %r may only take explicitly'
279 ' named arguments, not "*%s"' % (func_label, fb.varargs))
280 if fb.varkw:
281 raise TypeError('middleware function %r may only take explicitly'
282 ' named arguments, not "**%s"' % (func_label, fb.varkw))
284 provides = provides if provides is not None else func._face_provides
285 conflict_args = list(set(_BUILTIN_PROVIDES) & set(provides))
286 if conflict_args:
287 raise TypeError('middleware function %r provides conflict with'
288 ' reserved face builtins: %r' % (func_label, conflict_args))
290 return