sqlobjects 1.8.0__tar.gz → 1.9.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 (82) hide show
  1. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/CHANGELOG.md +18 -0
  2. {sqlobjects-1.8.0/sqlobjects.egg-info → sqlobjects-1.9.0}/PKG-INFO +1 -1
  3. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/pyproject.toml +1 -1
  4. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/__init__.py +2 -2
  5. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/executor.py +1 -2
  6. sqlobjects-1.9.0/sqlobjects/sql_logging.py +200 -0
  7. {sqlobjects-1.8.0 → sqlobjects-1.9.0/sqlobjects.egg-info}/PKG-INFO +1 -1
  8. sqlobjects-1.8.0/sqlobjects/sql_logging.py +0 -156
  9. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/LICENSE +0 -0
  10. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/README.md +0 -0
  11. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/01-database-session-guide.md +0 -0
  12. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/02-model-definition-guide.md +0 -0
  13. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/03-query-operations-guide.md +0 -0
  14. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/04-crud-operations-guide.md +0 -0
  15. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/05-relationships-guide.md +0 -0
  16. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/06-validation-signals-guide.md +0 -0
  17. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/07-performance-guide.md +0 -0
  18. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/docs/rules/README.md +0 -0
  19. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/setup.cfg +0 -0
  20. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/_install_rules.py +0 -0
  21. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/cascade.py +0 -0
  22. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/__init__.py +0 -0
  23. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/asgi.py +0 -0
  24. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/contrib/fastapi.py +0 -0
  25. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/__init__.py +0 -0
  26. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/config.py +0 -0
  27. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/database/manager.py +0 -0
  28. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/exceptions.py +0 -0
  29. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/__init__.py +0 -0
  30. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/aggregate.py +0 -0
  31. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/base.py +0 -0
  32. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/cte.py +0 -0
  33. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/explain.py +0 -0
  34. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/function.py +0 -0
  35. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/mixins.py +0 -0
  36. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/scalar.py +0 -0
  37. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/subquery.py +0 -0
  38. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/terminal.py +0 -0
  39. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/expressions/window.py +0 -0
  40. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/__init__.py +0 -0
  41. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/core.py +0 -0
  42. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/functions.py +0 -0
  43. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/proxies.py +0 -0
  44. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/__init__.py +0 -0
  45. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/descriptors.py +0 -0
  46. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/managers.py +0 -0
  47. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/prefetch.py +0 -0
  48. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/strategies.py +0 -0
  49. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/relations/utils.py +0 -0
  50. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/shortcuts.py +0 -0
  51. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/__init__.py +0 -0
  52. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/base.py +0 -0
  53. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/comparators.py +0 -0
  54. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/types/registry.py +0 -0
  55. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/fields/utils.py +0 -0
  56. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/__init__.py +0 -0
  57. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/operations.py +0 -0
  58. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/internal/results.py +0 -0
  59. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/metadata.py +0 -0
  60. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/mixins.py +0 -0
  61. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/model.py +0 -0
  62. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/__init__.py +0 -0
  63. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/bulk.py +0 -0
  64. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/core.py +0 -0
  65. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/objects/upsert.py +0 -0
  66. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/__init__.py +0 -0
  67. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/builder.py +0 -0
  68. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queries/dialect.py +0 -0
  69. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/queryset.py +0 -0
  70. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/session.py +0 -0
  71. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/signals.py +0 -0
  72. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/__init__.py +0 -0
  73. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/inspect.py +0 -0
  74. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/naming.py +0 -0
  75. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/utils/pattern.py +0 -0
  76. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects/validators.py +0 -0
  77. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/SOURCES.txt +0 -0
  78. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/dependency_links.txt +0 -0
  79. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/entry_points.txt +0 -0
  80. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/requires.txt +0 -0
  81. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/sqlobjects.egg-info/top_level.txt +0 -0
  82. {sqlobjects-1.8.0 → sqlobjects-1.9.0}/tests/test_config.py +0 -0
