docs for muutils v0.6.17
View Source on GitHub

# muutils.logger.loggingstream

View Source
View on GitHub

     1from __future__ import annotations
     2
     3import time
     4from dataclasses import dataclass, field
     5from typing import Any, Callable
     6
     7from muutils.logger.simplelogger import AnyIO, NullIO
     8from muutils.misc import sanitize_fname
     9
    10
    11@dataclass
    12class LoggingStream:
    13    """properties of a logging stream
    14
    15    - `name: str` name of the stream
    16    - `aliases: set[str]` aliases for the stream
    17            (calls to these names will be redirected to this stream. duplicate alises will result in errors)
    18            TODO: perhaps duplicate alises should result in duplicate writes?
    19    - `file: str|bool|AnyIO|None` file to write to
    20            - if `None`, will write to standard log
    21            - if `True`, will write to `name + ".log"`
    22            - if `False` will "write" to `NullIO` (throw it away)
    23            - if a string, will write to that file
    24            - if a fileIO type object, will write to that object
    25    - `default_level: int|None` default level for this stream
    26    - `default_contents: dict[str, Callable[[], Any]]` default contents for this stream
    27    - `last_msg: tuple[float, Any]|None` last message written to this stream (timestamp, message)
    28    """
    29
    30    name: str | None
    31    aliases: set[str | None] = field(default_factory=set)
    32    file: str | bool | AnyIO | None = None
    33    default_level: int | None = None
    34    default_contents: dict[str, Callable[[], Any]] = field(default_factory=dict)
    35    handler: AnyIO | None = None
    36
    37    # TODO: implement last-message caching
    38    # last_msg: tuple[float, Any]|None = None
    39
    40    def make_handler(self) -> AnyIO | None:
    41        if self.file is None:
    42            return None
    43        elif isinstance(self.file, str):
    44            # if its a string, open a file
    45            return open(
    46                self.file,
    47                "w",
    48                encoding="utf-8",
    49            )
    50        elif isinstance(self.file, bool):
    51            # if its a bool and true, open a file with the same name as the stream (in the current dir)
    52            # TODO: make this happen in the same dir as the main logfile?
    53            if self.file:
    54                return open(  # type: ignore[return-value]
    55                    f"{sanitize_fname(self.name)}.log.jsonl",
    56                    "w",
    57                    encoding="utf-8",
    58                )
    59            else:
    60                return NullIO()
    61        else:
    62            # if its neither, check it has `.write()` and `.flush()` methods
    63            if (
    64                (
    65                    not hasattr(self.file, "write")
    66                    or (not callable(self.file.write))
    67                    or (not hasattr(self.file, "flush"))
    68                    or (not callable(self.file.flush))
    69                )
    70                or (not hasattr(self.file, "close"))
    71                or (not callable(self.file.close))
    72            ):
    73                raise ValueError(f"stream {self.name} has invalid handler {self.file}")
    74            # ignore type check because we know it has a .write() method,
    75            # assume the user knows what they're doing
    76            return self.file  # type: ignore
    77
    78    def __post_init__(self):
    79        self.aliases = set(self.aliases)
    80        if any(x.startswith("_") for x in self.aliases if x is not None):
    81            raise ValueError(
    82                "stream names or aliases cannot start with an underscore, sorry"
    83            )
    84        self.aliases.add(self.name)
    85        self.default_contents["_timestamp"] = time.time
    86        self.default_contents["_stream"] = lambda: self.name
    87        self.handler = self.make_handler()
    88
    89    def __del__(self):
    90        if self.handler is not None:
    91            self.handler.flush()
    92            self.handler.close()
    93
    94    def __str__(self):
    95        return f"LoggingStream(name={self.name}, aliases={self.aliases}, file={self.file}, default_level={self.default_level}, default_contents={self.default_contents})"

