-
Notifications
You must be signed in to change notification settings - Fork 13
[LLM-authored] EPICS record alias support to softioc #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
||
| `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 | ||
|
|
@@ -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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should really be a common function, probably inside of 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| _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() | ||
| 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 | ||
|
|
@@ -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.''' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
|
|
||
| # ---------------------------------------------------------------------------- | ||
|
|
@@ -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) | ||
|
|
||
|
|
||
|
|
@@ -347,6 +377,7 @@ def UnsetDevice(): | |
| # Other builder support functions | ||
| 'LoadDatabase', 'ClearRecords', | ||
| 'SetDeviceName', 'UnsetDevice', | ||
| 'Alias', 'AddDeviceAlias', | ||
| # Device support functions | ||
| 'SetBlocking' | ||
| ] | ||
There was a problem hiding this comment.
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 docsto see the warning.