From c1b9304917292eebc707f724eae396a44e44d393 Mon Sep 17 00:00:00 2001 From: I335851 Date: Fri, 17 Jul 2026 11:10:41 +0200 Subject: [PATCH] model: fix stripping single-quotes from string values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit from_json() received a plain Python string already decoded from JSON — OData JSON responses never wrap Edm.String values in single quotes, so the strip was wrong and silently truncated values that legitimately begin or end with "'". Return the value unchanged instead. from_literal() receives an OData URI literal of the form 'value' and must remove exactly one wrapping pair. Replace strip("'") with a guarded slice (value[1:-1]) that fires only when both ends carry a quote, leaving inner leading/trailing quotes intact. Existing unit tests were enhanced to fill test gaps. Two pre-existing tests were also corrected: they passed OData-literal-wrapped strings (e.g. "'x'") as from_json input, which only passed because the buggy strip silently unwrapped them. Real JSON payloads carry bare string values; the test data has been updated to reflect that. Closes #295 --- CHANGELOG.md | 2 ++ pyodata/v2/model.py | 6 ++++-- tests/test_model_v2.py | 20 ++++++++++++++++++- .../test_model_v2_EdmStructTypeSerializer.py | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a01ad..17347c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - model: replace regexp-based ISO datetime parsing with `datetime.fromisoformat` for `Edm.DateTime` and `Edm.DateTimeOffset` - Petr Hanak +- model: fix stripping single-quotes from string values - Petr Hanak - service: guard against cross-origin __next URL redirection - Petr Hanak + ### Removed - Python 3.9 is no longer supported by pyodata. Python 3.10 is now the minimal supported version. diff --git a/pyodata/v2/model.py b/pyodata/v2/model.py index 9a8e60b..7312b01 100644 --- a/pyodata/v2/model.py +++ b/pyodata/v2/model.py @@ -574,10 +574,12 @@ def to_literal(self, value): # pylint: disable=no-self-use def from_json(self, value): - return value.strip('\'') + return value def from_literal(self, value): - return value.strip('\'') + if len(value) >= 2 and value[0] == "'" and value[-1] == "'": + return value[1:-1] + return value class EdmBooleanTypTraits(TypTraits): diff --git a/tests/test_model_v2.py b/tests/test_model_v2.py index d2e9bc5..32aa207 100644 --- a/tests/test_model_v2.py +++ b/tests/test_model_v2.py @@ -412,6 +412,24 @@ def test_traits(): assert typ.traits.to_literal('Foo Foo') == "'Foo Foo'" assert typ.traits.from_literal("'Alice Bob'") == 'Alice Bob' + # EdmStringTypTraits.from_json — must return value unchanged + assert typ.traits.from_json('hello') == 'hello' + assert typ.traits.from_json("'A' crew") == "'A' crew" + assert typ.traits.from_json("Eng Department'") == "Eng Department'" + assert typ.traits.from_json("'wrapped'") == "'wrapped'" + + # EdmStringTypTraits.from_literal — must remove exactly one wrapping pair + assert typ.traits.from_literal("''A' crew'") == "'A' crew" + assert typ.traits.from_literal("'Eng Department''") == "Eng Department'" + assert typ.traits.from_literal("''") == '' + assert typ.traits.from_literal('') == '' + + # EdmStringTypTraits.from_literal — guard conditions quotes around the value + assert typ.traits.from_literal('no quotes') == 'no quotes' + assert typ.traits.from_literal("'only left") == "'only left" + assert typ.traits.from_literal("only right'") == "only right'" + assert typ.traits.from_literal("'") == "'" + # bool typ = Types.from_name('Edm.Boolean') assert repr(typ.traits) == 'EdmBooleanTypTraits' @@ -941,7 +959,7 @@ def test_complex_serializer(schema): # entity with properties of ODATA primitive types entity_type = schema.entity_type('TemperatureMeasurement') assert srl.to_literal(entity_type, {'ignored-key': 'ignored-value', 'Sensor': 'x'}) == {'Sensor': "'x'"} - assert srl.from_json(entity_type, {'ignored-key': 'ignored-value', 'Sensor': "'x'"}) == {'Sensor': 'x'} + assert srl.from_json(entity_type, {'ignored-key': 'ignored-value', 'Sensor': 'x'}) == {'Sensor': 'x'} @patch('logging.Logger.warning') diff --git a/tests/test_model_v2_EdmStructTypeSerializer.py b/tests/test_model_v2_EdmStructTypeSerializer.py index 9b6eee7..d1dc41c 100644 --- a/tests/test_model_v2_EdmStructTypeSerializer.py +++ b/tests/test_model_v2_EdmStructTypeSerializer.py @@ -9,7 +9,7 @@ @pytest.fixture def complex_type_property_declarations(): return { - 'TestString': (Types.parse_type_name('Edm.String'), "'FooBar'", "'FooBar'", 'FooBar'), + 'TestString': (Types.parse_type_name('Edm.String'), 'FooBar', "'FooBar'", 'FooBar'), 'TestBoolean': (Types.parse_type_name('Edm.Boolean'), False, 'false', False), 'TestInt64': (Types.parse_type_name('Edm.Int64'), '123L', '123L', 123), 'TestDateTime': (Types.parse_type_name('Edm.DateTime'), "/Date(2147483647000)/", "datetime'2038-01-19T03:14:07'",