@@ -1,3 +1,21 @@
1
+ ## 1.9.0 (2026-03-27)
2
+
3
+ ### Feat
4
+
5
+ - **logging**: wire ObjectLogger into QueryExecutor, zero-config caller rewriting
6
+ - **logging**: replace SQLCallerFilter with ObjectLogger, remove SQLCallerFilter
7
+ - **logging**: add ObjectLogger and _install_object_logger
8
+
9
+ ### Fix
10
+
11
+ - **logging**: skip stdlib logging frames in _should_skip_frame, add end-to-end test
12
+ - **logging**: use logging._lock context manager for Python 3.13 compatibility
13
+ - **logging**: add _should_skip_frame tests and fix assertion quality
14
+
15
+ ### Refactor
16
+
17
+ - **logging**: extract _should_skip_frame and add _find_user_frame
18
+
1
19
  ## 1.8.0 (2026-03-26)
2
20
 
3
21
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlobjects"
3
- version = "1.8.0"
3
+ version = "1.9.0"
4
4
  description = "Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -16,7 +16,7 @@ from .objects import (
16
16
  TransactionMode,
17
17
  )
18
18
  from .queryset import Q, QuerySet
19
- from .sql_logging import SQLCallerFilter, get_caller_frame
19
+ from .sql_logging import ObjectLogger, get_caller_frame
20
20
 
21
21
 
22
22
  __version__ = "0.3.0"
@@ -43,6 +43,6 @@ __all__ = [
43
43
  "ErrorHandling",
44
44
  "ConflictResolution",
45
45
  # SQL logging
46
- "SQLCallerFilter",
46
+ "ObjectLogger",
47
47
  "get_caller_frame",
48
48
  ]
@@ -14,8 +14,7 @@ from sqlalchemy import (
14
14
  update,
15
15
  )
16
16
 
17
-
18
- _sql_logger = logging.getLogger("sqlobjects.sql")
17
+ from ..sql_logging import _sql_logger
19
18
 
20
19
 
21
20
  _T = TypeVar("_T")
