From a288d8fe5279bfd20ab5a64146e22ff46d05a00a Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 14 May 2025 20:19:17 +0200 Subject: [PATCH 1/9] Prepare transition to new Log protocol --- treelog/proto.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/treelog/proto.py b/treelog/proto.py index 1339c27..0eba351 100644 --- a/treelog/proto.py +++ b/treelog/proto.py @@ -33,3 +33,40 @@ def pushcontext(self, title: str) -> None: ... def popcontext(self) -> None: ... def recontext(self, title: str) -> None: ... def write(self, msg: Union[str, Data], level: Level) -> None: ... + + +# TRANSITIONAL, TEMPORARY +class oldproto: + @classmethod + def fromnew(cls, NewLog): + return type(NewLog.__name__, (cls,), {"wrapped": NewLog}) + + def __init__(self, *args, **kwargs): + self.context = [self.wrapped(*args, **kwargs)] + + @property + def current(self): + return self.context[-1] + + def pushcontext(self, title): + self.context.append(self.current.branch(title)) + + def popcontext(self): + self.context.pop().close() + + def recontext(self, title): + self.popcontext() + self.pushcontext(title) + + def write(self, msg, level): + self.current.write(msg, level) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.context.pop().close() + assert not self.context + + def __getattr__(self, attr): + return getattr(self.context[0], attr) From 03ab4f5dbca6ee25239151b9110bbfa20449f4ee Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 11:50:03 +0200 Subject: [PATCH 2/9] Convert StdoutLog to new log protocol --- treelog/_stdout.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/treelog/_stdout.py b/treelog/_stdout.py index b10ae06..e1257cb 100644 --- a/treelog/_stdout.py +++ b/treelog/_stdout.py @@ -1,26 +1,25 @@ import sys -from . import proto +from .proto import Level, oldproto +@oldproto.fromnew class StdoutLog: """Output plain text to stream.""" - def __init__(self, file=sys.stdout): + def __init__(self, file=sys.stdout, prefix=""): self.file = file - self.currentcontext = [] # type: typing.List[str] + self.prefix = prefix - def pushcontext(self, title: str) -> None: - self.currentcontext.append(title + " > ") + def branch(self, title): + return self.__class__(self.file, self.prefix + title + " > ") - def popcontext(self) -> None: - self.currentcontext.pop() + def write(self, msg, level: Level) -> None: + if self.prefix: + msg = self.prefix + str(msg).replace( + "\n", "\n" + " > ".rjust(len(self.prefix)) + ) + print(msg, file=self.file, flush=True) - def recontext(self, title: str) -> None: - self.currentcontext[-1] = title + " > " - - def write(self, msg, level: proto.Level) -> None: - if self.currentcontext: - prefix = "".join(self.currentcontext) - msg = prefix + str(msg).replace("\n", "\n" + " > ".rjust(len(prefix))) - print(msg, file=self.file) + def close(self): + pass From e5ff87ee6c9854b246c828fdcbc8462375dc9323 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 11:49:16 +0200 Subject: [PATCH 3/9] Convert RichOutputLog to new log protocol --- tests.py | 98 ++++++++++++++++++++++++--------------- treelog/_richoutput.py | 103 ++++++++++++++++++----------------------- 2 files changed, 106 insertions(+), 95 deletions(-) diff --git a/tests.py b/tests.py index 7d677f4..010e127 100644 --- a/tests.py +++ b/tests.py @@ -105,43 +105,67 @@ def test_output(self): def check_output(self, f): self.assertEqual( - f.getvalue(), - "\x1b[1;34mmy message\x1b[0m\n" - "test.dat > " - "\r\x1b[K" - "\x1b[1mtest.dat\x1b[0m [5 bytes]\n" - "my context > " - "iter 0 " - "> \x1b[4D1 > " - "\x1b[1ma\x1b[0m\nmy context > iter 1 > " - "\x1b[4D2 > " - "\x1b[1mb\x1b[0m\nmy context > iter 2 > " - "\x1b[4D3 > " - "\x1b[1mc\x1b[0m\nmy context > iter 3 > " - "\x1b[9D\x1b[K" - "empty > " - "\x1b[8D\x1b[K" - "\x1b[1;31mmultiple..\x1b[0m\n > \x1b[1;31m ..lines\x1b[0m\nmy context > test.dat > " - "\x1b[1mgenerating\x1b[0m\nmy context > test.dat > " - "\x1b[11D\x1b[K" - "\x1b[1;34mtest.dat\x1b[0m [5 bytes]\nmy context > " - "\r\x1b[Kgenerate_test > test.dat > " - "\x1b[11D\x1b[K" - "\x1b[1;35mtest.dat\x1b[0m [5 bytes]\ngenerate_test > " - "\r\x1b[K" - "context step=0 > " - "\x1b[1mfoo\x1b[0m\n" - "context step=0 > " - "\x1b[4D1 > " - "\x1b[1mbar\x1b[0m\n" - "context step=1 > " - "\r\x1b[K" - "\x1b[1;31msame.dat\x1b[0m [5 bytes]\n" - "dbg.jpg > " - "\r\x1b[K" - "\x1b[1;30mdbg.jpg\x1b[0m [image/jpg; 5 bytes]\n" - "\x1b[1;30mdbg\x1b[0m\n" - "\x1b[1;35mwarn\x1b[0m\n", + f.getvalue().split("\r"), + [ + "", + "\x1b[K", + "\x1b[1;34mmy message\x1b[0m\x1b[K\n", + "\x1b[K", + "test.dat > \x1b[K", + "\x1b[K", + "\x1b[1mtest.dat [5 bytes]\x1b[0m\x1b[K\n", + "\x1b[K", + "my context > \x1b[K", + "my context > iter 0 > \x1b[K", + "my context > \x1b[K", + "my context > iter 1 > \x1b[K", + "my context > iter 1 > \x1b[1ma\x1b[0m\x1b[K\n", + "my context > iter 1 > \x1b[K", + "my context > \x1b[K", + "my context > iter 2 > \x1b[K", + "my context > iter 2 > \x1b[1mb\x1b[0m\x1b[K\n", + "my context > iter 2 > \x1b[K", + "my context > \x1b[K", + "my context > iter 3 > \x1b[K", + "my context > iter 3 > \x1b[1mc\x1b[0m\x1b[K\n", + "my context > iter 3 > \x1b[K", + "my context > \x1b[K", + "my context > empty > \x1b[K", + "my context > \x1b[K", + "my context > \x1b[1;31mmultiple..\x1b[0m\n > \x1b[1;31m ..lines\x1b[0m\x1b[K\n", + "my context > \x1b[K", + "my context > test.dat > \x1b[K", + "my context > test.dat > \x1b[1mgenerating\x1b[0m\x1b[K\n", + "my context > test.dat > \x1b[K", + "my context > \x1b[K", + "my context > \x1b[1;34mtest.dat [5 bytes]\x1b[0m\x1b[K\n", + "my context > \x1b[K", + "\x1b[K", + "generate_test > \x1b[K", + "generate_test > test.dat > \x1b[K", + "generate_test > \x1b[K", + "generate_test > \x1b[1;35mtest.dat [5 bytes]\x1b[0m\x1b[K\n", + "generate_test > \x1b[K", + "\x1b[K", + "context step=0 > \x1b[K", + "context step=0 > \x1b[1mfoo\x1b[0m\x1b[K\n", + "context step=0 > \x1b[K", + "\x1b[K", + "context step=1 > \x1b[K", + "context step=1 > \x1b[1mbar\x1b[0m\x1b[K\n", + "context step=1 > \x1b[K", + "\x1b[K", + "\x1b[1;31msame.dat [5 bytes]\x1b[0m\x1b[K\n", + "\x1b[K", + "dbg.jpg > \x1b[K", + "\x1b[K", + "\x1b[1;30mdbg.jpg [image/jpg; 5 bytes]\x1b[0m\x1b[K\n", + "\x1b[K", + "\x1b[1;30mdbg\x1b[0m\x1b[K\n", + "\x1b[K", + "\x1b[1;35mwarn\x1b[0m\x1b[K\n", + "\x1b[K", + ], ) diff --git a/treelog/_richoutput.py b/treelog/_richoutput.py index 71c6348..b3e6363 100644 --- a/treelog/_richoutput.py +++ b/treelog/_richoutput.py @@ -1,10 +1,36 @@ import sys -import typing -from .proto import Level, Data +from .proto import Level, oldproto -class RichOutputLog: +def RichOutputLog(file=sys.stdout): + set_ansi_console() + return _RichOutputLog(file, prefix="", status=Status(file)) + + +class Status: + def __init__(self, file): + self.file = file + self.c = [] + + def add(self, prefix): + self.c.append(prefix) + + def remove(self, prefix): + self.c.remove(prefix) + + def __str__(self): + # later, when we support simultaneously opened contexts, we can replace + # this with something more sophisticated + return max(self.c, default="") + + def print(self): + self.file.write(f"\r{self}\033[K") + self.file.flush() + + +@oldproto.fromnew +class _RichOutputLog: """Output rich (colored,unicode) text to stream.""" _cmap = ( @@ -15,68 +41,29 @@ class RichOutputLog: "\033[1;31m", ) # error: bold red - def __init__(self, file=sys.stdout) -> None: - self._current = "" # currently printed context + def __init__(self, file, prefix, status) -> None: self.file = file - set_ansi_console() - self.currentcontext = [] # type: typing.List[str] - - def pushcontext(self, title: str) -> None: - self.currentcontext.append(title) - self.contextchangedhook() - - def popcontext(self) -> None: - self.currentcontext.pop() - self.contextchangedhook() - - def recontext(self, title: str) -> None: - self.currentcontext[-1] = title - self.contextchangedhook() - - def contextchangedhook(self) -> None: - _current = "".join(item + " > " for item in self.currentcontext) - if _current == self._current: - return - n = first(c1 != c2 for c1, c2 in zip(_current, self._current)) - items = [] - if n == 0 and self._current: - items.append("\r") - elif n < len(self._current): - items.append("\033[{}D".format(len(self._current) - n)) - if n < len(_current): - items.append(_current[n:]) - if len(_current) < len(self._current): - items.append("\033[K") - self.file.write("".join(items)) - self.file.flush() - self._current = _current + self.prefix = prefix + self.status = status + status.add(prefix) + status.print() + + def branch(self, title): + return self.__class__(self.file, self.prefix + title + " > ", self.status) def write(self, msg, level: Level) -> None: - if isinstance(msg, Data): - info = f" [{msg.info}]" - msg = msg.name - else: - info = "" - if self._current and "\n" in msg: + msg = str(msg) + if self.prefix and "\n" in msg: msg = msg.replace( "\n", - "\033[0m\n" + " > ".rjust(len(self._current)) + self._cmap[level.value], - ) - self.file.write( - "".join( - [self._cmap[level.value], msg, "\033[0m", info, "\n", self._current] + "\033[0m\n" + " > ".rjust(len(self.prefix)) + self._cmap[level.value], ) - ) - + self.file.write(f"\r{self.prefix}{self._cmap[level.value]}{msg}\033[0m\033[K\n") + self.status.print() -def first(items: typing.Iterable[bool]) -> int: - "return index of first truthy item, or len(items) of all items are falsy" - i = 0 - for item in items: - if item: - break - i += 1 - return i + def close(self): + self.status.remove(self.prefix) + self.status.print() def set_ansi_console() -> None: From fa0b1cb1d022dddb195d888f3d11aa77d7a47c68 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 11:51:09 +0200 Subject: [PATCH 4/9] Convert LoggingLog to new log protocol --- treelog/_logging.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/treelog/_logging.py b/treelog/_logging.py index 4258506..68f5994 100644 --- a/treelog/_logging.py +++ b/treelog/_logging.py @@ -1,29 +1,27 @@ import logging -import typing -from .proto import Level +from .proto import oldproto -class LoggingLog: - """Log to Python's built-in logging facility.""" +def LoggingLog(name: str = "nutils"): + return _LoggingLog(logging.getLogger(name), prefix="") - # type: typing.ClassVar[typing.Tuple[int, int, int, int, int]] - _levels = logging.DEBUG, logging.INFO, 25, logging.WARNING, logging.ERROR - def __init__(self, name: str = "nutils") -> None: - self._logger = logging.getLogger(name) - self.currentcontext = [] # type: typing.List[str] +@oldproto.fromnew +class _LoggingLog: + """Output plain text to stream.""" + + _levels = logging.DEBUG, logging.INFO, 25, logging.WARNING, logging.ERROR - def pushcontext(self, title: str) -> None: - self.currentcontext.append(title) + def __init__(self, logger, prefix): + self._logger = logger + self._prefix = prefix - def popcontext(self) -> None: - self.currentcontext.pop() + def branch(self, title): + return self.__class__(self._logger, self._prefix + title + " > ") - def recontext(self, title: str) -> None: - self.currentcontext[-1] = title + def write(self, msg, level) -> None: + self._logger.log(self._levels[level.value], self._prefix + str(msg)) - def write(self, msg, level: Level, data: typing.Optional[bytes] = None) -> None: - self._logger.log( - self._levels[level.value], " > ".join((*self.currentcontext, str(msg))) - ) + def close(self): + pass From 19f7df1d843f9d0b3491dda28d4f0eb2e455f6d1 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 11:50:41 +0200 Subject: [PATCH 5/9] Convert DataLog to new log protocol --- treelog/_data.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/treelog/_data.py b/treelog/_data.py index 22ca6cf..4cedfd5 100644 --- a/treelog/_data.py +++ b/treelog/_data.py @@ -3,9 +3,10 @@ import typing from ._path import makedirs, sequence, non_existent -from .proto import Level, Data +from .proto import Level, Data, oldproto +@oldproto.fromnew class DataLog: """Output only data.""" @@ -17,19 +18,16 @@ def __init__( self._names = functools.lru_cache(maxsize=32)(names) self._path = makedirs(dirpath) - def pushcontext(self, title: str) -> None: - pass - - def popcontext(self) -> None: - pass + def branch(self, title: str): + return self - def recontext(self, title: str) -> None: - pass - - def write(self, msg, level: Level) -> None: + def write(self, msg, level: Level): if isinstance(msg, Data): _, f = non_existent( self._path, self._names(msg.name), lambda p: p.open("xb") ) with f: f.write(msg.data) + + def close(self): + pass From 4101ceb566724544bec1d918f390db50d17c5eb1 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 16:08:21 +0200 Subject: [PATCH 6/9] Convert NullLog to new log protocol --- treelog/_null.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/treelog/_null.py b/treelog/_null.py index 249f87d..9191840 100644 --- a/treelog/_null.py +++ b/treelog/_null.py @@ -1,15 +1,13 @@ -from .proto import Level +from .proto import Level, oldproto +@oldproto.fromnew class NullLog: - def pushcontext(self, title: str) -> None: - pass - - def popcontext(self) -> None: - pass + def branch(self, title: str): + return self - def recontext(self, title: str) -> None: + def write(self, msg, level: Level) -> None: pass - def write(self, msg, level: Level) -> None: + def close(self) -> None: pass From a618d0028ee6bb2d8adab43d72511e71e6d0acdd Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 20 May 2025 16:34:55 +0200 Subject: [PATCH 7/9] Convert HtmlLog to new log protocol --- treelog/_html.py | 91 +++++++++++++++++++++++------------------------- treelog/_path.py | 4 +-- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/treelog/_html.py b/treelog/_html.py index fad9d4a..93889c8 100644 --- a/treelog/_html.py +++ b/treelog/_html.py @@ -2,13 +2,11 @@ import html import os import sys -import types import typing import urllib.parse -import warnings from ._path import makedirs, sequence, non_existent -from .proto import Level, Data +from .proto import Level, Data, oldproto class HtmlLog: @@ -23,38 +21,49 @@ def __init__( htmltitle: typing.Optional[str] = None, favicon: typing.Optional[str] = None, ) -> None: - self._path = makedirs(dirpath) - self.filename, self._file = non_existent( - self._path, sequence(filename), lambda p: p.open("x", encoding="utf-8") + self.dirpath = dirpath + self.filename = filename + self.title = title or " ".join(sys.argv) + self.htmltitle = htmltitle or html.escape(self.title) + self.favicon = favicon or FAVICON + + def __enter__(self): + _path = makedirs(self.dirpath) + filename, self._file = non_existent( + _path, sequence(self.filename), lambda p: p.open("x", encoding="utf-8") ) - css = self._write_hash(CSS.encode(), ".css") - js = self._write_hash(JS.encode(), ".js") - if title is None: - title = " ".join(sys.argv) - if htmltitle is None: - htmltitle = html.escape(title) - if favicon is None: - favicon = FAVICON + _dir = _DirPath(_path) self._file.write( HTMLHEAD.format( - title=title, htmltitle=htmltitle, css=css, js=js, favicon=favicon + title=self.title, + htmltitle=self.htmltitle, + css=_dir._write_hash(CSS.encode(), ".css"), + js=_dir._write_hash(JS.encode(), ".js"), + favicon=self.favicon, ) ) - # active contexts that are not yet opened as html elements - self._unopened = [] # type: typing.List[str] + log = oldproto.fromnew(_HtmlBranch)(_dir, self._file, []) + log.filename = filename + return log - def pushcontext(self, title: str) -> None: - self._unopened.append(title) - - def popcontext(self) -> None: - if self._unopened: - self._unopened.pop() + def __exit__(self, *args) -> bool: + if hasattr(self, "_file") and not self._file.closed: + self._file.write(HTMLFOOT) + self._file.close() + return True else: - print('
', file=self._file) + return False + + +class _HtmlBranch: + def __init__(self, dirpath, file, unopened): + self._dir = dirpath + self._file = file + self._unopened = unopened - def recontext(self, title: str) -> None: - self.popcontext() - self.pushcontext(title) + def branch(self, title): + self._unopened.append(title) + return _HtmlBranch(self._dir, self._file, self._unopened) def write(self, msg, level: Level) -> None: for c in self._unopened: @@ -67,7 +76,7 @@ def write(self, msg, level: Level) -> None: self._unopened.clear() if isinstance(msg, Data): _, ext = os.path.splitext(msg.name) - filename = self._write_hash(msg.data, ext) + filename = self._dir._write_hash(msg.data, ext) text = '{name}'.format( href=urllib.parse.quote(filename), name=html.escape(msg.name) ) @@ -79,28 +88,16 @@ def write(self, msg, level: Level) -> None: flush=True, ) - def close(self) -> bool: - if hasattr(self, "_file") and not self._file.closed: - self._file.write(HTMLFOOT) - self._file.close() - return True + def close(self): + if self._unopened: + self._unopened.pop() else: - return False - - def __enter__(self) -> "HtmlLog": - return self + print('
', file=self._file) - def __exit__( - self, - t: typing.Optional[typing.Type[BaseException]], - value: typing.Optional[BaseException], - traceback: typing.Optional[types.TracebackType], - ) -> None: - self.close() - def __del__(self) -> None: - if self.close(): - warnings.warn("unclosed object {!r}".format(self), ResourceWarning) +class _DirPath: + def __init__(self, path): + self._path = path def _write_hash(self, data, ext): filename = hashlib.sha1(data).hexdigest() + ext diff --git a/treelog/_path.py b/treelog/_path.py index ac9da2c..630c9c0 100644 --- a/treelog/_path.py +++ b/treelog/_path.py @@ -7,9 +7,9 @@ supports_fd = os.open in os.supports_dir_fd -def makedirs(*pathsegments): +def makedirs(*pathsegments, exist_ok=True): path = pathlib.Path(*pathsegments) - path.mkdir(parents=True, exist_ok=True) + path.mkdir(parents=True, exist_ok=exist_ok) if not supports_fd: return path dir_fd = os.open(path, flags=os.O_RDONLY) From 6659919d4b4014b4d742430ac834559a43be995d Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 17 Jun 2025 16:12:53 +0200 Subject: [PATCH 8/9] Convert RecordLog to new log protocol --- tests.py | 371 ++++++++++++++++++++++----------------------- treelog/_record.py | 80 ++++------ 2 files changed, 204 insertions(+), 247 deletions(-) diff --git a/tests.py b/tests.py index 010e127..4fd43d1 100644 --- a/tests.py +++ b/tests.py @@ -298,13 +298,11 @@ def test_filename_sequence(self): class RecordLog(unittest.TestCase): - simplify = False - def test_output(self): - recordlog = treelog.RecordLog(simplify=self.simplify) + recordlog = treelog.RecordLog() with treelog.set(recordlog): generate() - self.check_output(recordlog._messages) + self.check_output(recordlog.current) with self.subTest("replay to StdoutLog"): f = io.StringIO() recordlog.replay(treelog.StdoutLog(f)) @@ -316,100 +314,68 @@ def test_output(self): with treelog.HtmlLog(tmpdir, title="test") as htmllog: recordlog.replay(htmllog) HtmlLog.check_output(self, tmpdir, htmllog.filename) - if not self.simplify: - with self.subTest("replay to RichOutputLog"): - f = io.StringIO() - recordlog.replay(treelog.RichOutputLog(f)) - RichOutputLog.check_output(self, f) + with self.subTest("replay to RichOutputLog"): + f = io.StringIO() + recordlog.replay(treelog.RichOutputLog(f)) + RichOutputLog.check_output(self, f) def check_output(self, messages): self.assertEqual( messages, [ - ("write", "my message", Level.user), - ("pushcontext", "test.dat"), - ("popcontext",), - ("write", Data("test.dat", b"test1"), Level.info), - ("pushcontext", "my context"), - ("pushcontext", "iter 0"), - ("recontext", "iter 1"), - ("write", "a", Level.info), - ("recontext", "iter 2"), - ("write", "b", Level.info), - ("recontext", "iter 3"), - ("write", "c", Level.info), - ("popcontext",), - ("pushcontext", "empty"), - ("popcontext",), - ("write", "multiple..\n ..lines", Level.error), - ("pushcontext", "test.dat"), - ("write", "generating", Level.info), - ("popcontext",), - ("write", Data("test.dat", b"test2"), Level.user), - ("popcontext",), - ("pushcontext", "generate_test"), - ("pushcontext", "test.dat"), - ("popcontext",), - ("write", Data("test.dat", b"test3"), Level.warning), - ("popcontext",), - ("pushcontext", "context step=0"), - ("write", "foo", Level.info), - ("recontext", "context step=1"), - ("write", "bar", Level.info), - ("popcontext",), - ("write", Data("same.dat", b"test3"), Level.error), - ("pushcontext", "dbg.jpg"), - ("popcontext",), - ("write", Data("dbg.jpg", b"test4", type="image/jpg"), Level.debug), - ("write", "dbg", Level.debug), - ("write", "warn", Level.warning), + ("my message", Level.user), + ("test.dat", []), + (Data(name="test.dat", data=b"test1", type=None), Level.info), + ( + "my context", + [ + ("iter 0", []), + ("iter 1", [("a", Level.info)]), + ("iter 2", [("b", Level.info)]), + ("iter 3", [("c", Level.info)]), + ("empty", []), + ("multiple..\n ..lines", Level.error), + ("test.dat", [("generating", Level.info)]), + (Data(name="test.dat", data=b"test2", type=None), Level.user), + ], + ), + ( + "generate_test", + [ + ("test.dat", []), + ( + Data(name="test.dat", data=b"test3", type=None), + Level.warning, + ), + ], + ), + ( + "context step=0", + [ + ("foo", Level.info), + ], + ), + ( + "context step=1", + [ + ("bar", Level.info), + ], + ), + (Data(name="same.dat", data=b"test3", type=None), Level.error), + ("dbg.jpg", []), + (Data(name="dbg.jpg", data=b"test4", type="image/jpg"), Level.debug), + ("dbg", Level.debug), + ("warn", Level.warning), ], ) def test_replay_in_current(self): - recordlog = treelog.RecordLog(simplify=self.simplify) + recordlog = treelog.RecordLog() recordlog.write("test", level=Level.info) with treelog.set(treelog.LoggingLog()), self.assertLogs("nutils"): recordlog.replay() -class SimplifiedRecordLog(RecordLog): - simplify = True - - def check_output(self, messages): - self.assertEqual( - messages, - [ - ("write", "my message", Level.user), - ("write", Data("test.dat", b"test1"), Level.info), - ("pushcontext", "my context"), - ("pushcontext", "iter 1"), - ("write", "a", Level.info), - ("recontext", "iter 2"), - ("write", "b", Level.info), - ("recontext", "iter 3"), - ("write", "c", Level.info), - ("popcontext",), - ("write", "multiple..\n ..lines", Level.error), - ("pushcontext", "test.dat"), - ("write", "generating", Level.info), - ("popcontext",), - ("write", Data("test.dat", b"test2"), Level.user), - ("recontext", "generate_test"), - ("write", Data("test.dat", b"test3"), Level.warning), - ("recontext", "context step=0"), - ("write", "foo", Level.info), - ("recontext", "context step=1"), - ("write", "bar", Level.info), - ("popcontext",), - ("write", Data("same.dat", b"test3"), Level.error), - ("write", Data("dbg.jpg", b"test4", type="image/jpg"), Level.debug), - ("write", "dbg", Level.debug), - ("write", "warn", Level.warning), - ], - ) - - class TeeLog(unittest.TestCase): def test_output(self): f = io.StringIO() @@ -424,7 +390,7 @@ def test_output(self): with self.subTest("DataLog"): DataLog.check_output(self, tmpdir) with self.subTest("RecordLog"): - RecordLog.check_output(self, recordlog._messages) + RecordLog.check_output(self, recordlog.current) with self.subTest("RichOutputLog"): RichOutputLog.check_output(self, f) @@ -443,21 +409,42 @@ def test_output(self): recordlog = treelog.RecordLog() with treelog.set(treelog.FilterLog(recordlog, minlevel=Level.user)): generate() - self.check_output(recordlog._messages) + self.check_output(recordlog.current) def check_output(self, messages): self.assertEqual( messages, [ - ("write", "my message", Level.user), - ("pushcontext", "my context"), - ("write", "multiple..\n ..lines", Level.error), - ("write", Data("test.dat", b"test2"), Level.user), - ("recontext", "generate_test"), - ("write", Data("test.dat", b"test3"), Level.warning), - ("popcontext",), - ("write", Data("same.dat", b"test3"), Level.error), - ("write", "warn", Level.warning), + ("my message", Level.user), + ("test.dat", []), + ( + "my context", + [ + ("iter 0", []), + ("iter 1", []), + ("iter 2", []), + ("iter 3", []), + ("empty", []), + ("multiple..\n ..lines", Level.error), + ("test.dat", []), + (Data(name="test.dat", data=b"test2", type=None), Level.user), + ], + ), + ( + "generate_test", + [ + ("test.dat", []), + ( + Data(name="test.dat", data=b"test3", type=None), + Level.warning, + ), + ], + ), + ("context step=0", []), + ("context step=1", []), + (Data(name="same.dat", data=b"test3", type=None), Level.error), + ("dbg.jpg", []), + ("warn", Level.warning), ], ) @@ -467,32 +454,48 @@ def test_output(self): recordlog = treelog.RecordLog() with treelog.set(treelog.FilterLog(recordlog, maxlevel=Level.user)): generate() - self.check_output(recordlog._messages) + self.check_output(recordlog.current) def check_output(self, messages): self.assertEqual( messages, [ - ("write", "my message", Level.user), - ("write", Data("test.dat", b"test1"), Level.info), - ("pushcontext", "my context"), - ("pushcontext", "iter 1"), - ("write", "a", Level.info), - ("recontext", "iter 2"), - ("write", "b", Level.info), - ("recontext", "iter 3"), - ("write", "c", Level.info), - ("recontext", "test.dat"), - ("write", "generating", Level.info), - ("popcontext",), - ("write", Data("test.dat", b"test2"), Level.user), - ("recontext", "context step=0"), - ("write", "foo", Level.info), - ("recontext", "context step=1"), - ("write", "bar", Level.info), - ("popcontext",), - ("write", Data("dbg.jpg", b"test4", type="image/jpg"), Level.debug), - ("write", "dbg", Level.debug), + ("my message", Level.user), + ("test.dat", []), + (Data(name="test.dat", data=b"test1", type=None), Level.info), + ( + "my context", + [ + ("iter 0", []), + ("iter 1", [("a", Level.info)]), + ("iter 2", [("b", Level.info)]), + ("iter 3", [("c", Level.info)]), + ("empty", []), + ("test.dat", [("generating", Level.info)]), + (Data(name="test.dat", data=b"test2", type=None), Level.user), + ], + ), + ( + "generate_test", + [ + ("test.dat", []), + ], + ), + ( + "context step=0", + [ + ("foo", Level.info), + ], + ), + ( + "context step=1", + [ + ("bar", Level.info), + ], + ), + ("dbg.jpg", []), + (Data(name="dbg.jpg", data=b"test4", type="image/jpg"), Level.debug), + ("dbg", Level.debug), ], ) @@ -504,33 +507,51 @@ def test_output(self): treelog.FilterLog(recordlog, minlevel=Level.info, maxlevel=Level.warning) ): generate() - self.check_output(recordlog._messages) + self.check_output(recordlog.current) def check_output(self, messages): self.assertEqual( messages, [ - ("write", "my message", Level.user), - ("write", Data("test.dat", b"test1"), Level.info), - ("pushcontext", "my context"), - ("pushcontext", "iter 1"), - ("write", "a", Level.info), - ("recontext", "iter 2"), - ("write", "b", Level.info), - ("recontext", "iter 3"), - ("write", "c", Level.info), - ("recontext", "test.dat"), - ("write", "generating", Level.info), - ("popcontext",), - ("write", Data("test.dat", b"test2"), Level.user), - ("recontext", "generate_test"), - ("write", Data("test.dat", b"test3"), Level.warning), - ("recontext", "context step=0"), - ("write", "foo", Level.info), - ("recontext", "context step=1"), - ("write", "bar", Level.info), - ("popcontext",), - ("write", "warn", Level.warning), + ("my message", Level.user), + ("test.dat", []), + (Data(name="test.dat", data=b"test1", type=None), Level.info), + ( + "my context", + [ + ("iter 0", []), + ("iter 1", [("a", Level.info)]), + ("iter 2", [("b", Level.info)]), + ("iter 3", [("c", Level.info)]), + ("empty", []), + ("test.dat", [("generating", Level.info)]), + (Data(name="test.dat", data=b"test2", type=None), Level.user), + ], + ), + ( + "generate_test", + [ + ("test.dat", []), + ( + Data(name="test.dat", data=b"test3", type=None), + Level.warning, + ), + ], + ), + ( + "context step=0", + [ + ("foo", Level.info), + ], + ), + ( + "context step=1", + [ + ("bar", Level.info), + ], + ), + ("dbg.jpg", []), + ("warn", Level.warning), ], ) @@ -580,7 +601,7 @@ def setUp(self): self.addCleanup(c.__exit__, None, None, None) def assertMessages(self, *msg): - self.assertEqual(self.recordlog._messages, list(msg)) + self.assertEqual(self.recordlog.current, list(msg)) def test_context(self): with treelog.iter.plain("test", enumerate("abc")) as myiter: @@ -588,14 +609,10 @@ def test_context(self): self.assertEqual(c, "abc"[i]) treelog.info("hi") self.assertMessages( - ("pushcontext", "test 0"), - ("recontext", "test 1"), - ("write", "hi", Level.info), - ("recontext", "test 2"), - ("write", "hi", Level.info), - ("recontext", "test 3"), - ("write", "hi", Level.info), - ("popcontext",), + ("test 0", []), + ("test 1", [("hi", Level.info)]), + ("test 2", [("hi", Level.info)]), + ("test 3", [("hi", Level.info)]), ) def test_nocontext(self): @@ -603,14 +620,10 @@ def test_nocontext(self): self.assertEqual(c, "abc"[i]) treelog.info("hi") self.assertMessages( - ("pushcontext", "test 0"), - ("recontext", "test 1"), - ("write", "hi", Level.info), - ("recontext", "test 2"), - ("write", "hi", Level.info), - ("recontext", "test 3"), - ("write", "hi", Level.info), - ("popcontext",), + ("test 0", []), + ("test 1", [("hi", Level.info)]), + ("test 2", [("hi", Level.info)]), + ("test 3", [("hi", Level.info)]), ) def test_break_entered(self): @@ -624,12 +637,7 @@ def test_break_entered(self): break gc.collect() self.assertEqual(w, []) - self.assertMessages( - ("pushcontext", "test 0"), - ("recontext", "test 1"), - ("write", "hi", Level.info), - ("popcontext",), - ) + self.assertMessages(("test 0", []), ("test 1", [("hi", Level.info)])) def test_break_notentered(self): with self.assertWarns(ResourceWarning): @@ -638,12 +646,7 @@ def test_break_notentered(self): treelog.info("hi") break gc.collect() - self.assertMessages( - ("pushcontext", "test 0"), - ("recontext", "test 1"), - ("write", "hi", Level.info), - ("popcontext",), - ) + self.assertMessages(("test 0", []), ("test 1", [("hi", Level.info)])) def test_multiple(self): with treelog.iter.plain("test", "abc", [1, 2]) as items: @@ -653,44 +656,28 @@ def test_plain(self): with treelog.iter.plain("test", "abc") as items: self.assertEqual(list(items), list("abc")) self.assertMessages( - ("pushcontext", "test 0"), - ("recontext", "test 1"), - ("recontext", "test 2"), - ("recontext", "test 3"), - ("popcontext",), + ("test 0", []), ("test 1", []), ("test 2", []), ("test 3", []) ) def test_plain_withbraces(self): with treelog.iter.plain("t{es}t", "abc") as items: self.assertEqual(list(items), list("abc")) self.assertMessages( - ("pushcontext", "t{es}t 0"), - ("recontext", "t{es}t 1"), - ("recontext", "t{es}t 2"), - ("recontext", "t{es}t 3"), - ("popcontext",), + ("t{es}t 0", []), ("t{es}t 1", []), ("t{es}t 2", []), ("t{es}t 3", []) ) def test_fraction(self): with treelog.iter.fraction("test", "abc") as items: self.assertEqual(list(items), list("abc")) self.assertMessages( - ("pushcontext", "test 0/3"), - ("recontext", "test 1/3"), - ("recontext", "test 2/3"), - ("recontext", "test 3/3"), - ("popcontext",), + ("test 0/3", []), ("test 1/3", []), ("test 2/3", []), ("test 3/3", []) ) def test_percentage(self): with treelog.iter.percentage("test", "abc") as items: self.assertEqual(list(items), list("abc")) self.assertMessages( - ("pushcontext", "test 0%"), - ("recontext", "test 33%"), - ("recontext", "test 67%"), - ("recontext", "test 100%"), - ("popcontext",), + ("test 0%", []), ("test 33%", []), ("test 67%", []), ("test 100%", []) ) def test_send(self): @@ -704,12 +691,10 @@ def titles(): self.assertEqual(item, "abc"[i]) treelog.info("hi") self.assertMessages( - ("pushcontext", "value"), - ("recontext", "value='a'"), - ("recontext", "value='b'"), - ("recontext", "value='c'"), - ("write", "hi", Level.info), - ("popcontext",), + ("value", []), + ("value='a'", []), + ("value='b'", []), + ("value='c'", [("hi", Level.info)]), ) diff --git a/treelog/_record.py b/treelog/_record.py index 07d9fd7..9a39e96 100644 --- a/treelog/_record.py +++ b/treelog/_record.py @@ -1,12 +1,17 @@ import typing -from .proto import Level, Log +from .proto import Level, Log, oldproto -class RecordLog: - """Record log messages. +def RecordLog(simplify: bool = True): + return _RecordLog() - The recorded messages can be replayed to the logs that are currently active + +@oldproto.fromnew +class _RecordLog(list): + """Record log events. + + The recorded events can be replayed to the logs that are currently active by :meth:`replay`. Typical usage is caching expensive operations: >>> import treelog, pickle @@ -26,59 +31,26 @@ class RecordLog: Exceptions raised while in a :meth:`Log.context` are not recorded. """ - def __init__(self, simplify: bool = True): - # Replayable log messages. Each entry is a tuple of `(cmd, *args)`, where - # `cmd` is either 'pushcontext', 'popcontext', 'open', - # 'close' or 'write'. See `self.replay` below. - self._simplify = simplify - self._messages = [] # type: typing.List[typing.Any] - self._fid = 0 # internal file counter - - def pushcontext(self, title: str) -> None: - if self._simplify and self._messages and self._messages[-1][0] == "popcontext": - self._messages[-1] = "recontext", title - else: - self._messages.append(("pushcontext", title)) + def branch(self, title: str): + ctx = self.__class__() + self.append((title, ctx)) + return ctx - def recontext(self, title: str) -> None: - if ( - self._simplify - and self._messages - and self._messages[-1][0] in ("pushcontext", "recontext") - ): - self._messages[-1] = self._messages[-1][0], title - else: - self._messages.append(("recontext", title)) + def write(self, msg, level: Level): + self.append((msg, level)) - def popcontext(self) -> None: - if ( - not self._simplify - or not self._messages - or self._messages[-1][0] not in ("pushcontext", "recontext") - or self._messages.pop()[0] == "recontext" - ): - self._messages.append(("popcontext",)) - - def write(self, msg, level: Level) -> None: - self._messages.append(("write", msg, level)) + def close(self): + pass def replay(self, log: typing.Optional[Log] = None) -> None: - """Replay this recorded log. - - All recorded messages and files will be written to the log that is either - directly specified or currently active.""" - if log is None: from ._state import current as log - for cmd, *args in self._messages: - if cmd == "pushcontext": - (title,) = args - log.pushcontext(title) - elif cmd == "recontext": - (title,) = args - log.recontext(title) - elif cmd == "popcontext": - log.popcontext() - elif cmd == "write": - msg, level = args - log.write(msg, level) + for text, arg in self: + if isinstance(arg, Level): + log.write(text, arg) + elif isinstance(arg, self.__class__): + ctx = log.branch(text) + arg.replay(ctx) + ctx.close() + else: + raise ValueError(arg) From 7458e3068050927977126fb96b6fef76b750b230 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Tue, 17 Jun 2025 16:42:54 +0200 Subject: [PATCH 9/9] Switch to new log protocol --- tests.py | 12 +++++----- treelog/_data.py | 3 +-- treelog/_filter.py | 13 +++++------ treelog/_html.py | 4 ++-- treelog/_logging.py | 5 +---- treelog/_null.py | 3 +-- treelog/_record.py | 7 +++--- treelog/_richoutput.py | 5 ++--- treelog/_state.py | 28 ++++++++++++++++++----- treelog/_stdout.py | 5 ++--- treelog/_tee.py | 17 +++++--------- treelog/iter.py | 10 ++++++--- treelog/proto.py | 50 ++++-------------------------------------- 13 files changed, 62 insertions(+), 100 deletions(-) diff --git a/tests.py b/tests.py index 4fd43d1..607eace 100644 --- a/tests.py +++ b/tests.py @@ -302,7 +302,7 @@ def test_output(self): recordlog = treelog.RecordLog() with treelog.set(recordlog): generate() - self.check_output(recordlog.current) + self.check_output(recordlog) with self.subTest("replay to StdoutLog"): f = io.StringIO() recordlog.replay(treelog.StdoutLog(f)) @@ -390,7 +390,7 @@ def test_output(self): with self.subTest("DataLog"): DataLog.check_output(self, tmpdir) with self.subTest("RecordLog"): - RecordLog.check_output(self, recordlog.current) + RecordLog.check_output(self, recordlog) with self.subTest("RichOutputLog"): RichOutputLog.check_output(self, f) @@ -409,7 +409,7 @@ def test_output(self): recordlog = treelog.RecordLog() with treelog.set(treelog.FilterLog(recordlog, minlevel=Level.user)): generate() - self.check_output(recordlog.current) + self.check_output(recordlog) def check_output(self, messages): self.assertEqual( @@ -454,7 +454,7 @@ def test_output(self): recordlog = treelog.RecordLog() with treelog.set(treelog.FilterLog(recordlog, maxlevel=Level.user)): generate() - self.check_output(recordlog.current) + self.check_output(recordlog) def check_output(self, messages): self.assertEqual( @@ -507,7 +507,7 @@ def test_output(self): treelog.FilterLog(recordlog, minlevel=Level.info, maxlevel=Level.warning) ): generate() - self.check_output(recordlog.current) + self.check_output(recordlog) def check_output(self, messages): self.assertEqual( @@ -601,7 +601,7 @@ def setUp(self): self.addCleanup(c.__exit__, None, None, None) def assertMessages(self, *msg): - self.assertEqual(self.recordlog.current, list(msg)) + self.assertEqual(self.recordlog, list(msg)) def test_context(self): with treelog.iter.plain("test", enumerate("abc")) as myiter: diff --git a/treelog/_data.py b/treelog/_data.py index 4cedfd5..081b500 100644 --- a/treelog/_data.py +++ b/treelog/_data.py @@ -3,10 +3,9 @@ import typing from ._path import makedirs, sequence, non_existent -from .proto import Level, Data, oldproto +from .proto import Level, Data -@oldproto.fromnew class DataLog: """Output only data.""" diff --git a/treelog/_filter.py b/treelog/_filter.py index 76b4661..0feecda 100644 --- a/treelog/_filter.py +++ b/treelog/_filter.py @@ -16,14 +16,8 @@ def __init__( self._minlevel = minlevel self._maxlevel = maxlevel - def pushcontext(self, title: str) -> None: - self._baselog.pushcontext(title) - - def popcontext(self) -> None: - self._baselog.popcontext() - - def recontext(self, title: str) -> None: - self._baselog.recontext(title) + def branch(self, title: str) -> None: + return FilterLog(self._baselog.branch(title), self._minlevel, self._maxlevel) def _passthrough(self, level: Level) -> bool: """Return True if messages of the given level should pass through.""" @@ -36,3 +30,6 @@ def _passthrough(self, level: Level) -> bool: def write(self, msg, level: Level) -> None: if self._passthrough(level): self._baselog.write(msg, level) + + def close(self) -> None: + self._baselog.close() diff --git a/treelog/_html.py b/treelog/_html.py index 93889c8..0a6077d 100644 --- a/treelog/_html.py +++ b/treelog/_html.py @@ -6,7 +6,7 @@ import urllib.parse from ._path import makedirs, sequence, non_existent -from .proto import Level, Data, oldproto +from .proto import Level, Data class HtmlLog: @@ -42,7 +42,7 @@ def __enter__(self): favicon=self.favicon, ) ) - log = oldproto.fromnew(_HtmlBranch)(_dir, self._file, []) + log = _HtmlBranch(_dir, self._file, []) log.filename = filename return log diff --git a/treelog/_logging.py b/treelog/_logging.py index 68f5994..827c487 100644 --- a/treelog/_logging.py +++ b/treelog/_logging.py @@ -1,13 +1,10 @@ import logging -from .proto import oldproto - def LoggingLog(name: str = "nutils"): return _LoggingLog(logging.getLogger(name), prefix="") -@oldproto.fromnew class _LoggingLog: """Output plain text to stream.""" @@ -18,7 +15,7 @@ def __init__(self, logger, prefix): self._prefix = prefix def branch(self, title): - return self.__class__(self._logger, self._prefix + title + " > ") + return _LoggingLog(self._logger, self._prefix + title + " > ") def write(self, msg, level) -> None: self._logger.log(self._levels[level.value], self._prefix + str(msg)) diff --git a/treelog/_null.py b/treelog/_null.py index 9191840..0b0e527 100644 --- a/treelog/_null.py +++ b/treelog/_null.py @@ -1,7 +1,6 @@ -from .proto import Level, oldproto +from .proto import Level -@oldproto.fromnew class NullLog: def branch(self, title: str): return self diff --git a/treelog/_record.py b/treelog/_record.py index 9a39e96..7c03386 100644 --- a/treelog/_record.py +++ b/treelog/_record.py @@ -1,13 +1,12 @@ import typing -from .proto import Level, Log, oldproto +from .proto import Level, Log def RecordLog(simplify: bool = True): return _RecordLog() -@oldproto.fromnew class _RecordLog(list): """Record log events. @@ -32,7 +31,7 @@ class _RecordLog(list): """ def branch(self, title: str): - ctx = self.__class__() + ctx = _RecordLog() self.append((title, ctx)) return ctx @@ -48,7 +47,7 @@ def replay(self, log: typing.Optional[Log] = None) -> None: for text, arg in self: if isinstance(arg, Level): log.write(text, arg) - elif isinstance(arg, self.__class__): + elif isinstance(arg, _RecordLog): ctx = log.branch(text) arg.replay(ctx) ctx.close() diff --git a/treelog/_richoutput.py b/treelog/_richoutput.py index b3e6363..daf008a 100644 --- a/treelog/_richoutput.py +++ b/treelog/_richoutput.py @@ -1,6 +1,6 @@ import sys -from .proto import Level, oldproto +from .proto import Level def RichOutputLog(file=sys.stdout): @@ -29,7 +29,6 @@ def print(self): self.file.flush() -@oldproto.fromnew class _RichOutputLog: """Output rich (colored,unicode) text to stream.""" @@ -49,7 +48,7 @@ def __init__(self, file, prefix, status) -> None: status.print() def branch(self, title): - return self.__class__(self.file, self.prefix + title + " > ", self.status) + return _RichOutputLog(self.file, self.prefix + title + " > ", self.status) def write(self, msg, level: Level) -> None: msg = str(msg) diff --git a/treelog/_state.py b/treelog/_state.py index 82bcde5..583b859 100644 --- a/treelog/_state.py +++ b/treelog/_state.py @@ -50,21 +50,29 @@ def context( given the title is used as a format string, and a callable is returned that allows for recontextualization from within the current with-block.""" + global current log = current if initargs or initkwargs: format = title.format + # type: typing.Optional[typing.Callable[..., None]] def reformat(*args, **kwargs): - log.recontext(format(*args, **kwargs)) + global current + nonlocal context + context.close() + context = log.branch(format(*args, **kwargs)) + current = context title = title.format(*initargs, **initkwargs) else: reformat = None - log.pushcontext(title) + context = log.branch(title) try: + current = context yield reformat finally: - log.popcontext() + current = log + context.close() T = typing.TypeVar("T") @@ -111,11 +119,19 @@ def file(level: Level, name: str, mode: str, type: typing.Optional[str] = None): binary = False else: raise ValueError(f"invalid mode {mode!r}") - with tempfile.TemporaryFile() as f, context(name): - yield f if binary else io.TextIOWrapper(f, write_through=True) + global current + log = current + with tempfile.TemporaryFile() as f: + context = log.branch(name) + try: + current = context + yield f if binary else io.TextIOWrapper(f, write_through=True) + finally: + current = log + context.close() f.seek(0) data = f.read() - current.write(Data(name, data, type), level) + log.write(Data(name, data, type), level) def data(level: Level, name: str, data: bytes, type: typing.Optional[str] = None): diff --git a/treelog/_stdout.py b/treelog/_stdout.py index e1257cb..9e64af9 100644 --- a/treelog/_stdout.py +++ b/treelog/_stdout.py @@ -1,9 +1,8 @@ import sys -from .proto import Level, oldproto +from .proto import Level -@oldproto.fromnew class StdoutLog: """Output plain text to stream.""" @@ -12,7 +11,7 @@ def __init__(self, file=sys.stdout, prefix=""): self.prefix = prefix def branch(self, title): - return self.__class__(self.file, self.prefix + title + " > ") + return StdoutLog(self.file, self.prefix + title + " > ") def write(self, msg, level: Level) -> None: if self.prefix: diff --git a/treelog/_tee.py b/treelog/_tee.py index b237ce3..a131504 100644 --- a/treelog/_tee.py +++ b/treelog/_tee.py @@ -8,18 +8,13 @@ def __init__(self, baselog1: Log, baselog2: Log) -> None: self._baselog1 = baselog1 self._baselog2 = baselog2 - def pushcontext(self, title: str) -> None: - self._baselog1.pushcontext(title) - self._baselog2.pushcontext(title) - - def popcontext(self) -> None: - self._baselog1.popcontext() - self._baselog2.popcontext() - - def recontext(self, title: str) -> None: - self._baselog1.recontext(title) - self._baselog2.recontext(title) + def branch(self, title: str) -> None: + return TeeLog(self._baselog1.branch(title), self._baselog2.branch(title)) def write(self, msg, level: Level) -> None: self._baselog1.write(msg, level) self._baselog2.write(msg, level) + + def close(self) -> None: + self._baselog1.close() + self._baselog2.close() diff --git a/treelog/iter.py b/treelog/iter.py index 654830d..4f6f70d 100644 --- a/treelog/iter.py +++ b/treelog/iter.py @@ -40,20 +40,23 @@ def __enter__(self) -> typing.Iterator[T]: if self._log is not None: raise Exception("iter.wrap is not reentrant") self._log = _state.current - self._log.pushcontext(next(self._titles)) + self._context = self._log.branch(next(self._titles)) + _state.current = self._context return iter(self) def __iter__(self) -> typing.Generator[T, None, None]: if self._log is not None: cansend = inspect.isgenerator(self._titles) for value in self._iterable: - self._log.recontext( + self._context.close() + self._context = self._log.branch( typing.cast(typing.Generator[str, T, None], self._titles).send( value ) if cansend else next(self._titles) ) + _state.current = self._context yield value else: with self: @@ -70,7 +73,8 @@ def __exit__( raise Exception("iter.wrap has not yet been entered") if self._warn and exctype is GeneratorExit: warnings.warn("unclosed iter.wrap", ResourceWarning) - self._log.popcontext() + _state.current = self._log + self._context.close() self._log = None diff --git a/treelog/proto.py b/treelog/proto.py index 0eba351..4ca5287 100644 --- a/treelog/proto.py +++ b/treelog/proto.py @@ -17,56 +17,14 @@ class Data: data: bytes type: Optional[str] = None - @property - def info(self): + def __str__(self): info = f"{len(self.data)} bytes" if self.type: info = f"{self.type}; {info}" - return info - - def __str__(self): - return f"{self.name} [{self.info}]" + return f"{self.name} [{info}]" class Log(Protocol): - def pushcontext(self, title: str) -> None: ... - def popcontext(self) -> None: ... - def recontext(self, title: str) -> None: ... + def branch(self, title: str) -> "Log": ... def write(self, msg: Union[str, Data], level: Level) -> None: ... - - -# TRANSITIONAL, TEMPORARY -class oldproto: - @classmethod - def fromnew(cls, NewLog): - return type(NewLog.__name__, (cls,), {"wrapped": NewLog}) - - def __init__(self, *args, **kwargs): - self.context = [self.wrapped(*args, **kwargs)] - - @property - def current(self): - return self.context[-1] - - def pushcontext(self, title): - self.context.append(self.current.branch(title)) - - def popcontext(self): - self.context.pop().close() - - def recontext(self, title): - self.popcontext() - self.pushcontext(title) - - def write(self, msg, level): - self.current.write(msg, level) - - def __enter__(self): - return self - - def __exit__(self, *args): - self.context.pop().close() - assert not self.context - - def __getattr__(self, attr): - return getattr(self.context[0], attr) + def close(self) -> None: ...