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
25 changes: 13 additions & 12 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@

import sys
import os
sys.path.insert(0, os.path.abspath('..'))

sys.path.insert(0, os.path.abspath(".."))

# -- Project information -----------------------------------------------------

project = 'Treelog'
copyright = '2018, Evalf'
author = 'Evalf'
project = "Treelog"
copyright = "2018, Evalf"
author = "Evalf"

# -- General configuration ---------------------------------------------------

extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.doctest',
'sphinx.ext.napoleon',
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.doctest",
"sphinx.ext.napoleon",
]

master_doc = 'index'
master_doc = "index"

# -- Options for HTML output -------------------------------------------------

html_theme = 'default'
html_favicon = 'favicon.ico'
html_static_path = ['_static']
html_theme = "default"
html_favicon = "favicon.ico"
html_static_path = ["_static"]
829 changes: 435 additions & 394 deletions tests.py

Large diffs are not rendered by default.

53 changes: 26 additions & 27 deletions treelog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
'Logging framework that organizes messages in a tree'
"Logging framework that organizes messages in a tree"

__version__ = '2.0'
__version__ = "2.0"

from importlib import import_module

_sub_mods = {
'proto',
'iter'
}
_sub_mods = {"proto", "iter"}
_state_attrs = {
'set',
'add',
'disable',
'context',
'withcontext',
"set",
"add",
"disable",
"context",
"withcontext",
}
_state_funcs = {
level + op
for level in ['debug', 'info', 'user', 'warning', 'error']
for op in ['', 'data', 'file']
for level in ["debug", "info", "user", "warning", "error"]
for op in ["", "data", "file"]
}
_log_objs = {
'DataLog',
'FilterLog',
'HtmlLog',
'LoggingLog',
'NullLog',
'RecordLog',
'RichOutputLog',
'StdoutLog',
'TeeLog',
"DataLog",
"FilterLog",
"HtmlLog",
"LoggingLog",
"NullLog",
"RecordLog",
"RichOutputLog",
"StdoutLog",
"TeeLog",
}


def __dir__():
return (
'__version__',
"__version__",
*_sub_mods,
*_state_attrs,
*_state_funcs,
*_log_objs,
)


def __getattr__(attr):
if attr in _state_attrs:
_state = import_module(f'._state', 'treelog')
_state = import_module("._state", "treelog")
obj = getattr(_state, attr)
elif attr in _state_funcs:
_state = import_module(f'._state', 'treelog')
_state = import_module("._state", "treelog")
obj = _state.partial(attr)
elif attr in _log_objs:
m = import_module(f'._{attr[:-3].lower()}', 'treelog')
m = import_module(f"._{attr[:-3].lower()}", "treelog")
obj = getattr(m, attr)
elif attr in _sub_mods:
obj = import_module(f'.{attr}', 'treelog')
obj = import_module(f".{attr}", "treelog")
else:
raise AttributeError(attr)
globals()[attr] = obj
Expand Down
12 changes: 9 additions & 3 deletions treelog/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@


class DataLog:
'''Output only data.'''
"""Output only data."""

def __init__(self, dirpath: str = os.curdir, names: typing.Callable[[str], typing.Iterable[str]] = sequence) -> None:
def __init__(
self,
dirpath: str = os.curdir,
names: typing.Callable[[str], typing.Iterable[str]] = sequence,
) -> None:
self._names = functools.lru_cache(maxsize=32)(names)
self._path = makedirs(dirpath)

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

def write(self, msg, level: Level) -> None:
if isinstance(msg, Data):
_, f = non_existent(self._path, self._names(msg.name), lambda p: p.open('xb'))
_, f = non_existent(
self._path, self._names(msg.name), lambda p: p.open("xb")
)
with f:
f.write(msg.data)
13 changes: 9 additions & 4 deletions treelog/_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@


class FilterLog:
'''Filter messages based on level.'''

def __init__(self, baselog: Log, minlevel: typing.Optional[Level] = None, maxlevel: typing.Optional[Level] = None) -> None:
"""Filter messages based on level."""

def __init__(
self,
baselog: Log,
minlevel: typing.Optional[Level] = None,
maxlevel: typing.Optional[Level] = None,
) -> None:
self._baselog = baselog
self._minlevel = minlevel
self._maxlevel = maxlevel
Expand All @@ -21,7 +26,7 @@ def recontext(self, title: str) -> None:
self._baselog.recontext(title)

def _passthrough(self, level: Level) -> bool:
'''Return True if messages of the given level should pass through.'''
"""Return True if messages of the given level should pass through."""
if self._minlevel is not None and level.value < self._minlevel.value:
return False
if self._maxlevel is not None and level.value > self._maxlevel.value:
Expand Down
95 changes: 62 additions & 33 deletions treelog/_html.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import contextlib
import hashlib
import html
import os
Expand All @@ -13,21 +12,34 @@


class HtmlLog:
'''Output html nested lists.'''

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:
"""Output html nested lists."""

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)
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')
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:
title = ' '.join(sys.argv)
title = " ".join(sys.argv)
if htmltitle is None:
htmltitle = html.escape(title)
if favicon is None:
favicon = FAVICON
self._file.write(HTMLHEAD.format(
title=title, htmltitle=htmltitle, css=css, js=js, favicon=favicon))
self._file.write(
HTMLHEAD.format(
title=title, htmltitle=htmltitle, css=css, js=js, favicon=favicon
)
)
# active contexts that are not yet opened as html elements
self._unopened = [] # type: typing.List[str]

