Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Versioning <https://semver.org/spec/v2.0.0.html>`_.
Unreleased_
-----------

Added:

- Add support for EPICS record aliases: an ``alias`` keyword argument on
record creation functions, a standalone ``Alias`` function, and an
``AddDeviceAlias`` function for aliasing every record under a device.
- Add the ``dbla`` command (list record aliases) to the ``interactive_ioc``
shell.

Fixed:

- `Correctly set alarms during blocking processing <../../pull/209>`_
Expand Down
50 changes: 50 additions & 0 deletions docs/reference/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Test Facilities`_ documentation for more details of each function.

.. autofunction:: dba
.. autofunction:: dbl
.. autofunction:: dbla
.. autofunction:: dbnr
.. autofunction:: dbgrep
.. autofunction:: dbgf
Expand Down Expand Up @@ -361,6 +362,23 @@ and stderr streams, is sent directly to the terminal.

:class:`~softioc.autosave.Autosave` for how to track PVs with autosave inside a context manager.

.. _alias:

`alias`
~~~~~~~

Available on all record types. If given, registers an additional alias
name for the record just created, equivalent to calling :func:`Alias`
with the name of the record just created and this value immediately
afterwards. As with :func:`Alias`, this may be given relative to the
current device name, or as an absolute name if it contains a colon.

.. seealso::
`Alias` for creating an alias for an arbitrary existing record.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This links back to the "alias" title just a few lines above. It also produces a warning when built, which explains why it's done that.

Run pipenv run docs to see the warning.


`AddDeviceAlias` for automatically aliasing every record under the
current device.


For all of these functions any EPICS database field can be assigned a value by
passing it as a keyword argument for the corresponding field name (in upper
Expand Down Expand Up @@ -508,6 +526,38 @@ record creation function.
prevent the accidential creation of records with the currently set device
name.

.. function:: Alias(name, alias_name)

Adds ``alias_name`` as an alias for the record called ``name``. Both
``name`` and ``alias_name`` may be given relative to the current device
name (see `SetDeviceName`), or as absolute names if they contain a
colon.

The target record must already have been created earlier in this
session; aliasing a record that will only be created later, or one
Comment on lines +536 to +537

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We don't really have a concept of a "session" defined anywhere. Best not to mention it and simply say "must have been created earlier".

that only exists in some other already-running IOC, is not supported.

.. seealso::
`alias` for adding an alias at the point a record is created.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another ambiguous target, that also produces a warning when built.


.. function:: AddDeviceAlias(prefix)

Registers ``prefix`` as an alias for the current device name: every
record subsequently created under the current device will also be
given an alias with ``prefix`` in place of the device name -- including
any relative alias names given via the `alias` parameter or `Alias`
function, which are aliased again under ``prefix`` rather than chained
onto the original alias. For example, if the device name is ``foo``
and, after calling ``AddDeviceAlias("test")``, a PV called ``bar`` is
created with ``alias="baz"``, the result is a record ``foo:bar`` with
aliases ``foo:baz``, ``test:bar`` and ``test:baz`` -- all four names
refer to the same record.

Like `SetDeviceName`, this only affects records (and their aliases)
created *after* the call, not ones already created. Must be called
after `SetDeviceName`. Alias prefix registrations are cleared whenever
the device name changes.

.. function:: SetBlocking(blocking)

This can be used to globally set the default of the `blocking` flag, which
Expand Down
172 changes: 172 additions & 0 deletions softioc/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'''Support for adding aliases to EPICS records.

An alias is an alternative name by which an existing record can be reached.
This module resolves alias and target names relative to the current device
name (see `softioc.builder.SetDeviceName`), validates names eagerly so that
mistakes are reported when they are made rather than when the generated
database is loaded, and glues together the alias support already provided
by `epicsdbbuilder`.
'''

from epicsdbbuilder import GetRecordNames, LookupRecord


def _validate_length(full_name):
# GetRecordNames() already length-checks relative record names as they
# are resolved, but that's not the only way a name reaches us: absolute
# names bypass it, and so does device-alias-prefix substitution (which
# builds a name by string surgery, not by resolving it). Applied
# uniformly here so every alias name is checked no matter how it was
# constructed, catching e.g. a pathological AddDeviceAlias prefix that
# would otherwise generate a name long enough to crash the EPICS
# database parser instead of failing cleanly in Python.
max_length = GetRecordNames().maxLength
shown = full_name if len(full_name) <= 80 else full_name[:80] + '...'
assert 0 < len(full_name) <= max_length, \
'Record name %r too long' % shown
Comment on lines +14 to +26

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should really be a common function, probably inside of epicsdbbuilder, rather than a very brittle implementation of it being placed here.

Also I'm not sure I understand the value of cutting the record name to 80 characters. That's already too long for EPICS, too long for a standard 80 character terminal.



def resolve_name(name):
'''Resolves name to an absolute record name. If name contains a colon
it is treated as already absolute, otherwise it is resolved relative to
the current device name.'''
if ':' in name:
full_name = name
else:
full_name = GetRecordNames()(name)
_validate_length(full_name)
return full_name


# Every alias name declared so far this session, mapping the full alias
# name to the record it aliases. Real record names are already tracked by
# epicsdbbuilder (LookupRecord); this supplements that with alias names, so
# collisions between the two are caught immediately rather than only when
# the generated database is loaded.
_alias_names = {}


def _lookup_record(full_name):
'''Returns the record called full_name, or None if no such record has
been created.'''
try:
return LookupRecord(full_name)
except KeyError:
return None


def check_record_name(name):
'''Called before creating a new record: raises if name is already in
use as an alias. (A collision with an existing record is already
caught by epicsdbbuilder itself.)'''
full_name = resolve_name(name)
assert full_name not in _alias_names, \
'Alias %r already defined' % full_name

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a need to use %r rather than just %s? I don't particular see the need to use the repr() representation of a string argument.

This comment applies to many places in this code.



def add_alias(record, alias_name):
'''Adds alias_name as an alias for record. If alias_name is relative
(no colon), it is resolved under the current device name and also
propagated to every registered device alias prefix, exactly like the
record's own name. If alias_name is absolute (contains a colon), it
is added as a single alias with no propagation: an absolute name has
no device-relative part to substitute a device alias prefix into.'''
full_alias = resolve_name(alias_name)
assert full_alias not in _alias_names, \
'Alias %r already defined' % full_alias
assert _lookup_record(full_alias) is None, \
'Record %r already defined' % full_alias
record.add_alias(full_alias)
_alias_names[full_alias] = record
if ':' not in alias_name:
_propagate_device_alias(record, full_alias)


def create_alias(name, alias_name):
'''Creates alias_name as an alias for the record called name. Both
name and alias_name may be relative to the current device name or
absolute. The target record must already have been created earlier in
this session.'''
full_name = resolve_name(name)
record = _lookup_record(full_name)
assert record is not None, \
'Cannot alias unknown record %r' % full_name
add_alias(record, alias_name)


# -----------------------------------------------------------------------
# Support for AddDeviceAlias. Like SetDeviceName, this only affects
# records (and their own aliases) created after it is called.

# Alias prefixes registered for the current device.
Comment on lines +97 to +101

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This whole section is very closely related to the DeviceName, but being kept completely separate leads to a lot of complexity. I think an ideal solution would be to in some way combine this functionality into that feature - perhaps with an optional "alias" keyword on that function. But again that may require changes to epicsdbbuilder and not here.

_device_aliases = []


def _device_prefix():
'''Returns (prefix, separator) for the current device, or (None, None)
if no device name is currently set.'''
names = GetRecordNames()
if names.prefix:
return names.prefix[-1], names.separator
else:
return None, None


def _propagate_device_alias(record, full_name):
'''Called for a record's own primary name (from register_record) or a
relative alias (from add_alias): if any device alias prefixes are
registered, adds a copy of full_name under each prefix, pointing
directly at record.

The names generated here (e.g. "A:t") always contain a colon, so
add_alias treats them as absolute and never propagates them again --
there is no alias-of-alias chaining.

If device alias prefixes are registered but full_name isn't actually
part of the current device (e.g. a nested epicsdbbuilder PushPrefix is
in play), there is no sensible substitution to make. Silently
skipping it would mean AddDeviceAlias quietly failing to do its job
for that name, so this raises instead.'''
if not _device_aliases:
return
device_prefix, separator = _device_prefix()
assert device_prefix is not None, \
'No device name set while device alias prefixes are registered'
assert full_name.startswith(device_prefix + separator), \
'Cannot apply device alias prefixes to %r: not part of the ' \
'current device %r' % (full_name, device_prefix)
short_name = full_name[len(device_prefix) + len(separator):]
for prefix in _device_aliases:
add_alias(record, separator.join((prefix, short_name)))


def register_record(record):
'''Called for every record created. Applies any device alias prefixes
already registered with AddDeviceAlias to the record's primary name.'''
_propagate_device_alias(record, record.name)


def add_device_alias(prefix):
'''Registers prefix as an alias prefix for the current device. Only
affects records (and their aliases) created after this call, exactly
as SetDeviceName only affects subsequently created records.'''
assert isinstance(prefix, str) and prefix, \
'AddDeviceAlias prefix must be a non-empty string'
assert prefix not in _device_aliases, \
'Device alias prefix %r already registered' % prefix
_device_aliases.append(prefix)


def clear_device_aliases():
'''Called whenever the device name changes (SetDeviceName/UnsetDevice):
forgets alias prefix registrations, as they are only meaningful for the
device they were registered against.'''
_device_aliases.clear()


def forget_all():
'''Called when all created records are discarded (ClearRecords):
forgets every alias name declared so far, since the records behind
them are gone. Alias prefix registrations for the (unchanged) device
survive, matching SetDeviceName's scoping.'''
_alias_names.clear()
31 changes: 31 additions & 0 deletions softioc/builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import numpy

from . import alias
from .device_core import RecordLookup
from .softioc import dbLoadDatabase
from .autosave import load_autosave
Expand Down Expand Up @@ -317,6 +318,33 @@ def ClearRecords():
'Record database has already been loaded'
RecordLookup._RecordDirectory.clear()
ResetRecords()
alias.forget_all()


# ----------------------------------------------------------------------------
# Support for record aliases.

def Alias(name, alias_name):
'''Adds alias_name as an alias for the record called name. Both name
and alias_name may be given relative to the current device name (see
SetDeviceName), or as absolute names if they contain a colon.

The target record must already have been created earlier in this
session.'''

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another use of the word "session" that I would like removed.

alias.create_alias(name, alias_name)


def AddDeviceAlias(prefix):
'''Registers prefix as an alias for the current device name: every
record subsequently created under the current device will also be
given an alias under prefix instead of the device name, including any
of that record's own aliases. Like SetDeviceName, this only affects
records created after the call, not ones already created. Must be
called after SetDeviceName. Alias prefix registrations are cleared
whenever the device name changes.'''
assert GetRecordNames().prefix, \
'Must call SetDeviceName() before AddDeviceAlias()'
alias.add_device_alias(prefix)


# ----------------------------------------------------------------------------
Expand All @@ -325,9 +353,11 @@ def ClearRecords():
SetSimpleRecordNames(None, ':')

def SetDeviceName(name):
alias.clear_device_aliases()
SetPrefix(name)

def UnsetDevice():
alias.clear_device_aliases()
SetPrefix(None)


Expand All @@ -347,6 +377,7 @@ def UnsetDevice():
# Other builder support functions
'LoadDatabase', 'ClearRecords',
'SetDeviceName', 'UnsetDevice',
'Alias', 'AddDeviceAlias',
# Device support functions
'SetBlocking'
]
8 changes: 7 additions & 1 deletion softioc/pythonSoftIoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


import epicsdbbuilder
from . import device
from . import alias, device


class RecordWrapper(object):
Expand All @@ -32,8 +32,14 @@ def __init__(self, builder, device, name, **fields):
if keyword in fields:
device_kargs[keyword] = fields.pop(keyword)

record_alias = fields.pop('alias', None)

alias.check_record_name(name)
record = builder(name, **fields)
record.address = '@' + record.name
if record_alias is not None:
alias.add_alias(record, record_alias)
alias.register_record(record)
self.__set('__builder', record)
self.__set('__device', device(record.name, **device_kargs))
self.__Instances.append(self)
Expand Down
6 changes: 6 additions & 0 deletions softioc/softioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ def call_f(*args):
separated) list of fields is also given then the values of the fields are also
printed.''')

ExportTest('dbla', (auto_encode,), ('',), '''\
dbla(pattern='')

Prints "alias -> record" for each alias in the database whose name matches
pattern (or all aliases if pattern is empty).''')

ExportTest('dbnr', (c_int,), (0,), '''\
dbnr(all=0)

Expand Down
1 change: 1 addition & 0 deletions tests/expected_records.db
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ record(stringout, "TS-DI-TEST-01:STRINGOUT")
field(DTYP, "Python")
field(OMSL, "supervisory")
field(OUT, "@TS-DI-TEST-01:STRINGOUT")
alias("TS-DI-TEST-01:STRINGOUT_ALIAS")
}

record(waveform, "TS-DI-TEST-01:WAVEFORM")
Expand Down
4 changes: 3 additions & 1 deletion tests/sim_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def create_records():

boolOut('BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update)
longOut('LONGOUT', initial_value=2008, on_update=on_update)
stringOut('STRINGOUT', initial_value='watevah', on_update=on_update)
stringOut(
'STRINGOUT', initial_value='watevah', on_update=on_update,
alias='STRINGOUT_ALIAS')
mbbOut('MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1)

def update_sin_wf(value):
Expand Down
Loading