custom-python-logger 3.0.0__tar.gz → 4.0.0__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.
Files changed (19) hide show
  1. {custom_python_logger-3.0.0/custom_python_logger.egg-info → custom_python_logger-4.0.0}/PKG-INFO +50 -11
  2. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/README.md +49 -4
  3. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger/__init__.py +7 -0
  4. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger/consts.py +5 -0
  5. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger/logger.py +32 -27
  6. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0/custom_python_logger.egg-info}/PKG-INFO +50 -11
  7. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger.egg-info/SOURCES.txt +1 -0
  8. custom_python_logger-4.0.0/custom_python_logger.egg-info/requires.txt +2 -0
  9. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/pyproject.toml +6 -3
  10. custom_python_logger-4.0.0/tests/test_short_path_filter.py +161 -0
  11. custom_python_logger-3.0.0/custom_python_logger.egg-info/requires.txt +0 -8
  12. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/LICENSE +0 -0
  13. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/MANIFEST.in +0 -0
  14. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger.egg-info/dependency_links.txt +0 -0
  15. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/custom_python_logger.egg-info/top_level.txt +0 -0
  16. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/setup.cfg +0 -0
  17. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/tests/test_logger.py +0 -0
  18. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/tests/test_logger_pytest.py +0 -0
  19. {custom_python_logger-3.0.0 → custom_python_logger-4.0.0}/tests/test_usage_example_pytest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: custom-python-logger
3
- Version: 3.0.0
3
+ Version: 4.0.0
4
4
  Summary: A custom logger with color support and additional features.
5
5
  Author: Avi Zaguri
6
6
  License: MIT
@@ -17,13 +17,7 @@ Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: colorlog>=6.10.1
20
- Requires-Dist: pathlib>=1.0.1
21
- Requires-Dist: pre-commit>=4.5.0
22
- Requires-Dist: pytest>=9.0.1
23
- Requires-Dist: python-dotenv>=1.2.1
24
20
  Requires-Dist: pyyaml>=6.0.3
25
- Requires-Dist: setuptools>=80.9.0
26
- Requires-Dist: wheel>=0.45.1
27
21
  Dynamic: license-file
28
22
 
