.. _to9.4_non_bound_user_funcs:

==============================================================
Non-bound user functions for read/write/isallowed and commands
==============================================================

When providing the user functions that are executed on attribute
and command access, the general requirement was that they
had to be methods on the :class:`tango.server.Device` class.
This has led to some confusion when developers try a plain function
instead. There were some ways to work around this, e.g., by patching the
Python device instance ``__dict__``.  From Pytango 9.4.x this is
no longer necessary.  User functions can now be defined outside of the
device class, if desired.

This feature applies to both static and dynamic attributes/commands:

    - attribute read method (``fget``/``fread`` kwarg)
    - attribute write method (``fset``/``fwrite`` kwarg)
    - attribute is allowed method (``fisallowed`` kwarg)
    - command is allowed method (``fisallowed`` kwarg)


Trivial example::

    from tango import AttrWriteType, AttReqType
    from tango.server import Device, command, attribute

    global_data = {"example_attr1": 100}


    def read_example_attr1():
        return global_data["example_attr1"]


    def write_example_attr1(value):
        global_data["example_attr1"] = value


    def is_example_attr1_allowed(req_type):
        assert req_type in (AttReqType.READ_REQ, AttReqType.WRITE_REQ)
        return True


    def is_cmd1_allowed():
        return True


    class Test(Device):

        example_attr1 = attribute(
            fget=read_example_attr1,
            fset=write_example_attr1,
            fisallowed=is_example_attr1_allowed,
            dtype=int,
            access=AttrWriteType.READ_WRITE
        )

        @command(dtype_in=int, dtype_out=int, fisallowed=is_cmd1_allowed)
        def identity1(self, value):
            return value

As can be seen from the user functions above, they do not get any context about the request.  They do
not know which device it came from.  This can definitely cause problems if a device server hosts multiple
instances of the same device.

It is also possible to include another argument in these functions.  If present, the first argument will
be a reference to the device instance.

For example::

    from tango import AttrWriteType, AttReqType
    from tango.server import Device, command, attribute

    global_data = {"example_attr2": 200}


    def read_example_attr2(device):
        print(f"read from device {device.get_name()}")
        return global_data["example_attr2"]


    def write_example_attr2(device, value):
        print(f"write to device {device.get_name()}")
        global_data["example_attr2"] = value


    def is_example_attr2_allowed(device, req_type):
        print(f"is_allowed attr for device {device.get_name()}")
        assert req_type in (AttReqType.READ_REQ, AttReqType.WRITE_REQ)
        return True


    def is_cmd2_allowed(device):
        print(f"is_allowed cmd for device {device.get_name()}")
        return True


    class Test(Device):

        example_attr2 = attribute(
            fget=read_example_attr2,
            fset=write_example_attr2,
            fisallowed=is_example_attr2_allowed,
            dtype=int,
            access=AttrWriteType.READ_WRITE
        )

        @command(dtype_in=int, dtype_out=int, fisallowed=is_cmd2_allowed)
        def identity2(self, value):
            return value

