Commit 12bcf796 authored by Cyril Guilloud's avatar Cyril Guilloud
Browse files

add __info__ methods to get objects info in shell for:

* Axis generic class.
* mockup controller.
parent 82c30c8a
Pipeline #14020 passed with stages
in 37 minutes and 51 seconds
......@@ -28,18 +28,18 @@ as calls to :meth:`~bliss.config.static.Config.get`. Example::
READY (Axis is READY)
"""
from bliss.common.task import task
from bliss.common.cleanup import cleanup, error_cleanup, capture_exceptions
from bliss.common.cleanup import capture_exceptions
from bliss.common.motor_config import StaticConfig
from bliss.common.motor_settings import AxisSettings
from bliss.common import event
from bliss.common.greenlet_utils import protect_from_one_kill
from bliss.common.utils import Null, with_custom_members
from bliss.common.utils import with_custom_members
from bliss.common.encoder import Encoder
from bliss.common.hook import MotionHook
from bliss.config.channels import Channel
from bliss.physics.trajectory import LinearTrajectory
from bliss.common.logtools import *
import bliss
import gevent
import re
import sys
......@@ -446,7 +446,7 @@ class CyclicTrajectory(Trajectory):
@property
def pvt(self):
"""Returns the full PVT table. Positions are absolute"""
"""Return the full PVT table. Positions are absolute"""
pvt_pattern = self.pvt_pattern
if self.is_closed:
# take first point out because it is equal to the last
......@@ -686,7 +686,7 @@ class Axis:
self.settings.set(*args)
def get_setting(self, *args):
"""Returns the values for the given settings"""
"""Return the values for the given settings"""
return self.settings.get(*args)
def has_tag(self, tag):
......@@ -696,7 +696,7 @@ class Axis:
Args:
tag (str): tag name
Returns:
Return:
bool: True if the axis has the tag or False otherwise
"""
for t, axis_list in self.__controller._tagged.items():
......@@ -746,9 +746,9 @@ class Axis:
@lazy_init
def measured_position(self):
"""
Returns the encoder value in user units.
Return the encoder value in user units.
Returns:
Return:
float: encoder value in user units
"""
return self.dial2user(self.dial_measured_position)
......@@ -757,9 +757,9 @@ class Axis:
@lazy_init
def dial_measured_position(self):
"""
Returns the dial encoder position.
Return the dial encoder position.
Returns:
Return:
float: dial encoder position
"""
if self.encoder is not None:
......@@ -790,9 +790,9 @@ class Axis:
@lazy_init
def dial(self):
"""
Returns current dial position, or set dial
Return current dial position, or set dial
Returns:
Return:
float: current dial position (dimensionless)
"""
dial_pos = self.settings.get("dial_position")
......@@ -820,9 +820,9 @@ class Axis:
@lazy_init
def position(self):
"""
Returns current user position, or set new user position
Return current user position, or set new user position
Returns:
Return:
float: current user position (user units)
"""
pos = self.settings.get("position")
......@@ -876,12 +876,12 @@ class Axis:
@lazy_init
def state(self):
"""
Returns the axis state
Return the axis state
Keyword Args:
read_hw (bool): read from hardware [default: False]
Returns:
Return:
AxisState: axis state
"""
if self.is_moving:
......@@ -896,17 +896,83 @@ class Axis:
@lazy_init
def hw_state(self):
"""
Returns the current hardware axis state
Return the current hardware axis state
Returns:
Return:
AxisState: axis state
"""
return self.__controller.state(self)
@lazy_init
def get_info(self):
"""Returns controller specific information about the axis"""
return self.__controller.get_info(self)
def info(self):
"""Return common axis information about the axis.
PLUS controller specific information.
"""
_info_string = ""
_info_string += f"axis name: {self.name}\n"
_info_string += f" state: {self.state}\n"
_info_string += f" unit: {self.unit}\n"
_info_string += f" offset: {self.offset}\n"
_info_string += f" backlash: {self.backlash}\n"
_info_string += f" sign: {self.sign}\n"
_info_string += f" steps_per_unit: {self.steps_per_unit}\n"
_info_string += f" tolerance: {self.tolerance}\n"
# To avoid error if no encoder.
try:
_enc = self.encoder
_meas_pos = self.measured_position
_dial_meas_pos = self.dial_measured_position
_info_string += f" encoder: {_enc}\n"
_info_string += f" measured_position: {_meas_pos}\n"
_info_string += f" dial_measured_position: {_dial_meas_pos}\n"
except RuntimeError:
_info_string += f" encoder: None\n"
_info_string += f" motion_hooks: {self.motion_hooks}\n"
_info_string += f" dial: {self.dial}\n"
_info_string += f" position: {self.position}\n"
_info_string += f" _hw_position: {self._hw_position}\n"
_info_string += f" hw_state: {self.hw_state}\n"
_info_string += f" limits: {self.limits} (config: {self.config_limits})\n"
# To avoid error if no acceleration.
try:
_acc = self.acceleration
_acc_config = self.config_acceleration
_acc_time = self.acctime
_acc_time_config = self.config_acctime
_info_string += f" acceleration: {_acc} (config: {_acc_config})\n"
_info_string += f" acctime: {_acc_time} (config: {_acc_time_config})\n"
except Exception as e:
_info_string += f" acceleration: None\n"
if isinstance(self.controller, bliss.controllers.motor.CalcController):
_info_string += "CalcController\n"
else:
_info_string += (
f" velocity: {self.velocity} (config: {self.config_velocity})\n"
)
try:
_info_string += self.__controller.get_info(self)
except Exception as e:
_info_string += f"{self.controller}\n"
return _info_string
def __info__(self):
"""Standard function called by BLISS Shell typing helper to get info
about objects.
"""
try:
return self.info()
except:
log_error(
self,
"An error happend during execution of __info__(), use .info() to get it.",
)
def sync_hard(self):
"""Forces an axis synchronization with the hardware"""
......@@ -919,20 +985,20 @@ class Axis:
@lazy_init
def velocity(self):
"""
Returns the current velocity. If *new_velocity* is given it sets
Return the current velocity. If *new_velocity* is given it sets
the new velocity on the controller.
Keyword Args:
new_velocity (float): new velocity (user units/second) [default: \
None, meaning return the current velocity]
from_config (bool): if reading velocity (new_velocity is None), \
if True, returns the current static configuration velocity, \
otherwise, False returns velocity from the motor axis \
if True, return the current static configuration velocity, \
otherwise, False return velocity from the motor axis \
[default: False]
Returns:
Return:
float: current velocity (user units/second)
"""
# Read -> Returns velocity read from motor axis.
# Read -> Return velocity read from motor axis.
_user_vel = self.settings.get("velocity")
if _user_vel is None:
_user_vel = self.__controller.read_velocity(self) / abs(self.steps_per_unit)
......@@ -951,9 +1017,9 @@ class Axis:
@property
def config_velocity(self):
"""
Returns the config velocity.
Return the config velocity.
Returns:
Return:
float: current velocity (user units/second)
"""
return self.config.get("velocity", float)
......@@ -994,9 +1060,9 @@ class Axis:
@lazy_init
def acctime(self):
"""
Returns the current acceleration time.
Return the current acceleration time.
Returns:
Return:
float: current acceleration time (second)
"""
return self.velocity / self.acceleration
......@@ -1011,7 +1077,7 @@ class Axis:
@property
def config_acctime(self):
"""
Returns the config acceleration time.
Return the config acceleration time.
"""
return self.config_velocity / self.config_acceleration
......@@ -1019,9 +1085,9 @@ class Axis:
@lazy_init
def limits(self):
"""
Returns or set the current software limits in USER units.
Return or set the current software limits in USER units.
Returns:
Return:
tuple<float, float>: axis software limits (user units)
"""
return self.low_limit, self.high_limit
......@@ -1064,7 +1130,7 @@ class Axis:
@property
@lazy_init
def high_limit(self):
# Returns High Limit in USER units.
# Return High Limit in USER units.
limit = self.settings.get("high_limit")
if limit is not None:
return self.dial2user(limit)
......@@ -1106,7 +1172,7 @@ class Axis:
Keyword Args:
offset (float): alternative offset. None (default) means use current offset
Returns:
Return:
float: position in axis user units
"""
if position is None:
......@@ -1123,7 +1189,7 @@ class Axis:
Args:
position (float): position in user units
Returns:
Return:
float: position in axis dial units
"""
return (position - self.offset) / self.sign
......@@ -1673,7 +1739,7 @@ class AxisState(object):
def states_list(self):
"""
Returns a list of available/created states for this axis.
Return a list of available/created states for this axis.
"""
return list(self._state_desc)
......@@ -1761,9 +1827,9 @@ class AxisState(object):
def current_states(self):
"""
Returns a string of current states.
Return a string of current states.
Returns:
Return:
str: *|* separated string of current states or string *UNKNOWN* \
if there is no current state
"""
......@@ -1849,7 +1915,7 @@ class AxisState(object):
are shared with the new AxisState. Otherwise, a copy
of possible states is created for the new AxisState.
Returns:
Return:
AxisState: a copy of this AxisState with no current states
"""
result = AxisState()
......
......@@ -401,17 +401,18 @@ class Controller:
raise NotImplementedError
def set_position(self, axis, new_position):
"""Set the position of <axis> in controller to <new_position>.
This method is called by `position` property of <axis>.
"""
raise NotImplementedError
def read_encoder(self, encoder):
"""
Returns the encoder value in *encoder steps*.
"""Return the encoder value in *encoder steps*.
"""
raise NotImplementedError
def set_encoder(self, encoder, new_value):
"""
Sets encoder value. <new_value> is in encoder steps.
"""Set encoder value. <new_value> is in encoder steps.
"""
raise NotImplementedError
......
......@@ -183,7 +183,7 @@ class Mockup(Controller):
def read_position(self, axis, t=None):
"""
Returns the position (measured or desired) taken from controller
Return the position (measured or desired) taken from controller
in controller unit (steps).
"""
gevent.sleep(0.005) # simulate I/O
......@@ -199,7 +199,7 @@ class Mockup(Controller):
def read_encoder(self, encoder):
"""
returns encoder position.
Return encoder position.
unit : 'encoder steps'
"""
if self.__encoders[encoder]["steps"] is not None:
......@@ -235,7 +235,7 @@ class Mockup(Controller):
def read_velocity(self, axis):
"""
Returns the current velocity taken from controller
Return the current velocity taken from controller
in motor units.
"""
return axis.settings.get("velocity") * abs(axis.steps_per_unit)
......@@ -355,23 +355,34 @@ class Mockup(Controller):
self._axis_moves[axis]["motion"] = motion
def get_info(self, axis):
return "turlututu chapo pointu : %s" % (axis.name)
"""Return information about Controller and Axis"""
info_string = f"Axis: {axis.name}\n"
info_string += f"Controller:\n"
info_string += f" class: {self.__class__}\n"
info_string += f" name: {self.name}\n"
return info_string
def get_id(self, axis):
return "MOCKUP AXIS %s" % (axis.name)
def set_position(self, axis, pos):
def set_position(self, axis, new_position):
""" Set the position of <axis> in controller to <new_position>.
This method is the way to define an offset for <axis>.
"""
motion = self._get_axis_motion(axis)
if motion:
raise RuntimeError("Cannot set position while moving !")
self.set_hw_position(axis, pos)
self._axis_moves[axis]["target"] = pos
self.set_hw_position(axis, new_position)
self._axis_moves[axis]["target"] = new_position
self._axis_moves[axis]["end_t"] = None
return pos
return new_position
def put_discrepancy(self, axis, disc):
"""Create a discrepancy (for testing purposes) between axis and
controller.
"""
self.set_position(axis, self.read_position(axis) + disc)
"""
......
......@@ -4,33 +4,75 @@ Here you can find somt tips about the wrinting of a BLISS controller.
## @autocomplete_property decorator
in many controllers the `@property` decorator is heavily used to protect certain attributes of the instance or to limit the access to read-only. When using the bliss command line interface the autocompletion will __not__ suggeste any completion based on the return value of the method underneath the property. This is a wanted behavior e.g. in case this would trigger hardware communication. There are however also usecases where a _deeper_ autocompletion is wanted. E.g. the `.counter` namespace of a controller. If implemented as `@property`
In many controllers, the `@property` decorator is heavily used to protect certain
attributes of the instance or to limit the access to read-only. When using the
bliss command line interface the autocompletion will **not** suggeste any
completion based on the return value of the method underneath the property.
BLISS [1]: lima_simulator.counters.
This is a wanted behavior e.g. in case this would trigger hardware
communication. There are however also usecases where a *deeper* autocompletion
is wanted.
would not show any autocompletion suggestions. To enable _deeper_ autocompletion there is a special decorator called `@autocomplete_property` that can be imported via `from bliss.common.utils import autocomplete_property`. Using the `@autocomplete_property` decorator befor the `def counters(self):` method of the controller would e.g. result in
!!! note
"↹" represents the action of pressing the "Tab" key of the keyboard.
BLISS [1]: lima_simulator.counters.
_roi1_
_roi2_
_bpm_
Example: the `.counter` namespace of a controller. If implemented as
`@property`:
```
BLISS [1]: lima_simulator.counters. ↹
```
autocompletion suggestions.
Would not show any autocompletion suggestions. To enable *deeper* autocompletion
a special decorator called `@autocomplete_property` must be used.
```python
from bliss.common.utils import autocomplete_property
## The `__info__` methode for Bliss shell
!!! info
class Lima(object):
@autocomplete_property
def counters(self):
all_counters = [self.image]
...
```
Using this decorator would result in autocompletion suggestions:
```
BLISS [1]: lima_simulator.counters. ↹
_roi1_
_roi2_
_bpm_
```
- Any Bliss controller that is visible to the user in the command line should have an `__info__` function implemented!
- The return type of `__info__` must be `str`, otherwhise it fails and `repr` is used as fallback!
- As a rule of thumb: the retrun value of a custom `__repr__` implementation should not contain `\n` and should be inspired by the standard implementation of `__repr__` in python.
## The `__info__()` method for Bliss shell
In Bliss `__info__` is used by the command line interface (Bliss shell or Bliss repl) to enquire information of the internal state of any object / controller in case it is available.
This is used to have simple way to get (detailed) information that is needed from a __user point of view__ to use the object. This is in contrast to the build-in python function `__repr__`, which should return a short summary of the concerned object from the __developer point of view__. The Protocol that is put in place in the Bliss shell is the following:
* if the return value of a statement entered into the Bliss shel is a python object with `__info__` implemented this `__info__` function will be called by the Bliss shell to display the output. As a fallback option (`__info__` not implemented) the standard behavior of the interactive python interpreter involving `__repr__` is used. (For details about `__repr__` see next section.)
!!! info
- Any Bliss controller that is visible to the user in the command line
should have an `__info__()` function implemented!
- The return type of `__info__()` must be `str`, otherwhise it fails and
`__repr__()` is used as fallback!
- As a rule of thumb: the retrun value of a custom `__repr__()` implementation
should not contain `\n` and should be inspired by the standard
implementation of `__repr__()` in python.
In Bliss, `__info__()` is used by the command line interface (Bliss shell or Bliss
repl) to enquire information of the internal state of any object / controller in
case it is available.
This is used to have simple way to get (detailed) information that is needed
from a **user point of view** to use the object. This is in contrast to the
build-in python function `__repr__()`, which should return a short summary of the
concerned object from the **developer point of view**. The Protocol that is put
in place in the Bliss shell is the following:
* if the return value of a statement entered into the Bliss shel is a python
object with `__info__()` implemented this `__info__()` function will be called
by the Bliss shell to display the output. As a fallback option (`__info__()`
not implemented) the standard behavior of the interactive python interpreter
involving `__repr__` is used. (For details about `__repr__` see next section.)
Here is an example for the lima controller that is using `__info__`:
```
LIMA_TEST_SESSION [3]: lima_simulator
LIMA_TEST_SESSION [3]: lima_simulator
Out [3]: Simulator - Generator (Simulator) - Lima Simulator
Image:
......@@ -58,39 +100,68 @@ LIMA_TEST_SESSION [3]: lima_simulator
---- ------------------
r1 <0, 0> <100 x 200>
```
the information given above is usefull from a __user point of view__. As a __developer__ one might want to work in the Bliss shell with live object e.g.
The information given above is usefull from a **user point of view**. As a
**developer** one might want to work in the Bliss shell with live object e.g.
```python
LIMA [4]: my_detectors = {'my_lima':lima_simulator,'my_mca':simu1}
LIMA [5]: my_detectors
Out [5]: {'my_lima': <Lima Controller for Simulator (Lima Simulator)>,
'my_mca': <bliss.controllers.mca.simulation.SimulatedMCA
object at 0x7f2f535b5f60>}
```
LIMA_TEST_SESSION [4]: my_detectors = {'my_lima':lima_simulator,'my_mca':simu1}
LIMA_TEST_SESSION [5]: my_detectors
Out [5]: {'my_lima': <Lima Controller for Simulator (Lima Simulator)>,
'my_mca': <bliss.controllers.mca.simulation.SimulatedMCA object at 0x7f2f535b5f60>}
```
in this case it is desirable that the python objects themselves are clearly represented, which is exactly the role of `__repr__` (in this example the `lima_simulator` has a custom `__repr__` while in `simu1` there is no `__repr__` implemented so the bulid in python implementation is used).
The signature of `__info__` should be `def __info__(self):` the return value should be a string.
In this case it is desirable that the python objects themselves are clearly
represented, which is exactly the role of `__repr__` (in this example the
`lima_simulator` has a custom `__repr__` while in `simu1` there is no `__repr__`
implemented so the bulid in python implementation is used).
```
BLISS [1]: class A(object):
...: def __repr__(self):
...: return "my repl"
...: def __str__(self):
...: return "my str"
...: def __info__(self):
...: return "my info"
The signature of `__info__()` should be `def __info__(self):` the return value
musst be a string.
```python
BLISS [1]: class A(object):
...: def __repr__(self):
...: return "my repl"
...: def __str__(self):
...: return "my str"
...: def __info__(self):
...: return "my info"
BLISS [2]: a=A()
BLISS [2]: a=A()
BLISS [3]: a
BLISS [3]: a
Out [3]: my info
BLISS [4]: [a]
BLISS [4]: [a]
Out [4]: [my repl]
```
Note : if for any reason there is an exception raised inside `__info__` the fallback option will be used and `__repr__` is evaluated in this case.
!!! warning
If, for any reason, there is an exception raised inside `__info__`, the
fallback option will be used and `__repr__` is evaluated in this case.
And **this will hide the error**.
The equivalent of `repr(obj)` or `str(obj)` is also availabe in `bliss.common.standard` as `info(obj)` which can be used also outside the Bliss shell.
So, *any* error musst be treated and displayed before returning.
Example:
```python
def __info__(self):
"""Standard function called by BLISS Shell typing helper to get info
about objects.
"""
try:
return = self.info()
except:
log_error(self, "An error happend during execution of __info__(), use .info() to get it.")
```
The equivalent of `repr(obj)` or `str(obj)` is also availabe in
`bliss.common.standard` as `info(obj)` which can be used also outside the Bliss
shell.
```
Python 3.7.3 (default, Mar 27 2019, 22:11:17)
......@@ -117,7 +188,7 @@ Type "help", "copyright", "credits" or "license" for more information.
'my repl'
```
### Recap `__str__` and `__repr__` methods
## `__str__()` and `__repr__()`
If implemented in a Python class, `__repr__` and `__str__` methods are
build-in functions Python to return information about an object instantiating this class.
......@@ -125,20 +196,20 @@ build-in functions Python to return information about an object instantiating th
* `__str__` should print a readable message
* `__repr__` should print a __short__ message obout the objec that is unambigous (e.g. name of an identifier, class name, etc).
`__str__` is called:
* when the object is passed to the print() function (e.g. `print(my_obj)`).
* wheh the object is used in string operations (e.g. `str(my_obj)` or `'{}'.format(my_obj)` or `f'some text {my_obj}'`)
`__repr__` method is called:
* when user type the name of the object in an interpreter session (a python shell).
* when displaying containers like lists and dicts (the result of `__repr__` is used to represent the objects they contain)
* when explicitly asking for it in the print() function. (e.g. `print("%r" % my_object)`)
* `__str__` is called:
- when the object is passed to the print() function (e.g. `print(my_obj)`).
- wheh the object is used in string operations (e.g. `str(my_obj)` or
`'{}'.format(my_obj)` or `f'some text {my_obj}'`)
* `__repr__` method is called:
- when user type the name of the object in an interpreter session (a python
shell).
- when displaying containers like lists and dicts (the result of `__repr__`
is used to represent the objects they contain)
- when explicitly asking for it in the print() function. (e.g. `print("%r" % my_object)`)
By default when no `__str__` or `__repr__` methods are defined, the
`__repr__` returns the name of the class (Length) and `__str__` calls `__repr__`.
By default when no `__str__` or `__repr__` methods are defined, the `__repr__`
returns the name of the class (Length) and `__str__` calls `__repr__`.