From 5c1f28efa0cd3db9fbc2691419a91cfae5a45067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 9 May 2023 22:53:51 +0200 Subject: [PATCH] Do not replace strftime placehoders escaped with double % --- trytond/trytond/ir/lang.py | 32 +++++++++++++++++++++++++++----- trytond/trytond/tests/test_ir.py | 19 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/trytond/trytond/ir/lang.py b/trytond/trytond/ir/lang.py index 0e36732989..ad09338b91 100644 --- a/trytond/trytond/ir/lang.py +++ b/trytond/trytond/ir/lang.py @@ -4,6 +4,7 @@ import datetime from ast import literal_eval from decimal import Decimal +from itertools import takewhile from locale import CHAR_MAX from sql import Table @@ -41,6 +42,27 @@ class DeleteDefaultError(UserError): NO_BREAKING_SPACE = '\u00A0' +def _replace(src, old, new, escape='%'): + """Return a copy of src with all occurrences of substring old replaced + by new if not escaped using double escape""" + def is_escape(x): + return x == escape + assert old.startswith(escape) + start = 0 + while start < len(src): + i = src.find(old, start) + if i < 0: + break + # if there is an odd number of percentage before the + # placeholder then it is not escaped + if len(list(takewhile(is_escape, reversed(src[:i])))) % 2: + start = i + len(old) + else: + src = src[:i] + new + src[i + len(old):] + start = i + len(new) + return src + + class Lang(DeactivableMixin, ModelSQL, ModelView): "Language" __name__ = "ir.lang" @@ -547,15 +569,15 @@ class Lang(DeactivableMixin, ModelSQL, ModelView): if isinstance(value, datetime.datetime): format += ' %H:%M:%S' if '%x' in format: - format = format.replace('%x', self.date) - format = format.replace('%X', '%H:%M:%S') + format = _replace(format, '%x', self.date) + format = _replace(format, '%X', '%H:%M:%S') if isinstance(value, datetime.date): for f, i, klass in (('%A', 6, Day), ('%B', 1, Month)): for field, f in [('name', f), ('abbreviation', f.lower())]: if f in format: locale = klass.locale(self, field=field) - format = format.replace( - f, locale[value.timetuple()[i]]) + format = _replace( + format, f, locale[value.timetuple()[i]]) if '%p' in format: if isinstance(value, datetime.time): time = value @@ -569,7 +591,7 @@ class Lang(DeactivableMixin, ModelSQL, ModelView): p = self.am or 'AM' else: p = self.pm or 'PM' - format = format.replace('%p', p) + format = _replace(format, '%p', p) return value.strftime(format).replace(' ', NO_BREAKING_SPACE) def format_number(self, value, digits=None, grouping=True, monetary=None): diff --git a/trytond/trytond/tests/test_ir.py b/trytond/trytond/tests/test_ir.py index 42150e810a..1269c967a7 100644 --- a/trytond/trytond/tests/test_ir.py +++ b/trytond/trytond/tests/test_ir.py @@ -13,6 +13,7 @@ except ImportError: from dateutil.relativedelta import relativedelta from trytond.config import config +from trytond.ir.lang import _replace from trytond.pool import Pool from trytond.pyson import Eval, If, PYSONEncoder from trytond.tools import timezone @@ -174,6 +175,17 @@ class IrTestCase(ModuleTestCase): self.assertEqual( lang.format(percent, value, grouping, monetary, *add), result) + def test_lang_replace(self): + "Test string _replace" + for src, result in [ + ('%x', 'foo'), + ('%%x', '%%x'), + ('%x %x', 'foo foo'), + ('%x %y %x %%x', 'foo %y foo %%x'), + ]: + with self.subTest(src=src): + self.assertEqual(_replace(src, '%x', 'foo'), result) + @with_transaction() def test_lang_strftime(self): "Test Lang.strftime" @@ -192,9 +204,14 @@ class IrTestCase(ModuleTestCase): '%H:%M:%S', "14:30:12"), (datetime.datetime(2018, 11, 1, 14, 30, 12), None, "11/01/2018 14:30:12"), + (datetime.date(2016, 8, 3), '%d %%m %Y', "03 %m 2016"), + (datetime.date(2018, 11, 1), '%d %%x', "01 %x"), + (datetime.date(2018, 11, 1), '%d %%a', "01 %a"), + (datetime.datetime(2018, 11, 1, 14, 30, 12), '%d %%p', "01 %p"), ] for date, format_, result in test_data: - self.assertEqual(lang.strftime(date, format_), result) + with self.subTest(date=date, format=format_): + self.assertEqual(lang.strftime(date, format_), result) @with_transaction() def test_lang_format_number(self):