custom-python-logger 3.0.2__tar.gz → 4.0.1__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 (18) hide show
  1. {custom_python_logger-3.0.2/custom_python_logger.egg-info → custom_python_logger-4.0.1}/PKG-INFO +50 -5
  2. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/README.md +49 -4
  3. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger/__init__.py +7 -0
  4. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger/consts.py +5 -0
  5. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger/logger.py +33 -3
  6. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1/custom_python_logger.egg-info}/PKG-INFO +50 -5
  7. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger.egg-info/SOURCES.txt +1 -0
  8. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/pyproject.toml +1 -1
  9. custom_python_logger-4.0.1/tests/test_short_path_filter.py +161 -0
  10. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/LICENSE +0 -0
  11. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/MANIFEST.in +0 -0
  12. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger.egg-info/dependency_links.txt +0 -0
  13. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger.egg-info/requires.txt +0 -0
  14. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/custom_python_logger.egg-info/top_level.txt +0 -0
  15. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/setup.cfg +0 -0
  16. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/tests/test_logger.py +0 -0
  17. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/tests/test_logger_pytest.py +0 -0
  18. {custom_python_logger-3.0.2 → custom_python_logger-4.0.1}/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.2
3
+ Version: 4.0.1
4
4
  Summary: A custom logger with color support and additional features.
5
5
  Author: Avi Zaguri
6
6
  License: MIT
@@ -42,6 +42,7 @@ Easily integrate structured, readable, and context-rich logging into your Python
42
42
  - ✅ **Contextual Logging**: Add extra fields (like user, environment, etc.) to every log message.
43
43
  - ✅ **UTC Support**: Optionally log timestamps in UTC for consistency across environments.
44
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.
45
46
  - ✅ **Easy Integration**: Simple API for getting a ready-to-use logger anywhere in your codebase.
46
47
 
47
48
  ---
@@ -80,6 +81,7 @@ logger.critical("This is a critical message.")
80
81
  ```
81
82
 
82
83
  #### Advanced Usage
84
+
83
85
  - Log to a file:
84
86
  ```python
85
87
  from custom_python_logger import build_logger
@@ -111,19 +113,62 @@ logger.critical("This is a critical message.")
111
113
  logger.info(yaml_pretty_format({'foo': 'bar'}))
112
114
  ```
113
115
 
114
- - use an existing logger (CustomLoggerAdapter) and set a custom name:
116
+ - Use an existing logger with a custom name:
115
117
  ```python
116
118
  from custom_python_logger import get_logger
117
119
 
118
120
  logger = get_logger('some-name')
119
121
 
120
- logger.debug("This is a debug message.")
121
- logger.info("This is an info message.")
122
- 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)
123
138
  ```
124
139
 
125
140
  ---
126
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
+
127
172
  ## 🤝 Contributing
128
173
  If you have a helpful tool, pattern, or improvement to suggest:
129
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_FILENAME, CustomLoggerLevel
14
14
 
15
15
  CUSTOM_LOGGER = "custom_logger"
16
16
 
@@ -48,6 +48,30 @@ 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
+
67
+ class _StripNamespaceFilter(logging.Filter):
68
+ def filter(self, record: logging.LogRecord) -> bool:
69
+ prefix = CUSTOM_LOGGER + "."
70
+ if record.name.startswith(prefix):
71
+ record.name = record.name[len(prefix) :]
72
+ return True
73
+
74
+
51
75
  class CustomLoggerAdapter(logging.LoggerAdapter):
52
76
  def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
53
77
  logging.addLevelName(CustomLoggerLevel.EXCEPTION.value, "EXCEPTION")
@@ -105,8 +129,11 @@ def add_console_handler(logger: Logger, log_format: str) -> None:
105
129
 
106
130
  def get_logger(name: str, log_level: int | None = None, extra: dict | None = None) -> CustomLoggerAdapter:
107
131
  custom_logger = logging.getLogger(CUSTOM_LOGGER)
132
+
108
133
  full_name = f"{CUSTOM_LOGGER}.{name}"
109
- new_logger = CustomLoggerAdapter(logging.getLogger(full_name), extra=extra)
134
+ underlying = logging.getLogger(full_name)
135
+ underlying.addFilter(_StripNamespaceFilter())
136
+ new_logger = CustomLoggerAdapter(underlying, extra=extra)
110
137
 
111
138
  if log_level is None:
112
139
  log_level = custom_logger.level
@@ -118,7 +145,7 @@ def get_logger(name: str, log_level: int | None = None, extra: dict | None = Non
118
145
  def build_logger( # pylint: disable=R0913
119
146
  project_name: str,
120
147
  extra: dict[str, Any] | None = None,
121
- log_format: str = "%(asctime)s | %(levelname)-9s | l.%(levelno)s | %(name)s | %(filename)s:%(lineno)s | %(message)s", # pylint: disable=C0301
148
+ log_format: str = LOG_FORMAT_FILENAME,
122
149
  log_level: int = logging.DEBUG,
123
150
  log_file: bool = False,
124
151
  log_file_path: str | None = None,
@@ -156,6 +183,9 @@ def build_logger( # pylint: disable=R0913
156
183
  log_file_path = log_file_path.lower().replace(" ", "_")
157
184
  add_file_handler(logger=root_logger, log_file_path=log_file_path, log_format=log_format)
158
185
 
186
+ for handler in root_logger.handlers:
187
+ handler.addFilter(_ShortPathFilter())
188
+
159
189
  logger = CustomLoggerAdapter(logging.getLogger(CUSTOM_LOGGER), extra)
160
190
  logger.setLevel(log_level)
161
191
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: custom-python-logger
3
- Version: 3.0.2
3
+ Version: 4.0.1
4
4
  Summary: A custom logger with color support and additional features.
5
5
  Author: Avi Zaguri
6
6
  License: MIT
@@ -42,6 +42,7 @@ Easily integrate structured, readable, and context-rich logging into your Python
42
42
  - ✅ **Contextual Logging**: Add extra fields (like user, environment, etc.) to every log message.
43
43
  - ✅ **UTC Support**: Optionally log timestamps in UTC for consistency across environments.
44
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.
45
46
  - ✅ **Easy Integration**: Simple API for getting a ready-to-use logger anywhere in your codebase.
46
47
 
47
48
  ---
@@ -80,6 +81,7 @@ logger.critical("This is a critical message.")
80
81
  ```
81
82
 
82
83
  #### Advanced Usage
84
+
83
85
  - Log to a file:
84
86
  ```python
85
87
  from custom_python_logger import build_logger
@@ -111,19 +113,62 @@ logger.critical("This is a critical message.")
111
113
  logger.info(yaml_pretty_format({'foo': 'bar'}))
112
114
  ```
113
115
 
114
- - use an existing logger (CustomLoggerAdapter) and set a custom name:
116
+ - Use an existing logger with a custom name:
115
117
  ```python
116
118
  from custom_python_logger import get_logger
117
119
 
118
120
  logger = get_logger('some-name')
119
121
 
120
- logger.debug("This is a debug message.")
121
- logger.info("This is an info message.")
122
- 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)
123
138
  ```
124
139
 
125
140
  ---
126
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
+
127
172
  ## 🤝 Contributing
128
173
  If you have a helpful tool, pattern, or improvement to suggest:
129
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "custom-python-logger"
7
- version = "3.0.2"
7
+ version = "4.0.1"
8
8
  description = "A custom logger with color support and additional features."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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}"