Skip to content

0.2.0rc4

@Monarda Monarda tagged this 04 Aug 08:45
First release candidate for 0.2.0. Changes since 0.1.4.5.

BREAKING
--------

* Recipe classes in p4pillon.pvrecipe are no longer flavor-agnostic:
  build_pv() raises TypeError. Import PVScalarRecipe,
  PVScalarArrayRecipe or PVEnumRecipe from p4pillon.thread.pvrecipe
  or p4pillon.asyncio.pvrecipe instead (previously they silently
  built raw-flavored PVs with no executor or locking).

* p4pillon.asyncio PVs must call open()/post() on their event loop;
  off-loop calls raise. Use post_deferred() from other threads.

* CalcRule.init_rule() now matches BaseRule: init_rule(newpvstate)
  returning a RulesFlow, not init_rule(value, **kwargs) returning
  None.

* The "test" and "dist" extras are now dependency-groups, plus a
  "dev" group containing both. Use "uv sync --group dev" instead of
  "pip install p4pillon[test]".

ADDED
-----

* IOC-style RECORD.FIELD sub-PVs. Serve the fields common to every
  EPICS record (dbCommon.dbd) plus RTYP, DTYP, NAME and DESC (and
  ADEL/MDEL for numeric scalars) as independent read-only channels -
  e.g. EXAMPLE:PV.SCAN - so a plain Python server looks like an IOC
  to tools expecting record.FIELD access, without adding those
  fields to the base PV's structure. Defaults follow EPICS Base:
  RTYP inferred from value type, DTYP "Soft Channel", DESC mirroring
  display.description. String fields also get a $ long-string alias.
  Three entry points: StaticRecordProvider (eager, sub-PVs visible
  in pvlist), IOCMimicProvider (lazy, built on client connect), and
  IOCMimicServer (drop-in Server giving any plain {name: pv} dict
  the same treatment).

  - RTYP inference: int64 to int64in (not longin, whose 32-bit VAL
    would truncate), bool to bi, string to stringin, array to
    waveform, NTEnum to mbbi; always the input side of a record
    pair (ai, not ao).
  - NT types with no matching record (NTTable, NTNDArray, ...) are
    served on their own with no sub-PVs, as a real IOC serves a
    Q:group, rather than being rejected. An explicit RTYP opts them
    into record treatment.
  - IOCMimicProvider re-reads display.description on each client
    connect, via a weak reference to the base PV, so DESC tracks
    later changes for new connections. set_desc_record overrides it
    explicitly.
  - Each field channel's timeStamp is taken from the base PV's own,
    mirroring an IOC where RECORD.FIELD reports the record's process
    time; where the base PV has none to give, the field is stamped
    with the time it was built. It is never left at the 0s 0ns that
    wrap() produces, which a client renders as 1970-01-01. The eager
    path (StaticRecordProvider) snapshots once at add(); the lazy
    path re-reads per connect, then holds it static for the life of
    the channel - also what a real IOC does.

* A client's first update can now carry the complete structure.
  pvAccess transmits only the fields a value has marked as changed,
  and a SharedPV's mask is the union of everything marked since
  open() - so NTScalar(...).wrap(v), which marks only "value",
  leaves alarm.*, display.* and valueAlarm.* never reaching a client
  at all. p4p and pvxs clients hide this by zero-filling what they
  did not receive; the Java org.epics.pva client behind
  Phoebus/CS-Studio and the EPICS Archiver Appliance leaves an
  untransmitted string as null, which is enough to make the archiver
  throw an NPE while saving a channel's metadata. A real IOC (QSRV)
  sends the whole structure and signals "unset" in-band.

  - New tri-state p4pillon.server.raw.InitialUpdate - COMPLETE,
    AS_POSTED, DEFAULT - set per PV with a keyword-only
    initial_update= on any p4pillon SharedPV or SharedNT.
  - DEFAULT defers to whatever serves the PV. StaticRecordProvider,
    IOCMimicProvider and IOCMimicServer resolve it to COMPLETE,
    since they exist to mimic an IOC; everywhere else it falls back
    to AS_POSTED, so nothing changes for existing code unless you
    ask. An explicit COMPLETE or AS_POSTED always outranks the
    server's choice.
  - If you serve PVs through the record-field providers, the bytes
    on the wire change: clients now receive alarm, display and
    valueAlarm on their first update where previously they received
    only "value". Pass initial_update=InitialUpdate.AS_POSTED to
    keep the old behaviour.
  - p4pillon.server.raw.apply_initial_update(pv, mode) is the seam a
    provider calls, exposed for providers you write yourself. It
    works retroactively - a PV built with initial= is already open
    with its mask fixed - and accepts a plain p4p SharedPV, which
    just cannot carry the setting across a close()/open().
  - p4pillon.utils.mark_all(): mark every field of a Value as
    changed, in place. The primitive underneath, and still the right
    tool for a hand-built Value or a post() that must carry a whole
    substructure.

* Handler-dispatch serialization: a per-PV re-entrant lock
  serializes open()/post()/close() hooks against executor-side
  put/rpc handlers (thread flavor); CompositeHandler has its own
  lock for one handler shared across several PVs.

* post_deferred() on both flavors - marshals a post onto the asyncio
  loop, or onto the PV's work queue in the thread flavor - so a
  handler can safely post to another PV regardless of flavor. It
  returns a concurrent.futures.Future.

