Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

Commit

Permalink
feat(Intepreter): Add compile method
Browse files Browse the repository at this point in the history
Exposes code parsing as the `compile` method on `Interpreter`, accessible via `Server`. This enables Executa to delegate the `compile` method to Pyla for Python code chunks.

Does some refactoring so that `Server` does nothing but dispatch methods calls to `Interpreter` as in Executa itself.
  • Loading branch information
Nokome Bentley committed Dec 16, 2019
1 parent 4705b2a commit 96b5bbd
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 153 deletions.
14 changes: 14 additions & 0 deletions stencila/pyla/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Custom error classes"""

class CapabilityError(Exception):
"""
Custom error class to indicate that an executor is not capable of performing a method call.
Python implementation of Executa's
[CapabilityError](https://github.com/stencila/executa/blob/v1.4.0/src/base/errors.ts#L57).
Is translated to a JSON-RPC error with code `CapabilityError`.
"""

def __init__(self, method: str, **kwargs):
params = ', '.join(['{} = {}'.format(name, value) for name, value in kwargs.items()])
super().__init__('Incapable of method "{}" with params "{}"'.format(method, params))
95 changes: 60 additions & 35 deletions stencila/pyla/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@

import ast
import astor
from stencila.schema.types import Parameter, CodeChunk, Article, Entity, CodeExpression, \
from stencila.schema.types import Node, Parameter, CodeChunk, Article, Entity, CodeExpression, \
ConstantSchema, EnumSchema, BooleanSchema, NumberSchema, IntegerSchema, StringSchema, \
ArraySchema, TupleSchema, ImageObject, Datatable, DatatableColumn, Function
from stencila.schema.util import from_json, to_json

from .code_parsing import CodeChunkExecution, set_code_error, CodeChunkParser
from .errors import CapabilityError
from .code_parsing import CodeChunkExecution, set_code_error, CodeChunkParser, simple_code_chunk_parse

try:
import matplotlib.figure
Expand Down Expand Up @@ -228,8 +229,14 @@ class Interpreter:
"""Execute a list of code blocks, maintaining its own `globals` scope for this execution run."""

"""
JSON Schema specification of the types of nodes that `compile` and
`execute` methods are capable of handling
List of values for the `programmingLanguage` property that are handled
by this interpreter.
"""
PROGRAMMING_LANGUAGES = ['py', 'python']

"""
JSON Schema specification of the types of nodes that this intertpreter's
`compile` and `execute` methods are capable of handling
"""
CODE_CAPABILITIES = {
'type': 'object',
Expand All @@ -243,15 +250,15 @@ class Interpreter:
'enum': ['CodeChunk', 'CodeExpression']
},
'programmingLanguage': {
'enum': ['python', 'py']
'enum': PROGRAMMING_LANGUAGES
}
}
}
}
}

"""
The manifest of this interpreters capabilities and addresses.
The manifest of this interpreter's capabilities and addresses.
Conforms to Executa's
[Manifest](https://github.com/stencila/executa/blob/v1.4.0/src/base/Executor.ts#L63)
Expand Down Expand Up @@ -279,6 +286,50 @@ def __init__(self) -> None:
self.globals = {}
self.locals = {}

@staticmethod
def compile(node: Node) -> Node:
"""Compile a `CodeChunk`"""
if isinstance(node, CodeChunk) and Interpreter.is_python_code(node):
chunk, parse_result = simple_code_chunk_parse(node)
if parse_result.imports: chunk.imports = parse_result.imports
if parse_result.assigns: chunk.assigns = parse_result.assigns
if parse_result.declares: chunk.declares = parse_result.declares
if parse_result.alters: chunk.alters = parse_result.alters
if parse_result.uses: chunk.uses = parse_result.uses
if parse_result.reads: chunk.reads = parse_result.reads
if parse_result.error: chunk.errors = parse_result.error
return chunk
raise CapabilityError('compile', node=node)

def execute(self, node: Node, parameter_values: typing.Dict[str, typing.Any] = None) -> Node:
"""Execute a `CodeChunk` or `CodeExpression`"""
_locals = self.locals
if parameter_values is not None:
_locals.update(parameter_values)

if isinstance(node, CodeExpression):
return self.execute_code_expression(node, _locals)
if isinstance(node, CodeChunk):
cce = simple_code_chunk_parse(node)
return self.execute_code_chunk(cce, _locals)
if isinstance(node, CodeChunkExecution):
return self.execute_code_chunk(cce, _locals)
raise CapabilityError('execute', node=node)

@staticmethod
def is_python_code(code: typing.Union[CodeChunk, CodeExpression]) -> bool:
"""Is a `CodeChunk` or `CodeExpression` Python code?"""
return code.programmingLanguage.lower() in Interpreter.PROGRAMMING_LANGUAGES

def execute_code_expression(self, expression: CodeExpression, _locals: typing.Dict[str, typing.Any]) -> None:
"""Evaluate `CodeExpression.text`, and get the result. Catch any exception the occurs."""
try:
# pylint: disable=W0123 # Disable warning that eval is being used.
expression.output = self.decode_output(eval(expression.text, self.globals, _locals))
# pylint: disable=W0703 # we really don't know what Exception some eval'd code might raise.
except Exception as exc:
set_code_error(expression, exc)

def execute_code_chunk(self, chunk_execution: CodeChunkExecution, _locals: typing.Dict[str, typing.Any]) -> None:
"""Execute a `CodeChunk` that has been parsed and stored in a `CodeChunkExecution`."""
chunk, parse_result = chunk_execution
Expand Down Expand Up @@ -388,34 +439,6 @@ def parse_statement_runtime(statement: ast.stmt) -> StatementRuntime:
code_to_run = compile(mod, '<ast>', 'exec')
return capture_result, code_to_run, run_function

def execute_code_expression(self, expression: CodeExpression, _locals: typing.Dict[str, typing.Any]) -> None:
"""eval `CodeExpression.text`, and get the result. Catch any exception the occurs."""
try:
# pylint: disable=W0123 # Disable warning that eval is being used.
expression.output = self.decode_output(eval(expression.text, self.globals, _locals))
# pylint: disable=W0703 # we really don't know what Exception some eval'd code might raise.
except Exception as exc:
set_code_error(expression, exc)

def execute(self, code: typing.List[ExecutableCode], parameter_values: typing.Dict[str, typing.Any]) -> None:
"""
For each piece of code (`CodeChunk` or `CodeExpression`) execute it.
The `parameter_values` are used as the locals values for all executions or evals throughout the code in this
`Article`.
"""
_locals = self.locals
_locals.update(parameter_values)
#_ locals = parameter_values.copy()

for piece in code:
if isinstance(piece, CodeChunkExecution):
self.execute_code_chunk(piece, _locals)
elif isinstance(piece, CodeExpression):
self.execute_code_expression(piece, _locals)
else:
raise TypeError('Unknown Code node type found: {}'.format(piece))

@staticmethod
def value_is_mpl(value: typing.Any) -> bool:
"""Basic type checking to determine if a variable is a MatPlotLib figure."""
Expand Down Expand Up @@ -593,7 +616,9 @@ def execute_compilation(compilation_result: DocumentCompilationResult, parameter
"""Compile an `Article`, and interpret it with the given parameters (in a format that would be read from CLI)."""
param_parser = ParameterParser(compilation_result.parameters)
param_parser.parse_cli_args(parameter_flags)
Interpreter().execute(compilation_result.code, param_parser.parameter_values)
interpreter = Interpreter()
for code in compilation_result.code:
interpreter.execute(code, param_parser.parameter_values)


def compile_article(article: Article) -> DocumentCompilationResult:
Expand Down
21 changes: 7 additions & 14 deletions stencila/pyla/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import logging
import typing
from socket import socket
from stencila.schema.types import CodeChunk, Node
from stencila.schema.types import Node
from stencila.schema.util import from_dict, object_encode

from .code_parsing import simple_code_chunk_parse
from .errors import CapabilityError
from .interpreter import Interpreter

StreamType = typing.Union[typing.BinaryIO, socket]
Expand Down Expand Up @@ -215,16 +215,6 @@ def write_message(self, message: str) -> None:
"""Write a length-prefixed message to the output stream."""
message_write(self.output_stream, message)

def execute_node(self, node: dict) -> Node:
"""Parse a `CodeChunk` or `CodeExpression` from `node` and execute it with the `interpreter`."""
code = from_dict(node)
if isinstance(code, CodeChunk):
to_execute = simple_code_chunk_parse(code)
else:
to_execute = code
self.interpreter.execute([to_execute], {})
return code

def receive_message(self, message: str) -> str:
"""
Receive a JSON-RPC request and send back a JSON-RPC response.
Expand All @@ -250,15 +240,18 @@ def receive_message(self, message: str) -> str:

if method == 'manifest':
result = Interpreter.MANIFEST
elif method == 'execute':
elif method in ('compile', 'execute'):
node = params.get('node')
if node is None:
raise JsonRpcError(JsonRpcErrorCode.InvalidParams, 'Invalid params: "node" is missing')
result = self.execute_node(node)
node = from_dict(node)
result = self.interpreter.compile(node) if method == 'compile' else self.interpreter.execute(node)
else:
raise JsonRpcError(JsonRpcErrorCode.MethodNotFound, 'Method not found: {}'.format(method))
except JsonRpcError as exc:
error = exc
except CapabilityError as exc:
error = JsonRpcError(JsonRpcErrorCode.CapabilityError, 'Capability error: {}'.format(exc))
except Exception as exc: # pylint: disable=broad-except
logging.exception(exc)
error = JsonRpcError(JsonRpcErrorCode.ServerError, 'Internal error: {}'.format(exc))
Expand Down
23 changes: 8 additions & 15 deletions tests/test_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,25 @@

def execute_code_chunk(text: str) -> CodeChunk:
cc = CodeChunk(text)
cce = CodeChunkExecution(
cc, CodeChunkParser().parse(cc)
)
Interpreter().execute([cce], {})
Interpreter().execute(cc)
return cc


def test_execute_simple_code_expression():
ce = CodeExpression('4 + 5')
Interpreter().execute([ce], {})
Interpreter().execute(ce)
assert ce.output == 9


def test_execute_parameterised_code_expression():
ce = CodeExpression('p1 + p2')
Interpreter().execute([ce], {'p1': 1, 'p2': 10})
Interpreter().execute(ce, {'p1': 1, 'p2': 10})
assert ce.output == 11


def test_catch_code_expression_error():
ce = CodeExpression('1 / 0')
Interpreter().execute([ce], {})
Interpreter().execute(ce)
assert ce.output is None
assert ce.errors[0].kind == 'ZeroDivisionError'
assert ce.errors[0].message == 'division by zero'
Expand Down Expand Up @@ -76,14 +73,10 @@ def test_code_chunk_exception_capture():
cc1 = CodeChunk('a = 5\na + 2\nprint(\'Goodbye world!\')\nbadref += 1\nprint(\'After exception!\')')
cc2 = CodeChunk('2 + 2\nprint(\'CodeChunk2\')')

cce1 = CodeChunkExecution(
cc1, CodeChunkParser().parse(cc1)
)
cce2 = CodeChunkExecution(
cc2, CodeChunkParser().parse(cc2)
)
interpreter = Interpreter()
for cc in [cc1, cc2]:
interpreter.execute(cc)

Interpreter().execute([cce1, cce2], {})
assert cc1.outputs == [7, 'Goodbye world!\n']
assert cc1.errors[0].kind == 'NameError'

Expand Down Expand Up @@ -113,7 +106,7 @@ def test_execute_compilation(mock_interpreter_class, mock_pp_class):

mock_pp_class.assert_called_with(compilation_result.parameters)
parameter_parser.parse_cli_args.assert_called_with(parameters)
interpreter.execute.assert_called_with(compilation_result.code, parameter_parser.parameter_values)
interpreter.execute.assert_not_called() #Because nothing in compilation_result


def test_sempahore_skipping():
Expand Down
Loading

0 comments on commit 96b5bbd

Please sign in to comment.