Skip to content

Add a validation decorator#214

Draft
TeresiaOlsson wants to merge 1 commit intomainfrom
validator-decorator
Draft

Add a validation decorator#214
TeresiaOlsson wants to merge 1 commit intomainfrom
validator-decorator

Conversation

@TeresiaOlsson
Copy link
Contributor

I have started an idea to introduce a validator decorator that you can use to specify which ConfigModel to use to validate a specific class. The idea is:

  • Instead of enforcing that a module needs to contain a main class defined by the PYAMLCLASS and a configuration model that has to be named ConfigModel use a decorator to attach a validation model to a specific class. For example like:

    @validator(ConfigModel)
    class Accelerator(object):
    

    This would add an attribute _validator_model but also two functions validate and from_validated. validate allows you to validate the input data without creating an object of the class (for example if you want to create the dict separately but want some way to check that it's okay before loading it). from_validated validates the input data and creates the object.

  • This would make it easier to integrate devices/applications from third-party packages where it's not possible to enforce a certain module structure. As long as a class has the attributes/methods required by pyAML I think it should be possible to integrate and configure it in the same way as the standard pyAML classes despite that the module has been written differently.

  • The docs are currently difficult to understand because you can't see which attributes a class needs without having to also look at the config class. Instead I suggest to not build the config model into the class but separate validation and object creation.

    This means you need to write the required attributes twice which is a bit annoying, but at the same time it makes it possible to create objects without mandatory validation (which could be useful if you already have a validated config and don't need to validate every time) and make the dependency of pydantic lighter.

I have so far made a test only including the Accelerator to show the idea and try how it can be implemented. Before I continue I would like input on what you think about the idea and my suggestion for how to implement it?

  • I know the field_locations are currently not included. I'm working on getting them back.

  • It will also not yet work for building any other objects than the accelerator. For testing only a minimal config will work so far:

    type: pyaml.accelerator
    facility: BESSY2
    machine: storage_ring
    energy: 1.7e9
    

@TeresiaOlsson TeresiaOlsson marked this pull request as draft March 10, 2026 15:46
@TeresiaOlsson TeresiaOlsson self-assigned this Mar 10, 2026
@TeresiaOlsson TeresiaOlsson requested review from JeanLucPons, gubaidulinvadim and gupichon and removed request for JeanLucPons March 10, 2026 15:47
@JeanLucPons
Copy link
Contributor

OK
If I understand well your idea, you would like to have a type field (in the config) that refer to a class and not to a module ?
The doc is structured in a way that the ConfigModel appears at the beginning and no duplication are needed or I miss something ?

Please do not remove the method def handle_validation_error() that creates a comprehensible pydantic error messages in union with other error detection message in the yaml file. Or at least take care of providing good error messages.

@TeresiaOlsson
Copy link
Contributor Author

TeresiaOlsson commented Mar 10, 2026

OK If I understand well your idea, you would like to have a type field (in the config) that refer to a class and not to a module ? The doc is structured in a way that the ConfigModel appears at the beginning and no duplication are needed or I miss something ?

Please do not remove the method def handle_validation_error() that creates a comprehensible pydantic error messages in union with other error detection message in the yaml file. Or at least take care of providing good error messages.

No, the type field is still referring to a module. Nothing has changed in the yaml file. I also likely want to change it to refer to a class rather than a module later but that's not part of this pull request.

I know that the ConfigModel appears in the beginning but it's still confusing because people are not used to having to look at another class to understand what input parameters they have to put together to be able to create the object they are interested in. Also if I in python do help(Accelerator) it currently looks like this:

Help on class Accelerator in module pyaml.accelerator:

class Accelerator(builtins.object)
 |  Accelerator(cfg: pyaml.accelerator.ConfigModel)
 |  
 |  PyAML top level class
 |  
 |  Methods defined here:
 |  
 |  __init__(self, cfg: pyaml.accelerator.ConfigModel)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  get_description(self) -> str
 |      Returns the description of the accelerator
....

which means that I need to instead do help on the ConfigModel to get the information I was interested in.

handle_validation_error() I have understood. I have just moved it to a dedicated validation module. I'm still working on refactoring it so all the functionality that was in it is still there.

@JeanLucPons
Copy link
Contributor

OK I see.
For the API reference, some example code (as in Arrays) can be eventually generated as all PYAMLCLASS follows same rules.
For python help function, things can also be done to see the corresponding ConfigModel.

@TeresiaOlsson
Copy link
Contributor Author

Problem is that the PYAMLCLASS will also not work if we want to make it possible to configure third-party devices/applications and use them as if they were pyAML standard devices/applications. I don't see how we can enforce the use of either ConfigModel or PYAMLCLASS and especially not since this is code that the labs already have and aren't writing from scratch. It becomes difficult then to require a certain module structure because that would likely mean major refactoring for them. The only thing I think we can do is to enforce protocols or rely on duck typing. Or you think there is some other way?

I think the ConfigModel problem can easily be fixed with this decorator that links the class to it's config class. And if you don't care about validation you can just ignore it by not adding the decorator.

For the PYAMLCLASS my suggestion at the moment would be to change the config to be based on class rather than module. I think that would also be more logical for the user because you want to build an object of a class and not an object of a module. But that is another topic and more complicated because it would mean rewriting all existing config files. So it's not part of this PR because it requires it's own discussion. And maybe there is also other solutions.

I will continue working on it then and see what happens if I start to add this decorator also to the other classes.

@JeanLucPons
Copy link
Contributor

JeanLucPons commented Mar 11, 2026

I have nothing against removing the PYAMLCLASS and having a class in the type field instead of a module but this may prevent to use introspection to generate doc, dynamic API, etc...
We can also support both a type field that refer to a module as today and a class field that refer to a random class (with or without pydantic validation) and let the user choose between one of the 2.
Edit: In any case, we need a mechanism to validate interface of measurement or tuning tools so a user has to connect something compatible. For instance, a Tune monitor object should have an attribute tune ReadFloatArray that returns [qx,qy].

@JeanLucPons
Copy link
Contributor

@TeresiaOlsson
It would be nice to have a decorator @wrap_config that map _cfg items to avoid each to write myObject._cfg.something.

i.e.

class ConfigModel(BaseModel):
    """
    Configuration model for response matrix

    Parameters
    ----------
    matrix : list[list[float]]
        Response matrix data
    input_names : list[str], optional
        Input names, basically the actuators
    output_names : list[str]
        Output names, basically the measurements
    rf_response : list[float], optional
        RF response data
    """

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

    matrix: list[list[float]]
    input_names: Optional[list[str]]
    output_names: list[str]
    rf_response: Optional[list[float]] = None
    input_planes: Optional[list[str]] = None
    output_planes: Optional[list[str]] = None

@wrap_config
class ResponseMatrix(object):

To allow

resp : ResponseMatrix = sr.design.orbit.reponse_matrix
print(resp.input_names)

@TeresiaOlsson
Copy link
Contributor Author

I have nothing against removing the PYAMLCLASS and having a class in the type field instead of a module but this may prevent to use introspection to generate doc, dynamic API, etc... We can also support both a type field that refer to a module as today and a class field that refer to a random class (with or without pydantic validation) and let the user choose between one of the 2. Edit: In any case, we need a mechanism to validate interface of measurement or tuning tools so a user has to connect something compatible. For instance, a Tune monitor object should have an attribute tune ReadFloatArray that returns [qx,qy].

I like the idea of supporting a type field and a class field. I can explore options for that after I have finished the work with this decorator for the validation.

For validating the interface I think the easiest is to use protocols. Then if a third-party application doesn't already fulfill them it doesn't feel so complicated to me to write a little wrapper that imports the protocols and adds the missing functionality. But maybe there are other options also.

@JeanLucPons
Copy link
Contributor

I like the idea of supporting a type field and a class field. I can explore options for that after I have finished the work with this decorator for the validation.

OK so we may also rename type to module.

@JeanLucPons
Copy link
Contributor

For validating the interface I think the easiest is to use protocols. Then if a third-party application doesn't already fulfill them it doesn't feel so complicated to me to write a little wrapper that imports the protocols and adds the missing functionality. But maybe there are other options also.

My idea was rather abstract interfaces together with the possibility to override existing tools. So, for instance, if you want to write your own tune monitor, you just override the present one add your specific stuff. This will allow to preserve the attachment to simulator part and you can do want you want for the CS part.

@TeresiaOlsson
Copy link
Contributor Author

@TeresiaOlsson It would be nice to have a decorator @wrap_config that map _cfg items to avoid each to write myObject._cfg.something.

i.e.

class ConfigModel(BaseModel):
    """
    Configuration model for response matrix

    Parameters
    ----------
    matrix : list[list[float]]
        Response matrix data
    input_names : list[str], optional
        Input names, basically the actuators
    output_names : list[str]
        Output names, basically the measurements
    rf_response : list[float], optional
        RF response data
    """

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

    matrix: list[list[float]]
    input_names: Optional[list[str]]
    output_names: list[str]
    rf_response: Optional[list[float]] = None
    input_planes: Optional[list[str]] = None
    output_planes: Optional[list[str]] = None

@wrap_config
class ResponseMatrix(object):

To allow

resp : ResponseMatrix = sr.design.orbit.reponse_matrix
print(resp.input_names)

You mean a decorator which unpacks the config object and makes individual attributes of the attributes in the ConfigModel? So you can do something like resp.matrix and it will give you the matrix attribute of the ._cfg without having to do resp._cfg.matrix or explicitly code a property for matrix?

@TeresiaOlsson
Copy link
Contributor Author

For validating the interface I think the easiest is to use protocols. Then if a third-party application doesn't already fulfill them it doesn't feel so complicated to me to write a little wrapper that imports the protocols and adds the missing functionality. But maybe there are other options also.

My idea was rather abstract interfaces together with the possibility to override existing tools. So, for instance, if you want to write your own tune monitor, you just override the present one add your specific stuff. This will allow to preserve the attachment to simulator part and you can do want you want for the CS part.

Okay. That would also work. My impression was that protocols in python is very similar to abstract classes. I'm still trying to understand what the actual difference is between typing.Protocol and abc.ABCMeta. To me it seems like the functionality is very similar.

@JeanLucPons
Copy link
Contributor

You mean a decorator which unpacks the config object and makes individual attributes of the attributes in the ConfigModel? So you can do something like resp.matrix and it will give you the matrix attribute of the ._cfg without having to do resp._cfg.matrix or explicitly code a property for matrix?

Yes.

@TeresiaOlsson
Copy link
Contributor Author

You mean a decorator which unpacks the config object and makes individual attributes of the attributes in the ConfigModel? So you can do something like resp.matrix and it will give you the matrix attribute of the ._cfg without having to do resp._cfg.matrix or explicitly code a property for matrix?

Yes.

Okay, that's a nice idea. Probably better than what I was trying to do to remove the cfg completely from the __init__. Then maybe by adding validate_assignment to the BaseModel it will even be possible to automatically have validation if you change an attribute for an existing object which would be nice. I will add that decorator too and see what happens.

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.

2 participants