* BaseRule.applies_to(nttype): the setup-time test for whether a
  Rule attaches to a PV of a given Type, as distinct from the
  per-operation is_applicable(value). BaseRule.run_last orders a
  Rule after every other handler, including user handlers;
  TimestampRule sets it, so the stored value carries the time it
  was finalised.

* p4pillon.utils.as_raw(): the underlying raw Value of an
  NT-wrapped value, tolerating unwrapped values.

* IOCMimicProvider.keys(): the base PV names, mirroring
  StaticProvider.keys.

* Docs: docs/guide/record-fields.md (record-field providers, the
  full value-type to RTYP table, eager/lazy trade-offs) and
  docs/design/dispatch_fix.md.

CHANGED
-------

* CalcRule no longer evaluates calc expressions with eval() (#58).
  Expressions run under simpleeval's SimpleEval and see only "pv"
  (monitored variable values) and "m" (public attributes of math),
  so a calc string cannot execute arbitrary Python. Adds a runtime
  dependency on simpleeval.

* overwrite_marked()/overwrite_unmarked() are substantially faster,
  using p4p's changedSet(expand=True) instead of recursing over
  every leaf in Python. Behaviour and the "fields" argument are
  unchanged.

* pyproject.toml modernised: PEP 639 license = "BSD-3-Clause" and
  the deprecated license classifier removed, URLs pointing at
  ISISNeutronMuon/p4pillon and the published docs, testpaths set,
  coverage scoped to the p4pillon package.

* Linting moves to ruff 0.16.1 with many more rule sets enabled
  (bugbear, async, simplify, and RUF, ARG, N, RET, PERF, LOG, PT,
  TC, T20, S, TRY, EM), so previously accepted code may now fail
  lint. Test deps bumped: pytest>=9.1.1, coverage>7.15.2,
  pyyaml>=6.0.3 (#61). CI prints the ruff version it ran.

FIXED
-----

* A field provider no longer prints "TypeError: makeChannel(...)
  must return SharedPV, not NoneType" on every base PV connection.
  pvxs offers channel creation to every source in (order, name)
  sequence regardless of which one claimed the search, and a field
  provider - serving only <name>.<FIELD> - has to decline each base
  PV name it is offered; p4p's only way to express that is None from
  makeChannel, which it complains about on stderr. Field providers
  built by IOCMimicProvider and IOCMimicServer are now sorted behind
  every provider serving base PVs, so they are never offered them. A
  DynamicProvider you build yourself is still yours to order.

* IOCMimicServer keeps every provider it depends on alive.
  p4p.server.Server does that only for a StaticProvider it builds
  from a bare dict, not for the field providers p4pillon builds nor
  for an IOCMimicProvider it takes apart - so a caller dropping
  their provider left the base PVs working (the C++ Server holds
  those) while every sub-PV silently stopped resolving.

* IOCMimicProvider.set_desc_record no longer stops the fields'
  timeStamps tracking the base PV. It previously dropped the
  registry's weak reference to the base PV outright, switching off
  DESC tracking and timestamp tracking together; it now opts out of
  DESC alone.

* p4pillon.server.asyncio.SharedPV was re-exporting p4p's thread
  SharedPV; it now derives from p4p.server.asyncio.SharedPV.

* Handler hooks are added by ordinary inheritance
  (HandlerHooksMixin, SharedNTMixin) instead of monkey-patching
  p4p.server.raw.SharedPV, which silently did nothing if p4p was
  imported first.

* open()/post() no longer wrap values twice.

* Server.__init__ uses self._context, so the thread and asyncio
  Server subclasses no longer mutate the shared base _context.

* timeStamp is no longer falsely reported as changed:
  CompositeHandler.put posts the raw client Value rather than
  op.value(), whose re-wrap re-marked timeStamp even when the
  client touched only "value". With the changed-set trustworthy,
  TimestampRule now preserves a caller-supplied timeStamp instead
  of always overwriting it.

* TimestampRule declared its supported types as "nttype" rather
  than "nttypes", so the attribute the rule machinery reads was
  never set. Regression tests now cover the declaration of every
  BaseRule subclass.

* ValueAlarmRule:

  - Turning off valueAlarm.active clears the alarm the rule raised,
    instead of leaving it stale.
  - An alarm severity supplied with an update is respected rather
    than overwritten by the limit verdict. The two are maximised as
    EPICS Base does (recGblSetSevrVMsg): highest severity wins, and
    on a tie the first contributor keeps alarm.message. An alarm
    left from a previous update no longer suppresses the limit
    check.
  - NaN is reported as undefined - INVALID_ALARM, message "UDF",
    status UNDEFINED_STATUS - matching Base, checked before and
    independently of the limits. alarm.status is otherwise left to
    whatever raised it; the rule clears only the UNDEFINED_STATUS
    it set itself.
  - Alarm fields are written only when the alarm state moves. The
    previous unconditional write marked the field changed on every
    update, which on the array path re-posted an unchanged alarm to
    every monitoring client.
  - Malformed limit names no longer raise SyntaxError; the four
    limits are a fixed table.

* ScalarToArrayWrapperRule handles arrays that change length:
  elements past the end of the previous array get the wrapped
  rule's init_rule (there is no previous element to compare
  against) rather than a post_rule with a None old state, and
  indices dropped by a shrunken array are not checked. Empty arrays
  (which p4p returns as None, not an empty sequence) no longer
  raise TypeError, and element values are converted to Python
  built-ins so p4p accepts them back into scalar fields.
Assets 2
Loading