python-coloured-logger 1.0.1__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-coloured-logger
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Simple coloured logging for Flask and FastAPI apps
5
5
  Author: Skulldorom
6
6
  License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Skulldorom/python-coloured-logger
8
+ Project-URL: Repository, https://github.com/Skulldorom/python-coloured-logger
9
+ Project-URL: Issues, https://github.com/Skulldorom/python-coloured-logger/issues
7
10
  Keywords: logging,flask,fastapi,color
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -60,10 +63,10 @@ logger.critical("System crash")
60
63
  Example output:
61
64
 
62
65
  ```text
63
- [2026-05-27 12:00:00] INFO Server started
64
- [2026-05-27 12:00:01] SUCCESS Database connected
65
- [2026-05-27 12:00:02] WARNING High memory usage
66
- [2026-05-27 12:00:03] ERROR Request failed
66
+ [20/06/2026] [INFO] Server started
67
+ [20/06/2026] [SUCCESS] Database connected
68
+ [20/06/2026] [WARNING] High memory usage
69
+ [20/06/2026] [ERROR] Request failed
67
70
  ```
68
71
 
69
72
  ---
@@ -172,7 +175,7 @@ FLASK_LOG_DATE_FORMAT=%H:%M:%S
172
175
 
173
176
  ## Requirements
174
177
 
175
- - Python 3.9+
178
+ - Python 3.8+
176
179
  - Flask (optional)
177
180
  - FastAPI / Uvicorn (optional)
178
181
 
@@ -45,10 +45,10 @@ logger.critical("System crash")
45
45
  Example output:
46
46
 
47
47
  ```text
48
- [2026-05-27 12:00:00] INFO Server started
49
- [2026-05-27 12:00:01] SUCCESS Database connected
50
- [2026-05-27 12:00:02] WARNING High memory usage
51
- [2026-05-27 12:00:03] ERROR Request failed
48
+ [20/06/2026] [INFO] Server started
49
+ [20/06/2026] [SUCCESS] Database connected
50
+ [20/06/2026] [WARNING] High memory usage
51
+ [20/06/2026] [ERROR] Request failed
52
52
  ```
53
53
 
54
54
  ---
@@ -157,7 +157,7 @@ FLASK_LOG_DATE_FORMAT=%H:%M:%S
157
157
 
158
158
  ## Requirements
159
159
 
160
- - Python 3.9+
160
+ - Python 3.8+
161
161
  - Flask (optional)
162
162
  - FastAPI / Uvicorn (optional)
163
163
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-coloured-logger"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "Simple coloured logging for Flask and FastAPI apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -18,8 +18,23 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  ]
20
20
 
21
+ [project.urls]
22
+ Homepage = "https://github.com/Skulldorom/python-coloured-logger"
23
+ Repository = "https://github.com/Skulldorom/python-coloured-logger"
24
+ Issues = "https://github.com/Skulldorom/python-coloured-logger/issues"
25
+
21
26
  [tool.setuptools]
22
27
  package-dir = { "" = "src" }
23
28
 
29
+ [tool.setuptools.package-data]
30
+ "coloured_logger" = ["py.typed"]
31
+
24
32
  [tool.setuptools.packages.find]
25
33
  where = ["src"]
34
+
35
+ [tool.mypy]
36
+ strict = true
37
+ python_version = "3.8"
38
+ files = ["src"]
39
+ warn_unused_ignores = true
40
+ warn_redundant_casts = true
@@ -0,0 +1,10 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .formatter import ColouredFormatter, SUCCESS_LEVEL, get_logger, log, setup_logging
4
+
5
+ try:
6
+ __version__ = version("python-coloured-logger")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
9
+
10
+ __all__ = ["ColouredFormatter", "SUCCESS_LEVEL", "get_logger", "log", "setup_logging", "__version__"]
@@ -4,6 +4,8 @@ from typing import Optional, TextIO
4
4
 
5
5
  SUCCESS_LEVEL = 25
6
6
 
