Review
Setting out to review the code, I discover my old InputBuilder experiment. I go one way then, the other, then back. An actual revert of a commit. No new code, but some useful thinking.
I’m moving directly here from the preceding article, with just a quick walk around the house as a break.
I have a few things that we should look at:
- Can we quickly move from our one action per request to multiple actions in one request?
- Is there general code clean-up that we need?
- Is it time to do a builder?
- Is there code in World that needs revision?
- Are there tests against World that need revision?
You may recall that I’ve done some experiments with a request builder, none quite like the request we’re building but they certainly seem promising. I ran across this code in World:
def update_client_for_test(self, client_bot):
result_dict = self.fetch(client_bot.id)
client_bot._knowledge.update(result_dict)
def process(self, world_input):
from tests.test_world_batch import WorldOutput
output = WorldOutput()
for request in world_input:
requestor_id = request.identifier
for action in request:
self.command(action.action, requestor_id, action.parameter)
output.append(self.fetch(requestor_id))
return output
def command(self, action, bot_id, parameter=None):
world_bot = self.entity_from_id(bot_id)
if action == 'step':
self.step(world_bot)
elif action == 'take':
self.take_forward(world_bot)
elif action == 'drop':
block = self.entity_from_id(parameter)
self.drop_forward(world_bot, block)
elif action == 'turn':
self.set_direction(world_bot, parameter)
else:
raise Exception(f'Unknown command {action}')
The first method may lead us to some tests that need improvement. The second and third clearly need to be removed or replaced with the new execute
and execute_command
. The process
method is the only user of command
. Let’s see who’s using process
:
Ah, this is interesting, and a bit challenging. There are five tests that are creating instances of my experimental classes for building requests, and running them through the old command processing.
Just to see what will happen, I will run them through the new execute
method, if I can wire it up quickly.
def process(self, world_input):
from tests.test_world_batch import WorldOutput
output = WorldOutput()
for request in world_input:
requestor_id = request.identifier
self.execute(request)
output.append(self.fetch(requestor_id))
return output
Four tests fail. I expected plenty of failures: the structures surely don’t quite line up.
def execute(self, request):
> id = request["entity"]
E TypeError: 'EntityRequest' object is not subscriptable
What is that class like?
class EntityRequest:
def __init__(self, entity_identifier, world_input=None):
self._world_input = world_input
self._entity_identifier = entity_identifier
self._actions = []
def add_action(self, action: EntityAction):
self._actions.append(action)
@property
def identifier(self):
return self._entity_identifier
def __iter__(self):
return iter(self._actions)
We could make it subscriptable.
def __getitem__(self, item):
if item == 'entity':
return self._entity_identifier
raise Exception(f'cannot get {item}')
Let’s see what we find.
E Exception: cannot get actions
def __getitem__(self, item):
if item == 'entity':
return self._entity_identifier
elif item == 'actions':
return self._actions
raise Exception(f'cannot get {item}')
One of the tests passes, three still fail.
def execute_action(self, entity, action):
> verb = action["verb"]
E TypeError: tuple indices must be integers or slices, not str
OK this was interesting but not as interesting as it might be. Let’s roll back, just to clear my mind.
This time, let’s see if we can make our builder build a request the way we like them.
If we can, then we can change process
as we did above and run it all through the existing execute
logic. Here’s the most robust test, the one we’d actually like to have run:
def test_new_syntax(self):
world = World(10, 10)
block_id = world.add_block(6, 5)
batch_in = InputBuilder(world) \
.request(world.add_bot(5, 5, direction=Direction.EAST)) \
.action('take', None) \
.action('turn', 'SOUTH') \
.action('step', None) \
.action('step', None) \
.action('drop', block_id) \
.request(world.add_bot(7, 7)) \
.action('step') \
.result()
batch_out = world.process(batch_in)
result = batch_out.results[0]
assert result['location'] == Location(5, 7)
result = batch_out.results[1]
assert result['location'] == Location(8, 7)
item = world.map.at_xy(5, 8)
assert item.id == block_id
That code relies on InputBuilder, the experimental class that builds the highly structured object referenced in process
.
- Huh!
- I’m about to make a mistake. The InputBuilder builds a rather nice structured object:
WorldInput has a collection of
EntityRequest, which has
identifier: an entity id
actions, a collection of
EntityAction, which has
action: a verb
parameter: whatever
I was about to change things so that the builder would create the dictionary structure that execute
currently uses. It would be far better to adapt the World to use these new objects. And … well … the fact that these tests run suggest that it already does.
We’ll compare the execution parts:
def execute_action(self, entity, action):
verb = action["verb"]
match verb:
case 'step':
self.step(entity)
case 'take':
self.take_forward(entity)
case 'drop':
holding_id = action["param1"]
holding = self.entity_from_id(holding_id)
self.drop_forward(entity, holding)
case 'turn':
direction = action["param1"]
self.set_direction(entity, direction)
case _:
raise Exception(f'Unknown action {action}')
def command(self, action, bot_id, parameter=None):
world_bot = self.entity_from_id(bot_id)
if action == 'step':
self.step(world_bot)
elif action == 'take':
self.take_forward(world_bot)
elif action == 'drop':
block = self.entity_from_id(parameter)
self.drop_forward(world_bot, block)
elif action == 'turn':
self.set_direction(world_bot, parameter)
else:
raise Exception(f'Unknown command {action}')
Very similar. The process
scheme relies on the fact that it can fetch the parameter unconditionally, and it comes back silently as None
if there is none.
As an experiment, let’s try changing one of DirectConnection’s methods to create a WorldInput type request. Before we do that, I think we should promote these builder and built objects out of the tests into at least one production file.
This is going to be a bit tricky. I’ve already tried and rolled back once. I think I have the files the way I want but the tests can’t build. I’ll need to sort out imports. Importing namedtuple
helps. Now just a few are broken.
Found one more import to fix and we are back to green with the objects moved into prod. Commit: moving world request spike objects to prod, preparing to use them.
OK, I need to clear my head. Let’s think about what we’re going to do.
We have this nice InputBuilder class that builds the structured request objects I described above. The plan was, a few moments ago, to use them. There is, I think, an issue that I wish I had thought of before I did this most recent commit.
The connections will be sending JSON back and forth. JSON is really good at sending things like dictionaries back and forth. It is not so good at sending and receiving structured objects.
This is no good. I am not convinced that these structured objects are the way to go, given client server. If we didn’t have the JSON in the middle, I would decide differently.
- Huh-squared
- I was wrong above when I thought I was mistaken. I think the dictionary is actually better for our situation.
Bah. Revert that commit.
So. What about that process code and the InputBuilder and all that jazz? Well, they are a good spike from which to learn, but I think we really do want an InputBuilder that just builds a structured dictionary. Much as I prefer having objects of my own rather than system collections … I think that in this case, with the JSON bridge in between and because my client is really an example of a client, using the dictionary makes more sense.
So we have thought about this, tried a few things, and decided …
I think what I’ll do is build up the dictionary, and a list of dictionaries, to be sent across. Whether I’ll create a convenient builder first, I’m not sure. It would be nice to have, and I do like the look of the code that uses the existing builder:
batch_in = InputBuilder(world) \
.request(world.add_bot(5, 5)) \
.action('take') \
.action('turn','SOUTH') \
.action('step') \
.action('step') \
.action('drop', block_id) \
.request(world.add_bot(7, 7)) \
.action('step') \
.action('step') \
.result()
I’ll decide that later. For now, I need a snack. See you next time!