Expand All @@ -46,46 +58,61 @@ def recontext(self, title: str) -> None:

def write(self, msg, level: Level) -> None:
for c in self._unopened:
print('<div class="context"><div class="title">{}</div><div class="children">'.format(
html.escape(c)), file=self._file)
print(
'<div class="context"><div class="title">{}</div><div class="children">'.format(
html.escape(c)
),
file=self._file,
)
self._unopened.clear()
if isinstance(msg, Data):
_, ext = os.path.splitext(msg.name)
filename = self._write_hash(msg.data, ext)
text = '<a href="{href}" download="{name}">{name}</a>'.format(href=urllib.parse.quote(filename), name=html.escape(msg.name))
text = '<a href="{href}" download="{name}">{name}</a>'.format(
href=urllib.parse.quote(filename), name=html.escape(msg.name)
)
else:
text = html.escape(msg)
print('<div class="item" data-loglevel="{}">{}</div>'.format(level.value, text), file=self._file, flush=True)
print(
'<div class="item" data-loglevel="{}">{}</div>'.format(level.value, text),
file=self._file,
flush=True,
)

def close(self) -> bool:
if hasattr(self, '_file') and not self._file.closed:
if hasattr(self, "_file") and not self._file.closed:
self._file.write(HTMLFOOT)
self._file.close()
return True
else:
return False

def __enter__(self) -> 'HtmlLog':
def __enter__(self) -> "HtmlLog":
return self

def __exit__(self, t: typing.Optional[typing.Type[BaseException]], value: typing.Optional[BaseException], traceback: typing.Optional[types.TracebackType]) -> None:
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)
warnings.warn("unclosed object {!r}".format(self), ResourceWarning)

def _write_hash(self, data, ext):
filename = hashlib.sha1(data).hexdigest() + ext
try:
with (self._path / filename).open('xb') as f:
with (self._path / filename).open("xb") as f:
f.write(data)
except FileExistsError:
pass
return filename


HTMLHEAD = '''\
HTMLHEAD = """\
<!DOCTYPE html>
<html>
<head>
Expand All @@ -99,13 +126,13 @@ def _write_hash(self, data, ext):
<body>
<div id="header"><div id="bar"><div id="text"><div id="title">{htmltitle}</div></div></div></div>
<div id="log">
'''
"""

HTMLFOOT = '''\
HTMLFOOT = """\
</div></body></html>
'''
"""

CSS = '''\
CSS = """\
body { font-family: monospace; font-size: 12px; }

a, a:visited, a:hover { color: inherit; text-decoration: underline; }
Expand Down Expand Up @@ -192,9 +219,9 @@ def _write_hash(self, data, ext):
#theater.overview .plot_container3 { height: calc(100% - 20px); display: flex; align-items: center; justify-content: center; }
#theater.overview .plot { background: white; max-width: 100%; max-height: 100%; }
#theater.overview .label { position: absolute; width: 100%; left: 0px; right: 0px; bottom: 0px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
'''
"""

JS = '''\
JS = """\
'use strict';

// LOW LEVEL UTILS
Expand Down Expand Up @@ -750,10 +777,12 @@ def _write_hash(self, data, ext):
apply_state(state);
state_control = 'enabled';
});
'''

FAVICON = 'data:image/png;base64,' \
'iVBORw0KGgoAAAANSUhEUgAAANIAAADSAgMAAABC93bRAAAACVBMVEUAAGcAAAD////NzL25' \
'AAAAAXRSTlMAQObYZgAAAFtJREFUaN7t2SEOACEMRcEa7ofh/ldBsJJAS1bO86Ob/MZY9ViN' \
'TD0oiqIo6qrOURRFUVRepQ4TRVEURdXVV6MoiqKoV2UJpCiKov7+p1AURVFUWZWiKIqiqI2a' \
'8O8qJ0n+GP4AAAAASUVORK5CYII='
"""

FAVICON = (
"data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAANIAAADSAgMAAABC93bRAAAACVBMVEUAAGcAAAD////NzL25"
"AAAAAXRSTlMAQObYZgAAAFtJREFUaN7t2SEOACEMRcEa7ofh/ldBsJJAS1bO86Ob/MZY9ViN"
"TD0oiqIo6qrOURRFUVRepQ4TRVEURdXVV6MoiqKoV2UJpCiKov7+p1AURVFUWZWiKIqiqI2a"
"8O8qJ0n+GP4AAAAASUVORK5CYII="
)
9 changes: 5 additions & 4 deletions treelog/_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@


class LoggingLog:
'''Log to Python's built-in logging facility.'''
"""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:
def __init__(self, name: str = "nutils") -> None:
self._logger = logging.getLogger(name)
self.currentcontext = [] # type: typing.List[str]

Expand All @@ -24,5 +24,6 @@ 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(
(*self.currentcontext, str(msg))))
self._logger.log(
self._levels[level.value], " > ".join((*self.currentcontext, str(msg)))
)
1 change: 0 additions & 1 deletion treelog/_null.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class NullLog:

def pushcontext(self, title: str) -> None:
pass

Expand Down
Loading
Loading