Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions protowhat/State.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from protowhat.selectors import DispatcherInterface
from protowhat.Feedback import Feedback, InstructorError
from protowhat.Test import Fail, Test, TestFail
from protowhat.utils import _debug
from protowhat.utils import _debug, parameters_attr


class DummyDispatcher(DispatcherInterface):
Expand All @@ -27,6 +27,7 @@ def describe(self, *args, **kwargs):
return "code"


@parameters_attr
class State:
def __init__(
self,
Expand All @@ -47,11 +48,9 @@ def __init__(
ast_dispatcher=None,
):
args = locals().copy()
self.params = list()

for k, v in args.items():
if k != "self":
self.params.append(k)
setattr(self, k, v)

self.messages = messages if messages else []
Expand Down Expand Up @@ -156,9 +155,11 @@ def do_tests(self, tests):
def to_child(self, append_message="", **kwargs):
"""Basic implementation of returning a child state"""

bad_pars = set(kwargs) - set(self.params)
if bad_pars:
raise ValueError("Invalid init params for State: %s" % ", ".join(bad_pars))
bad_parameters = set(kwargs) - set(self.parameters)
if bad_parameters:
raise ValueError(
"Invalid init parameters for State: %s" % ", ".join(bad_parameters)
)

child = copy(self)
for k, v in kwargs.items():
Expand Down
124 changes: 118 additions & 6 deletions protowhat/sct_syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
import copy
import builtins
from functools import wraps, reduce, partial
from importlib import import_module
from typing import Union, Type, Callable, Dict, Any, Tuple

from protowhat.Reporter import Reporter
from protowhat.State import State
from protowhat.utils import get_class_parameters

def state_dec_gen(state_cls, attr_scts):

def state_dec_gen(state_cls: Type[State], attr_scts):
def state_dec(f):
"""Decorate check_* functions to return F chain if no state passed"""

Expand All @@ -21,7 +27,7 @@ def wrapper(*args, **kwargs):
return state_dec


def link_to_state(check):
def link_to_state(check: Callable[..., State]) -> Callable[..., State]:
@wraps(check)
def wrapper(state, *args, **kwargs):
new_state = check(state, *args, **kwargs)
Expand All @@ -40,7 +46,7 @@ def wrapper(state, *args, **kwargs):


class Chain:
def __init__(self, state, attr_scts=None):
def __init__(self, state: Union[State, None], attr_scts=None):
self._state = state
self._crnt_sct = None
self._waiting_on_call = False
Expand Down Expand Up @@ -163,7 +169,7 @@ def __call__(self, state=None):
Ex = ExGen(None, {})


def get_checks_dict(checks_module):
def get_checks_dict(checks_module) -> Dict[str, Callable]:
return {
k: v
for k, v in vars(checks_module).items()
Expand All @@ -173,8 +179,21 @@ def get_checks_dict(checks_module):
}


def create_sct_context(State, sct_dict, root_state=None):
state_dec = state_dec_gen(State, sct_dict)
def create_sct_context(
state_cls: Type[State], sct_dict, root_state: State = None
) -> Dict[str, Callable]:
"""
Create the globals that will be available when running the SCT.

Args:
state_cls: the State class of the technology to create the context for
sct_dict: a dictionary of the functions to make available
root_state: a State instance with the exercise information available to the SCT

Returns:
dict: the globals available to the SCT code
"""
state_dec = state_dec_gen(state_cls, sct_dict)
sct_ctx = {k: state_dec(v) for k, v in sct_dict.items()}

ctx = {
Expand All @@ -185,3 +204,96 @@ def create_sct_context(State, sct_dict, root_state=None):
}

return ctx


def create_embed_state(
xstate: Type[State],
parent_state: State,
derive_custom_state_args: Callable[[State], Dict[str, Any]] = None,
highlight_offset: dict = None,
) -> State:
"""
Create the state for running checks in the embedded technology.

This function also connects the created state with the state of the host technology.

Args:
xstate: the State class of the embedded technology
parent_state: state of the host technology to derive the embedded state from
derive_custom_state_args: function to calculate instructor ovextra arguments to pass to the constructor of the embedded state
highlight_offset: position of the embedded code in the student code

Returns:
an instance of xstate
"""
# gather the kwargs the xstate will be created with
args = {}

# find all arguments the xstate constructor can handle
embedded_state_parameters = getattr(
xstate, "parameters", get_class_parameters(xstate)
)

# copy all allowed arguments from the parent state
for arg in embedded_state_parameters:
if hasattr(parent_state, arg):
args[arg] = getattr(parent_state, arg)

# configure the reporter to collaborate with the parent state reporter
args["reporter"] = Reporter(
parent_state.reporter, highlight_offset=highlight_offset or {}
)

custom_args = (
derive_custom_state_args(parent_state) if derive_custom_state_args else {}
)

# manually add / override arguments
args.update(**custom_args)

embed_state = xstate(**args)
# TODO: other params? set manually through chain constructor or add State args
# to pass: path, debug; don't pass: highlight, ast_dispatcher, params
embed_state.creator = {"type": "embed", "args": {"state": parent_state}}

return embed_state


def create_embed_context(technology: str, context: Chain, **kwargs):
"""
Create the globals that will be available when running the checks for the embedded technology.

Extra keyword arguments are passed to the constructor of the State for the embedded technology.

Args:
technology: the name of the embedded technology (the x in xwhat)
context: the Chain of the host technology
the checks for the embedded technology will use as starting point

Returns:
dict: the variables available to the SCT code for the embedded technology
"""
parent_state = context._state

xwhat = import_module("{}what".format(technology))
xwhat_checks = import_module("{}what.checks".format(technology))
xwhat_state = xwhat.State.State

xwhat_scts = get_checks_dict(xwhat_checks)

embed_state = create_embed_state(xwhat_state, parent_state, **kwargs)

return create_sct_context(xwhat_state, xwhat_scts, root_state=embed_state)


def get_embed_chain_constructors(*args, **kwargs) -> Tuple[Type, Type]:
"""
Get the chain constructors for the embedded technology.

This is a wrapper around create_embed_context

Returns:
tuple: Ex and F for the embedded xwhat
"""
new_context = create_embed_context(*args, **kwargs)
return new_context["Ex"], new_context["F"]
61 changes: 55 additions & 6 deletions protowhat/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import itertools
from functools import wraps
from inspect import signature, Parameter
from typing import Type, Iterator, Callable, Dict


def _debug(state, msg="", on_error=False):
Expand Down Expand Up @@ -32,17 +35,63 @@ def _debug(state, msg="", on_error=False):
return state


def legacy_signature(**kwargs_mapping):
def get_class_parameters(cls: Type) -> Iterator[str]:
"""
Get an iterator over all arguments that can be used to construct a class.

This works for class hierarchies that have compatible constructors.

Args:
cls: class to inspect constructor parameters in class hierarchy for

Returns:
Iterator over parameter names
"""
return itertools.chain(
*(get_class_parameters(cls) for cls in cls.__bases__),
(
name
for name, param in signature(cls).parameters.items()
if param.kind is not Parameter.VAR_POSITIONAL
and param.kind is not Parameter.VAR_KEYWORD
)
)


def parameters_attr(cls: Type) -> Type:
"""Add a parameters attribute to the class with all arguments that can be used to construct it.

Why?
- calculated once: not manual list, not built every init
- accessible on class: not only usable on instances
- set difference between vars() and parameters doesn't include this class attribute
- more noticeable than manually setting attribute after class definition

Args:
cls: class to add parameters attribute to

Returns:
cls: modified class
"""
cls.parameters = list(get_class_parameters(cls))
return cls


def legacy_signature(
**kwargs_mapping: Dict[str, str]
) -> Callable[[Callable], Callable]:
"""
This decorator makes it possible to call a function using old argument names
when they are passed as keyword arguments.

@legacy_signature(old_arg1='arg1', old_arg2='arg2')
def func(arg1, arg2=1):
return arg1 + arg2
:Example:

@legacy_signature(old_arg1='arg1', old_arg2='arg2')
def func(arg1, arg2=1):
return arg1 + arg2

func(old_arg1=1) == 2
func(old_arg1=1, old_arg2=2) == 3
func(old_arg1=1) == 2
func(old_arg1=1, old_arg2=2) == 3
"""

def signature_decorator(f):
Expand Down
Loading