From e0b1cf7d204d3a8fc42b8debcaf2b47f31c448d4 Mon Sep 17 00:00:00 2001 From: Sourabh Gandhi Date: Thu, 16 Oct 2025 10:35:15 +0000 Subject: [PATCH 1/4] allow nan values to replicate --- singer/messages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/singer/messages.py b/singer/messages.py index de6e076..4d7593c 100644 --- a/singer/messages.py +++ b/singer/messages.py @@ -218,12 +218,12 @@ def parse_message(msg): return None -def format_message(message, ensure_ascii=True): - return json.dumps(message.asdict(), use_decimal=True, ensure_ascii=ensure_ascii) +def format_message(message, ensure_ascii=True, allow_nan=False): + return json.dumps(message.asdict(), use_decimal=True, ensure_ascii=ensure_ascii, allow_nan=allow_nan) -def write_message(message, ensure_ascii=True): - sys.stdout.write(format_message(message, ensure_ascii=ensure_ascii) + '\n') +def write_message(message, ensure_ascii=True, allow_nan=False): + sys.stdout.write(format_message(message, ensure_ascii=ensure_ascii, allow_nan=allow_nan) + '\n') sys.stdout.flush() From 434f787c32531262e83930f213405d9bfb051686 Mon Sep 17 00:00:00 2001 From: Sourabh Gandhi Date: Thu, 16 Oct 2025 10:47:02 +0000 Subject: [PATCH 2/4] update setup and changelog --- CHANGELOG.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ce58e..a878fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 6.3.0 + * Support allow_nan in message JSON output [#183](https://github.com/singer-io/singer-python/pull/183) + ## 6.2.3 * Default type for non-standard data types is string [#182](https://github.com/singer-io/singer-python/pull/182) diff --git a/setup.py b/setup.py index 9940901..a25f30b 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import subprocess setup(name="singer-python", - version='6.2.3', + version='6.3.0', description="Singer.io utility library", author="Stitch", classifiers=['Programming Language :: Python :: 3 :: Only'], From a9095ace4c40b647e98645fdcdf2c901a84b5bf8 Mon Sep 17 00:00:00 2001 From: Sourabh Gandhi Date: Thu, 16 Oct 2025 11:38:50 +0000 Subject: [PATCH 3/4] make pylint happy --- singer/messages.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/singer/messages.py b/singer/messages.py index 4d7593c..941670c 100644 --- a/singer/messages.py +++ b/singer/messages.py @@ -219,7 +219,12 @@ def parse_message(msg): def format_message(message, ensure_ascii=True, allow_nan=False): - return json.dumps(message.asdict(), use_decimal=True, ensure_ascii=ensure_ascii, allow_nan=allow_nan) + return json.dumps( + message.asdict(), + use_decimal=True, + ensure_ascii=ensure_ascii, + allow_nan=allow_nan + ) def write_message(message, ensure_ascii=True, allow_nan=False): From cbc0a147ab93703398efc4ce3ea058cc91947f97 Mon Sep 17 00:00:00 2001 From: Sourabh Gandhi Date: Wed, 22 Oct 2025 18:32:32 +0530 Subject: [PATCH 4/4] add test cases for allow nan --- tests/test_transform.py | 60 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/test_transform.py b/tests/test_transform.py index 308ac4e..b398e93 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -1,9 +1,12 @@ +import io +import sys import unittest import decimal +import simplejson as json +import singer.messages as messages from singer import transform from singer.transform import * - class TestTransform(unittest.TestCase): def test_integer_transform(self): schema = {'type': 'integer'} @@ -486,3 +489,58 @@ def test_pattern_properties_match_multiple(self): dict_value = {"name": "chicken", "unit_cost": 1.45, "SKU": '123456'} expected = dict(dict_value) self.assertEqual(expected, transform(dict_value, schema)) + +class DummyMessage: + """A dummy message object with an asdict() method.""" + def __init__(self, value): + self.value = value + + def asdict(self): + return {"value": self.value} + + +class TestAllowNan(unittest.TestCase): + """Unit tests for allow_nan support in singer.messages.""" + + def test_format_message_allow_nan_true(self): + """Should serialize NaN successfully when allow_nan=True.""" + msg = DummyMessage(float("nan")) + result = messages.format_message(msg, allow_nan=True) + + # The output JSON should contain NaN literal (not quoted) + self.assertIn("NaN", result) + + # Replace NaN with null to make it valid JSON for parsing check + json.loads(result.replace("NaN", "null")) + + def test_format_message_allow_nan_false(self): + """Should raise ValueError when allow_nan=False and value is NaN.""" + msg = DummyMessage(float("nan")) + with self.assertRaises(ValueError): + messages.format_message(msg, allow_nan=False) + + def test_write_message_allow_nan_true(self): + """Should write to stdout successfully when allow_nan=True.""" + msg = DummyMessage(float("nan")) + fake_stdout = io.StringIO() + original_stdout = sys.stdout + sys.stdout = fake_stdout + try: + messages.write_message(msg, allow_nan=True) + output = fake_stdout.getvalue() + self.assertIn("NaN", output) + self.assertTrue(output.endswith("\n")) + finally: + sys.stdout = original_stdout + + def test_write_message_allow_nan_false(self): + """Should raise ValueError when allow_nan=False and message has NaN.""" + msg = DummyMessage(float("nan")) + fake_stdout = io.StringIO() + original_stdout = sys.stdout + sys.stdout = fake_stdout + try: + with self.assertRaises(ValueError): + messages.write_message(msg, allow_nan=False) + finally: + sys.stdout = original_stdout