Skip to content

Commit

Permalink
Merge pull request #2 from MurdoMaclachlan/dev
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
MurdoMaclachlan authored Apr 23, 2024
2 parents fe8ca24 + 664681d commit ef10d9a
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 74 deletions.
111 changes: 72 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,110 @@ A simple logger made primarily for my own personal use. Was made out of a combin

## Installation

smooth-logger can be installed through pip. Either download the latest release from Codeberg/GitHub, or do `pip install smooth-logger` to install from PyPi. For the latest commits, check the `dev` branches on the repositories.
smooth_logger can be installed through pip. Either download the latest release from Codeberg/GitHub, or do `pip install smooth_logger` to install from PyPi. For the latest commits, check the `dev` branches on the repositories.

smooth-logger was written in Python 3.9, but should work with Python 3.5 and up. A minimum of 3.5 is required due to the project's use of type hinting, which was introduced in that version.
smooth_logger is currently devloped using Python 3.11, but should work with Python 3.5 and up. A minimum of 3.5 is required due to the project's use of type hinting, which was introduced in that version.

smooth-logger supports Linux, macOS and Windows.
smooth_logger supports Linux, macOS and Windows.

## Usage

Usage of smooth-logger is, as it should be, quite simple.

The `Logger` model provides a number of methods for your use:

- `Logger.add_scope()` adds a new scope.
- `Logger.clean()` erases all log entries currently in memory.
- `Logger.edit_scope()` modifies the category of an existing scope.
- `Logger.get()` allows you to retrieve either the most recent log entry or all log entries, optionally filtered by scope.
- `Logger.get_time()` returns the full date & time, or optionally just the date, in ISO-8601 formatting.
- `Logger.init_bar()` initialises the `ProgressBar` model imported from the `smooth_progress` dependency.
- `Logger.notify()` sends a desktop notification using the `plyer` dependency.
- `Logger.new()` creates and, depending on scope, prints a new log entry.
- `Logger.output()` saves all log entries of appropriate scope to the log file and cleans the log array for the next group of log entries. A new log file is created for each new day. This method only attempts to create or update the log file if there are entries of an appropriate scope to be written to it; if there are none, it just executes `Logger.clean()`.
- `Logger.remove_scope()` removes an existing scope.

When initialising the Logger, you can optionally provide values to associate with each scope:

- 0: disabled, do not print to console or save to log file
- 1: enabled, print to console but do not save to log file
- 2: maximum, print to console and save to log file

The scopes available, along with their default values and suggested use cases, are:

- DEBUG (0): Information for debugging the program.
- ERROR (2): Errors that the program can recover from but impact functionality or performance.
- FATAL (2): Errors that mean the program must continue; handled crashes.
- INFO (1): General information for the user.
- WARNING (2): Things that have no immediate impact to functionality but could cause errors later on.
### Initialisation

Here is a simple example showing the initialisation of the logger:

```py
import smooth_logger

Log = smooth_logger.Logger("Example", "~/.config/example")
Log = smooth_logger.Logger("Example")
Log.new("This is a log message!", "INFO")
```

## Roadmap
In the case above, the logger will automatically create a folder called `Example` under `~.config` on Linux and macOS, or `APPDATA\Roaming` on Windows, which will contain a subfolder called `logs`, where the log files will be saved.

You can use the format below to provide a custom location:

```py
Log = smooth_logger.Logger("Example", config_path="~/this/is/an/example")
```

In this case, logs would be stored under, `~/this/is/an/example/logs`.

A roadmap of planned future improvements and features:
### Scopes

- Allow the creation of custom scopes. These would be instance-specific and not hard saved in any way. Suggested format and example:
Every log message is associated with a scope. This is an all-caps prefix to the message that should, in a single word, communicate what the message is about. The default scopes available, along with their suggested use cases, are:

```
Log.add_scope(name: str, description: str, default_value: int)
Log.add_scope("NEWSCOPE", "A new scope of mine!", 1)
```

Potentially also allow removal of scopes. In this situation, default scopes should be removable, but doing so should log a warning.

- DEBUG: Information for debugging the program.
- ERROR: Errors that the program can recover from but impact functionality or performance.
- FATAL: Errors that mean the program must continue; handled crashes.
- INFO: General information for the user.
- WARNING: Things that have no immediate impact to functionality but could cause errors later on.

- Allow editing of the values of existing scopes post-initialisation. For example:
You can also use the value "NOSCOPE" to indicate that a message should be printed without a prefixed scope. Messages with no scope are printed to the console, not saved to the output file, and are not accompanied by a timestamp.

### Categories

When initialising the Logger, you can optionally provide categories to associate with each scope:

- DISABLED (will not print to console or save to log file)
- ENABLED (will print to console but not save to log file)
- MAXIMUM (will print to console and save to log file)

By default, the DEBUG scope is disabled, the INFO scope is enabled, and the ERROR, FATAL and WARNING scopes are all set to maximum. Scopes set to maximum are not *automatically* saved to the log file; calling `Logger.output()` will save them and then clean the in-memory log to avoid duplication.