7
+ _success_level_registered: bool = False
8
+
7
9
 
8
10
  def _env_bool(name: str, default: bool) -> bool:
9
11
  value = os.getenv(name)
@@ -23,7 +25,7 @@ def _resolve_use_color(use_color: Optional[bool]) -> bool:
23
25
  def _resolve_datefmt(datefmt: Optional[str]) -> str:
24
26
  if datefmt:
25
27
  return datefmt
26
- return os.getenv("FLASK_LOG_DATE_FORMAT") or os.getenv("COLOURED_LOGGER_DATE_FORMAT") or "%d/%b/%Y %H:%M:%S"
28
+ return os.getenv("FLASK_LOG_DATE_FORMAT") or os.getenv("COLOURED_LOGGER_DATE_FORMAT") or "%d/%m/%Y"
27
29
 
28
30
 
29
31
  class ColouredFormatter(logging.Formatter):
@@ -60,6 +62,10 @@ class ColouredFormatter(logging.Formatter):
60
62
 
61
63
 
62
64
  def _ensure_success_level() -> None:
65
+ global _success_level_registered
66
+ if _success_level_registered:
67
+ return
68
+
63
69
  if logging.getLevelName(SUCCESS_LEVEL) != "SUCCESS":
64
70
  logging.addLevelName(SUCCESS_LEVEL, "SUCCESS")
65
71
 
@@ -71,11 +77,12 @@ def _ensure_success_level() -> None:
71
77
  self._log(SUCCESS_LEVEL, msg, args, **kwargs)
72
78
 
73
79
  setattr(logging.Logger, "success", success)
80
+ _success_level_registered = True
74
81
 
75
82
 
