class StatusBase: """ Track the status of a potentially-lengthy action like moving or triggering. Parameters ---------- timeout: float, optional The amount of time to wait before marking the Status as failed. If ``None`` (default) wait forever. It is strongly encouraged to set a finite timeout. If settle_time below is set, that time is added to the effective timeout. settle_time: float, optional The amount of time to wait between the caller specifying that the status has completed to running callbacks. Default is 0. Notes ----- Theory of operation: This employs two ``threading.Event`` objects, one thread the runs for (timeout + settle_time) seconds, and one thread that runs for settle_time seconds (if settle_time is nonzero). At __init__ time, a *timeout* and *settle_time* are specified. A thread is started, on which user callbacks, registered after __init__ time via :meth:`add_callback`, will eventually be run. The thread waits on an Event be set or (timeout + settle_time) seconds to pass, whichever happens first. If (timeout + settle_time) expires and the Event has not been set, an internal Exception is set to ``StatusTimeoutError``, and a second Event is set, marking the Status as done and failed. The callbacks are run. If a callback is registered after the Status is done, it will be run immediately. If the first Event is set before (timeout + settle_time) expires, then the second Event is set and no internal Exception is set, marking the Status as done and successful. The callbacks are run. There are two methods that directly set the first Event. One, :meth:set_exception, sets it directly after setting the internal Exception. The other, :meth:`set_finished`, starts a ``threading.Timer`` that will set it after a delay (the settle_time). One of these methods may be called, and at most once. If one is called twice or if both are called, ``InvalidState`` is raised. If they are called too late to prevent a ``StatusTimeoutError``, they are ignored but one call is still allowed. Thus, an external callback, e.g. pyepics, may reports success or failure after the Status object has expired, but to no effect because the callbacks have already been called and the program has moved on. """ def __init__(self, *, timeout=None, settle_time=0, done=None, success=None): super().__init__() self._tname = None self._lock = threading.RLock() self._event = threading.Event() # state associated with done-ness self._settled_event = threading.Event() # "Externally initiated" means set_finished() or set_exception(exc) was # called, as opposed to completion via an internal timeout. self._externally_initiated_completion_lock = threading.Lock() self._externally_initiated_completion = False self._callbacks = deque() self._exception = None self.log = LoggerAdapter(logger=logger, extra={'status': self}) if settle_time is None: settle_time = 0.0 self._settle_time = float(settle_time) if timeout is not None: timeout = float(timeout) self._timeout = timeout # We cannot know that we are successful if we are not done. if success and not done: raise ValueError( "Cannot initialize with done=False but success=True.") if done is not None or success is not None: warn( "The 'done' and 'success' parameters will be removed in a " "future release. Use the methods set_finished() or " "set_exception(exc) to mark success or failure, respectively, " "after the Status has been instantiated.", DeprecationWarning) self._callback_thread = threading.Thread(target=self._run_callbacks, daemon=True, name=self._tname) self._callback_thread.start() if done: if success: self.set_finished() else: exc = UnknownStatusFailure( f"The status {self!r} has failed. To obtain more specific, " "helpful errors in the future, update the Device to use " "set_exception(...) instead of setting success=False " "at __init__ time.") self.set_exception(exc) @property def timeout(self): """ The timeout for this action. This is set when the Status is created, and it cannot be changed. """ return self._timeout @property def settle_time(self): """ A delay between when :meth:`set_finished` is when the Status is done. This is set when the Status is created, and it cannot be changed. """ return self._settle_time @property def done(self): """ Boolean indicating whether associated operation has completed. This is set to True at __init__ time or by calling :meth:`set_finished`, :meth:`set_exception`, or (deprecated) :meth:`_finished`. Once True, it can never become False. """ return self._event.is_set() @done.setter def done(self, value): # For now, allow this setter to work only if it has no effect. # In a future release, make this property not settable. if bool(self._event.is_set()) != bool(value): raise RuntimeError( "The done-ness of a status object cannot be changed by " "setting its `done` attribute directly. Call `set_finished()` " "or `set_exception(exc).") warn( "Do not set the `done` attribute of a status object directly. " "It should only be set indirectly by calling `set_finished()` " "or `set_exception(exc)`. " "Direct setting was never intended to be supported and it will be " "disallowed in a future release of ophyd, causing this code path " "to fail.", UserWarning) @property def success(self): """ Boolean indicating whether associated operation has completed. This is set to True at __init__ time or by calling :meth:`set_finished`, :meth:`set_exception`, or (deprecated) :meth:`_finished`. Once True, it can never become False. """ return self.done and self._exception is None @success.setter def success(self, value): # For now, allow this setter to work only if it has no effect. # In a future release, make this property not settable. if bool(self.success) != bool(value): raise RuntimeError( "The success state of a status object cannot be changed by " "setting its `success` attribute directly. Call " "`set_finished()` or `set_exception(exc)`.") warn( "Do not set the `success` attribute of a status object directly. " "It should only be set indirectly by calling `set_finished()` " "or `set_exception(exc)`. " "Direct setting was never intended to be supported and it will be " "disallowed in a future release of ophyd, causing this code path " "to fail.", UserWarning) def _handle_failure(self): pass def _settled(self): """Hook for when status has completed and settled""" pass def _run_callbacks(self): """ Set the Event and run the callbacks. """ if self.timeout is None: timeout = None else: timeout = self.timeout + self.settle_time if not self._settled_event.wait(timeout): # We have timed out. It's possible that set_finished() has already # been called but we got here before the settle_time timer expired. # And it's possible that in this space be between the above # statement timing out grabbing the lock just below, # set_exception(exc) has been called. Both of these possibilties # are accounted for. self.log.warning("%r has timed out", self) with self._externally_initiated_completion_lock: # Set the exception and mark the Status as done, unless # set_exception(exc) was called externally before we grabbed # the lock. if self._exception is None: exc = StatusTimeoutError( f"Status {self!r} failed to complete in specified timeout." ) self._exception = exc # Mark this as "settled". try: self._settled() except Exception: # No alternative but to log this. We can't supersede set_exception, # and we have to continue and run the callbacks. self.log.exception("%r encountered error during _settled()", self) # Now we know whether or not we have succeed or failed, either by # timeout above or by set_exception(exc), so we can set the Event that # will mark this Status as done. with self._lock: self._event.set() if self._exception is not None: try: self._handle_failure() except Exception: self.log.exception( "%r encountered an error during _handle_failure()", self) # The callbacks have access to self, from which they can distinguish # success or failure. for cb in self._callbacks: try: cb(self) except Exception: self.log.exception( "An error was raised on a background thread while " "running the callback %r(%r).", cb, self) self._callbacks.clear() def set_exception(self, exc): """ Mark as finished but failed with the given Exception. This method should generally not be called by the *recipient* of this Status object, but only by the object that created and returned it. Parameters ---------- exc: Exception """ # Since we rely on this being raise-able later, check proactively to # avoid potentially very confusing failures. if not (isinstance(exc, Exception) or isinstance(exc, type) and issubclass(exc, Exception)): # Note that Python allows `raise Exception` or raise Exception()` # so we allow a class or an instance here too. raise ValueError(f"Expected an Exception, got {exc!r}") # Ban certain Timeout subclasses that have special significance. This # would probably never come up except due to some rare user error, but # if it did it could be very confusing indeed! for exc_class in (StatusTimeoutError, WaitTimeoutError): if (isinstance(exc, exc_class) or isinstance(exc, type) and issubclass(exc, exc_class)): raise ValueError( f"{exc_class} has special significance and cannot be set " "as the exception. Use a plain TimeoutError or some other " "subclass thereof.") with self._externally_initiated_completion_lock: if self._externally_initiated_completion: raise InvalidState( "Either set_finished() or set_exception() has " f"already been called on {self!r}") self._externally_initiated_completion = True if isinstance(self._exception, StatusTimeoutError): # We have already timed out. return self._exception = exc self._settled_event.set() def set_finished(self): """ Mark as finished successfully. This method should generally not be called by the *recipient* of this Status object, but only by the object that created and returned it. """ with self._externally_initiated_completion_lock: if self._externally_initiated_completion: raise InvalidState( "Either set_finished() or set_exception() has " f"already been called on {self!r}") self._externally_initiated_completion = True # Note that in either case, the callbacks themselves are run from the # same thread. This just sets an Event, either from this thread (the # one calling set_finished) or the thread created below. if self.settle_time > 0: threading.Timer(self.settle_time, self._settled_event.set).start() else: self._settled_event.set() def _finished(self, success=True, **kwargs): """ Inform the status object that it is done and if it succeeded. This method is deprecated. Please use :meth:`set_finished` or :meth:`set_exception`. .. warning:: kwargs are not used, but are accepted because pyepics gives in a bunch of kwargs that we don't care about. This allows the status object to be handed directly to pyepics (but this is probably a bad idea for other reason. This may be deprecated in the future. Parameters ---------- success : bool, optional if the action succeeded. """ if success: self.set_finished() else: # success=False does not give any information about *why* it # failed, so set a generic exception. exc = UnknownStatusFailure( f"The status {self!r} has failed. To obtain more specific, " "helpful errors in the future, update the Device to use " "set_exception(...) instead of _finished(success=False).") self.set_exception(exc) def exception(self, timeout=None): """ Return the exception raised by the action. If the action has completed successfully, return ``None``. If it has finished in error, return the exception. Parameters ---------- timeout: Union[Number, None], optional If None (default) wait indefinitely until the status finishes. Raises ------ WaitTimeoutError If the status has not completed within ``timeout`` (starting from when this method was called, not from the beginning of the action). """ if not self._event.wait(timeout=timeout): raise WaitTimeoutError("Status has not completed yet.") return self._exception def wait(self, timeout=None): """ Block until the action completes. When the action has finished succesfully, return ``None``. If the action has failed, raise the exception. Parameters ---------- timeout: Union[Number, None], optional If None (default) wait indefinitely until the status finishes. Raises ------ WaitTimeoutError If the status has not completed within ``timeout`` (starting from when this method was called, not from the beginning of the action). StatusTimeoutError If the status has failed because the *timeout* that it was initialized with has expired. Exception This is ``status.exception()``, raised if the status has finished with an error. This may include ``TimeoutError``, which indicates that the action itself raised ``TimeoutError``, distinct from ``WaitTimeoutError`` above. """ if not self._event.wait(timeout=timeout): raise WaitTimeoutError("Status has not completed yet.") if self._exception is not None: raise self._exception @property def callbacks(self): """ Callbacks to be run when the status is marked as finished """ return self._callbacks @property def finished_cb(self): with self._lock: if len(self.callbacks) == 1: warn( "The property `finished_cb` is deprecated, and must raise " "an error if a status object has multiple callbacks. Use " "the `callbacks` property instead.", stacklevel=2) cb, = self.callbacks assert cb is not None return cb else: raise UseNewProperty( "The deprecated `finished_cb` property " "cannot be used for status objects that have " "multiple callbacks. Use the `callbacks` " "property instead.") def add_callback(self, callback): """ Register a callback to be called once when the Status finishes. The callback will be called exactly once. If the Status is finished before a callback is added, it will be called immediately. This is threadsafe. The callback will be called regardless of success of failure. The callback has access to this status object, so it can distinguish success or failure by inspecting the object. Parameters ---------- callback: callable Expected signature: ``callback(status)``. The signature ``callback()`` is also supported for backward-compatibility but will issue warnings. Support will be removed in a future release of ophyd. """ # Handle func with signature callback() for back-compat. callback = adapt_old_callback_signature(callback) with self._lock: if self.done: # Call it once and do not hold a reference to it. callback(self) else: # Hold a strong reference to this. In other contexts we tend to # hold weak references to callbacks, but this is a single-shot # callback, so we will hold a strong reference until we call it, # and then clear this cache to drop the reference(s). self._callbacks.append(callback) @finished_cb.setter def finished_cb(self, cb): with self._lock: if not self.callbacks: warn( "The setter `finished_cb` is deprecated, and must raise " "an error if a status object already has one callback. Use " "the `add_callback` method instead.", stacklevel=2) self.add_callback(cb) else: raise UseNewProperty( "The deprecated `finished_cb` setter cannot " "be used for status objects that already " "have one callback. Use the `add_callbacks` " "method instead.") def __and__(self, other): """ Returns a new 'composite' status object, AndStatus, with the same base API. It will finish when both `self` or `other` finish. """ return AndStatus(self, other)
async def receive_message(service_bus_client, logger_adapter: logging.LoggerAdapter, config: dict): """ This method is run per process. Each process will connect to service bus and try to establish a session. If messages are there, the process will continue to receive all the messages associated with that session. If no messages are there, the session connection will time out, sleep, and retry. """ q_name = config["resource_request_queue"] while True: try: logger_adapter.info("Looking for new session...") # max_wait_time=1 -> don't hold the session open after processing of the message has finished async with service_bus_client.get_queue_receiver( queue_name=q_name, max_wait_time=1, session_id=NEXT_AVAILABLE_SESSION) as receiver: logger_adapter.info("Got a session containing messages") async with AutoLockRenewer() as renewer: # allow a session to be auto lock renewed for up to an hour - if it's processing a message renewer.register(receiver, receiver.session, max_lock_renewal_duration=3600) async for msg in receiver: result = True message = "" try: message = json.loads(str(msg)) logger_adapter.info( f"Message received for resource_id={message['id']}, operation_id={message['operationId']}, step_id={message['stepId']}" ) message_logger_adapter = get_message_id_logger( message['operationId'] ) # correlate messages per operation result = await invoke_porter_action( message, service_bus_client, message_logger_adapter, config) except (json.JSONDecodeError) as e: logging.error( f"Received bad service bus resource request message: {e}" ) if result: logging.info( f"Resource request for {message} is complete") else: logging.error('Message processing failed!') logger_adapter.info( f"Message for resource_id={message['id']}, operation_id={message['operationId']} processed as {result} and marked complete." ) await receiver.complete_message(msg) logger_adapter.info("Closing session") await renewer.close() except OperationTimeoutError: # Timeout occurred whilst connecting to a session - this is expected and indicates no non-empty sessions are available logger_adapter.info( "No sessions for this process. Will look again...") except ServiceBusConnectionError: # Occasionally there will be a transient / network-level error in connecting to SB. logger_adapter.info( "Unknown Service Bus connection error. Will retry...") except Exception: # Catch all other exceptions, log them via .exception to get the stack trace, sleep, and reconnect logger_adapter.exception("Unknown exception. Will retry...")
class OphydObject: '''The base class for all objects in Ophyd Handles: * Subscription/callback mechanism Parameters ---------- name : str, optional The name of the object. attr_name : str, optional The attr name on it's parent (if it has one) ex ``getattr(self.parent, self.attr_name) is self`` parent : parent, optional The object's parent, if it exists in a hierarchy kind : a member of the :class:`~ophydobj.Kind` :class:`~enum.IntEnum` (or equivalent integer), optional Default is ``Kind.normal``. See :class:`~ophydobj.Kind` for options. Attributes ---------- name ''' # Any callables appended to this mutable class variable will be notified # one time when a new instance of OphydObj is instantiated. See # OphydObject.add_instantiation_callback(). __instantiation_callbacks = [] _default_sub = None # This is set to True when the first OphydObj is instantiated. This may be # of interest to code that adds something to instantiation_callbacks, which # may want to know whether it has already "missed" any instances. __any_instantiated = False def __init__(self, *, name=None, attr_name='', parent=None, labels=None, kind=None): if labels is None: labels = set() self._ophyd_labels_ = set(labels) if kind is None: kind = Kind.normal self.kind = kind super().__init__() # base name and ref to parent, these go with properties if name is None: name = '' self._attr_name = attr_name if not isinstance(name, str): raise ValueError("name must be a string.") self._name = name self._parent = parent self.subscriptions = { getattr(self, k) for k in dir(type(self)) if (k.startswith('SUB') or k.startswith('_SUB')) } # dictionary of wrapped callbacks self._callbacks = {k: {} for k in self.subscriptions} # this is to maintain api on clear_sub self._unwrapped_callbacks = {k: {} for k in self.subscriptions} # map cid -> back to which event it is in self._cid_to_event_mapping = dict() # cache of last inputs to _run_subs, the semi-private way # to trigger the callbacks for a given subscription to be run self._args_cache = {k: None for k in self.subscriptions} # count of subscriptions we have handed out, used to give unique ids self._cb_count = count() # Create logger name from parent or from module class if self.parent: base_log = self.parent.log.name name = self.name.lstrip(self.parent.name + '_') else: base_log = self.__class__.__module__ name = self.name self.log = LoggerAdapter(logger, { 'base_log': base_log, 'ophyd_object_name': name }) self.control_layer_log = LoggerAdapter(control_layer_logger, {'ophyd_object_name': name}) if not self.__any_instantiated: self.log.info("first instance of OphydObject: id=%s", id(self)) OphydObject._mark_as_instantiated() self.__register_instance(self) @classmethod def _mark_as_instantiated(cls): cls.__any_instantiated = True @classmethod def add_instantiation_callback(cls, callback, fail_if_late=False): """ Register a callback which will receive each OphydObject instance. Parameters ---------- callback : callable Expected signature: ``f(ophydobj_instance)`` fail_if_late : boolean If True, verify that OphydObj has not yet been instantiated and raise ``RuntimeError`` if it has, as a way of verify that no instances will be "missed" by this registry. False by default. """ if fail_if_late and OphydObject.__any_instantiated: raise RuntimeError( "OphydObject has already been instantiated at least once, and " "this callback will not be notified of those instances that " "have already been created. If that is acceptable for this " "application, set fail_if_false=False.") # This is a class variable. cls.__instantiation_callbacks.append(callback) @classmethod def __register_instance(cls, instance): """ Notify the callbacks in OphydObject.instantiation_callbacks of an instance. """ for callback in cls.__instantiation_callbacks: callback(instance) def __init_subclass__(cls, version=None, version_of=None, version_type=None, **kwargs): 'This is called automatically in Python for all subclasses of OphydObject' super().__init_subclass__(**kwargs) if version is None: if version_of is not None: raise RuntimeError('Must specify a version if `version_of` ' 'is specified') if version_type is None: return # Allow specification of version_type without specifying a version, # for use in a base class cls._class_info_ = dict(versions={}, version=None, version_type=version_type, version_of=version_of) return if version_of is None: versions = {} version_of = cls else: versions = version_of._class_info_['versions'] if version_type is None: version_type = version_of._class_info_['version_type'] elif version_type != version_of._class_info_['version_type']: raise RuntimeError( "version_type with in a family must be consistent, " f"you passed in {version_type}, to {cls.__name__} " f"but {version_of.__name__} has version_type " f"{version_of._class_info_['version_type']}") if not issubclass(cls, version_of): raise RuntimeError( f'Versions are only valid for classes in the same ' f'hierarchy. {cls.__name__} is not a subclass of ' f'{version_of.__name__}.') if versions is not None and version in versions: logger.warning('Redefining %r version %s: old=%r new=%r', version_of, version, versions[version], cls) versions[version] = cls cls._class_info_ = dict(versions=versions, version=version, version_type=version_type, version_of=version_of) def _validate_kind(self, val): if isinstance(val, str): return Kind[val.lower()] return Kind(val) @property def kind(self): return self._kind @kind.setter def kind(self, val): self._kind = self._validate_kind(val) @property def dotted_name(self) -> str: """Return the dotted name """ names = [] obj = self while obj.parent is not None: names.append(obj.attr_name) obj = obj.parent return '.'.join(names[::-1]) @property def name(self): '''name of the device''' return self._name @name.setter def name(self, name): self._name = name @property def attr_name(self): return self._attr_name @property def connected(self): '''If the device is connected. Subclasses should override this''' return True def destroy(self): '''Disconnect the object from the underlying control layer''' self.unsubscribe_all() @property def parent(self): '''The parent of the ophyd object. If at the top of its hierarchy, `parent` will be None ''' return self._parent @property def root(self): "Walk parents to find ultimate ancestor (parent's parent...)." root = self while True: if root.parent is None: return root root = root.parent @property def report(self): '''A report on the object.''' return {} @property def event_types(self): '''Events that can be subscribed to via `obj.subscribe` ''' return tuple(self.subscriptions) def _run_subs(self, *args, sub_type, **kwargs): '''Run a set of subscription callbacks Only the kwarg ``sub_type`` is required, indicating the type of callback to perform. All other positional arguments and kwargs are passed directly to the callback function. The host object will be injected into kwargs as 'obj' unless that key already exists. If the `timestamp` is None, then it will be replaced by the current time. No exceptions are raised if the callback functions fail. ''' if sub_type not in self.subscriptions: raise UnknownSubscription( "Unknown subscription {!r}, must be one of {!r}".format( sub_type, self.subscriptions)) kwargs['sub_type'] = sub_type # Guarantee that the object will be in the kwargs kwargs.setdefault('obj', self) # And if a timestamp key exists, but isn't filled -- supply it with # a new timestamp if 'timestamp' in kwargs and kwargs['timestamp'] is None: kwargs['timestamp'] = time.time() # Shallow-copy the callback arguments for replaying the # callback at a later time (e.g., when a new subscription is made) self._args_cache[sub_type] = (tuple(args), dict(kwargs)) for cb in list(self._callbacks[sub_type].values()): cb(*args, **kwargs) def subscribe(self, callback, event_type=None, run=True): '''Subscribe to events this event_type generates. The callback will be called as ``cb(*args, **kwargs)`` with the values passed to `_run_subs` with the following additional keys: sub_type : the string value of the event_type obj : the host object, added if 'obj' not already in kwargs if the key 'timestamp' is in kwargs _and_ is None, then it will be replaced with the current time before running the callback. The ``*args``, ``**kwargs`` passed to _run_subs will be cached as shallow copies, be aware of passing in mutable data. .. warning:: If the callback raises any exceptions when run they will be silently ignored. Parameters ---------- callback : callable A callable function (that takes kwargs) to be run when the event is generated. The expected signature is :: def cb(*args, obj: OphydObject, sub_type: str, **kwargs) -> None: The exact args/kwargs passed are whatever are passed to ``_run_subs`` event_type : str, optional The name of the event to subscribe to (if None, defaults to the default sub for the instance - obj._default_sub) This maps to the ``sub_type`` kwargs in `_run_subs` run : bool, optional Run the callback now See Also -------- clear_sub, _run_subs Returns ------- cid : int id of callback, can be passed to `unsubscribe` to remove the callback ''' if not callable(callback): raise ValueError("callback must be callable") # do default event type if event_type is None: # warnings.warn("Please specify which call back you wish to " # "attach to defaulting to {}" # .format(self._default_sub), stacklevel=2) event_type = self._default_sub if event_type is None: raise ValueError('Subscription type not set and object {} of class' ' {} has no default subscription set' ''.format(self.name, self.__class__.__name__)) # check that this is a valid event type if event_type not in self.subscriptions: raise UnknownSubscription( "Unknown subscription {!r}, must be one of {!r}".format( event_type, self.subscriptions)) # wrapper for callback to snarf exceptions def wrap_cb(cb): @functools.wraps(cb) def inner(*args, **kwargs): try: cb(*args, **kwargs) except Exception: sub_type = kwargs['sub_type'] self.log.exception( 'Subscription %s callback exception (%s)', sub_type, self) return inner # get next cid cid = next(self._cb_count) wrapped = wrap_cb(callback) self._unwrapped_callbacks[event_type][cid] = callback self._callbacks[event_type][cid] = wrapped self._cid_to_event_mapping[cid] = event_type if run: cached = self._args_cache[event_type] if cached is not None: args, kwargs = cached wrapped(*args, **kwargs) return cid def _reset_sub(self, event_type): '''Remove all subscriptions in an event type''' self._callbacks[event_type].clear() self._unwrapped_callbacks[event_type].clear() def clear_sub(self, cb, event_type=None): '''Remove a subscription, given the original callback function See also :meth:`subscribe`, :meth:`unsubscribe` Parameters ---------- cb : callable The callback event_type : str, optional The event to unsubscribe from (if None, removes it from all event types) ''' if event_type is None: event_types = self.event_types else: event_types = [event_type] cid_list = [] for et in event_types: for cid, target in self._unwrapped_callbacks[et].items(): if cb == target: cid_list.append(cid) for cid in cid_list: self.unsubscribe(cid) def unsubscribe(self, cid): """Remove a subscription See also :meth:`subscribe`, :meth:`clear_sub` Parameters ---------- cid : int token return by :meth:`subscribe` """ ev_type = self._cid_to_event_mapping.pop(cid, None) if ev_type is None: return del self._unwrapped_callbacks[ev_type][cid] del self._callbacks[ev_type][cid] def unsubscribe_all(self): for ev_type in self._callbacks: self._reset_sub(ev_type) def check_value(self, value, **kwargs): '''Check if the value is valid for this object This function does no normalization, but may raise if the value is invalid. Raises ------ ValueError ''' pass def __repr__(self): info = self._repr_info() info = ', '.join('{}={!r}'.format(key, value) for key, value in info) return '{}({})'.format(self.__class__.__name__, info) def _repr_info(self): 'Yields pairs of (key, value) to generate the object repr' if self.name is not None: yield ('name', self.name) if self._parent is not None: yield ('parent', self.parent.name) def __copy__(self): '''Copy the ophyd object Shallow copying ophyd objects uses the repr information from the _repr_info method to create a new object. ''' kwargs = dict(self._repr_info()) return self.__class__(**kwargs) def __getnewargs_ex__(self): '''Used by pickle to serialize an ophyd object Returns ------- (args, kwargs) Arguments to be passed to __init__, necessary to recreate this object ''' kwargs = dict(self._repr_info()) return ((), kwargs)