Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
469 changes: 239 additions & 230 deletions tests.py

Large diffs are not rendered by default.

15 changes: 6 additions & 9 deletions treelog/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,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
13 changes: 5 additions & 8 deletions treelog/_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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()
89 changes: 43 additions & 46 deletions treelog/_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
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
Expand All @@ -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 = _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('</div><div class="end"></div></div>', 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:
Expand All @@ -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 = '<a href="{href}" download="{name}">{name}</a>'.format(
href=urllib.parse.quote(filename), name=html.escape(msg.name)
)
Expand All @@ -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('</div><div class="end"></div></div>', 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
Expand Down
33 changes: 14 additions & 19 deletions treelog/_logging.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import logging
import typing

from .proto import Level

def LoggingLog(name: str = "nutils"):
return _LoggingLog(logging.getLogger(name), prefix="")

class LoggingLog:
"""Log to Python's built-in logging facility."""

# type: typing.ClassVar[typing.Tuple[int, int, int, int, int]]
_levels = logging.DEBUG, logging.INFO, 25, logging.WARNING, logging.ERROR
class _LoggingLog:
"""Output plain text to stream."""

def __init__(self, name: str = "nutils") -> None:
self._logger = logging.getLogger(name)
self.currentcontext = [] # type: typing.List[str]
_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 _LoggingLog(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
11 changes: 4 additions & 7 deletions treelog/_null.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@


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
4 changes: 2 additions & 2 deletions treelog/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
77 changes: 24 additions & 53 deletions treelog/_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
from .proto import Level, Log


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

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
Expand All @@ -26,59 +30,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 = _RecordLog()
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, _RecordLog):
ctx = log.branch(text)
arg.replay(ctx)
ctx.close()
else:
raise ValueError(arg)
Loading
Loading