Categories are accessed like so:

```py
from smooth_logger.enums import Categories

Categories.ENABLED
```

```
Log.edit_scope(name: str, new_value: int)
Log.edit_scope("DEBUG", 1)
```

to temporarily enable debug statements. This feature would probably see the most use from custom scopes.

### Customising scopes

- Add an optional argument `notify: bool` to `Logger.new()` to allow log entries to be created and notified in one statement, rather than the two currently required.
You can create custom scopes using the `Logger.add_scope()` method. These are currently instance-specific and not hard saved in any way. A simple usage of this is as follows:

```py
Log.add_scope("NEWSCOPE", Categories.ENABLED)
```

Similarly, you can use `Logger.edit_scope()` to modify the category of an existing scope (for the specific instance only), like so:

```py
Log.edit_scope("DEBUG", Categories.ENABLED)
```

The above statement could, for example, be used to temporarily enable debug statements if an error is detected.

Only the categories defined in the `Categories` enum will be recognised; attempting to pass anything else as a category will prompt a warning, and the scope will not be added/edited.

Finally, you can remove any scope with the following method:

```py
Log.remove_scope("DEBUG")
```

It is recommended to be careful with this method. Removing scopes, like adding or editing them, is ephemeral and won't be hard-saved anywhere, but removing a scope during run-time will produce warnings if you attempt to use that scope anywhere in your program.

## Roadmap

- Rework `Logger.get()` to allow passing of a specific number of log values to be fetched. If these values exceed the number in the log, all matching log values should be returned, and a warning should be issued (but not returned).

- Possibly replace some internal warnings with Exceptions so they can be more easily-handled by end-user programs.
- Possibly replace some internal warnings with Exceptions so they can be more easily-handled by end-user programs.

- Add a category that saves to the log file but doesn't print to the console.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def readme():

