Source code for pyunity.logger

## Copyright (c) 2020-2022 The PyUnity Team
## This file is licensed under the MIT License.
## See https://docs.pyunity.x10.bz/en/latest/license.html

"""
Utility functions to log output of PyUnity.

This will be imported as ``pyunity.Logger``.

"""

__all__ = ["ResetStream", "LogException", "LogTraceback", "LogSpecial",
           "SetStream", "Log", "LogLine", "Save", "Level", "Special",
           "TempRedirect", "Elapsed", "TIME_FORMAT"]

import io
import os
import sys
import platform
import traceback
import inspect
import re
import atexit
import threading
import time
from pathlib import Path

TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

[docs]def getDataFolder(): if os.getenv("ANDROID_DATA") == "/data" and os.getenv("ANDROID_ROOT") == "/system": # Android (p4a, termux etc) pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path): result = path.split("/files")[0] break else: raise OSError("Cannot find path to android app folder") folder = Path(result) / "files/usr/local/pyunity" elif platform.system().startswith("Windows"): # Windows folder = Path(os.environ["appdata"]) / "PyUnity" elif platform.system().startswith("Darwin"): # MacOS folder = Path.home() / "Library/Application Support/PyUnity" else: # Linux folder = Path("/tmp/pyunity") return folder
folder = getDataFolder() / "Logs" if not folder.is_dir(): folder.mkdir(parents=True, exist_ok=True) stream = sys.stdout timestamp = time.strftime(TIME_FORMAT.replace(":", "-")) # No : allowed in path start = time.time() with open(folder / "latest.log", "w+") as f: f.write("YYYY-MM-DD HH:MM:SS Module:Line |(O)utput / (I)nfo / (D)ebug / (E)rror / (W)arning| Message\n") lineno = inspect.getframeinfo(inspect.currentframe()).lineno f.write(time.strftime(TIME_FORMAT) + f" {__name__}:{lineno + 1} |I| Started logger\n")
[docs]class Level: """ Represents a level or severity to log. You should never instantiate this directly, instead use one of ``Logging.OUTPUT``, ``Logging.INFO``, ``Logging.DEBUG``, ``Logging.ERROR`` or ``Logging.WARN``. """ def __init__(self, abbr): self.abbr = abbr def __eq__(self, other): if isinstance(other, Level): return self.abbr == other.abbr return False def __hash__(self): return hash(self.abbr)
OUTPUT = Level("O") INFO = Level("I") DEBUG = Level("D") ERROR = Level("E") WARN = Level("W")
[docs]class Special: """ Class to represent a special line to log. You should never instantiate this class, instead use one of ``Logger.RUNNING_TIME`` or ``Logger.ELAPSED_TIME``. """ def __init__(self, name, func): self.name = name self.func = func
[docs]class Elapsed: def __init__(self): self.time = time.time()
[docs] def tick(self): old = self.time self.time = time.time() return self.time - old
elapsed = Elapsed() RUNNING_TIME = Special("RUNNING_TIME", lambda: str(time.time() - start)) ELAPSED_TIME = Special("ELAPSED_TIME", lambda: str(elapsed.tick()))
[docs]def Log(*message, stacklevel=1): """ Logs a message with level OUTPUT. """ LogLine(OUTPUT, *message, stacklevel=stacklevel + 1)
[docs]def LogLine(level, *message, stacklevel=1, silent=False): """ Logs a line in ``latest.log`` found in these two locations: Windows: ``%appdata%\\PyUnity\\Logs\\latest.log`` Other: ``/tmp/pyunity/logs/latest.log`` Parameters ---------- level : Level Level or severity of log. """ try: stack = inspect.stack() if len(stack) <= stacklevel: module = "sys" lineno = 1 else: frameinfo = inspect.stack()[stacklevel] module = frameinfo.frame.f_globals.get("__name__", "<string>") lineno = frameinfo.lineno except ValueError: # call stack is not deep enough module = "sys" lineno = 1 location = f"{module}:{lineno}" stamp = time.strftime(TIME_FORMAT) msg = " ".join(str(a).rstrip() for a in message) if level == WARN: msg = f"Warning: " + msg if msg.count("\n") > 0: for line in msg.split("\n"): if not line.isspace(): LogLine(level, line, stacklevel=stacklevel + 1, silent=silent) return stamp, msg if not silent: output = False if level == DEBUG: if os.environ["PYUNITY_DEBUG_MODE"] != "0": output = True elif level != INFO: output = True if output: if level == ERROR: sys.stderr.write(msg + "\n") else: stream.write(msg + "\n") with open(folder / "latest.log", "a") as f: f.write(f"{stamp} {location} |{level.abbr}| {msg}\n") return stamp, msg
[docs]def LogException(e, stacklevel=1, silent=False): """ Log an exception. Parameters ---------- e : Exception Exception to log """ exception = traceback.format_exception(type(e), e, e.__traceback__) for line in exception: for line2 in line.split("\n"): if line2: LogLine(ERROR, line2, stacklevel=stacklevel + 1, silent=silent)
[docs]def LogTraceback(exctype, value, tb): """ Log an exception. Parameters ---------- exctype : type Type of exception that is to be raised value : Any Value of the exception contents tb : traceback Traceback object to log Notes ----- This function is not meant to be used by general users. """ exception = traceback.format_exception(exctype, value, tb) for line in exception: for line2 in line.split("\n"): if line2: LogLine(ERROR, line2, stacklevel=2)
sys.excepthook = LogTraceback
[docs]def LogSpecial(level, type): """ Log a line of level ``level`` with a special line that is generated at runtime. Parameters ---------- level : Level Level of log type : Special The special line to log """ LogLine(level, "(" + type.name + ")", type.func(), stacklevel=2)
[docs]@atexit.register def Save(): """ Saves a new log file with a timestamp of initializing PyUnity for the first time. """ LogLine(INFO, "Saving new log at", folder / (timestamp + ".log")) with open(folder / "latest.log") as f: with open(folder / (timestamp + ".log"), "w+") as f2: f2.write(f.read())
[docs]class TempRedirect: def __init__(self, *, silent=False): self.silent = silent self.stream = None
[docs] def get(self): if self.stream is None: raise Exception("Context manager not used") return self.stream.getvalue()
def __enter__(self): global stream self.stream = io.StringIO() if self.silent: stream = self.stream else: SetStream(self.stream) return self def __exit__(self, exc_type, exc_val, exc_tb): global stream if self.silent: stream = sys.stdout else: ResetStream()
[docs]def SetStream(s): global stream stream = s stream.write(f"Changed stream to {s}\n") LogLine(INFO, f"Changed stream to {s}")
[docs]def ResetStream(): global stream stream = sys.stdout stream.write("Changed stream back to stdout\n") LogLine(INFO, "Changed stream back to stdout")
def _upload(): # Upload to ftp try: import urllib.request url = "https://ftp.pyunity.repl.co/upload?confirm=1" with urllib.request.urlopen(url): pass except Exception as e: LogException(e, silent=True) t = threading.Thread(target=_upload) t.daemon = True t.start()