@dataclass
class LoggingStream:
View Source
View on GitHub

    12@dataclass
    13class LoggingStream:
    14    """properties of a logging stream
    15
    16    - `name: str` name of the stream
    17    - `aliases: set[str]` aliases for the stream
    18            (calls to these names will be redirected to this stream. duplicate alises will result in errors)
    19            TODO: perhaps duplicate alises should result in duplicate writes?
    20    - `file: str|bool|AnyIO|None` file to write to
    21            - if `None`, will write to standard log
    22            - if `True`, will write to `name + ".log"`
    23            - if `False` will "write" to `NullIO` (throw it away)
    24            - if a string, will write to that file
    25            - if a fileIO type object, will write to that object
    26    - `default_level: int|None` default level for this stream
    27    - `default_contents: dict[str, Callable[[], Any]]` default contents for this stream
    28    - `last_msg: tuple[float, Any]|None` last message written to this stream (timestamp, message)
    29    """
    30
    31    name: str | None
    32    aliases: set[str | None] = field(default_factory=set)
    33    file: str | bool | AnyIO | None = None
    34    default_level: int | None = None
    35    default_contents: dict[str, Callable[[], Any]] = field(default_factory=dict)
    36    handler: AnyIO | None = None
    37
    38    # TODO: implement last-message caching
    39    # last_msg: tuple[float, Any]|None = None
    40
    41    def make_handler(self) -> AnyIO | None:
    42        if self.file is None:
    43            return None
    44        elif isinstance(self.file, str):
    45            # if its a string, open a file
    46            return open(
    47                self.file,
    48                "w",
    49                encoding="utf-8",
    50            )
    51        elif isinstance(self.file, bool):
    52            # if its a bool and true, open a file with the same name as the stream (in the current dir)
    53            # TODO: make this happen in the same dir as the main logfile?
    54            if self.file:
    55                return open(  # type: ignore[return-value]
    56                    f"{sanitize_fname(self.name)}.log.jsonl",
    57                    "w",
    58                    encoding="utf-8",
    59                )
    60            else:
    61                return NullIO()
    62        else:
    63            # if its neither, check it has `.write()` and `.flush()` methods
    64            if (
    65                (
    66                    not hasattr(self.file, "write")
    67                    or (not callable(self.file.write))
    68                    or (not hasattr(self.file, "flush"))
    69                    or (not callable(self.file.flush))
    70                )
    71                or (not hasattr(self.file, "close"))
    72                or (not callable(self.file.close))
    73            ):
    74                raise ValueError(f"stream {self.name} has invalid handler {self.file}")
    75            # ignore type check because we know it has a .write() method,
    76            # assume the user knows what they're doing
    77            return self.file  # type: ignore
    78
    79    def __post_init__(self):
    80        self.aliases = set(self.aliases)
    81        if any(x.startswith("_") for x in self.aliases if x is not None):
    82            raise ValueError(
    83                "stream names or aliases cannot start with an underscore, sorry"
    84            )
    85        self.aliases.add(self.name)
    86        self.default_contents["_timestamp"] = time.time
    87        self.default_contents["_stream"] = lambda: self.name
    88        self.handler = self.make_handler()
    89
    90    def __del__(self):
    91        if self.handler is not None:
    92            self.handler.flush()
    93            self.handler.close()
    94
    95    def __str__(self):
    96        return f"LoggingStream(name={self.name}, aliases={self.aliases}, file={self.file}, default_level={self.default_level}, default_contents={self.default_contents})"

properties of a logging stream

- `name: str` name of the stream
- `aliases: set[str]` aliases for the stream (calls to these names will be redirected to this stream. duplicate alises will result in errors) TODO: perhaps duplicate alises should result in duplicate writes?
- `file: str|bool|AnyIO|None` file to write to
  - if `None`, will write to standard log
  - if `True`, will write to `name + ".log"`
  - if `False` will "write" to `NullIO` (throw it away)
  - if a string, will write to that file
  - if a fileIO type object, will write to that object
- `default_level: int|None` default level for this stream
- `default_contents: dict[str, Callable[[], Any]]` default contents for this stream
- `last_msg: tuple[float, Any]|None` last message written to this stream (timestamp, message)

LoggingStream( name: str | None, aliases: set[str | None] = <factory>, file: Union[str, bool, TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None, default_level: int | None = None, default_contents: dict[str, typing.Callable[[], typing.Any]] = <factory>, handler: Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None)
name: str | None
aliases: set[str | None]
file: Union[str, bool, TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None
default_level: int | None = None
default_contents: dict[str, typing.Callable[[], typing.Any]]
handler: Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType] = None
def make_handler(self) -> Union[TextIO, muutils.logger.simplelogger.NullIO, NoneType]:
View Source
View on GitHub

    41    def make_handler(self) -> AnyIO | None:
    42        if self.file is None:
    43            return None
    44        elif isinstance(self.file, str):
    45            # if its a string, open a file
    46            return open(
    47                self.file,
    48                "w",
    49                encoding="utf-8",
    50            )
    51        elif isinstance(self.file, bool):
    52            # if its a bool and true, open a file with the same name as the stream (in the current dir)
    53            # TODO: make this happen in the same dir as the main logfile?
    54            if self.file:
    55                return open(  # type: ignore[return-value]
    56                    f"{sanitize_fname(self.name)}.log.jsonl",
    57                    "w",
    58                    encoding="utf-8",
    59                )
    60            else:
    61                return NullIO()
    62        else:
    63            # if its neither, check it has `.write()` and `.flush()` methods
    64            if (
    65                (
    66                    not hasattr(self.file, "write")
    67                    or (not callable(self.file.write))
    68                    or (not hasattr(self.file, "flush"))
    69                    or (not callable(self.file.flush))
    70                )
    71                or (not hasattr(self.file, "close"))
    72                or (not callable(self.file.close))
    73            ):
    74                raise ValueError(f"stream {self.name} has invalid handler {self.file}")
    75            # ignore type check because we know it has a .write() method,
    76            # assume the user knows what they're doing
    77            return self.file  # type: ignore