Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple scripts from WorkContext #609

Merged
merged 38 commits into from
Sep 9, 2021
Merged

Conversation

kmazurek
Copy link
Contributor

@kmazurek kmazurek commented Aug 26, 2021

Resolves #564, #261

Public API changes

  • introduced a new entity (script/__init__.py#Script) which replaces direct command calls on the WorkContext.
  • all command methods on WorkContext have been marked as deprecated in version 0.7.0.
  • WorkContext now includes a new_script method which returns an instance of Script bound to the given WorkContext instance.
  • each call adding a new command to a Script (e.g. run, send_json or direct add call) now returns an awaitable which, once the script is executed, will contain the event with the result of that particular command.

Other changes

  • Script is now used as the expected yield type from worker functions.
  • removed the use of CommandContainer, Steps and ExecOptions classes. Their former responsibilities are now fulfilled by Script (i.e. storing commands, defining options for a batch and serializing a series of commands for transfer to the provider).
  • all classes representing commands (e.g. Run, SendJson) have been moved to script/command.py. They have a new common base class (Command) with a new interface (after, before, evaluate).

TODO (optional, perhaps as separate PRs)

  • update all existing examples and integration tests to use Script instances instead of calls to WorkContext
  • remove stuff deprecated in 0.6.0?
  • (once possible) update the API reference

@kmazurek kmazurek self-assigned this Aug 26, 2021
@kmazurek kmazurek force-pushed the km/context-script-decouple branch from 95363d6 to 6f5fcfd Compare August 27, 2021 08:10
yapapi/script/command.py Outdated Show resolved Hide resolved
@kmazurek kmazurek marked this pull request as ready for review September 2, 2021 10:44
@kmazurek kmazurek requested review from a team, johny-b and shadeofblue September 2, 2021 10:44
Copy link
Contributor

@johny-b johny-b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't check the tests. I will do this later.

I have a general remark I think we might need to discuss. I really dislike the documentation being duplicated between yapapi.script.command.X and yapapi.script.Script.x. This is hard to maintain: docs will be changing and it's hard to keep such things in sync. Also, I double-dislike duplicated logic, like in run (I added a separate comment for this).

I think all those functions should look like this:

def run(self, *args, **kwargs):
    '''Arguments are passed to command.Run(). Returns awaitable ...'''
    return self.add(Run(*args, **kwargs))

This is clear. This is easy to maintain. If we have links in docs (as Filip suggested), this wouldn't even make it much harder to read.

Also: chapeau bas for finding a path through all of this stuff!

yapapi/script/command.py Outdated Show resolved Hide resolved
yapapi/script/command.py Outdated Show resolved Hide resolved
yapapi/script/command.py Outdated Show resolved Hide resolved
yapapi/engine.py Outdated Show resolved Hide resolved
yapapi/engine.py Outdated Show resolved Hide resolved
yapapi/script/__init__.py Show resolved Hide resolved
yapapi/script/__init__.py Show resolved Hide resolved
yapapi/script/__init__.py Outdated Show resolved Hide resolved
yapapi/script/__init__.py Show resolved Hide resolved
yapapi/script/__init__.py Outdated Show resolved Hide resolved
@kmazurek kmazurek force-pushed the km/context-script-decouple branch from 7dd990d to 5378db3 Compare September 3, 2021 13:21
@kmazurek
Copy link
Contributor Author

kmazurek commented Sep 7, 2021

I have a general remark I think we might need to discuss. I really dislike the documentation being duplicated between yapapi.script.command.X and yapapi.script.Script.x. This is hard to maintain: docs will be changing and it's hard to keep such things in sync. Also, I double-dislike duplicated logic, like in run (I added a separate comment for this).

I think all those functions should look like this:

def run(self, *args, **kwargs):
    '''Arguments are passed to command.Run(). Returns awaitable ...'''
    return self.add(Run(*args, **kwargs))

This is clear. This is easy to maintain. If we have links in docs (as Filip suggested), this wouldn't even make it much harder to read.

While I agree on the part about not duplicating docstrings I'm not convinced about having these methods accept *args, **kwargs. These methods (e.g. Script.run) are intended as helpers, providing a higher level of abstraction over the more generic Script.add.
As such, I think their signature should be unambiguous so that the user immediately understands how to use a given method, relying solely on its signature without having to jump to a docstring of a different class (for example: send_file(src_path: str, dst_path: str) versus send_file(*args, **kwargs)).
Another case here is type checking the user's code e.g. with mypy. With generic arguments (*args, **kwargs) it's not going to work.
Another thing to consider here is whether these existing commands/methods are likely to change their interface in the future. From my point of view they're more or less a closed interface (e.g. all download and upload commands will probably not change their arguments).

@johny-b What's your take on this?

@johny-b
Copy link
Contributor

johny-b commented Sep 7, 2021

Another case here is type checking the user's code e.g. with mypy. With generic arguments (*args, **kwargs) it's not going to work.

Are sure about this?
I never tried that, but I don't really understand why this wouldn't work, mypy has all the information it needs from the Command invocation (e.g. Run(...)). I think decorators usually have *args, **kwargs and they work with mypy?

Anyway, that's not really important because ...

@johny-b What's your take on this?

... I think you're right. *args, **kwargs are unhelpful for the end user, this was a bad idea.


@pytest.fixture
def work_context(self):
return WorkContext(mock.MagicMock(), mock.MagicMock(), storage=mock.AsyncMock())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I think this could be converted into a factoryboy factory so that it's reusable anywhere else where we'd like to have a mock WorkContext....

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 53d23eb

Copy link
Contributor

@shadeofblue shadeofblue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally, looks good as far as I could tell... adding a few non-critical comments

@staticmethod
def _assert_dst_path(script: Script, dst_path):
batch = script._evaluate()
# transfer_cmd = {'transfer': {'from': 'some/mock/path', 'to': 'container:expected/path'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover cruft?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a helper comment to show what transfer_cmd should look like. I see how it can be confusing, I'm going to remove it.

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 53d23eb

@staticmethod
def _assert_src_path(script: Script, src_path):
batch = script._evaluate()
# transfer_cmd = {'transfer': {'from': 'container:expected/path', 'to': 'some/mock/path'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover cruft?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 53d23eb

assert c.commands().pop()["transfer"]["to"] == f"container:{dst_path}"
def _assert_dst_path(script: Script, dst_path):
batch = script._evaluate()
# transfer_cmd = {'transfer': {'from': 'some/mock/path', 'to': 'container:expected/path'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same leftover cruft...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not cruft!

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed anyway in 53d23eb

assert c.commands().pop()["transfer"]["from"] == f"container:{src_path}"
def _assert_src_path(script: Script, src_path):
batch = script._evaluate()
# transfer_cmd = {'transfer': {'from': 'container:expected/path', 'to': 'some/mock/path'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same leftover cruft...

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 53d23eb

"""Schedule a Terminate command on the provider."""
return self.add(Terminate())

def send_json(self, data: dict, dst_path: str) -> Awaitable[CommandExecuted]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be upload_json to better correspond with download_json ?

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 4741009

"""
return self.add(SendBytes(data, dst_path))

def send_file(self, src_path: str, dst_path: str) -> Awaitable[CommandExecuted]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be upload_file to better correspond with download_file ?

Copy link
Contributor Author

@kmazurek kmazurek Sep 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed in 4741009

steps = self._pending_steps
self._pending_steps = []
return Steps(*steps, timeout=timeout)
assert self.__script._commands, "commit called with no commands scheduled"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm.... maybe it's not a bad thing per se? ... I mean, maybe there should be a way to vary commands run within an exescript from 0 to n ? ... it would make it simpler to create scripts based on variable number of elements - the client code wouldn't have to vary here and just process_batches would step over such a script without actually sending anything to the provider...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, though I'm thinking of potential debugging issues here (someone sending an empty script by mistake). Perhaps we should print a warning in process_batches whenever an empty script is encountered?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printing a warning is perfect! thanks :)

yapapi/script/__init__.py Outdated Show resolved Hide resolved
yapapi/script/__init__.py Show resolved Hide resolved
yapapi/script/command.py Outdated Show resolved Hide resolved
@johny-b johny-b self-requested a review September 9, 2021 10:38
Copy link
Contributor

@johny-b johny-b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job!

There's one more thing I don't understand, I left a commit suggestion.

Also: please take a look at
#641

yapapi/script/__init__.py Show resolved Hide resolved
@johny-b johny-b self-requested a review September 9, 2021 13:40
Copy link
Contributor

@johny-b johny-b left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I like the result : )

@kmazurek kmazurek merged commit cebb56c into master Sep 9, 2021
@kmazurek kmazurek deleted the km/context-script-decouple branch September 13, 2021 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: explicit representation of an exescript (batch)
5 participants