Skip to content

Commit 9229775

Browse files
authored
Merge pull request #82 from Hashmap-Software-Agency/11-improve-logging
11 improve logging
2 parents 83c91a7 + 085d4d2 commit 9229775

File tree

5 files changed

+78
-65
lines changed

5 files changed

+78
-65
lines changed

pyttman/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ def __getattr__(self, item):
3535
app: PyttmanApp | None = None
3636
settings = _SettingsNotConfigured
3737
is_configured = False
38-
logger = PyttmanLogger
38+
39+
logger = PyttmanLogger()
40+
"""
41+
Logs function return value and/or exceptions to the application
42+
log file.
43+
"""
44+
3945

4046
"""
4147
I love you

pyttman/core/internals.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import warnings
55
from dataclasses import dataclass, field
66
from datetime import datetime
7+
from pathlib import Path
78
from typing import Any
89
import json
910
from collections import UserDict
1011

12+
import pytz
1113

1214
import pyttman
1315
from pyttman.core.containers import MessageMixin, Reply
@@ -35,6 +37,7 @@ def depr_graceful(message: str, version: str):
3537
out = f"{message} - This was deprecated in version {version}."
3638
warnings.warn(out, DeprecationWarning)
3739

40+
3841
class Settings:
3942
"""
4043
Dataclass holding settings configured in the settings.py
@@ -89,6 +92,7 @@ def __repr__(self):
8992
def _dict_to_object(dictionary):
9093
return json.loads(json.dumps(dictionary), object_hook=Settings)
9194

