Skip to content
Merged
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
608 changes: 277 additions & 331 deletions tests.py

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions treelog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,9 @@
'NullLog',
'RecordLog',
'RichOutputLog',
'StderrLog',
'StdoutLog',
'TeeLog',
}
_legacy = {
'version': __version__,
'Log': None,
}

def __dir__():
return (
Expand All @@ -44,7 +39,6 @@ def __dir__():
*_state_attrs,
*_state_funcs,
*_log_objs,
*_legacy,
)

def __getattr__(attr):
Expand All @@ -59,8 +53,6 @@ def __getattr__(attr):
obj = getattr(m, attr)
elif attr in _sub_mods:
obj = import_module(f'.{attr}', 'treelog')
elif attr in _legacy:
obj = _legacy[attr]
else:
raise AttributeError(attr)
globals()[attr] = obj
Expand Down
28 changes: 0 additions & 28 deletions treelog/_context.py

This file was deleted.

11 changes: 2 additions & 9 deletions treelog/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import typing

from ._path import makedirs, sequence
from ._path import makedirs, sequence, non_existent
from .proto import Level, Data


Expand All @@ -24,13 +24,6 @@ def recontext(self, title: str) -> None:

def write(self, msg, level: Level) -> None:
if isinstance(msg, Data):
for filename in self._names(msg.name):
try:
f = (self._path / filename).open('xb')
except FileExistsError:
continue
break
else:
raise ValueError('all filenames are in use')
_, f = non_existent(self._path, self._names(msg.name), lambda p: p.open('xb'))
with f:
f.write(msg.data)
11 changes: 2 additions & 9 deletions treelog/_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import urllib.parse
import warnings

from ._path import makedirs, sequence
from ._path import makedirs, sequence, non_existent
from .proto import Level, Data


Expand All @@ -17,14 +17,7 @@ class HtmlLog:

def __init__(self, dirpath: str, *, filename: str = 'log.html', title: typing.Optional[str] = None, htmltitle: typing.Optional[str] = None, favicon: typing.Optional[str] = None) -> None:
self._path = makedirs(dirpath)
for self.filename in sequence(filename):
try:
self._file = (self._path / self.filename).open('x', encoding='utf-8')
except FileExistsError:
continue
break
else:
raise ValueError('all filenames are in use')
self.filename, self._file = non_existent(self._path, sequence(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:
Expand Down
14 changes: 11 additions & 3 deletions treelog/_logging.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import logging
import typing

from ._context import ContextLog
from .proto import Level


class LoggingLog(ContextLog):
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

def __init__(self, name: str = 'nutils') -> None:
self._logger = logging.getLogger(name)
super().__init__()
self.currentcontext = [] # type: typing.List[str]

def pushcontext(self, title: str) -> None:
self.currentcontext.append(title)

def popcontext(self) -> None:
self.currentcontext.pop()

def recontext(self, title: str) -> None:
self.currentcontext[-1] = title

def write(self, msg, level: Level, data: typing.Optional[bytes] = None) -> None:
self._logger.log(self._levels[level.value], ' > '.join(
Expand Down
17 changes: 17 additions & 0 deletions treelog/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ def sequence(filename: str) -> typing.Generator[str, None, None]:
i += 1


def non_existent(path, names, f):
if isinstance(path, str):
path = pathlib.Path(path)
for name in names:
try:
return name, f(path / name)
except FileExistsError:
pass
except PermissionError:
# On Windows, trying to open a path that exists as a directory
# triggers a permission error. To avoid a runaway iteration, we
# continue to the next name only if the path indeed exists.
if not isinstance(path, pathlib.Path) or not (path / name).exists():
raise
raise Exception('names exhausted')


class _FDDirPath:

def __init__(self, dir_fd: int) -> None:
Expand Down
1 change: 0 additions & 1 deletion treelog/_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def replay(self, log: typing.Optional[Log] = None) -> None:
All recorded messages and files will be written to the log that is either
directly specified or currently active.'''

files = {}
if log is None:
from ._state import current as log
for cmd, *args in self._messages:
Expand Down
31 changes: 23 additions & 8 deletions treelog/_richoutput.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import sys
import typing

from ._context import ContextLog
from .proto import Level


class RichOutputLog(ContextLog):
class RichOutputLog:
'''Output rich (colored,unicode) text to stream.'''

_cmap = (
Expand All @@ -15,10 +14,23 @@ class RichOutputLog(ContextLog):
'\033[1;35m', # warning: bold purple
'\033[1;31m') # error: bold red

def __init__(self) -> None:
super().__init__()
def __init__(self, file=sys.stdout) -> None:
self._current = '' # currently printed context
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)
Expand All @@ -34,13 +46,16 @@ def contextchangedhook(self) -> None:
items.append(_current[n:])
if len(_current) < len(self._current):
items.append('\033[K')
sys.stdout.write(''.join(items))
sys.stdout.flush()
self.file.write(''.join(items))
self.file.flush()
self._current = _current

def write(self, msg, level: Level) -> None:
sys.stdout.write(
''.join([self._cmap[level.value], str(msg), '\033[0m\n', self._current]))
msg = str(msg)
if self._current 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\n', self._current]))


def first(items: typing.Iterable[bool]) -> int:
Expand Down
11 changes: 0 additions & 11 deletions treelog/_stderr.py

This file was deleted.

23 changes: 20 additions & 3 deletions treelog/_stdout.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import sys

from . import proto
from ._context import ContextLog


class StdoutLog(ContextLog):
class StdoutLog:
'''Output plain text to stream.'''

def __init__(self, file=sys.stdout):
self.file = file
self.currentcontext = [] # type: typing.List[str]

def pushcontext(self, title: str) -> None:
self.currentcontext.append(title + ' > ')

def popcontext(self) -> None:
self.currentcontext.pop()

def recontext(self, title: str) -> None:
self.currentcontext[-1] = title + ' > '

def write(self, msg, level: proto.Level) -> None:
print(*self.currentcontext, msg, sep=' > ')
if self.currentcontext:
prefix = ''.join(self.currentcontext)
msg = prefix + str(msg).replace('\n', '\n' + ' > '.rjust(len(prefix)))
print(msg, file=self.file)