29
23
  ![PyPI version](https://img.shields.io/pypi/v/custom-python-logger)
@@ -48,6 +42,7 @@ Easily integrate structured, readable, and context-rich logging into your Python
48
42
  - ✅ **Contextual Logging**: Add extra fields (like user, environment, etc.) to every log message.
49
43
  - ✅ **UTC Support**: Optionally log timestamps in UTC for consistency across environments.
50
44
  - ✅ **Pretty Formatting**: Built-in helpers for pretty-printing JSON and YAML data in logs.
45
+ - ✅ **Short Path Display**: Automatically trims log file paths to project-relative or `.venv`-relative format for cleaner output.
51
46
  - ✅ **Easy Integration**: Simple API for getting a ready-to-use logger anywhere in your codebase.
52
47
 
53
48
  ---
@@ -86,6 +81,7 @@ logger.critical("This is a critical message.")
86
81
  ```
87
82
 
88
83
  #### Advanced Usage
84
+
89
85
  - Log to a file:
90
86
  ```python
91
87
  from custom_python_logger import build_logger
@@ -117,19 +113,62 @@ logger.critical("This is a critical message.")
117
113
  logger.info(yaml_pretty_format({'foo': 'bar'}))
118
114
  ```
119
115
 
120
- - use an existing logger (CustomLoggerAdapter) and set a custom name:
116
+ - Use an existing logger with a custom name:
121
117
  ```python
122
118
  from custom_python_logger import get_logger
123
119
 
124
120
  logger = get_logger('some-name')
125
121
 
126
- logger.debug("This is a debug message.")
127
- logger.info("This is an info message.")
128
- logger.step("This is a step message.")
122
+ logger.debug("This is a debug message.")
123
+ logger.info("This is an info message.")
124
+ logger.step("This is a step message.")
125
+ ```
126
+
127
+ - Use a custom log format:
128
+ ```python
129
+ from custom_python_logger import build_logger, LOG_FORMAT_FILENAME, LOG_FORMAT_SHORTPATH
130
+
131
+ # Default — shows project-relative or .venv-relative path:
132
+ # 2026-05-18 | INFO | l.20 | my_app | my_project/app/main.py:42 | message
133
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_SHORTPATH)
134
+
135
+ # Classic — shows filename only (no path):
136
+ # 2026-05-18 | INFO | l.20 | my_app | main.py:42 | message
137
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_FILENAME)
129
138
  ```
130
139
 
131
140
  ---
132
141
 
142
+ ## 🗂️ Short Path Display
143
+
144
+ By default, `build_logger` uses `LOG_FORMAT_SHORTPATH`, which trims the file path in every log line:
145
+
146
+ | Path type | Raw `record.pathname` | Displayed as |
147
+ |---|---|---|
148
+ | Project file | `/home/user/my_project/app/main.py` | `my_project/app/main.py` |
149
+ | Dependency in `.venv` | `/home/user/my_project/.venv/lib/python3.13/site-packages/urllib3/pool.py` | `.venv/lib/python3.13/site-packages/urllib3/pool.py` |
150
+ | Unrecognised path | `/tmp/some_script.py` | `/tmp/some_script.py` (full path) |
151
+
152
+ ### Setting your project name
153
+
154
+ The short-path logic uses the `PROJECT_NAME` environment variable to identify your project root.
155
+ Set it in your `.env` file (loaded automatically on import) or export it before running:
156
+
157
+ ```bash
158
+ # .env
159
+ PROJECT_NAME=my_project
160
+ ```
161
+
162
+ ```bash
163
+ # or inline
164
+ PROJECT_NAME=my_project python main.py
165
+ ```
166
+
167
+ > **Note:** `custom-python-logger` calls `load_dotenv()` on import, which reads your `.env` file automatically.
168
+ > If you set `PROJECT_NAME` programmatically, do so **before** importing `custom_python_logger` to ensure it takes effect.
169
+
170
+ ---
171
+
133
172
  ## 🤝 Contributing
134
173
  If you have a helpful tool, pattern, or improvement to suggest:
135
174
  Fork the repo <br>
@@ -20,6 +20,7 @@ Easily integrate structured, readable, and context-rich logging into your Python
20
20
  - ✅ **Contextual Logging**: Add extra fields (like user, environment, etc.) to every log message.
21
21
  - ✅ **UTC Support**: Optionally log timestamps in UTC for consistency across environments.
22
22
  - ✅ **Pretty Formatting**: Built-in helpers for pretty-printing JSON and YAML data in logs.
23
+ - ✅ **Short Path Display**: Automatically trims log file paths to project-relative or `.venv`-relative format for cleaner output.
23
24
  - ✅ **Easy Integration**: Simple API for getting a ready-to-use logger anywhere in your codebase.
24
25
 
25
26
  ---
@@ -58,6 +59,7 @@ logger.critical("This is a critical message.")
58
59
  ```
59
60
 
60
61
  #### Advanced Usage
62
+
61
63
  - Log to a file:
62
64
  ```python
63
65
  from custom_python_logger import build_logger
@@ -89,19 +91,62 @@ logger.critical("This is a critical message.")
89
91
  logger.info(yaml_pretty_format({'foo': 'bar'}))
90
92
  ```
91
93
 
92
- - use an existing logger (CustomLoggerAdapter) and set a custom name:
94
+ - Use an existing logger with a custom name:
93
95
  ```python
94
96
  from custom_python_logger import get_logger
95
97
 
96
98
  logger = get_logger('some-name')
97
99
 
98
- logger.debug("This is a debug message.")
99
- logger.info("This is an info message.")
100
- logger.step("This is a step message.")
100
+ logger.debug("This is a debug message.")
101
+ logger.info("This is an info message.")
102
+ logger.step("This is a step message.")
103
+ ```
104
+
105
+ - Use a custom log format:
106
+ ```python
107
+ from custom_python_logger import build_logger, LOG_FORMAT_FILENAME, LOG_FORMAT_SHORTPATH
108
+
109
+ # Default — shows project-relative or .venv-relative path:
110
+ # 2026-05-18 | INFO | l.20 | my_app | my_project/app/main.py:42 | message
111
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_SHORTPATH)
112
+
113
+ # Classic — shows filename only (no path):
114
+ # 2026-05-18 | INFO | l.20 | my_app | main.py:42 | message
115
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_FILENAME)
101
116
  ```
102
117
 
103
118
  ---
104
119
 