95+
9296
def _generate_name(name):
9397
"""
9498
Generates a user-friendly name out of
@@ -126,12 +130,18 @@ def _generate_error_entry(message: MessageMixin, exc: BaseException) -> Reply:
126130
traceback.print_exc()
127131
warnings.warn(f"{datetime.now()} - A critical error occurred in the "
128132
f"application logic. Error id: {error_id}")
129-
pyttman.logger.log(level="error",
130-
message=f"CRITICAL ERROR: ERROR ID={error_id} - "
131-
f"The error was caught while processing "
132-
f"message: '{message}'. Error message: '{exc}'")
133-
134-
auto_reply = pyttman.settings.MIDDLEWARE['FATAL_EXCEPTION_AUTO_REPLY']
133+
error_message = (f"CRITICAL ERROR: ERROR ID={error_id} - "
134+
f"The error was caught while processing message: "
135+
f"'{message}'. Error message: '{exc}'")
136+
try:
137+
pyttman.logger.log(level="error", message=error_message)
138+
except Exception:
139+
print(error_message)
140+
141+
try:
142+
auto_reply = pyttman.settings.MIDDLEWARE['FATAL_EXCEPTION_AUTO_REPLY']
143+
except Exception:
144+
auto_reply = "An internal error occurred in the application."
135145
return Reply(f"{auto_reply} ({error_id})")
136146

137147

pyttman/tools/logger/logger.py

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,9 @@ class PyttmanLogger:
1515
choice, configuring it the way you want, then
1616
pass it to the logger.set_logger method.
1717
18-
__verify_complete (method):
19-
Internal use only. Used upon importing the package
20-
in __init__.py, to ensure the PyttmanLogger class has a
21-
dedicated `logger` instance to work with.
22-
23-
loggedmethod (decorator method):
24-
This method is designed to be a decorator for bound
25-
and unbound methods in a software stack. The log method
26-
is static and has a closure method called inner, where
27-
the wrapped method is executed. Exceptions & return from
28-
the wrapped method are both logged to the log file using
29-
the static 'logging' instance, configured for the class.
30-
Simply add the decorator above your method to enable logging
31-
for it. Presuming you import this package as pyttman;
32-
33-
@pyttman.logger.log
34-
def myfunc(self, *args, **kwargs):
35-
...
18+
@pyttman.logger
19+
def myfunc(self, *args, **kwargs):
20+
...
3621
3722
log (method):
3823
If you want to manually log custom messages in your code,
@@ -42,57 +27,56 @@ def myfunc(self, *args, **kwargs):
4227

4328
LOG_INSTANCE = None
4429

45-
@staticmethod
46-
def __verify_config_complete():
47-
if pyttman.logger.LOG_INSTANCE is None:
48-
raise RuntimeError('Internal Pyttman Error: '
49-
'No Logger instance set.\r\n')
50-
51-
@staticmethod
52-
def loggedmethod(func):
30+
def __call__(self, func):
5331
"""
5432
Wrapper method for providing logging functionality.
5533
Use @logger to implement this method where logging
5634
of methods are desired.
57-
:param func:
58-
method that will be wrapped
59-
:returns:
60-
function
6135
"""
62-
@functools.wraps(func)
63-
def inner(*args, **kwargs):
36+
def inner(*args, log_level="debug", log_exception=True, **kwargs):
6437
"""
6538
Inner method, executing the func parameter function,
6639
as well as executing the logger.
6740
:returns:
6841
Output from executed function in parameter func
6942
"""
70-
PyttmanLogger.__verify_config_complete()
71-
43+
PyttmanLogger._verify_config_complete()
7244
try:
7345
results = func(*args, **kwargs)
46+
message = f"Return value from '{func.__name__}': '{results}'"
47+
self.log(message=message, level=log_level)
7448
return results
7549
except Exception as e:
76-
pyttman.logger.LOG_INSTANCE.error(
77-
f'Exception occurred in {func.__name__}. Traceback '
78-
f'{traceback.format_exc()} {e}')
50+
if log_exception:
51+
message = (f"Exception occurred in {func.__name__}. "
52+
f"Traceback: {traceback.format_exc()} {e}")
53+
self.log(message=message, level="error")
7954
raise e
8055
return inner
8156

8257
@staticmethod
83-
def log(message: str, level="debug") -> None:
58+
def _verify_config_complete():
59+
if pyttman.logger.LOG_INSTANCE is None:
60+
raise RuntimeError('Internal Pyttman Error: '
61+
'No Logger instance set.\r\n')
62+
63+
def loggedmethod(self, func: callable):
64+
"""
65+
Backward compatibility only; use @logger
66+
"""
67+
def inner(*args, **kwargs):
68+
return self.__call__(func)(*args, **kwargs)
69+
return inner
70+
71+
def log(self, message: str, level="debug") -> None:
8472
"""
8573
Allow for manual logging during runtime.
86-
:param message: str, message to be logged
87-
:param level: level for logging
88-
:returns:
89-
arbitrary
9074
"""
91-
PyttmanLogger.__verify_config_complete()
92-
log_levels = {'info': lambda _message: pyttman.logger.LOG_INSTANCE.info(_message),
93-
'debug': lambda _message: pyttman.logger.LOG_INSTANCE.debug(_message),
94-
'error': lambda _message: pyttman.logger.LOG_INSTANCE.error(_message)}
75+
PyttmanLogger._verify_config_complete()
76+
log_levels = {"info": self.LOG_INSTANCE.info,
77+
"debug": self.LOG_INSTANCE.debug,
78+
"error": self.LOG_INSTANCE.error}
9579
try:
9680
log_levels[level](message)
9781
except KeyError:
98-
log_levels['debug'](message)
82+
log_levels["debug"](message)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"discord.py",
4040
"requests",
4141
"py7zr",
42-
"ordered_set"
42+
"ordered_set",
43+
"pytz"
4344
],
4445
entry_points={
4546
"console_scripts": [

tests/tools/logger/test_logger.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import logging
32
import os
43
import sys
@@ -8,23 +7,33 @@
87
from tests.module_helper import PyttmanInternalBaseTestCase
98

109

11-
@pyttman.logger.loggedmethod
12-
def some_func():
13-
raise Exception("This is a log message")
14-
15-
1610
class TestPyttmanLogger(PyttmanInternalBaseTestCase):
1711

12+
cleanup_after = False
13+
1814
def test_logger_as_class(self):
1915
expected_output_in_file = "DEBUG:PyttmanTestLogger:This is a log message"
2016
pyttman.logger.log("This is a log message")
2117
self.logfile_meets_expectation(expected_output_in_file)
2218

2319
def test_logger_as_decorator(self):
24-
expected_output_in_file = 'raise Exception("This is a log message")'
20+
@pyttman.logger
21+
def broken():
22+
raise Exception("This is a log message")
23+
24+
@pyttman.logger()
25+
def working():
26+
return "I work"
2527

26-
self.assertRaises(Exception, some_func)
28+
working()
2729
self.assertTrue(Path(self.log_file_name).exists())
30+
31+
expected_output_in_file = "Return value from 'working': 'I work'"
32+
self.logfile_meets_expectation(expected_output_in_file)
33+
34+
with self.assertRaises(Exception):
35+
broken()
36+
expected_output_in_file = 'raise Exception("This is a log message")'
2837
self.logfile_meets_expectation(expected_output_in_file)
2938

3039
def test_shell_handler(self):
@@ -39,12 +48,15 @@ def logfile_meets_expectation(self, expected_output_in_file):
3948
with open(self.log_file_name, "r") as file:
4049
match = False
4150
for line in file.readlines():
42-
if line.strip() == expected_output_in_file:
51+
if expected_output_in_file in line:
4352
match = True
53+
break
4454
self.assertTrue(match)
4555

4656
def cleanup(self):
47-
print(Path().cwd())
57+
if not self.cleanup_after:
58+
return
59+
4860
for logfile in Path().cwd().parent.parent.glob("*.log"):
4961
try:
5062
os.remove(logfile)

0 commit comments

Comments
 (0)