setup(
name="smooth_logger",
version="0.2.0",
version="0.3.0",
author="Murdo Maclachlan",
author_email="murdomaclachlan@duck.com",
description=(
Expand Down
107 changes: 73 additions & 34 deletions smooth_logger/Logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
from os import environ, makedirs
from os.path import expanduser, isdir
from plyer import notification
from plyer.facades import Notification
from smooth_progress import ProgressBar
from time import time
from .LogEntry import LogEntry
from typing_extensions import Union

from plyer.facades import Notification
from typing import Dict, List, Union
from .enums import Categories
from .LogEntry import LogEntry


class Logger:
"""
Class for controlling the entirety of logging. The logging works on a scope-based system where
(almost) every message has a defined scope, and the scopes are each associated with a specific
value between 0 and 2 inclusive. The meanings of the values are as follows:
category between 0 and 2 inclusive. The meanings of the categories are as follows:
0: disabled, do not print to console or save to log file
1: enabled, print to console but do not save to log file
Expand All @@ -23,17 +24,17 @@ class Logger:
def __init__(self,
program_name: str,
config_path: str = None,
debug: int = 0,
error: int = 2,
fatal: int = 2,
info: int = 1,
warning: int = 2) -> None:
debug: int = Categories.DISABLED,
error: int = Categories.MAXIMUM,
fatal: int = Categories.MAXIMUM,
info: int = Categories.ENABLED,
warning: int = Categories.MAXIMUM) -> None:
self.bar: ProgressBar = ProgressBar()
self.__is_empty: bool = True
self.__log: List[LogEntry] = []
self.__log: list[LogEntry] = []
self.__notifier: Notification = notification
self.__program_name: str = program_name
self.__scopes: Dict[str, int] = {
self.__scopes: dict[str, int] = {
"DEBUG": debug, # information for debugging the program
"ERROR": error, # errors the program can recover from
"FATAL": fatal, # errors that mean the program cannot continue
Expand Down Expand Up @@ -120,7 +121,7 @@ def __display_log_entry(self,
:param is_bar: whether the progress bar is active
:param console: whether the message should be printed to the console
"""
if scope == "NOSCOPE" or (self.__scopes[scope] > 0 and print_to_console):
if scope == "NOSCOPE" or (self.__scopes[scope] != Categories.DISABLED and print_to_console):
print(entry.rendered)
if is_bar:
print(self.bar.state, end="\r", flush=True)
Expand All @@ -144,7 +145,7 @@ def __get_time(self, method: str = "time") -> str:
self.new("Bad method passed to Logger.get_time().", "ERROR")
return ""

def add_scope(self, name: str, value: int) -> bool:
def add_scope(self, name: str, category: int) -> bool:
"""
Adds a new logging scope for use with log entries. Users should be careful when doing this;
custom scopes would be best added immediately following initialisation. If a 'Logger.new()'
Expand All @@ -154,7 +155,7 @@ def add_scope(self, name: str, value: int) -> bool:
Custom scopes are instance specific and not hard saved.
:param name: the name of the new scope
:param value: the default value of the new scope (0-2)
:param category: the default category of the new scope (0-2)
:return: a boolean sucess status
"""
Expand All @@ -165,8 +166,15 @@ def add_scope(self, name: str, value: int) -> bool:
"WARNING"
)
else:
self.__scopes[name] = value
return True
if category in set(item for item in Categories):
self.__scopes[name] = category
return True
else:
self.new(
f"Attempt was made to add new scope with category {category}, but this is not "
+ "a valid category.",
"WARNING"
)
return False

def clean(self) -> None:
Expand All @@ -177,35 +185,43 @@ def clean(self) -> None:
self.__is_empty = True
self.__write_logs = False

def edit_scope(self, name: str, value: int) -> bool:
def edit_scope(self, name: str, category: int) -> bool:
"""
Edits an existing scope's value. Edited values are instance specific and not hard saved.
Edits an existing scope's category, if the scope exists. Edited categories are instance
specific and not hard saved.
:param name: the name of the scope to edit
:param value: the new value of the scope (0-2)
:param category: the new category of the scope (0-2)
:returns: a boolean success status
"""
if name in self.__scopes.keys():
self.__scopes[name] = value
return True
if category in set(item for item in Categories):
self.__scopes[name] = category
return True
else:
self.new(
f"Attempt was made to change category of scope {name} to {category}, but this "
+ "is not a valid category.",
"WARNING"
)
else:
self.new(
f"Attempt was made to edit a scope with name {name}, but no scope with "
+ "this name exists.",
f"Attempt was made to edit a scope with name {name}, but no scope with this name "
+ "exists.",
"WARNING"
)
return False

def get(self, mode: str = "all", scope: str = None) -> Union[List[LogEntry], LogEntry]:
def get(self, mode: str = "all", scope: str = None) -> Union[list[LogEntry], LogEntry, None]:
"""
Returns item(s) in the log. The entries returned can be controlled by passing optional
arguments.
If no entries match the query, nothing will be returned.
:param mode: optional; 'all' for all log entries or 'recent' for only the most recent one
:param scope: optional; if passed, only entries matching its value will be returned
:param scope: optional; if passed, only entries matching its category will be returned
:returns: a single log entry or list of log entries, or nothing
"""
Expand All @@ -217,16 +233,16 @@ def get(self, mode: str = "all", scope: str = None) -> Union[List[LogEntry], Log
# return all log entries matching the query
if mode == "all":
data: list[LogEntry] = []
for i in self.__log:
if scope is None or i.scope == scope:
data.append(i)
for entry in self.__log:
if scope is None or entry.scope == scope:
data.append(entry)
if data:
return data
# iterate through the log in reverse to find the most recent entry matching the query
elif mode == "recent":
for i in range(len(self.__log)-1, 0):
if scope is None or self.__log[i].scope == scope:
return self.__log[i]
for entry in reversed(self.__log):
if scope is None or entry.scope == scope:
return entry
else:
self.new("Unknown mode passed to Logger.get().", "WARNING")

Expand All @@ -242,7 +258,7 @@ def init_bar(self, limit: int) -> None:
def new(self,
message: str,
scope: str,
print_to_console: bool = False,
print_to_console: bool = True,
notify: bool = False) -> bool:
"""
Initiates a new log entry and prints it to the console. Optionally, if do_not_print is
Expand All @@ -259,7 +275,11 @@ def new(self,
:returns: a boolean success status
"""
if scope in self.__scopes or scope == "NOSCOPE":
output: bool = (self.__scopes[scope] == 2) if scope != "NOSCOPE" else False
output: bool = (
(self.__scopes[scope] == Categories.MAXIMUM)
if scope != "NOSCOPE" else
False
)
is_bar: bool = (self.bar is not None) and self.bar.opened

# if the progress bar is enabled, append any necessary empty characters to the message
Expand All @@ -268,7 +288,7 @@ def new(self,
message += " " * (len(self.bar.state) - len(message))

entry: LogEntry = self.__create_log_entry(message, output, scope)
self.__display_log_entry(entry, scope, notify, print_to_console, is_bar)
self.__display_log_entry(entry, scope, notify, is_bar, print_to_console)

self.__write_logs = self.__write_logs or output
self.__is_empty = False
Expand Down Expand Up @@ -300,3 +320,22 @@ def output(self) -> None:
if line.output:
log_file.write(line.rendered + "\n")
self.clean()

def remove_scope(self, name: str) -> bool:
"""
Removes an existing scope if it exists.
:param name: the name of the scope to remove
:returns: a boolean success status
"""
if name in self.__scopes.keys():
del self.__scopes[name]
return True
else:
self.new(
f"Attempt was made to remove a scope with name {name}, but no scope with this "
+ "name exists.",
"WARNING"
)
return False
2 changes: 2 additions & 0 deletions smooth_logger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from .enums import Categories

from .Logger import Logger
from .LogEntry import LogEntry
6 changes: 6 additions & 0 deletions smooth_logger/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import Enum

class Categories(Enum):
DISABLED = 0 # do not print to console or save to log file
ENABLED = 1 # print to console but do not save to log file
MAXIMUM = 2 # print to console and save to log file

0 comments on commit ef10d9a

Please sign in to comment.