120
+ ## 🗂️ Short Path Display
121
+
122
+ By default, `build_logger` uses `LOG_FORMAT_SHORTPATH`, which trims the file path in every log line:
123
+
124
+ | Path type | Raw `record.pathname` | Displayed as |
125
+ |---|---|---|
126
+ | Project file | `/home/user/my_project/app/main.py` | `my_project/app/main.py` |
127
+ | Dependency in `.venv` | `/home/user/my_project/.venv/lib/python3.13/site-packages/urllib3/pool.py` | `.venv/lib/python3.13/site-packages/urllib3/pool.py` |
128
+ | Unrecognised path | `/tmp/some_script.py` | `/tmp/some_script.py` (full path) |
129
+
130
+ ### Setting your project name
131
+
132
+ The short-path logic uses the `PROJECT_NAME` environment variable to identify your project root.
133
+ Set it in your `.env` file (loaded automatically on import) or export it before running:
134
+
135
+ ```bash
136
+ # .env
137
+ PROJECT_NAME=my_project
138
+ ```
139
+
140
+ ```bash
141
+ # or inline
142
+ PROJECT_NAME=my_project python main.py
143
+ ```
144
+
145
+ > **Note:** `custom-python-logger` calls `load_dotenv()` on import, which reads your `.env` file automatically.
146
+ > If you set `PROJECT_NAME` programmatically, do so **before** importing `custom_python_logger` to ensure it takes effect.
147
+
148
+ ---
149
+
105
150
  ## 🤝 Contributing
106
151
  If you have a helpful tool, pattern, or improvement to suggest:
107
152
  Fork the repo <br>
