Skip to content

Instantly share code, notes, and snippets.

@NicolasT
Created May 27, 2010 23:30
Show Gist options
  • Save NicolasT/416522 to your computer and use it in GitHub Desktop.
Save NicolasT/416522 to your computer and use it in GitHub Desktop.
# <License type="Sun Cloud BSD" version="2.2">
#
# Copyright (c) 2005-2009, Sun Microsystems, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Neither the name Sun Microsystems, Inc. nor the names of other
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY SUN MICROSYSTEMS, INC. "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN MICROSYSTEMS, INC. OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# </License>
'''Applicationserver service to host wizards following SPEC20/WizardServer
The basics
==========
The service needs to host several wizards at a time. One wizard is written as a
simple script, eg.
>>> def main(q, i, params, tags):
... name = q.gui.dialog.askString('What\'s your name?')
... age = q.gui.dialog.askInteger('What\'s your age?')
... if age < 18:
... q.gui.dialog.messages('Sorry %s, you\'re not allowed here' % name)
... return
...
... registerVisitor(name)
... q.gui.dialog.message('Welcome to the party, %s' % name)
These scripts are PyMonkey2 C{tasklets}, see the documentation of the tasklet
extension for more information.
There's one issue here since we're working in a web-based context: the HTTP
protocol is stateless and client-server based, and we can't just run the wizard
script in our HTTP server container, since this should be non-blocking. A
potential solution would be to run the wizard script in some thread, but even
then some rather complex inter-thread communication should be required to pass
information between the HTTP server (which handles client request/responses) and
a running wizard thread. The implementation of all q.gui.dialog.* methods would
become complex as well since they should block until a response is available and
handle it to the assignment (return it).
Luckily, Python comes to the rescue thanks to its support for coroutines
(introduced in Python 2.5). This way we are able to run a function (actually
it's a generator) inside our mainloop, but with a separate execution context,
while being able to interact with this generator. Here's some code to
demonstrate the idea (using fake wizard control messages):
>>> def askString(s):
... return 'askstringaction:%s' % s
...
>>> def askInteger(s):
... return 'askintegeraction:%s' % s
...
>>> def message(s):
... return 'messageaction:%s' % s
...
>>> def main():
... name = yield askString('What\'s your name?')
... age = yield askInteger('What\'s your age?')
... if age < 18:
... yield message('Sorry, you\'re not allowed yet')
... return
... yield message('Welcome, %s' % name)
...
>>> runner = main()
>>> action = runner.next()
>>> print action
askstringaction:What's your name?
>>> action = runner.send('Nicolas')
>>> print action
askintegeraction:What's your age?
>>> action = runner.send(23)
>>> print action
messageaction:Welcome, Nicolas
>>> runner.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
This shows how generators/coroutines can help us out to create wizards using
normal methods without the need of threading, complex blocking action functions,
etc. For an overview of coroutines in Python, see PEP342.
The problem
===========
There's one issue with the approach described above: it requires the wizard
author to add _yield_ statements to his code, which renders the wizard useless
in a non-webbased environment, ie. when the wizard is not running inside this
service. This can't be easily solved, unless we create a _runWizard_ method
which takes a wizard function and handles all coroutine interaction.
This approach would require rather strange constructed q.gui.dialog.* functions
though. Here's something which could work (for reference), but it's highly
suboptimal, since the main problem (wizards scripts being less intuitive to
write because of the required _yield_ statements) is not fixed.
>>> def askString(s):
... def f():
... return raw_input('%s ' % s).rstrip('\n')
... return f
...
>>> def message(s):
... def f():
... print s
... return f
...
>>> def main():
... name = yield askString('What\'s your name?')
... yield message('Hello, %s' % name)
...
>>> runner = main()
>>> action = runner.next()
>>> value = action()
What's your name? Nicolas
>>> action = runner.send(value)
>>> value = action()
Hello, Nicolas
>>> runner.send(value)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Getting rid of yield
====================
So, we want to get rid of the _yield_ statements in our wizard source code, but
still want to use coroutines (which require _yield_-based code).
One approach here would be source code rewriting, putting a _yield_ statement
before every occurrence of _q.gui.dialog_ in the original code, and recompile
the code string.
Rewriting source code in an automated manner can be very error-prone though, and
especially in this case, could easily result in the production of invalid Python
code, since a construct like the following is not legal:
>>> def f():
... if yield askYesNo('Is this correct?'):
File "<stdin>", line 2
if yield askYesNo('Is this correct?'):
^
SyntaxError: invalid syntax
So, we need a better solution.
Instead of rewriting source code, we can as well rewrite the CPython Virtual
Machine bytecode of the wizard functions.
This might sound easy at first, but there's one difficulty: where do we need to
insert statements, and which?
At first this might seem easy: we want to make sure a value is yielded after
every call to some function of _q.gui.dialog_, so we figure out how yield works
in bytecode:
>>> import dis
>>> def f():
... name = askString('What\'s your name?')
...
>>> def g():
... name = yield askString('What\'s your name?')
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (askString)
3 LOAD_CONST 1 ("What's your name?")
6 CALL_FUNCTION 1
9 STORE_FAST 0 (name)
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
>>> dis.dis(g)
2 0 LOAD_GLOBAL 0 (askString)
3 LOAD_CONST 1 ("What's your name?")
6 CALL_FUNCTION 1
9 YIELD_VALUE
10 STORE_FAST 0 (name)
13 LOAD_CONST 0 (None)
16 RETURN_VALUE
So, looks like we need to add a _YIELD_VALUE_ opcode after every call to a
_q.gui.dialog_ function.
Let's see how to figure out these:
>>> def f():
... name = yield q.gui.dialog.askString('What\'s your name?')
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (q)
3 LOAD_ATTR 1 (gui)
6 LOAD_ATTR 2 (dialog)
9 LOAD_ATTR 3 (askString)
12 LOAD_CONST 1 ("What's your name?")
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 STORE_FAST 0 (name)
22 LOAD_CONST 0 (None)
25 RETURN_VALUE
Right, figuring out where to add this _YIELD_VALUE_ opcode becomes less
obvious, but still possible if we keep some state in our opcode walker.
This becomes even more difficult though if the wizard developer starts doing
something like this:
>>> def f():
... test = q.gui.dialog.askYesNo('Is %d + %d %d?' % (1, 2, sum((1, 2, ))))
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (q)
3 LOAD_ATTR 1 (gui)
6 LOAD_ATTR 2 (dialog)
9 LOAD_ATTR 3 (askYesNo)
12 LOAD_CONST 1 ('Is %d + %d %d?')
15 LOAD_CONST 2 (1)
18 LOAD_CONST 3 (2)
21 LOAD_GLOBAL 4 (sum)
24 LOAD_CONST 4 ((1, 2))
27 CALL_FUNCTION 1
30 BUILD_TUPLE 3
33 BINARY_MODULO
34 CALL_FUNCTION 1
37 STORE_FAST 0 (test)
40 LOAD_CONST 0 (None)
43 RETURN_VALUE
We'd need to be able to keep track of which object is actually called for each
_CALL_FUNCTION_ opcode, which is not a trivial task at all.
So, looks like we're not able to figure out reliably where to add _YIELD_VALUE_
opcodes to the wizard function code.
We need to take other opcodes, next to _CALL_FUNCTION_, into account as well,
since _CALL_FUNCTION_ is not the only way to call functions: _CALL_FUNCTION_ is
used to call a function using simple arguments which are all pushed on the
stack. To support positional and keyword arguments (using _*args_ or _**kwargs_
in the caller code), one of _CALL_FUNCTION_VAR_, _CALL_FUNCTION_KW_ or
_CALL_FUNCTION_VAR_KW_ is used. From now on, we'll refer to the family of
function call opcodes as _CALL_FUNCTION*_.
It turns out there's a very simple (although suboptimal time-wise) solution for
this problem: instead of figuring out where to add _YIELD_VALUE_ opcodes, we can
just add one after every single _CALL_FUNCTION*_ opcode, and handle this inside
our wizard runner code: we just walk through the rewritten function (well,
generator produced by the function) and send back every value we got:
>>> def return_something(what):
... return what
...
>>> def f():
... a = yield return_something(123)
... print 'a =', a
... b = yield return_something(456)
... print 'b =', b
...
>>> runner = f()
>>> value = runner.next()
>>> while True:
... value = runner.send(value)
...
a = 123
b = 456
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
StopIteration
Here the _yield_ statements are hard-coded, but if you check the opcodes used,
it's easy to see we can generate this as well:
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (return_something)
3 LOAD_CONST 1 (123)
6 CALL_FUNCTION 1
9 YIELD_VALUE
10 STORE_FAST 0 (a)
3 13 LOAD_CONST 2 ('a =')
16 PRINT_ITEM
17 LOAD_FAST 0 (a)
20 PRINT_ITEM
21 PRINT_NEWLINE
4 22 LOAD_GLOBAL 0 (return_something)
25 LOAD_CONST 3 (456)
28 CALL_FUNCTION 1
31 YIELD_VALUE
32 STORE_FAST 1 (b)
5 35 LOAD_CONST 4 ('b =')
38 PRINT_ITEM
39 LOAD_FAST 1 (b)
42 PRINT_ITEM
43 PRINT_NEWLINE
44 LOAD_CONST 0 (None)
47 RETURN_VALUE
Using this method, we can insert a _YIELD_VALUE_ opcode after every single
occurrence of _CALL_FUNCTION*_. We only need some way to figure out whether we
should yield the value to the final caller or not, since now we're using 2
separate generators:
* One generator is the rewritten wizard function
* One generator wraps the rewritten one and only yields values we actually want
These values are everything returned by a _q.gui.dialog_ function, since these
are the ones to be handed over to the HTTP server, and HTTP responses should be
sent to the wizard function. Others can just be returned as-is without
interference by the HTTP server.
This is simply solved by wrapping all values returned by any _q.gui.dialog_
functions in a simple wrapper type and perform some type checking in the outer
generator.
There's one more thing we should check: are we allowed to add these
_YIELD_VALUE_ opcodes after every _CALL_FUNCTION*_ opcode?
Function calling and stacks
===========================
Note: this applies to CPython 2.5. Information provided here might change in
newer versions.
CPython is a stack-based virtual machine, where every execution context got its
own stack. We need to make sure, when inserting this extra opcode after
_CALL_FUNCTION*_, no side-effects are introduced, and the semantics of the
_YIELD_VALUE_ opcode are honoured.
Here's how _CALL_FUNCTION_ works:
1. Pop all arguments from the stack (number of arguments is provided as an
argument to the opcode)
2. Pop the callable to call from the stack
3. Execute the code object of this callable
4. Push the return value of the callable on the stack
Other _CALL_FUNCTION*_ opcodes work in a similar way.
In the end there can be 2 situations:
* We don't care about the function return value
>>> def f():
... test(1, 2, 3)
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (test)
3 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
9 LOAD_CONST 3 (3)
12 CALL_FUNCTION 3
15 POP_TOP
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
Notice the _POP_TOP_ opcode at location 15, which removes the return value of
the _test_ function (whatever this might be) from the stack, since we didn't
assign it to any variable.
* We want to store the function return value
>>> def f():
... t = test(1, 2, 3)
...
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (test)
3 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
9 LOAD_CONST 3 (3)
12 CALL_FUNCTION 3
15 STORE_FAST 0 (t)
18 LOAD_CONST 0 (None)
21 RETURN_VALUE
Notice the _STORE_FAST_ opcode at location 15, which pops an item from the
stack (in this case the return value of the _test_ function), and assigns it
to a certain name (how names are handled inside functions won't be discussed
here).
Now, how does _YIELD_VALUE_ work (since CPython 2.5)? Quite similar to
_CALL_FUNCTION*_: when a _YIELD_VALUE_ opcode is encountered by the virtual
machine, it pops a value from the stack, and yields this to the generator
caller. The value sent back by the caller (through the _send_ or _next_ methods,
where in case of the _next_ method this value is _None_) is pushed on top of the
stack.
If the yield statement comes at the right-hand-side (RHS) of an assignment, the
returned value is handled exactly like a function return value: the value is
popped from the stack and assigned to a name. If the yield statement is a
non-assignment statement, the value is discared using _POP_TOP_:
>>> def f():
... a = yield 123
... yield 456
...
>>> dis.dis(f)
2 0 LOAD_CONST 1 (123)
3 YIELD_VALUE
4 STORE_FAST 0 (a)
3 7 LOAD_CONST 2 (456)
10 YIELD_VALUE
11 POP_TOP
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
This means we can safely insert a _YIELD_VALUE_ opcode after every
_CALL_FUNCTION*_ occurrence: the stack will remain intact.
This also means we can, thanks to this binary rewrite, make our system work in
situations like
>>> if askYesNo('Something'):
... doSomething()
where a normal _yield_ would be invalid syntax (see before). The runtime handles
the changed opcode stream just fine.
Binary rewriting caveats
========================
When rewriting a normal function, this can be done by changing the _co_code_
attribute of the function code object (_func_code_ attribute). This is a simple
bytestring of opcodes and arguments.
When making big changes to a function (like, turning it into a generator
function) lots of other things need to be changed as well:
* The flags of the code object should be altered so the runtime knows it's a
generator function (need to be OR'ed with 0x0020, see CO_GENERATOR in
Include/code.h in your CPython distribution)
>>> def f():
... a = 123
...
>>> def g():
... a = yield 123
...
>>> f.func_code.co_flags
67
>>> g.func_code.co_flags
99
>>> f.func_code.co_flags & 0x0020
0
>>> g.func_code.co_flags & 0x0020
32
* The stacksize should be adjusted if necessary
* ...
Getting this straight is a tedious task and can be error-prone. Making mistakes
here can crash a CPython virtual machine.
This is why the _byteplay_ module is used to rewrite the existing function code.
This module will recalculate stack sizes, set code object flags correctly, etc.
For more information on the _byteplay_ module, see
http://code.google.com/p/byteplay/
Final overview
==============
Here's an overview of the total picture:
1. Given a wizard function, a new function object is generated. This generated
function has the same opcodes as the original one, except for one thing: a
_YIELD_VALUE_ opcode is added immediately after every _CALL_FUNCTION*_
opcode.
2. The generator function is called to create a generator. A helper walks
through the generator and sends back every value it receives, unless it's a
value wrapped in a well-known type.
3. If such special value is found, its actual value is extracted and send to the
caller code (ie. the runner code is a generator itself).
4. Any value the caller code sends into the generator is proxied to the
rewritten wizard function and assigned accordingly.
That's about it. Using above information you should be able to read the code
yourself and change it if ever necessary. Most hard work is done in the
___call___ and __generate_generator_ methods of _GeneratorGenerator_: the first
one provides the run helpers, the latter performs the bytecode rewriting. The
_step_ method of _RunningWizardManager_ shows the rewritten wizard can be used
as a normal generator, handling nothing but the original _q.gui.dialog_ calls.
'''
#pylint: disable-msg=C0302,R0903,W0142,C0103
#This is *only* supported on CPython 2.5. If this ever needs to run on some
#other version, make sure the bytecode rewriting tricks explained above also
#work on this new target.
import sys
assert (sys.version_info[:2] == (2, 5)), 'This service only runs in CPython 2.5'
assert hasattr(sys, 'subversion'), 'This service only runs in CPython 2.5'
assert (sys.subversion[0] == 'CPython'), 'This service only runs in CPython 2.5'
import os.path
import uuid
import operator
import byteplay
import threading
# We require this for the tests to run fine
if __name__ == '__main__':
from pymonkey.InitBase import q, i #pylint: disable-msg=F0401
else:
from pymonkey import q, i #pylint: disable-msg=F0401
class UnknownSessionException(Exception):
'''Exception raised when an invalid session ID is used'''
pass
class EndOfWizard(Exception):
'''The end of the wizard was reached'''
ACTION = '{"action": "endofwizard"}'
class DialogMessage(object):
'''Container for dialog messages from q.gui.dialog.*
Any wizard step response should be marshalled in this type so the wizard
runner can know it should yield the value.
'''
def __init__(self, value):
'''Wrap a new wizard message
@param value: Value to wrap
@type value: object
'''
self.value = value
class GeneratorGenerator(object):
'''Create a generator out of a normal funtion
This class will create a generator, based on a given function, by inserting
a YIELD_VALUE opcode after every CALL_FUNCTION* opcode. This generator is
executed in the __call__ method of this class, which is a generator itself:
it will send any value it receives from the internal generator back to the
generator, except if this value is of type C{special_value_wrapper}. In that
case, it will yield the value returned by C{special_value_getter} (providing
it the original value) to the caller, and forward the reply on this yield
to the internal generator.
See the module documentation for more information how this works.
'''
def __new__(cls, func, special_value_wrapper, special_value_getter):
'''Allocator for class instances
@param func: Function to create generator from
@type func: callable
@param special_value_wrapper: Wrapper class for 'special' values
@type special_value_wrapper: type
@param special_value_getter: Function to retrieve values from a wrapper
@type special_value_getter: callable
'''
key = (func, special_value_wrapper, special_value_getter)
if not hasattr(cls, '_generator_cache'):
cls._generator_cache = dict()
if key in cls._generator_cache:
return cls._generator_cache[key]
inst = object.__new__(cls, func, special_value_wrapper,
special_value_getter)
cls._generator_cache[key] = inst
return inst
def __init__(self, func, special_value_wrapper, special_value_getter):
'''Initialize a new GeneratorGenerator
@see: GeneratorGenerator.__new__
'''
#Check whether provided func is a generator. If it is, I'm unable to
#work with it
#The 0x0020 comes from Include/code.h in the CPython distribution:
# #define CO_GENERATOR 0x0020
if func.func_code.co_flags & 0x0020:
raise TypeError(
'Provided func is already a generator, I can\'t deal with this')
#Keep _initialized because we're using __new__. Don't re-init
if getattr(self, '_initialized', False):
return
assert callable(func), 'Provided function should be a callable'
assert callable(special_value_getter), \
'Provided special value getter should be a callable'
self._func = func
self._special_value_wrapper = special_value_wrapper
self._special_value_getter = special_value_getter
self._generator = None
self._initialized = True
def __call__(self, *args, **kwargs):
'''Call the generator
This will construct the generator if necessary, call it to get a
coroutine, and run it.
See the module documentation for more information how this works.
'''
if not self._generator:
self._generator = self._generate_generator(self._func)
runner = self._generator(*args, **kwargs)
try:
value = runner.next()
while True:
if isinstance(value, self._special_value_wrapper):
value = self._special_value_getter(value)
try:
#If you find this line in a backtrace, you most likely
#need to look one frame up, since that's where the
#exception went, from the caller of this generator to
#the actual generator
value = yield value
except (StopIteration, GeneratorExit):
raise
except:
#Forward any exception thrown into here into the inner
#coroutine
runner.throw(*sys.exc_info())
value = runner.send(value)
except StopIteration:
return
except GeneratorExit:
runner.close()
@classmethod
def _generate_generator(cls, func):
'''Create a generator out of the given func
This basicly creates a generator function by adding a YIELD_VALUE opcode
after every CALL_FUNCTION* opcode it finds in the original code. It also
rewrites the origin filename of the code object to denote it has been
modified by this method.
Do note C{func} should be a function, a general callable will most
likely not be sufficient.
@param func: Source function
@type func: function
'''
def insert_yields(opcodes):
for opcode, arg in opcodes:
yield (opcode, arg)
if opcode in (byteplay.CALL_FUNCTION,
byteplay.CALL_FUNCTION_VAR,
byteplay.CALL_FUNCTION_KW,
byteplay.CALL_FUNCTION_VAR_KW, ):
yield (byteplay.YIELD_VALUE, None)
code = byteplay.Code.from_code(func.func_code)
new_code = list(insert_yields(code.code))
code.code = new_code
code.filename = '%s%s(modified by %s:%s.%s)' % \
(code.filename, ' ' if code.filename else '', cls.__module__,
cls.__name__, '_generate_generator')
return type(func)(code.to_code(), func.func_globals, func.func_name,
func.func_defaults, func.func_closure)
class RunningWizardManager(object):
'''Manager for all running wizards'''
#TODO Garbage collection? Remove timed-out sessions?
def __init__(self):
#a dict to hold the running wizards.
self._wizards = dict()
#a dict to hold the running wizards locks,
#they are needed to add thread safety to the service, so calls to
#start, step, stop are synchronized
self._locks = dict()
def register(self, wizard_func, *args, **kwargs):
'''Register a new wizard
A session ID will be generated, and the wizard (implemented in the
callable wizard_func) will be created.
@param wizard_func: Function implementing the wizard
@type wizard_func: callable
@param args: Args sent to the generator constructor
@type args: tuple
@param kwargs: Kwargs sent to the generator constructor
@type kwargs: dict
@return: Session ID
@rtype: string
'''
if not callable(wizard_func):
raise TypeError('The wizard_func argument should be callable')
session = str(uuid.uuid4())
#This should never happen, but anyway
while session in self._wizards:
session = str(uuid.uuid4())
if not getattr(wizard_func,
'APPLICATIONSERVER_WIZARD_NO_YIELD_REWRITE', False):
wizard_func = GeneratorGenerator(wizard_func, DialogMessage,
operator.attrgetter('value'))
wizard = wizard_func(*args, **kwargs)
self._wizards[session] = wizard
#creating an RLock is created for the wizard, note that RLock is required since a thread that
#executing a step() call may try to re acquire the lock while calling stop() causing a deadlock in
#case of using simple Lock() object.
self._locks[session] = threading.RLock()
return session
def start(self, session):
'''Start the wizard
@param session: Session ID
@type session: string
@return: First wizard panel
@rtype: string
'''
lock = self._locks[session]
lock.acquire()
try:
wizard = self._get_wizard(session)
return wizard.next()
except StopIteration:
self.stop(session)
raise EndOfWizard
finally:
lock.release()
def stop(self, session):
'''Stop the wizard
@param session: Session ID
@type session: string
'''
lock = self._locks[session]
lock.acquire()
try:
try:
self._wizards[session].close()
except StopIteration:
pass
except RuntimeError, e:
if e.message == 'generator ignored GeneratorExit':
# Log and discard
q.logger.log('A wizard contains a catchall try/except '
'statement around a q.gui.dialog function. '
'This is considered bad style and might cause '
'memory leaks', 2)
else:
# Try to avoid a memleak
# The __del__ method of the generator might still bail out
# and send some warning/exception message to stderr. Nothing
# we can do about that
del self._wizards[session]
raise
del self._wizards[session]
except KeyError:
pass
finally:
del self._locks[session]
lock.release()
def step(self, session, data):
'''Execute one step of the wizard
@param session: Session ID
@type session: string
@param data: Data to send to the wizard method
@type data: object
@return: Next wizard panel
@rtype: string
'''
lock = self._locks[session]
lock.acquire()
try:
wizard = self._get_wizard(session)
return wizard.send(data)
except StopIteration:
self.stop(session)
raise EndOfWizard
finally:
lock.release()
def _get_wizard(self, session):
try:
return self._wizards[session]
except KeyError:
raise UnknownSessionException('Session %s is unknown' % session)
class ApplicationserverWizardService(object):
'''Wizard applicationserver service'''
def __init__(self):
self._manager = RunningWizardManager()
q.gui.dialog.chooseDialogType(q.enumerators.DialogType.WIZARDSERVER)
q.gui.dialog.MessageType = DialogMessage
# Tasklets go into (folder containing this service file)/tasklets
tasklet_path = q.system.fs.joinPaths(
os.path.dirname(__file__), 'tasklets')
self.taskletengine = q.getTaskletEngine(tasklet_path)
@q.manage.applicationserver.expose
def start(self, customerGuid, wizardName, extra=None):
q.logger.log('Start new wizard %s for customer %s' % \
(wizardName, customerGuid), 7)
extra = extra or dict()
wizard_methods = self.taskletengine.find(name=wizardName,
tags=('wizard', ))
if not wizard_methods:
raise RuntimeError('No matching wizard found')
if len(wizard_methods) > 1:
raise RuntimeError('Multiple matching wizards found')
wizard_method = wizard_methods[0].main
params = {
'customerGuid': customerGuid,
'extra': extra,
}
tags = ('wizard', )
session = self._manager.register(wizard_method, q, i, params, tags)
try:
step = self._manager.start(session)
except EndOfWizard, e:
return session, e.ACTION
return session, step
@q.manage.applicationserver.expose
def stop(self, sessionId):
q.logger.log('Stop wizard %s' % sessionId, 7)
self._manager.stop(sessionId)
return EndOfWizard.ACTION
@q.manage.applicationserver.expose
def result(self, sessionId, result):
q.logger.log('New result in wizard %s: %s' % (sessionId, result), 7)
try:
step = self._manager.step(sessionId, result)
except EndOfWizard, e:
q.logger.log('End of wizard %s' % sessionId, 7)
step = e.ACTION
return step
# Some testcases testing our function rewriting, static analysis
if __name__ == '__main__':
#pylint: disable-msg=C0111,C0103,E0602,W0142,W0612,W0212,W0613,R0911
import unittest
# This is the function we will rewrite in the yield insert test
def _yield_test_original(arg1, arg2, *args, **kwargs):
a = 123
b = 456
c = func1()
a = 789
d = func2(a, b, c)
a = 987
func3()
a = 654
e = func4(*abc)
a = 321
f = func5(a, b, c, 'abc', *abc, **{'a': 'b', 'c': 1})
a = 123
g = func6(**{'a': 123})
a = 456
h = q.some.function('abc')
a = 789
# This is the result we expect after rewriting _yield_test_original
def _yield_test_result(arg1, arg2, *args, **kwargs):
a = 123
b = 456
c = (yield func1())
a = 789
d = (yield func2(a, b, c))
a = 987
yield func3()
a = 654
e = (yield func4(*abc))
a = 321
f = (yield func5(a, b, c, 'abc', *abc, **{'a': 'b', 'c': 1}))
a = 123
g = (yield func6(**{'a': 123}))
a = 456
h = (yield q.some.function('abc'))
a = 789
class TestFunctionRewriting(unittest.TestCase):
def _compare_funcs(self, target, func):
'''Check whether 2 functions are code-wise equivalent'''
target_code = byteplay.Code.from_code(target.func_code).code
func_code = byteplay.Code.from_code(func.func_code).code
self.assertEqual(len(target_code), len(func_code))
# Make sure code and func attributes are equal
# Don't compare func_code and func_name, the first one will
# obviously not be the same, the second one neither but we don't
# care
for attr in ('func_closure', 'func_defaults', 'func_dict',
'func_doc', 'func_globals', ):
self.assertEqual(getattr(target, attr), getattr(func, attr),
'Function attribute %s is different' % attr)
# Don't compare co_code, which will obviously be different, nor
# co_filename since we rewrite it, nor co_firstlineno since it is
# indeed not the same (we're comparing 2 different functions), nor
# co_lnotab since it will just like co_firstlineno be different, nor
# co_name, since it will be different as well
for attr in ('co_argcount', 'co_cellvars', 'co_consts', 'co_flags',
'co_freevars', 'co_names', 'co_nlocals',
'co_stacksize', 'co_varnames', ):
self.assertEqual(getattr(target.func_code, attr),
getattr(func.func_code, attr),
'Code attribute %s is different' % attr)
known_labels = dict()
for op1, op2 in zip(target_code, func_code):
if op1[0] == byteplay.SetLineno:
self.assertEqual(op1[0], op2[0])
continue
if isinstance(op1[1], byteplay.Label):
self.assert_(isinstance(op2[1], byteplay.Label))
if op1[1] in known_labels:
self.assert_(op2[1] is known_labels[op1[1]])
else:
known_labels[op1[1]] = op2[1]
continue
if isinstance(op1[0], byteplay.Label):
self.assert_(isinstance(op2[0], byteplay.Label))
if op1[0] in known_labels:
self.assert_(op2[0] is known_labels[op1[0]])
else:
known_labels[op1[0]] = op2[0]
continue
self.assertEqual(op1, op2)
def test_yield_insert(self):
'''Test yield insertion rewriting'''
result = GeneratorGenerator._generate_generator(
_yield_test_original)
self._compare_funcs(_yield_test_result, result)
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment