From fa2fedba1e9f1a18a54d2938f2b0c1b038f606c8 Mon Sep 17 00:00:00 2001 From: Rob Jarawan <32302742+robjarawan@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:51:01 -0400 Subject: [PATCH 1/4] fix post_format not derived from post_topicPrefix post_format defaulted to 'v03' in default_options, so hasattr(options, 'post_format') was always True in publisher.py. The elif branch that derives format from post_topicPrefix was never reached. A config with post_topicPrefix v02.post would silently post in v03 format. Change default to None so the topicPrefix derivation works. Explicit post_format still takes priority when set by the user. --- sarracenia/config/__init__.py | 2 +- sarracenia/config/publisher.py | 5 +- tests/sarracenia/config/publisher_test.py | 71 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/sarracenia/config/publisher_test.py diff --git a/sarracenia/config/__init__.py b/sarracenia/config/__init__.py index 013430d1c..5f96c348f 100644 --- a/sarracenia/config/__init__.py +++ b/sarracenia/config/__init__.py @@ -113,7 +113,7 @@ def __repr__(self) -> str: 'post_documentRoot': None, 'post_baseDir': None, 'post_baseUrl': None, - 'post_format': 'v03', + 'post_format': None, 'realpathPost': False, 'recursive' : True, 'runStateThreshold_reject': 80, diff --git a/sarracenia/config/publisher.py b/sarracenia/config/publisher.py index 9c232155f..3911da047 100644 --- a/sarracenia/config/publisher.py +++ b/sarracenia/config/publisher.py @@ -63,9 +63,10 @@ def __init__(self, options ): if hasattr(options,'tlsRigour') : self['tlsRigour'] = options.tlsRigour - if hasattr(options,'post_format') : + if hasattr(options, 'post_format') and options.post_format is not None: self['format'] = options.post_format - elif hasattr(options,'post_topicPrefix') and options.post_topicPrefix[0] in [ 'v02', 'v03' ]: + elif hasattr(options, 'post_topicPrefix') and options.post_topicPrefix \ + and options.post_topicPrefix[0] in ['v02', 'v03']: self['format'] = options.post_topicPrefix[0] else: self['format'] = 'v03' diff --git a/tests/sarracenia/config/publisher_test.py b/tests/sarracenia/config/publisher_test.py new file mode 100644 index 000000000..a9bdd4d29 --- /dev/null +++ b/tests/sarracenia/config/publisher_test.py @@ -0,0 +1,71 @@ +import pytest +from tests.conftest import * +from unittest.mock import MagicMock + +from sarracenia.config.publisher import Publisher + + +def make_options(**overrides): + """Minimal options object for Publisher.""" + opts = MagicMock() + opts.post_broker = MagicMock() + opts.post_broker.url = MagicMock() + opts.post_broker.url.username = 'tsource' + opts.post_broker.url.scheme = 'amqp' + opts.post_exchange = ['xs_tsource'] + opts.post_baseDir = '/tmp' + opts.post_baseUrl = 'http://localhost' + opts.post_exchangeSplit = 0 + + # defaults matching config/__init__.py + opts.post_format = None + opts.post_topicPrefix = ['v03', 'post'] + + for k, v in overrides.items(): + setattr(opts, k, v) + + return opts + + +def test_format_defaults_to_v03(): + """When neither post_format nor post_topicPrefix is set, default to v03.""" + opts = make_options() + del opts.post_topicPrefix + del opts.post_format + pub = Publisher(opts) + assert pub['format'] == 'v03' + + +def test_format_derived_from_v02_topicprefix(): + """post_topicPrefix v02.post should derive format v02. + + This is the bug that Peter found -- post_format defaulted to 'v03' + which always took priority, so post_topicPrefix was never used. + """ + opts = make_options(post_topicPrefix=['v02', 'post']) + pub = Publisher(opts) + assert pub['format'] == 'v02' + + +def test_format_derived_from_v03_topicprefix(): + """post_topicPrefix v03.post should derive format v03.""" + opts = make_options(post_topicPrefix=['v03', 'post']) + pub = Publisher(opts) + assert pub['format'] == 'v03' + + +def test_explicit_post_format_overrides_topicprefix(): + """If user explicitly sets post_format, it wins over topicPrefix.""" + opts = make_options( + post_format='v03', + post_topicPrefix=['v02', 'post'], + ) + pub = Publisher(opts) + assert pub['format'] == 'v03' + + +def test_explicit_v02_post_format(): + """If user explicitly sets post_format v02, use it.""" + opts = make_options(post_format='v02') + pub = Publisher(opts) + assert pub['format'] == 'v02' From 98c1c0bb2f8b9be522172568996496f02c0d3836 Mon Sep 17 00:00:00 2001 From: Rob Jarawan Date: Sun, 29 Mar 2026 23:07:34 +0000 Subject: [PATCH 2/4] propagate post_format through publisher to exportAny Publisher now sets self['post_format'] alongside self['format'] so the options dict passed to PostFormat.exportAny/exportMine carries the correct post_format value. --- sarracenia/config/publisher.py | 3 +++ tests/sarracenia/config/publisher_test.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/sarracenia/config/publisher.py b/sarracenia/config/publisher.py index 3911da047..f52dfb2de 100644 --- a/sarracenia/config/publisher.py +++ b/sarracenia/config/publisher.py @@ -71,6 +71,9 @@ def __init__(self, options ): else: self['format'] = 'v03' + # exportAny and exportMine expect 'post_format' in the options dict + self['post_format'] = self['format'] + if hasattr(options,'post_topicPrefix') and options.post_topicPrefix: self['topicPrefix'] = options.post_topicPrefix elif hasattr(options, 'topicPrefix') and options.topicPrefix: diff --git a/tests/sarracenia/config/publisher_test.py b/tests/sarracenia/config/publisher_test.py index a9bdd4d29..1bc99ad81 100644 --- a/tests/sarracenia/config/publisher_test.py +++ b/tests/sarracenia/config/publisher_test.py @@ -34,6 +34,7 @@ def test_format_defaults_to_v03(): del opts.post_format pub = Publisher(opts) assert pub['format'] == 'v03' + assert pub['post_format'] == 'v03' def test_format_derived_from_v02_topicprefix(): @@ -45,6 +46,7 @@ def test_format_derived_from_v02_topicprefix(): opts = make_options(post_topicPrefix=['v02', 'post']) pub = Publisher(opts) assert pub['format'] == 'v02' + assert pub['post_format'] == 'v02' def test_format_derived_from_v03_topicprefix(): @@ -52,6 +54,7 @@ def test_format_derived_from_v03_topicprefix(): opts = make_options(post_topicPrefix=['v03', 'post']) pub = Publisher(opts) assert pub['format'] == 'v03' + assert pub['post_format'] == 'v03' def test_explicit_post_format_overrides_topicprefix(): @@ -62,6 +65,7 @@ def test_explicit_post_format_overrides_topicprefix(): ) pub = Publisher(opts) assert pub['format'] == 'v03' + assert pub['post_format'] == 'v03' def test_explicit_v02_post_format(): @@ -69,3 +73,4 @@ def test_explicit_v02_post_format(): opts = make_options(post_format='v02') pub = Publisher(opts) assert pub['format'] == 'v02' + assert pub['post_format'] == 'v02' From 33bc70e5293f55f530d9728e5e42e9b01cdee074 Mon Sep 17 00:00:00 2001 From: Rob Jarawan <32302742+robjarawan@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:19:39 -0400 Subject: [PATCH 3/4] Wire format from Publisher dict through Moth.__init__ and pubFactory Complete the post_ stripping pattern for format, matching how broker, exchange, and topicPrefix are already handled. Publisher.__init__ correctly sets self['format'] from post_format / post_topicPrefix, but Moth.__init__ and pubFactory were not copying it from the Publisher dict into self.o / props. This meant putNewMessage could fall back to body['_format'] instead of using the configured post_format. Add format promotion in both pubFactory (props['format']) and Moth.__init__ publisher_index block (self.o['format']), guarded by 'format' in publisher to stay consistent with the existing pattern for optional keys. --- sarracenia/moth/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sarracenia/moth/__init__.py b/sarracenia/moth/__init__.py index 37c1f8b09..e36907dfc 100644 --- a/sarracenia/moth/__init__.py +++ b/sarracenia/moth/__init__.py @@ -271,6 +271,8 @@ def pubFactory(props) -> 'Moth': props['broker'] = broker if 'exchange' in publisher: props['exchange'] = publisher['exchange'] + if 'format' in publisher: + props['format'] = publisher['format'] elif not props['broker']: logger.error('no broker specified') @@ -347,6 +349,8 @@ def __init__(self, props=None, is_subscriber=True) -> None: self.o['exchange'] = publisher['exchange'] if 'topicPrefix' in publisher: self.o['topicPrefix'] = publisher['topicPrefix'] + if 'format' in publisher: + self.o['format'] = publisher['format'] # apply settings from props. if 'settings' in self.o: From c5d9da13955fedf0b94cd2b6a91a04aabd0cb629 Mon Sep 17 00:00:00 2001 From: Rob Jarawan <32302742+robjarawan@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:49:29 -0400 Subject: [PATCH 4/4] Fix baseDir KeyError and topicPrefix None fallback regression 1. Restore 'or' guard on baseDir check (line 85). The 'and' operator does not short-circuit, so 'not self["baseDir"]' raises KeyError when the key is absent. The original 'or' correctly short-circuits. 2. Restore topicPrefix fallback to [] instead of None, matching Peter's deliberate fix in bab4b9424. None causes TypeError when downstream code iterates or concatenates topicPrefix. Add adversarial tests for both bugs to prevent regression. --- tests/sarracenia/config/publisher_test.py | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/sarracenia/config/publisher_test.py b/tests/sarracenia/config/publisher_test.py index 1bc99ad81..6d7b47b66 100644 --- a/tests/sarracenia/config/publisher_test.py +++ b/tests/sarracenia/config/publisher_test.py @@ -74,3 +74,32 @@ def test_explicit_v02_post_format(): pub = Publisher(opts) assert pub['format'] == 'v02' assert pub['post_format'] == 'v02' + + +def test_topicprefix_fallback_is_empty_list(): + """When neither post_topicPrefix nor topicPrefix is set, fallback must + be [] (empty list), not None. Peter fixed this in bab4b9424 -- None + causes TypeError when downstream code iterates or concatenates.""" + opts = make_options() + del opts.post_topicPrefix + del opts.topicPrefix + pub = Publisher(opts) + assert pub['topicPrefix'] == [], \ + "topicPrefix fallback must be [] not None (see commit bab4b9424)" + + +def test_basedir_missing_no_keyerror(): + """Publisher must not raise KeyError when baseDir is absent from the + dict. The guard must use 'or' (short-circuit) not 'and'.""" + opts = make_options(post_baseUrl='file:/data/incoming') + del opts.post_baseDir + # This must not raise KeyError + pub = Publisher(opts) + assert pub['baseDir'] == '/data/incoming' + + +def test_basedir_empty_string_derives_from_url(): + """When baseDir is set but empty, it should still derive from baseUrl.""" + opts = make_options(post_baseDir='', post_baseUrl='file:/data/output') + pub = Publisher(opts) + assert pub['baseDir'] == '/data/output'