@@ -1,3 +1,6 @@
1
+ from dotenv import load_dotenv
2
+
3
+ from custom_python_logger.consts import LOG_FORMAT_FILENAME, LOG_FORMAT_SHORTPATH
1
4
  from custom_python_logger.logger import (
2
5
  CustomLoggerAdapter,
3
6
  CustomLoggerLevel,
@@ -7,6 +10,8 @@ from custom_python_logger.logger import (
7
10
  yaml_pretty_format,
8
11
  )
9
12
 
13
+ load_dotenv()
14
+
10
15
  __all__ = [
11
16
  "CustomLoggerAdapter",
12
17
  "CustomLoggerLevel",
@@ -14,4 +19,6 @@ __all__ = [
14
19
  "get_logger",
15
20
  "json_pretty_format",
16
21
  "yaml_pretty_format",
22
+ "LOG_FORMAT_SHORTPATH",
23
+ "LOG_FORMAT_FILENAME",
17
24
  ]
@@ -1,5 +1,10 @@
1
1
  from enum import Enum
2
2
 
3
+ LOG_FORMAT_FILENAME = "%(asctime)s | %(levelname)-9s | l.%(levelno)s | %(name)s | %(filename)s:%(lineno)s | %(message)s"
4
+ LOG_FORMAT_SHORTPATH = (
5
+ "%(asctime)s | %(levelname)-9s | l.%(levelno)s | %(name)s | %(shortpath)s:%(lineno)s | %(message)s"
6
+ )
7
+
3
8
  LOG_COLORS = {
4
9
  "DEBUG": "white",
5
10
  "INFO": "green",
@@ -10,7 +10,7 @@ from typing import Any
10
10
  import yaml
11
11
  from colorlog import ColoredFormatter
12
12
 
13
- from custom_python_logger.consts import LOG_COLORS, CustomLoggerLevel
13
+ from custom_python_logger.consts import LOG_COLORS, LOG_FORMAT_SHORTPATH, CustomLoggerLevel
14
14
 
15
15
  CUSTOM_LOGGER = "custom_logger"
16
16
 
@@ -48,6 +48,22 @@ def print_before_logger(project_name: str, sleep_time: float = 0.3) -> None:
48
48
  time.sleep(sleep_time)
49
49
 
50
50
 
51
+ class _ShortPathFilter(logging.Filter):
52
+ def __init__(self) -> None:
53
+ super().__init__()
54
+ self._project_name = os.getenv("PROJECT_NAME")
55
+
56
+ def filter(self, record: logging.LogRecord) -> bool:
57
+ if self._project_name and self._project_name in record.pathname:
58
+ record.shortpath = self._project_name + record.pathname.rsplit(self._project_name, 1)[1]
59
+ else:
60
+ record.shortpath = record.pathname
61
+
62
+ if ".venv" in record.pathname:
63
+ record.shortpath = ".venv" + record.pathname.rsplit(".venv", 1)[1]
64
+ return True
65
+
66
+
51
67
  class CustomLoggerAdapter(logging.LoggerAdapter):
52
68
  def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
53
69
  logging.addLevelName(CustomLoggerLevel.EXCEPTION.value, "EXCEPTION")
@@ -80,11 +96,7 @@ def clear_existing_handlers(logger: Logger) -> None:
80
96
  logger.removeHandler(handler)
81
97
 
82
98
 
83
- def add_file_handler_if_specified(
84
- logger: Logger,
85
- log_file_path: str | None,
86
- log_format: str,
87
- ) -> None:
99
+ def add_file_handler(logger: Logger, log_file_path: str | None, log_format: str) -> None:
88
100
  if log_file_path is not None:
89
101
  log_file_formatter = logging.Formatter(log_format)
90
102
 
@@ -96,16 +108,15 @@ def add_file_handler_if_specified(
96
108
  logger.addHandler(file_handler)
97
109
 
98
110
 
99
- def add_console_handler_if_specified(logger: Logger, console_output: bool, log_format: str) -> None:
100
- if console_output:
101
- log_console_formatter = ColoredFormatter(
102
- "%(log_color)s " + log_format,
103
- log_colors=LOG_COLORS,
104
- )
111
+ def add_console_handler(logger: Logger, log_format: str) -> None:
112
+ log_console_formatter = ColoredFormatter(
113
+ "%(log_color)s " + log_format,
114
+ log_colors=LOG_COLORS,
115
+ )
105
116
 
106
- console_handler = logging.StreamHandler()
107
- console_handler.setFormatter(log_console_formatter)
108
- logger.addHandler(console_handler)
117
+ console_handler = logging.StreamHandler()
118
+ console_handler.setFormatter(log_console_formatter)
119
+ logger.addHandler(console_handler)
109
120
 
110
121
 
111
122
  def get_logger(name: str, log_level: int | None = None, extra: dict | None = None) -> CustomLoggerAdapter:
@@ -123,7 +134,7 @@ def get_logger(name: str, log_level: int | None = None, extra: dict | None = Non
123
134
  def build_logger( # pylint: disable=R0913
124
135
  project_name: str,
125
136
  extra: dict[str, Any] | None = None,
126
- log_format: str = "%(asctime)s | %(levelname)-9s | l.%(levelno)s | %(name)s | %(filename)s:%(lineno)s | %(message)s", # pylint: disable=C0301
137
+ log_format: str = LOG_FORMAT_SHORTPATH,
127
138
  log_level: int = logging.DEBUG,
128
139
  log_file: bool = False,
129
140
  log_file_path: str | None = None,
@@ -150,25 +161,19 @@ def build_logger( # pylint: disable=R0913
150
161
  logging.Formatter.converter = time.gmtime
151
162
 
152
163
  root_logger = logging.getLogger()
153
-
154
164
  clear_existing_handlers(logger=root_logger)
155
165
 
156
- add_console_handler_if_specified(
157
- logger=root_logger,
158
- console_output=console_output,
159
- log_format=log_format,
160
- )
166
+ if console_output:
167
+ add_console_handler(logger=root_logger, log_format=log_format)
161
168
 
162
169
  if log_file:
163
170
  if not log_file_path:
164
171
  log_file_path = f"{get_project_path_by_file()}/logs/{project_name}.log"
165
172
  log_file_path = log_file_path.lower().replace(" ", "_")
173
+ add_file_handler(logger=root_logger, log_file_path=log_file_path, log_format=log_format)
166
174
 
167
- add_file_handler_if_specified(
168
- logger=root_logger,
169
- log_file_path=log_file_path,
170
- log_format=log_format,
171
- )
175
+ for handler in root_logger.handlers:
176
+ handler.addFilter(_ShortPathFilter())
172
177
 
173
178
  logger = CustomLoggerAdapter(logging.getLogger(CUSTOM_LOGGER), extra)
174
179
  logger.setLevel(log_level)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: custom-python-logger
3
- Version: 3.0.0
3
+ Version: 4.0.0
4
4
  Summary: A custom logger with color support and additional features.
5
5
  Author: Avi Zaguri
6
6
  License: MIT
@@ -17,13 +17,7 @@ Requires-Python: >=3.12
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: colorlog>=6.10.1
20
- Requires-Dist: pathlib>=1.0.1
21
- Requires-Dist: pre-commit>=4.5.0
22
- Requires-Dist: pytest>=9.0.1
23
- Requires-Dist: python-dotenv>=1.2.1
24
20
  Requires-Dist: pyyaml>=6.0.3
25
- Requires-Dist: setuptools>=80.9.0
26
- Requires-Dist: wheel>=0.45.1
27
21
  Dynamic: license-file
28
22
 
29
23
  ![PyPI version](https://img.shields.io/pypi/v/custom-python-logger)
@@ -48,6 +42,7 @@ Easily integrate structured, readable, and context-rich logging into your Python
48
42
  - ✅ **Contextual Logging**: Add extra fields (like user, environment, etc.) to every log message.
49
43
  - ✅ **UTC Support**: Optionally log timestamps in UTC for consistency across environments.
50
44
  - ✅ **Pretty Formatting**: Built-in helpers for pretty-printing JSON and YAML data in logs.
45
+ - ✅ **Short Path Display**: Automatically trims log file paths to project-relative or `.venv`-relative format for cleaner output.
51
46
  - ✅ **Easy Integration**: Simple API for getting a ready-to-use logger anywhere in your codebase.
52
47
 
53
48
  ---
@@ -86,6 +81,7 @@ logger.critical("This is a critical message.")
86
81
  ```
87
82
 
88
83
  #### Advanced Usage
84
+
89
85
  - Log to a file:
90
86
  ```python
91
87
  from custom_python_logger import build_logger
@@ -117,19 +113,62 @@ logger.critical("This is a critical message.")
117
113
  logger.info(yaml_pretty_format({'foo': 'bar'}))
118
114
  ```
119
115
 
120
- - use an existing logger (CustomLoggerAdapter) and set a custom name:
116
+ - Use an existing logger with a custom name:
121
117
  ```python
122
118
  from custom_python_logger import get_logger
123
119
 
124
120
  logger = get_logger('some-name')
125
121
 
126
- logger.debug("This is a debug message.")
127
- logger.info("This is an info message.")
128
- logger.step("This is a step message.")
122
+ logger.debug("This is a debug message.")
123
+ logger.info("This is an info message.")
124
+ logger.step("This is a step message.")
125
+ ```
126
+
127
+ - Use a custom log format:
128
+ ```python
129
+ from custom_python_logger import build_logger, LOG_FORMAT_FILENAME, LOG_FORMAT_SHORTPATH
130
+
131
+ # Default — shows project-relative or .venv-relative path:
132
+ # 2026-05-18 | INFO | l.20 | my_app | my_project/app/main.py:42 | message
133
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_SHORTPATH)
134
+
135
+ # Classic — shows filename only (no path):
136
+ # 2026-05-18 | INFO | l.20 | my_app | main.py:42 | message
137
+ logger = build_logger(project_name='MyApp', log_format=LOG_FORMAT_FILENAME)
129
138
  ```
130
139
 
131
140
  ---
132
141
 
142
+ ## 🗂️ Short Path Display
143
+
144
+ By default, `build_logger` uses `LOG_FORMAT_SHORTPATH`, which trims the file path in every log line:
145
+
146
+ | Path type | Raw `record.pathname` | Displayed as |
147
+ |---|---|---|
148
+ | Project file | `/home/user/my_project/app/main.py` | `my_project/app/main.py` |
149
+ | Dependency in `.venv` | `/home/user/my_project/.venv/lib/python3.13/site-packages/urllib3/pool.py` | `.venv/lib/python3.13/site-packages/urllib3/pool.py` |
150
+ | Unrecognised path | `/tmp/some_script.py` | `/tmp/some_script.py` (full path) |
151
+
152
+ ### Setting your project name
153
+
154
+ The short-path logic uses the `PROJECT_NAME` environment variable to identify your project root.
155
+ Set it in your `.env` file (loaded automatically on import) or export it before running:
156
+
157
+ ```bash
158
+ # .env
159
+ PROJECT_NAME=my_project
160
+ ```
161
+
162
+ ```bash
163
+ # or inline
164
+ PROJECT_NAME=my_project python main.py
165
+ ```
166
+
167
+ > **Note:** `custom-python-logger` calls `load_dotenv()` on import, which reads your `.env` file automatically.
168
+ > If you set `PROJECT_NAME` programmatically, do so **before** importing `custom_python_logger` to ensure it takes effect.
169
+
170
+ ---
171
+
133
172
  ## 🤝 Contributing
134
173
  If you have a helpful tool, pattern, or improvement to suggest:
135
174
  Fork the repo <br>
@@ -12,4 +12,5 @@ custom_python_logger.egg-info/requires.txt
12
12
  custom_python_logger.egg-info/top_level.txt
13
13
  tests/test_logger.py
14
14
  tests/test_logger_pytest.py
15
+ tests/test_short_path_filter.py
15
16
  tests/test_usage_example_pytest.py
@@ -0,0 +1,2 @@
1
+ colorlog>=6.10.1
2
+ pyyaml>=6.0.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "custom-python-logger"
7
- version = "3.0.0"
7
+ version = "4.0.0"
8
8
  description = "A custom logger with color support and additional features."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -24,11 +24,14 @@ classifiers = [
24
24
 
25
25
  dependencies = [
26
26
  "colorlog>=6.10.1",
27
- "pathlib>=1.0.1",
27
+ "pyyaml>=6.0.3",
28
+ ]
29
+
30
+ [dependency-groups]
31
+ dev = [
28
32
  "pre-commit>=4.5.0",
29
33
  "pytest>=9.0.1",
30
34
  "python-dotenv>=1.2.1",
31
- "pyyaml>=6.0.3",
32
35
  "setuptools>=80.9.0",
33
36
  "wheel>=0.45.1",
34
37
  ]
@@ -0,0 +1,161 @@
1
+ # pylint: disable=E1101 # LogRecord.shortpath is a dynamic attribute set by _ShortPathFilter
2
+ import logging
3
+ import os
4
+ import tempfile
5
+ from collections.abc import Generator
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ from custom_python_logger import build_logger
11
+ from custom_python_logger.logger import _ShortPathFilter
12
+
13
+
14
+ def _make_record(pathname: str, name: str = "custom_logger.test") -> logging.LogRecord:
15
+ record = logging.LogRecord(
16
+ name=name,
17
+ level=logging.DEBUG,
18
+ pathname=pathname,
19
+ lineno=1,
20
+ msg="test",
21
+ args=(),
22
+ exc_info=None,
23
+ )
24
+ record.pathname = pathname
25
+ return record
26
+
27
+
28
+ @pytest.fixture
29
+ def temp_log_file() -> Generator[str, None, None]: # pylint: disable=W0621
30
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".log") as f:
31
+ yield f.name
32
+ os.remove(f.name)
33
+
34
+
35
+ class TestShortPathFilterInit:
36
+ def test_caches_project_name_from_env(self) -> None:
37
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
38
+ f = _ShortPathFilter()
39
+ assert f._project_name == "my_project", f"Expected 'my_project', got {f._project_name}" # pylint: disable=W0212
40
+
41
+ def test_project_name_none_when_env_not_set(self) -> None:
42
+ with patch.dict(os.environ, {}, clear=True):
43
+ f = _ShortPathFilter()
44
+ assert f._project_name is None, f"Expected None, got {f._project_name}" # pylint: disable=W0212
45
+
46
+
47
+ class TestShortPathFilterLogic:
48
+ def test_project_name_in_path_sets_relative_shortpath(self) -> None:
49
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
50
+ f = _ShortPathFilter()
51
+ record = _make_record("/home/user/my_project/app/main.py")
52
+
53
+ result = f.filter(record)
54
+
55
+ assert result is True, "filter() must always return True"
56
+ assert (
57
+ record.shortpath == "my_project/app/main.py"
58
+ ), ( # pylint: disable=E1101
59
+ f"Expected 'my_project/app/main.py', got '{record.shortpath}'" # pylint: disable=E1101
60
+ )
61
+
62
+ def test_project_name_not_in_path_falls_back_to_full_pathname(self) -> None:
63
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
64
+ f = _ShortPathFilter()
65
+ record = _make_record("/home/user/other_project/app/main.py")
66
+
67
+ f.filter(record)
68
+
69
+ assert (
70
+ record.shortpath == "/home/user/other_project/app/main.py"
71
+ ), f"Expected full pathname, got '{record.shortpath}'" # pylint: disable=E1101 # pylint: disable=E1101
72
+
73
+ def test_no_project_name_env_falls_back_to_full_pathname(self) -> None:
74
+ with patch.dict(os.environ, {}, clear=True):
75
+ f = _ShortPathFilter()
76
+ record = _make_record("/home/user/my_project/app/main.py")
77
+
78
+ f.filter(record)
79
+
80
+ assert (
81
+ record.shortpath == "/home/user/my_project/app/main.py"
82
+ ), f"Expected full pathname, got '{record.shortpath}'" # pylint: disable=E1101 # pylint: disable=E1101
83
+
84
+ def test_venv_in_path_sets_venv_relative_shortpath(self) -> None:
85
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
86
+ f = _ShortPathFilter()
87
+ record = _make_record("/home/user/my_project/.venv/lib/python3.13/site-packages/urllib3/pool.py")
88
+
89
+ f.filter(record)
90
+
91
+ assert (
92
+ record.shortpath == ".venv/lib/python3.13/site-packages/urllib3/pool.py"
93
+ ), f"Expected .venv-relative path, got '{record.shortpath}'" # pylint: disable=E1101 # pylint: disable=E1101
94
+
95
+ def test_venv_takes_precedence_over_project_name(self) -> None:
96
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
97
+ f = _ShortPathFilter()
98
+ record = _make_record("/home/user/my_project/.venv/lib/python3.13/site-packages/requests/api.py")
99
+
100
+ f.filter(record)
101
+
102
+ assert record.shortpath.startswith(
103
+ ".venv/"
104
+ ), f"Expected .venv-relative path, got '{record.shortpath}'" # pylint: disable=E1101 # pylint: disable=E1101
105
+
106
+ def test_filter_always_returns_true(self) -> None:
107
+ with patch.dict(os.environ, {"PROJECT_NAME": "my_project"}):
108
+ f = _ShortPathFilter()
109
+
110
+ for pathname in (
111
+ "/some/random/path.py",
112
+ "/my_project/app.py",
113
+ "/.venv/lib/pkg.py",
114
+ ):
115
+ record = _make_record(pathname)
116
+ assert f.filter(record) is True, f"filter() returned False for '{pathname}'"
117
+
118
+ def test_shortpath_always_set_on_record(self) -> None:
119
+ with patch.dict(os.environ, {}, clear=True):
120
+ f = _ShortPathFilter()
121
+ record = _make_record("/any/path/file.py")
122
+
123
+ f.filter(record)
124
+
125
+ assert hasattr(record, "shortpath"), "shortpath attribute must always be set on the record"
126
+
127
+
128
+ class TestBuildLoggerFilterIntegration:
129
+ def test_handler_has_short_path_filter_after_build(self) -> None:
130
+ root_logger = logging.getLogger()
131
+ build_logger(project_name="TestFilter", console_output=True)
132
+
133
+ filter_types = [type(f) for h in root_logger.handlers for f in h.filters]
134
+ assert _ShortPathFilter in filter_types, "Handlers must have _ShortPathFilter attached"
135
+
136
+ def test_calling_build_logger_twice_does_not_stack_filters(self) -> None:
137
+ build_logger(project_name="TestFilter", console_output=True)
138
+ build_logger(project_name="TestFilter", console_output=True)
139
+
140
+ root_logger = logging.getLogger()
141
+ for handler in root_logger.handlers:
142
+ short_path_filters = [f for f in handler.filters if isinstance(f, _ShortPathFilter)]
143
+ assert (
144
+ len(short_path_filters) == 1
145
+ ), f"Expected exactly 1 _ShortPathFilter per handler, found {len(short_path_filters)}"
146
+
147
+ def test_shortpath_set_on_logged_records(self, temp_log_file: str) -> None: # pylint: disable=W0621
148
+ with patch.dict(os.environ, {"PROJECT_NAME": "custom-python-logger"}):
149
+ logger = build_logger(
150
+ project_name="ShortPathTest",
151
+ log_file=True,
152
+ log_file_path=temp_log_file,
153
+ console_output=False,
154
+ )
155
+ logger.info("shortpath test message")
156
+
157
+ with open(temp_log_file) as f:
158
+ content = f.read()
159
+
160
+ assert "shortpath test message" in content, "Log message not found in file"
161
+ assert "custom-python-logger/" in content, f"Expected project-relative path in log output, got:\n{content}"
@@ -1,8 +0,0 @@
1
- colorlog>=6.10.1
2
- pathlib>=1.0.1
3
- pre-commit>=4.5.0
4
- pytest>=9.0.1
5
- python-dotenv>=1.2.1
6
- pyyaml>=6.0.3
7
- setuptools>=80.9.0
8
- wheel>=0.45.1