76
83
  def setup_logging(
77
84
  logger_name: Optional[str] = None,
78
- level: int = logging.DEBUG,
85
+ level: int = logging.NOTSET,
79
86
  stream: Optional[TextIO] = None,
80
87
  use_color: Optional[bool] = None,
81
88
  datefmt: Optional[str] = None,
@@ -83,7 +90,8 @@ def setup_logging(
83
90
  ) -> logging.Logger:
84
91
  _ensure_success_level()
85
92
  logger = logging.getLogger(logger_name)
86
- logger.setLevel(level)
93
+ if level != logging.NOTSET or logger.level == logging.NOTSET:
94
+ logger.setLevel(level)
87
95
 
88
96
  managed_handler_exists = any(
89
97
  isinstance(handler, logging.StreamHandler) and isinstance(handler.formatter, ColouredFormatter)
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-coloured-logger
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Simple coloured logging for Flask and FastAPI apps
5
5
  Author: Skulldorom
6
6
  License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Skulldorom/python-coloured-logger
8
+ Project-URL: Repository, https://github.com/Skulldorom/python-coloured-logger
9
+ Project-URL: Issues, https://github.com/Skulldorom/python-coloured-logger/issues
7
10
  Keywords: logging,flask,fastapi,color
8
11
  Classifier: Programming Language :: Python :: 3
9
12
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -60,10 +63,10 @@ logger.critical("System crash")
60
63
  Example output:
61
64
 
62
65
  ```text
63
- [2026-05-27 12:00:00] INFO Server started
64
- [2026-05-27 12:00:01] SUCCESS Database connected
65
- [2026-05-27 12:00:02] WARNING High memory usage
66
- [2026-05-27 12:00:03] ERROR Request failed
66
+ [20/06/2026] [INFO] Server started
67
+ [20/06/2026] [SUCCESS] Database connected
68
+ [20/06/2026] [WARNING] High memory usage
69
+ [20/06/2026] [ERROR] Request failed
67
70
  ```
68
71
 
69
72
  ---
@@ -172,7 +175,7 @@ FLASK_LOG_DATE_FORMAT=%H:%M:%S
172
175
 
173
176
  ## Requirements
174
177
 
175
- - Python 3.9+
178
+ - Python 3.8+
176
179
  - Flask (optional)
177
180
  - FastAPI / Uvicorn (optional)
178
181
 
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/coloured_logger/__init__.py
5
5
  src/coloured_logger/formatter.py
6
+ src/coloured_logger/py.typed
6
7
  src/python_coloured_logger.egg-info/PKG-INFO
7
8
  src/python_coloured_logger.egg-info/SOURCES.txt
8
9
  src/python_coloured_logger.egg-info/dependency_links.txt
@@ -0,0 +1,217 @@
1
+ import io
2
+ import logging
3
+ import os
4
+ import unittest
5
+
6
+ from coloured_logger import ColouredFormatter, SUCCESS_LEVEL, get_logger, setup_logging
7
+
8
+
9
+ class ColouredFormatterTests(unittest.TestCase):
10
+ @classmethod
11
+ def setUpClass(cls):
12
+ # Register the SUCCESS level name so direct formatter usage sees "SUCCESS"
13
+ if logging.getLevelName(SUCCESS_LEVEL) != "SUCCESS":
14
+ logging.addLevelName(SUCCESS_LEVEL, "SUCCESS")
15
+
16
+ def test_format_without_color(self):
17
+ formatter = ColouredFormatter(use_color=False, datefmt="%Y")
18
+ record = logging.LogRecord("test", logging.INFO, __file__, 1, "hello", (), None)
19
+ output = formatter.format(record)
20
+ self.assertIn("[INFO] hello", output)
21
+ self.assertNotIn("\033[", output)
22
+
23
+ def test_format_with_color(self):
24
+ formatter = ColouredFormatter(use_color=True, datefmt="%Y")
25
+ record = logging.LogRecord("test", logging.INFO, __file__, 1, "hello", (), None)
26
+ output = formatter.format(record)
27
+ self.assertIn("[INFO] hello", output)
28
+ self.assertIn("\033[", output)
29
+
30
+ def test_all_log_levels_have_colors(self):
31
+ levels = {
32
+ logging.DEBUG: "DEBUG",
33
+ logging.INFO: "INFO",
34
+ SUCCESS_LEVEL: "SUCCESS",
35
+ logging.WARNING: "WARNING",
36
+ logging.ERROR: "ERROR",
37
+ logging.CRITICAL: "CRITICAL",
38
+ }
39
+ for levelno, levelname in levels.items():
40
+ formatter = ColouredFormatter(use_color=True)
41
+ record = logging.LogRecord("test", levelno, __file__, 1, "msg", (), None)
42
+ output = formatter.format(record)
43
+ self.assertIn(f"[{levelname}] msg", output)
44
+ self.assertIn("\033[", output)
45
+
46
+ def test_unknown_level_returns_uncolored(self):
47
+ formatter = ColouredFormatter(use_color=True)
48
+ record = logging.LogRecord("test", 99, __file__, 1, "msg", (), None)
49
+ output = formatter.format(record)
50
+ self.assertIn("[Level 99] msg", output)
51
+ self.assertNotIn("\033[", output)
52
+
53
+ def test_default_date_format(self):
54
+ formatter = ColouredFormatter(use_color=False)
55
+ self.assertEqual(formatter.datefmt, "%d/%m/%Y")
56
+
57
+ def test_custom_date_format(self):
58
+ formatter = ColouredFormatter(datefmt="%H:%M", use_color=False)
59
+ self.assertEqual(formatter.datefmt, "%H:%M")
60
+
61
+ def test_default_message_format(self):
62
+ formatter = ColouredFormatter(use_color=False)
63
+ record = logging.LogRecord("test", logging.INFO, __file__, 1, "hello world", (), None)
64
+ output = formatter.format(record)
65
+ self.assertIn("[INFO] hello world", output)
66
+ self.assertNotIn("\033[", output)
67
+
68
+ def test_custom_message_format(self):
69
+ formatter = ColouredFormatter(fmt="%(levelname)s: %(message)s", use_color=False)
70
+ record = logging.LogRecord("test", logging.ERROR, __file__, 1, "oops", (), None)
71
+ output = formatter.format(record)
72
+ self.assertEqual(output, "ERROR: oops")
73
+
74
+
75
+ class SetupLoggingTests(unittest.TestCase):
76
+ def _make_stream_logger(self, **kwargs):
77
+ stream = io.StringIO()
78
+ logger = setup_logging(stream=stream, use_color=False, datefmt="%Y", **kwargs)
79
+ return logger, stream
80
+
81
+ def test_default_level_is_notset(self):
82
+ logger, _ = self._make_stream_logger(logger_name="test_notset")
83
+ self.assertEqual(logger.level, logging.NOTSET)
84
+
85
+ def test_custom_level(self):
86
+ logger, _ = self._make_stream_logger(logger_name="test_level_warn", level=logging.WARNING)
87
+ self.assertEqual(logger.level, logging.WARNING)
88
+
89
+ def test_default_level_does_not_override_existing_logger_level(self):
90
+ logger_name = "test_preserve_level"
91
+ original = logging.getLogger(logger_name)
92
+ original.setLevel(logging.ERROR)
93
+
94
+ logger, _ = self._make_stream_logger(logger_name=logger_name)
95
+ self.assertEqual(logger.level, logging.ERROR)
96
+
97
+ def test_handler_deduplication(self):
98
+ logger1 = setup_logging(logger_name="test_dedup", use_color=False)
99
+ count1 = len(logger1.handlers)
100
+ logger2 = setup_logging(logger_name="test_dedup", use_color=False)
101
+ count2 = len(logger2.handlers)
102
+ self.assertEqual(count1, count2)
103
+ self.assertGreaterEqual(count1, 1)
104
+
105
+ def test_success_level_output(self):
106
+ logger, stream = self._make_stream_logger(
107
+ logger_name="test_success", level=logging.DEBUG
108
+ )
109
+ logger.success("done")
110
+ value = stream.getvalue()
111
+ self.assertIn("[SUCCESS] done", value)
112
+
113
+ def test_debug_output(self):
114
+ logger, stream = self._make_stream_logger(
115
+ logger_name="test_debug", level=logging.DEBUG
116
+ )
117
+ logger.debug("trace")
118
+ self.assertIn("[DEBUG] trace", stream.getvalue())
119
+
120
+ def test_info_output(self):
121
+ logger, stream = self._make_stream_logger(
122
+ logger_name="test_info", level=logging.DEBUG
123
+ )
124
+ logger.info("running")
125
+ self.assertIn("[INFO] running", stream.getvalue())
126
+
127
+ def test_warning_output(self):
128
+ logger, stream = self._make_stream_logger(
129
+ logger_name="test_warn", level=logging.DEBUG
130
+ )
131
+ logger.warning("caution")
132
+ self.assertIn("[WARNING] caution", stream.getvalue())
133
+
134
+ def test_error_output(self):
135
+ logger, stream = self._make_stream_logger(
136
+ logger_name="test_err", level=logging.DEBUG
137
+ )
138
+ logger.error("fail")
139
+ self.assertIn("[ERROR] fail", stream.getvalue())
140
+
141
+ def test_critical_output(self):
142
+ logger, stream = self._make_stream_logger(
143
+ logger_name="test_crit", level=logging.DEBUG
144
+ )
145
+ logger.critical("boom")
146
+ self.assertIn("[CRITICAL] boom", stream.getvalue())
147
+
148
+ def test_respects_level_filtering(self):
149
+ logger, stream = self._make_stream_logger(
150
+ logger_name="test_filter", level=logging.WARNING
151
+ )
152
+ logger.info("should not appear")
153
+ logger.warning("should appear")
154
+ value = stream.getvalue()
155
+ self.assertNotIn("should not appear", value)
156
+ self.assertIn("should appear", value)
157
+
158
+
159
+ class GetLoggerTests(unittest.TestCase):
160
+ def test_get_logger_returns_logger(self):
161
+ logger = get_logger("test_gl", use_color=False)
162
+ self.assertIsInstance(logger, logging.Logger)
163
+ self.assertEqual(logger.name, "test_gl")
164
+
165
+ def test_get_logger_default_level(self):
166
+ logger = get_logger("test_gl_default", use_color=False)
167
+ self.assertEqual(logger.level, logging.NOTSET)
168
+
169
+
170
+ class EnvironmentVariableTests(unittest.TestCase):
171
+ def setUp(self):
172
+ self._saved = {}
173
+ for var in (
174
+ "COLOURED_LOGGER_COLOR",
175
+ "FLASK_LOG_COLOR",
176
+ "COLOURED_LOGGER_DATE_FORMAT",
177
+ "FLASK_LOG_DATE_FORMAT",
178
+ ):
179
+ self._saved[var] = os.environ.pop(var, None)
180
+
181
+ def tearDown(self):
182
+ for var, value in self._saved.items():
183
+ if value is not None:
184
+ os.environ[var] = value
185
+ elif var in os.environ:
186
+ del os.environ[var]
187
+
188
+ def test_colour_env_var_true(self):
189
+ os.environ["COLOURED_LOGGER_COLOR"] = "true"
190
+ formatter = ColouredFormatter()
191
+ self.assertTrue(formatter.use_color)
192
+
193
+ def test_colour_env_var_false(self):
194
+ os.environ["COLOURED_LOGGER_COLOR"] = "false"
195
+ formatter = ColouredFormatter()
196
+ self.assertFalse(formatter.use_color)
197
+
198
+ def test_flask_color_overrides_coloured_logger_color(self):
199
+ os.environ["COLOURED_LOGGER_COLOR"] = "true"
200
+ os.environ["FLASK_LOG_COLOR"] = "false"
201
+ formatter = ColouredFormatter()
202
+ self.assertFalse(formatter.use_color)
203
+
204
+ def test_date_format_env_var(self):
205
+ os.environ["COLOURED_LOGGER_DATE_FORMAT"] = "%H:%M"
206
+ formatter = ColouredFormatter(use_color=False)
207
+ self.assertEqual(formatter.datefmt, "%H:%M")
208
+
209
+ def test_flask_date_format_overrides_coloured_date_format(self):
210
+ os.environ["COLOURED_LOGGER_DATE_FORMAT"] = "%H:%M"
211
+ os.environ["FLASK_LOG_DATE_FORMAT"] = "%Y"
212
+ formatter = ColouredFormatter(use_color=False)
213
+ self.assertEqual(formatter.datefmt, "%Y")
214
+
215
+
216
+ if __name__ == "__main__":
217
+ unittest.main()
@@ -1,3 +0,0 @@
1
- from .formatter import ColouredFormatter, SUCCESS_LEVEL, get_logger, log, setup_logging
2
-
3
- __all__ = ["ColouredFormatter", "SUCCESS_LEVEL", "get_logger", "log", "setup_logging"]
@@ -1,31 +0,0 @@
1
- import io
2
- import logging
3
- import unittest
4
-
5
- from coloured_logger import ColouredFormatter, setup_logging
6
-
7
-
8
- class FormatterTests(unittest.TestCase):
9
- def test_formatter_without_color(self):
10
- formatter = ColouredFormatter(use_color=False, datefmt="%Y")
11
- record = logging.LogRecord("test", logging.INFO, __file__, 1, "hello", (), None)
12
- output = formatter.format(record)
13
- self.assertIn("[INFO] hello", output)
14
- self.assertNotIn("\033[", output)
15
-
16
- def test_logger_adds_success_level_output(self):
17
- stream = io.StringIO()
18
- logger = setup_logging(
19
- logger_name="coloured_logger_test",
20
- level=logging.DEBUG,
21
- stream=stream,
22
- use_color=False,
23
- datefmt="%Y",
24
- )
25
- logger.success("done")
26
- value = stream.getvalue()
27
- self.assertIn("[SUCCESS] done", value)
28
-
29
-
30
- if __name__ == "__main__":
31
- unittest.main()