@@ -0,0 +1,200 @@
1
+ """SQL logging utilities for sqlobjects.
2
+
3
+ Provides ObjectLogger (a logging.Logger subclass that auto-rewrites caller
4
+ fields to user-code location) and get_caller_frame() for custom scenarios.
5
+
6
+ Frame-skip strategy:
7
+ - Skips frames from site-packages (covers pip-installed sqlobjects/sqlalchemy)
8
+ - Skips frames whose module name starts with "sqlobjects." or "sqlalchemy."
9
+ (covers editable installs via `pip install -e .`)
10
+ - Skips frames from extra_skip_packages specified by the caller
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import inspect
16
+ import logging
17
+ import os
18
+ from collections.abc import Mapping
19
+ from typing import Any
20
+
21
+
22
+ __all__ = ["get_caller_frame", "ObjectLogger"]
23
+
24
+ # Exact module names that are always considered internal
25
+ _INTERNAL_MODULES = {"sqlobjects", "sqlalchemy", "logging"}
26
+
27
+ # Module name prefixes that are always considered internal
28
+ _INTERNAL_PREFIXES = ("sqlobjects.", "sqlalchemy.")
29
+
30
+ # Absolute path of this file, used to skip itself reliably
31
+ _THIS_FILE = os.path.abspath(__file__)
32
+
33
+
34
+ def _should_skip_frame(
35
+ filepath: str,
36
+ module: str,
37
+ extra_skip_prefixes: tuple[str, ...],
38
+ ) -> bool:
39
+ """Return True if this frame should be skipped (library/internal frame).
40
+
41
+ Args:
42
+ filepath: frame_info.filename
43
+ module: frame.f_globals.get("__name__", "")
44
+ extra_skip_prefixes: tuple of module name prefixes to skip in addition
45
+ to the built-in sqlobjects/sqlalchemy prefixes.
46
+ """
47
+ if "site-packages" in filepath:
48
+ return True
49
+ if filepath.startswith("<"):
50
+ return True
51
+ if module in _INTERNAL_MODULES or module.startswith(_INTERNAL_PREFIXES):
52
+ return True
53
+ if filepath == _THIS_FILE or os.path.abspath(filepath) == _THIS_FILE:
54
+ return True
55
+ if extra_skip_prefixes and module.startswith(extra_skip_prefixes):
56
+ return True
57
+ return False
58
+
59
+
60
+ def _find_user_frame(
61
+ extra_skip_packages: list[str] | None = None,
62
+ ) -> inspect.FrameInfo | None:
63
+ """Return the first user-code FrameInfo, skipping library frames.
64
+
65
+ Applies the same skip rules as get_caller_frame() but returns the raw
66
+ FrameInfo instead of a formatted string. Returns None if no frame found.
67
+ """
68
+ extra_skip_prefixes: tuple[str, ...] = tuple(extra_skip_packages) if extra_skip_packages else ()
69
+ for frame_info in inspect.stack():
70
+ module = frame_info.frame.f_globals.get("__name__", "")
71
+ if not _should_skip_frame(frame_info.filename, module, extra_skip_prefixes):
72
+ return frame_info
73
+ return None
74
+
75
+
76
+ def get_caller_frame(
77
+ extra_skip_packages: list[str] | None = None,
78
+ max_frames: int = 1,
79
+ ) -> str | list[str]:
80
+ """Inspect the call stack and return the first user-code frame(s).
81
+
82
+ Skips frames from:
83
+ - site-packages (pip install)
84
+ - sqlobjects.* and sqlalchemy.* modules (editable install)
85
+ - extra_skip_packages prefixes provided by the caller
86
+
87
+ Args:
88
+ extra_skip_packages: Additional module name prefixes to skip
89
+ (e.g. ["myapp.middleware"]). Matched against frame's __name__.
90
+ max_frames: How many user-code frames to return.
91
+ 1 returns a str; >1 returns a list[str].
92
+
93
+ Returns:
94
+ Frame string "path/to/file.py:lineno in funcname", or a list of such
95
+ strings when max_frames > 1.
96
+ """
97
+ extra_skip_prefixes: tuple[str, ...] = tuple(extra_skip_packages) if extra_skip_packages else ()
98
+ frames: list[str] = []
99
+
100
+ for frame_info in inspect.stack():
101
+ module = frame_info.frame.f_globals.get("__name__", "")
102
+ if _should_skip_frame(frame_info.filename, module, extra_skip_prefixes):
103
+ continue
104
+ rel_path = _relative_path(frame_info.filename)
105
+ frames.append(f"{rel_path}:{frame_info.lineno} in {frame_info.function}")
106
+ if len(frames) >= max_frames:
107
+ break
108
+
109
+ if not frames:
110
+ return "<unknown>" if max_frames == 1 else ["<unknown>"]
111
+ return frames[0] if max_frames == 1 else frames
112
+
113
+
114
+ def _relative_path(filepath: str) -> str:
115
+ """Return path relative to cwd, or absolute if outside cwd."""
116
+ try:
117
+ return os.path.relpath(filepath)
118
+ except ValueError:
119
+ return filepath
120
+
121
+
122
+ class ObjectLogger(logging.Logger):
123
+ """logging.Logger subclass that rewrites LogRecord caller fields to user-code location.
124
+
125
+ Overrides makeRecord() to replace the standard caller fields
126
+ (filename, funcName, lineno, pathname) with the first user-code frame
127
+ found by _find_user_frame(). This means any handler attached to this logger
128
+ — including loguru InterceptHandlers — will display the real call site
129
+ without any additional Filter configuration.
130
+
131
+ Args:
132
+ name: Logger name (passed to logging.Logger).
133
+ level: Initial log level (default NOTSET).
134
+ extra_skip_packages: Additional module name prefixes to skip when
135
+ searching for the user-code frame (e.g. ["myapp.middleware"]).
136
+ """
137
+
138
+ def __init__(
139
+ self,
140
+ name: str,
141
+ level: int = logging.NOTSET,
142
+ extra_skip_packages: list[str] | None = None,
143
+ ) -> None:
144
+ super().__init__(name, level)
145
+ self.extra_skip_packages = extra_skip_packages
146
+
147
+ def makeRecord( # noqa: N802
148
+ self,
149
+ name: str,
150
+ level: int,
151
+ fn: str,
152
+ lno: int,
153
+ msg: object,
154
+ args: Any,
155
+ exc_info: Any,
156
+ func: str | None = None,
157
+ extra: Mapping[str, object] | None = None,
158
+ sinfo: str | None = None,
159
+ ) -> logging.LogRecord:
160
+ record = super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)
161
+ frame_info = _find_user_frame(self.extra_skip_packages)
162
+ if frame_info:
163
+ record.pathname = os.path.abspath(frame_info.filename)
164
+ record.filename = os.path.basename(frame_info.filename)
165
+ record.module = os.path.splitext(record.filename)[0]
166
+ record.funcName = frame_info.function
167
+ record.lineno = frame_info.lineno
168
+ return record
169
+
170
+
171
+ def _install_object_logger(name: str) -> ObjectLogger:
172
+ """Create an ObjectLogger and register it in the logging system.
173
+
174
+ Directly writes into logging.root.manager.loggerDict so that
175
+ logging.getLogger(name) returns this ObjectLogger instance.
176
+ Migrates handlers, level, and propagate from any pre-existing Logger
177
+ (but not from PlaceHolder sentinels, which are logging internals).
178
+
179
+ Thread-safe: acquires the logging module lock during the operation.
180
+
181
+ Known limitation: code that obtained a reference via getLogger(name)
182
+ *before* this function runs will still hold the old Logger instance.
183
+ """
184
+ with logging._lock: # type: ignore[attr-defined] # noqa: SLF001
185
+ existing = logging.root.manager.loggerDict.get(name)
186
+ logger = ObjectLogger(name)
187
+ logger.parent = logging.root
188
+ logger.propagate = True
189
+ if isinstance(existing, logging.Logger):
190
+ for handler in existing.handlers:
191
+ logger.addHandler(handler)
192
+ if existing.level != logging.NOTSET:
193
+ logger.setLevel(existing.level)
194
+ logger.propagate = existing.propagate
195
+ logging.root.manager.loggerDict[name] = logger
196
+ return logger
197
+
198
+
199
+ # Module-level instance — imported by executor.py and usable by callers
200
+ _sql_logger: ObjectLogger = _install_object_logger("sqlobjects.sql")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlobjects
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Django-style async ORM library based on SQLAlchemy with chainable queries, Q objects, and relationship loading
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -1,156 +0,0 @@
1
- """SQL logging utilities for sqlobjects.
2
-
3
- Provides get_caller_frame() to surface user-code caller information in SQL log
4
- records, compatible with standard logging and loguru.
5
-
6
- Filter strategy:
7
- - Skips frames from site-packages (covers pip-installed sqlobjects/sqlalchemy)
8
- - Skips frames whose module name starts with "sqlobjects." or "sqlalchemy."
9
- (covers editable installs via `pip install -e .`)
10
- - Skips frames from extra_skip_packages specified by the caller
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import inspect
16
- import logging
17
- import os
18
-
19
-
20
- __all__ = ["get_caller_frame", "SQLCallerFilter"]
21
-
22
- # Exact module names that are always considered internal
23
- _INTERNAL_MODULES = {"sqlobjects", "sqlalchemy"}
24
-
25
- # Module name prefixes that are always considered internal
26
- _INTERNAL_PREFIXES = ("sqlobjects.", "sqlalchemy.")
27
-
28
- # Absolute path of this file, used to skip itself reliably
29
- _THIS_FILE = os.path.abspath(__file__)
30
-
31
-
32
- def get_caller_frame(
33
- extra_skip_packages: list[str] | None = None,
34
- max_frames: int = 1,
35
- ) -> str | list[str]:
36
- """Inspect the call stack and return the first user-code frame(s).
37
-
38
- Skips frames from:
39
- - site-packages (pip install)
40
- - sqlobjects.* and sqlalchemy.* modules (editable install)
41
- - extra_skip_packages prefixes provided by the caller
42
-
43
- Args:
44
- extra_skip_packages: Additional module name prefixes to skip
45
- (e.g. ["myapp.middleware"]). Matched against frame's __name__.
46
- max_frames: How many user-code frames to return.
47
- 1 returns a str; >1 returns a list[str].
48
-
49
- Returns:
50
- Frame string "path/to/file.py:lineno in funcname", or a list of such
51
- strings when max_frames > 1.
52
- """
53
- skip_prefixes = _INTERNAL_PREFIXES
54
- if extra_skip_packages:
55
- skip_prefixes = skip_prefixes + tuple(extra_skip_packages)
56
-
57
- frames: list[str] = []
58
-
59
- for frame_info in inspect.stack():
60
- filepath = frame_info.filename
61
- module = frame_info.frame.f_globals.get("__name__", "")
62
-
63
- # Skip site-packages frames (pip-installed third-party libs)
64
- if "site-packages" in filepath:
65
- continue
66
-
67
- # Skip sqlobjects/sqlalchemy frames (editable install)
68
- if module in _INTERNAL_MODULES or module.startswith(skip_prefixes):
69
- continue
70
-
71
- # Skip this helper file itself
72
- if os.path.abspath(filepath) == _THIS_FILE:
73
- continue
74
-
75
- rel_path = _relative_path(filepath)
76
- frames.append(f"{rel_path}:{frame_info.lineno} in {frame_info.function}")
77
-
78
- if len(frames) >= max_frames:
79
- break
80
-
81
- if not frames:
82
- return "<unknown>" if max_frames == 1 else ["<unknown>"]
83
-
84
- return frames[0] if max_frames == 1 else frames
85
-
86
-
87
- def _relative_path(filepath: str) -> str:
88
- """Return path relative to cwd, or absolute if outside cwd."""
89
- try:
90
- return os.path.relpath(filepath)
91
- except ValueError:
92
- return filepath
93
-
94
-
95
- class SQLCallerFilter(logging.Filter):
96
- """logging.Filter that rewrites LogRecord caller fields to user-code location.
97
-
98
- Inspects the call stack at filter time, skips library frames (site-packages
99
- and sqlobjects.*/sqlalchemy.* modules for editable installs), and overwrites
100
- record.filename / record.funcName / record.lineno / record.pathname so that
101
- any handler (including loguru interception) displays the real user-code
102
- call site.
103
-
104
- Also exposes record.caller (str or list[str]) for use in custom Formatters.
105
-
106
- Args:
107
- max_frames: Number of user-code frames to capture (default 1).
108
- When 1, record.caller is a str and record location fields point
109
- to that single frame.
110
- When > 1, record.caller is a list[str] and record location fields
111
- are set from the first (most recent) frame.
112
- extra_skip_packages: Additional module name prefixes to skip
113
- (matched against frame's __name__).
114
- """
115
-
116
- def __init__(
117
- self,
118
- max_frames: int = 1,
119
- extra_skip_packages: list[str] | None = None,
120
- ) -> None:
121
- super().__init__()
122
- self.max_frames = max_frames
123
- self.extra_skip_packages: list[str] | None = list(extra_skip_packages) if extra_skip_packages else None
124
-
125
- def filter(self, record: logging.LogRecord) -> bool:
126
- caller = get_caller_frame(
127
- extra_skip_packages=self.extra_skip_packages,
128
- max_frames=self.max_frames,
129
- )
130
- record.caller = caller
131
-
132
- # Overwrite standard location fields from the first user frame
133
- first = caller if isinstance(caller, str) else caller[0]
134
- self._overwrite_record_location(record, first)
135
-
136
- return True
137
-
138
- @staticmethod
139
- def _overwrite_record_location(record: logging.LogRecord, frame_str: str) -> None:
140
- """Parse 'path/to/file.py:lineno in funcname' and overwrite record fields.
141
-
142
- Frame string format: "relative/path/to/file.py:42 in func_name"
143
- Uses rsplit to handle edge cases where function name could have spaces.
144
- """
145
- try:
146
- # Split off the function name part (rightmost " in ")
147
- path_part, func_part = frame_str.rsplit(" in ", 1)
148
- # Split off the line number (rightmost ":")
149
- filepath, lineno_str = path_part.rsplit(":", 1)
150
- record.pathname = os.path.abspath(filepath)
151
- record.filename = os.path.basename(filepath)
152
- record.module = os.path.splitext(record.filename)[0]
153
- record.funcName = func_part.strip()
154
- record.lineno = int(lineno_str)
155
- except (ValueError, AttributeError):
156
- pass # Keep original fields if parsing fails
File without changes
File without changes
File without changes