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

Extensions: decorators #97

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Extensions: decorators #97

wants to merge 3 commits into from

Conversation

barneydobson
Copy link
Collaborator

@barneydobson barneydobson commented Jun 3, 2024

Description

My demonstration of decorators in the extensions file.

I have implemented it on one of the example tutorials.

If we're happy with it I think we will have two further issues:

  • Decorators behaviour, where further functions are added in extensions (e.g., extend.surface, etc.)
  • Decorators notebook, where we update existing tutorials with this new usage (they need updating anyway)

I think we will also want a notebook for extensions in general, but for me it makes sense to do this after we support subclasses via it.

@extend.node_attribute(obj=my_fwtw, attribute_name="pull_distributed")
def new_distributed(pull_distributed, vqip):
"""pull_distributed with the tag 'FWTW'."""
return pull_distributed(vqip, tag="FWTW")

Copy link
Collaborator

Choose a reason for hiding this comment

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

Due to my lack of knowledge on @extend, how does it call the new_distributed function? @barneydobson

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

extend is just our import name for the new wsimod.extensions.extensions.py module.

what happens here is this node_attribute is being used to wrap the existing pull_distributed - the @ syntax above our new function applies this wrapping

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, then what would be an example for replacing the attribute with a scalar value - just type the value below @extend or need a lambda function?

Copy link
Collaborator

@dalonsoa dalonsoa left a comment

Choose a reason for hiding this comment

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

To be flexible enough, we need something a little bit more elaborate. See my comments.


@extend.node_attribute(obj=my_fwtw, attribute_name="pull_distributed")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm afraid things need to be a bit more elaborate than this since for this approach to work, you need to already have access to the formed objects. This is the case in this tutorial since you are creating all the components programmatically so you can pass the object, but it will not be true when including the extensions in an extension file. Indeed, in this case it is not necessary to use decorators as you can simply do: my_fwtw.pull_distributed = new_distributed, and that's all.

I suggest an alternative below.

"""


def node_attribute(obj, attribute_name: str):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function should accept a string indicating the node name to update, not a fully formed object. Of course, there has to be a way for the function to get the right object. I would suggest to create a singleton class - a sort of global variable, but a bit tidier -, from where this one pulls the information it needs. A minimal implementation would be something like:

class ExtensionsHandler:
    __slots__ = "_model"
    _instance: Optional["ExtensionsHandler"] = None

    def __new__(cls, model: Optional[Model] = None):
        if not cls._instance:
            if not model:
                raise ValueError("The extension handler needs to be initialised with a model.")
            cls._instance = super().__new__(cls)
            cls._instance._model = model
        return cls._instance

    def __init__(self, model: Optional[Model] = None):
        # Do not assign `model` here.
        self._model: Model

    def get_node(self, name: str) -> Node:
        return self._model.nodes[name]

Now, a few more things:

In the __init__ method of Model class we initialise the handler as the very first thing to do. You do not need to assign it to anything, as it is just a question of making the extension handler aware of what model is to be extended:

class Model(...):
    def __init__(self, ...):
         ExtensionsHandler(self)

And then, within the decorators, now taking a string as input, we do:

def node_attribute(name: str, attribute_name: str):
    def decorator(...):
        obj = ExtensionsHandler().get_node(name)
        ...

You can implement a more complex logic, add handlers for arcs, etc, but this would be the basic idea. In summary:

  • You register your model with the extension handler.
  • You use the extension handler in the decorators to pull the relevant node/arc other from the model you are running.

Does any of this makes sense?

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.

None yet

3 participants