From e80152a422328b3bef435aff6fb2eb8959e9aaa8 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Mon, 27 Jan 2020 09:35:31 -0500 Subject: [PATCH 1/2] Fixing up ANSIString --- evennia/utils/ansi.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index ec2b1b3378..265ce774ae 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -673,6 +673,10 @@ class ANSIString(str, metaclass=ANSIMeta): and taken literally the second time around. """ + # A compiled Regex for the format mini-language: https://docs.python.org/3/library/string.html#formatspec + re_format = re.compile(r"(?i)(?P(?P.)?(?P\<|\>|\=|\^))?(?P\+|\-| )?(?P\#)?" + r"(?P0)?(?P\d+)?(?P\_|\,)?(?:\.(?P\d+))?" + r"(?Pb|c|d|e|E|f|F|g|G|n|o|s|x|X|%)?") def __new__(cls, *args, **kwargs): """ @@ -733,6 +737,47 @@ class ANSIString(str, metaclass=ANSIMeta): def __str__(self): return self._raw_string + def __format__(self, format_spec): + """ + This magic method covers ANSIString's behavior within a str.format() or f-string. + + Current features supported: fill, align, width. + + Args: + format_spec (str): The format specification passed by f-string or str.format(). This is a string such as + "0<30" which would mean "left justify to 30, filling with zeros". The full specification can be found + at https://docs.python.org/3/library/string.html#formatspec + + Returns: + ansi_str (str): The formatted ANSIString's .raw() form, for display. + """ + # This calls the compiled regex stored on ANSIString's class to analyze the format spec. + # It returns a dictionary. + format_data = self.re_format.match(format_spec).groupdict() + clean = self.clean() + base_output = ANSIString(self.raw()) + align = format_data.get('align', '<') + fill = format_data.get('fill', ' ') + + # Need to coerce width into an integer. We can be certain that it's numeric thanks to regex. + width = format_data.get('width', None) + if width is None: + width = len(clean) + else: + width = int(width) + + if align == '<': + base_output = self.ljust(width, fill) + elif align == '>': + base_output = self.rjust(width, fill) + elif align == '^': + base_output = self.center(width, fill) + elif align == '=': + pass + + # Return the raw string with ANSI markup, ready to be displayed. + return base_output.raw() + def __repr__(self): """ Let's make the repr the command that would actually be used to From 2521fc9030a0abf47a7919996098bd5ac4dcec26 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Wed, 29 Jan 2020 18:17:22 -0500 Subject: [PATCH 2/2] Adding unit tests for ANSIString! --- evennia/utils/tests/test_utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index c969a988a5..eb01e38a83 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -98,6 +98,30 @@ class TestMLen(TestCase): self.assertEqual(utils.m_len({"hello": True, "Goodbye": False}), 2) +class TestANSIString(TestCase): + """ + Verifies that ANSIString's string-API works as intended. + """ + + def setUp(self): + self.example_raw = "|relectric |cboogaloo|n" + self.example_ansi = ANSIString(self.example_raw) + self.example_str = "electric boogaloo" + self.example_output = "\x1b[1m\x1b[31melectric \x1b[1m\x1b[36mboogaloo\x1b[0m" + + def test_length(self): + self.assertEqual(len(self.example_ansi), 17) + + def test_clean(self): + self.assertEqual(self.example_ansi.clean(), self.example_str) + + def test_raw(self): + self.assertEqual(self.example_ansi.raw(), self.example_output) + + def test_format(self): + self.assertEqual(f"{self.example_ansi:0<20}", self.example_output + "000") + + class TestTimeformat(TestCase): """ Default function header from utils.py: