import modelverse_kernel.primitives as primitive_functions class KnownRequestHandled(Exception): """An exception that signifies that a known request was handled.""" pass class RequestHandler(object): """A type of object that intercepts logic-related Modelverse requests, and forwards Modelverse state requests.""" def __init__(self): # generator_stack is a stack of (generator, pending requests, request replies, has-reply) # tuples. self.generator_stack = [] # exception_handlers is a stack of (generator_stack index, exception handler function) # tuples. self.exception_handlers = [] self.handlers = { 'RUN' : self.execute_run } def is_active(self): """Tests if this request handler has a top-of-stack generator.""" return len(self.generator_stack) > 0 def handle_request(self, reply): """Replies to a request from the top-of-stack generator, and returns a new request.""" if not self.is_active(): raise ValueError('handle_request cannot be called with an empty generator stack.') # Append the server's replies to the list of replies. self.extend_replies(reply) while 1: try: if self.has_pending_requests(): try: # Try to pop a request for the modelverse state. result = self.pop_requests() return result except KnownRequestHandled: # Carry on. pass # Perform a single generator step. self.step() except StopIteration: # Done, so remove the generator self.generator_stack.pop() if self.is_active(): # This generator was called from another generator. # Append 'None' to the caller's list of replies. self.append_reply(None) else: # Looks like we're done here. return None except primitive_functions.PrimitiveFinished as ex: # Done, so remove the generator self.generator_stack.pop() if self.is_active(): # This generator was called from another generator. # Append the callee's result to the caller's list of replies. self.append_reply(ex.result) else: # Looks like we're done here. return None except: raise def set_finished_requests_flag(self): """Sets the finished_requests flag in the top-of-stack tuple.""" current_generator, requests, _, replies, has_reply = self.generator_stack[-1] self.generator_stack[-1] = (current_generator, requests, True, replies, has_reply) def has_pending_requests(self): """Tests if the top-of-stack generator has pending requests.""" _, _, finished_requests, _, _ = self.generator_stack[-1] return not finished_requests def push_generator(self, gen): """Pushes a new generator onto the stack.""" self.generator_stack.append((gen, None, True, [], False)) def append_reply(self, new_reply): """Appends a reply to the top-of-stack generator's list of pending replies.""" current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1] replies.append(new_reply) has_reply = True self.generator_stack[-1] = (current_generator, requests, requests_done, replies, has_reply) def extend_replies(self, new_replies): """Appends a list of replies to the top-of-stack generator's list of pending replies.""" current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1] if new_replies is not None: replies.extend(new_replies) has_reply = True self.generator_stack[-1] = ( current_generator, requests, requests_done, replies, has_reply) def step(self): """Performs a single step: accumulated replies are fed to the generator, which then produces requests.""" current_generator, _, _, replies, has_reply = self.generator_stack[-1] # Send the replies to the generator, and ask for new requests. requests = current_generator.send(replies if has_reply else None) # Update the entry on the stack. self.generator_stack[-1] = (current_generator, requests, False, [], False) def pop_requests(self): """Tries to pop a batch of Modelverse _state_ requests from the current list of requests. Known requests are executed immediately. A list of requests and a Boolean are returned. The latter is True if there are no more requests to process, and false otherwise.""" _, requests, _, _, _ = self.generator_stack[-1] if requests is None or len(requests) == 0: # Couldn't find a request for the state to handle. self.set_finished_requests_flag() return requests for i, elem in enumerate(requests): if elem[0] in self.handlers: # The kernel should handle known requests. if i > 0: # Handle any requests that precede the known request first. pre_requests = requests[:i] del requests[:i] return pre_requests # The known request must be the first element in the list. Pop it. requests.pop(0) # The list of requests might be empty now. If so, then flag this # batch of requests as finished. if len(requests) == 0: self.set_finished_requests_flag() # Handle the request. _, request_args = elem self.handlers[elem[0]](request_args) # We couldn't find a RUN-request in the batch of requests, so we might as well # handle them all at once then. self.set_finished_requests_flag() return requests def execute_run(self, request_args): """Executes a RUN-request with the given argument list.""" # The list of requests might be empty now. If so, then flag this # batch of requests as finished. if len(request_args) == 1: # Format: ("RUN", [gen]) gen, = request_args self.push_generator(gen) raise KnownRequestHandled() else: # Format: ("RUN", [func, kwargs]) # This format is useful because it also works for functions that # throw an exception but never yield. func, kwargs = request_args # We need to be extra careful here, because func(**kwargs) might # not be a generator at all: it might simply be a method that # raises an exception. To cope with this we need to push a dummy # entry onto the stack if a StopIteration or PrimtiveFinished # exception is thrown. The logic in execute_yields will then pop # that dummy entry. try: self.push_generator(func(**kwargs)) raise KnownRequestHandled() except StopIteration: self.push_generator(None) raise except primitive_functions.PrimitiveFinished: self.push_generator(None) raise