ansilogs 1.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.
@@ -0,0 +1,44 @@
1
+ name: Python application
2
+
3
+ on:
4
+ push:
5
+ branches: [ '**' ]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.14"
16
+ - name: Install
17
+ run: |
18
+ python -m pip install --upgrade pip
19
+ python -m pip install pre-commit pyright==1.1.411 -e ".[test]"
20
+ - name: Run pre-commit hooks
21
+ run: pre-commit run --all-files
22
+ - name: Run pyright
23
+ run: pyright
24
+
25
+ test:
26
+ runs-on: ${{ matrix.os }}
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15" ]
31
+ os: [ ubuntu-latest, macos-latest, windows-latest ]
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - name: Set up Python ${{ matrix.python-version }}
35
+ uses: actions/setup-python@v5
36
+ with:
37
+ python-version: ${{ matrix.python-version }}
38
+ allow-prereleases: true
39
+ - name: Install
40
+ run: |
41
+ python -m pip install --upgrade pip
42
+ python -m pip install ".[test]"
43
+ - name: Run tests
44
+ run: python -m pytest -v
@@ -0,0 +1,38 @@
1
+ # This workflow uploads a Python Package to PyPI using Trusted Publishing (OIDC).
2
+ # No API tokens or passwords are stored: PyPI verifies the workflow's identity.
3
+ # See: https://docs.pypi.org/trusted-publishers/
4
+
5
+ name: Upload Python Package
6
+
7
+ on:
8
+ release:
9
+ types: [created]
10
+
11
+ jobs:
12
+ deploy:
13
+
14
+ runs-on: ubuntu-latest
15
+
16
+ environment:
17
+ name: pypi
18
+ url: https://pypi.org/p/ansilogs
19
+
20
+ permissions:
21
+ id-token: write # required for Trusted Publishing (OIDC)
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0 # full history + tags so setuptools_scm can derive the version
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: '3.x'
31
+ - name: Install dependencies
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ pip install build
35
+ - name: Build
36
+ run: python -m build
37
+ - name: Publish package distributions to PyPI
38
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .DS_Store
2
+ .idea/
3
+ venv/
4
+ build/
5
+ dist/
6
+ version.txt
7
+ debug/
8
+ *.pyc
9
+ *.egg-info/
10
+ /ansilogs/_version.py
@@ -0,0 +1,9 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version.
4
+ rev: v0.14.2
5
+ hooks:
6
+ # Run the linter.
7
+ - id: ruff-check
8
+ # Run the formatter.
9
+ - id: ruff-format
ansilogs-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 doronz88
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: ansilogs
3
+ Version: 1.0.0
4
+ Summary: Colored, single-line log output for command-line tools: a dependency-free coloredlogs.install() replacement
5
+ Author-email: doronz88 <doron88@gmail.com>
6
+ Maintainer-email: doronz88 <doron88@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 doronz88
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/doronz88/ansilogs
30
+ Project-URL: Bug Reports, https://github.com/doronz88/ansilogs/issues
31
+ Keywords: logging,color,ansi,terminal,cli
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Programming Language :: Python :: 3.15
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Classifier: Topic :: System :: Logging
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.9
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE
48
+ Provides-Extra: test
49
+ Requires-Dist: pytest; extra == "test"
50
+ Dynamic: license-file
51
+
52
+ # ansilogs
53
+
54
+ Colored, single-line log output for command-line tools. A small, dependency-free replacement
55
+ for `coloredlogs.install()` with the same default look.
56
+
57
+ ```python
58
+ import logging
59
+
60
+ import ansilogs
61
+
62
+ ansilogs.install(logging.DEBUG)
63
+ logging.getLogger(__name__).warning("careful")
64
+ ```
65
+
66
+ ```
67
+ 2026-10-19 09:35:12 my-host my_tool.cli[4242] WARNING careful
68
+ ```
69
+
70
+ - One line per record: `<time> <host> <name>[<pid>] <LEVEL> <message>`; tracebacks follow
71
+ uncolored.
72
+ - Colors only when stderr is a terminal. `NO_COLOR` (non-empty) disables them, `FORCE_COLOR`
73
+ (non-empty) forces them. On Windows the console is switched to ANSI processing; if that
74
+ fails, output is plain.
75
+ - Records go to whatever `sys.stderr` is at the time (test capture and live progress displays
76
+ that redirect stderr keep working).
77
+
78
+ ## API
79
+
80
+ - `install(level=logging.INFO, colors=None)`: attach the handler to the root logger (once) and
81
+ show records from `level` up. `colors=None` means `supports_color()`.
82
+ - `set_level(level)`: change the shown level (installs the handler if needed).
83
+ - `supports_color(stream=None)`: the color decision used by `install()`.
84
+ - `ColoredFormatter(colors=True, hostname=None)` and `StderrHandler` for custom setups.
85
+
86
+ ## Migrating from coloredlogs
87
+
88
+ ```diff
89
+ -import coloredlogs
90
+ -coloredlogs.install(level=logging.DEBUG)
91
+ +import ansilogs
92
+ +ansilogs.install(logging.DEBUG)
93
+ ```
94
+
95
+ `coloredlogs.set_level(level)` becomes `ansilogs.set_level(level)`. `COLOREDLOGS_*` environment
96
+ variables and custom field/level styles are not supported.
@@ -0,0 +1,45 @@
1
+ # ansilogs
2
+
3
+ Colored, single-line log output for command-line tools. A small, dependency-free replacement
4
+ for `coloredlogs.install()` with the same default look.
5
+
6
+ ```python
7
+ import logging
8
+
9
+ import ansilogs
10
+
11
+ ansilogs.install(logging.DEBUG)
12
+ logging.getLogger(__name__).warning("careful")
13
+ ```
14
+
15
+ ```
16
+ 2026-10-19 09:35:12 my-host my_tool.cli[4242] WARNING careful
17
+ ```
18
+
19
+ - One line per record: `<time> <host> <name>[<pid>] <LEVEL> <message>`; tracebacks follow
20
+ uncolored.
21
+ - Colors only when stderr is a terminal. `NO_COLOR` (non-empty) disables them, `FORCE_COLOR`
22
+ (non-empty) forces them. On Windows the console is switched to ANSI processing; if that
23
+ fails, output is plain.
24
+ - Records go to whatever `sys.stderr` is at the time (test capture and live progress displays
25
+ that redirect stderr keep working).
26
+
27
+ ## API
28
+
29
+ - `install(level=logging.INFO, colors=None)`: attach the handler to the root logger (once) and
30
+ show records from `level` up. `colors=None` means `supports_color()`.
31
+ - `set_level(level)`: change the shown level (installs the handler if needed).
32
+ - `supports_color(stream=None)`: the color decision used by `install()`.
33
+ - `ColoredFormatter(colors=True, hostname=None)` and `StderrHandler` for custom setups.
34
+
35
+ ## Migrating from coloredlogs
36
+
37
+ ```diff
38
+ -import coloredlogs
39
+ -coloredlogs.install(level=logging.DEBUG)
40
+ +import ansilogs
41
+ +ansilogs.install(logging.DEBUG)
42
+ ```
43
+
44
+ `coloredlogs.set_level(level)` becomes `ansilogs.set_level(level)`. `COLOREDLOGS_*` environment
45
+ variables and custom field/level styles are not supported.
@@ -0,0 +1,173 @@
1
+ """
2
+ Colored, single-line log output for command-line tools.
3
+
4
+ A small, dependency-free replacement for ``coloredlogs.install()``::
5
+
6
+ import logging
7
+ import ansilogs
8
+
9
+ ansilogs.install(logging.DEBUG)
10
+
11
+ Each record is one line, ``<time> <host> <name>[<pid>] <LEVEL> <message>``, colored with ANSI
12
+ escape codes when stderr is a terminal (honoring ``NO_COLOR`` and ``FORCE_COLOR``, and switching
13
+ Windows consoles to ANSI processing). The handler writes to whatever ``sys.stderr`` is at the
14
+ time of each record, so redirections (test capture, live progress displays) are respected.
15
+ """
16
+
17
+ import logging
18
+ import os
19
+ import socket
20
+ import sys
21
+ from typing import IO, Optional, Union
22
+
23
+ __all__ = ["DATE_FORMAT", "ColoredFormatter", "StderrHandler", "install", "set_level", "supports_color"]
24
+
25
+ DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
26
+
27
+ _RESET = "\x1b[0m"
28
+ _TIME = "32" # green
29
+ _HOST = "35" # magenta
30
+ _NAME = "34" # blue
31
+ _LEVEL_NAME = "1;30" # bold black
32
+ _MESSAGE_BY_LEVEL = {
33
+ logging.DEBUG: "32", # green
34
+ logging.WARNING: "33", # yellow
35
+ logging.ERROR: "31", # red
36
+ logging.CRITICAL: "1;31", # bold red
37
+ }
38
+
39
+ # SetConsoleMode flag that makes a Windows console interpret ANSI escape sequences.
40
+ _ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
41
+ _STD_ERROR_HANDLE = -12
42
+
43
+
44
+ def _sgr(code: str, text: str) -> str:
45
+ return f"\x1b[{code}m{text}{_RESET}"
46
+
47
+
48
+ class ColoredFormatter(logging.Formatter):
49
+ """
50
+ ``<time> <host> <name>[<pid>] <LEVEL> <message>``, with ANSI colors when ``colors`` is set.
51
+
52
+ The colors are baked into the format string once; per record only the message is wrapped in
53
+ its level's color. Tracebacks and stack info are appended uncolored.
54
+ """
55
+
56
+ def __init__(self, colors: bool = True, hostname: Optional[str] = None) -> None:
57
+ host = (hostname or socket.gethostname()).replace("%", "%%")
58
+ fields = [
59
+ ("%(asctime)s", _TIME),
60
+ (host, _HOST),
61
+ ("%(name)s[%(process)d]", _NAME),
62
+ ("%(levelname)s", _LEVEL_NAME),
63
+ ]
64
+ prefix = " ".join(_sgr(code, text) if colors else text for text, code in fields)
65
+ super().__init__(f"{prefix} %(message)s", datefmt=DATE_FORMAT)
66
+ self.colors = colors
67
+
68
+ def formatMessage(self, record: logging.LogRecord) -> str:
69
+ code = _MESSAGE_BY_LEVEL.get(record.levelno) if self.colors else None
70
+ if code is None:
71
+ return super().formatMessage(record)
72
+ message = record.message
73
+ record.message = _sgr(code, message)
74
+ try:
75
+ return super().formatMessage(record)
76
+ finally:
77
+ record.message = message
78
+
79
+
80
+ class StderrHandler(logging.Handler):
81
+ """Writes each record to the current ``sys.stderr`` (looked up per record, never cached)."""
82
+
83
+ terminator = "\n"
84
+
85
+ def emit(self, record: logging.LogRecord) -> None:
86
+ try:
87
+ line = self.format(record)
88
+ stream = sys.stderr
89
+ if stream is None:
90
+ return
91
+ stream.write(line + self.terminator)
92
+ stream.flush()
93
+ except RecursionError:
94
+ raise
95
+ except Exception:
96
+ self.handleError(record)
97
+
98
+
99
+ def _enable_windows_ansi(stream: IO[str]) -> bool:
100
+ """Switch the Windows console behind ``stream`` to ANSI processing; False if it isn't a console."""
101
+ if sys.platform != "win32":
102
+ return True
103
+ import ctypes
104
+ from ctypes import wintypes
105
+
106
+ if stream is not sys.__stderr__:
107
+ # Only the process' own stderr maps to the standard error console handle.
108
+ return False
109
+ kernel32 = ctypes.windll.kernel32
110
+ handle = kernel32.GetStdHandle(_STD_ERROR_HANDLE)
111
+ mode = wintypes.DWORD()
112
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
113
+ return False
114
+ if mode.value & _ENABLE_VIRTUAL_TERMINAL_PROCESSING:
115
+ return True
116
+ return bool(kernel32.SetConsoleMode(handle, mode.value | _ENABLE_VIRTUAL_TERMINAL_PROCESSING))
117
+
118
+
119
+ def supports_color(stream: Optional[IO[str]] = None) -> bool:
120
+ """
121
+ Whether to color output written to ``stream`` (default: ``sys.stderr``).
122
+
123
+ ``NO_COLOR`` (non-empty) disables colors and ``FORCE_COLOR`` (non-empty) forces them;
124
+ otherwise the stream must be a terminal, and on Windows a console that accepts ANSI codes.
125
+ """
126
+ if os.environ.get("NO_COLOR"):
127
+ return False
128
+ if os.environ.get("FORCE_COLOR"):
129
+ return True
130
+ stream = sys.stderr if stream is None else stream
131
+ if stream is None:
132
+ return False
133
+ isatty = getattr(stream, "isatty", None)
134
+ try:
135
+ if isatty is None or not isatty():
136
+ return False
137
+ except ValueError: # closed stream
138
+ return False
139
+ return _enable_windows_ansi(stream)
140
+
141
+
142
+ def _installed_handler() -> Optional[StderrHandler]:
143
+ return next((h for h in logging.getLogger().handlers if isinstance(h, StderrHandler)), None)
144
+
145
+
146
+ def install(level: Union[int, str] = logging.INFO, colors: Optional[bool] = None) -> None:
147
+ """
148
+ Attach a colored stderr handler to the root logger and show records from ``level`` up.
149
+
150
+ Calling it again reconfigures the existing handler instead of adding another. ``colors``
151
+ defaults to :func:`supports_color` for the current ``sys.stderr``.
152
+ """
153
+ handler = _installed_handler()
154
+ if handler is None:
155
+ handler = StderrHandler()
156
+ logging.getLogger().addHandler(handler)
157
+ handler.setFormatter(ColoredFormatter(colors=supports_color() if colors is None else colors))
158
+ set_level(level)
159
+
160
+
161
+ def set_level(level: Union[int, str]) -> None:
162
+ """
163
+ Show records from ``level`` up: sets the handler's level (installing it if needed) and relaxes
164
+ the root logger's level if it would filter those records out.
165
+ """
166
+ handler = _installed_handler()
167
+ if handler is None:
168
+ install(level)
169
+ return
170
+ handler.setLevel(level)
171
+ root = logging.getLogger()
172
+ if root.getEffectiveLevel() > handler.level:
173
+ root.setLevel(handler.level)
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0'
22
+ __version_tuple__ = version_tuple = (1, 0, 0)
23
+
24
+ __commit_id__ = commit_id = 'gaa8e027c2'
File without changes
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: ansilogs
3
+ Version: 1.0.0
4
+ Summary: Colored, single-line log output for command-line tools: a dependency-free coloredlogs.install() replacement
5
+ Author-email: doronz88 <doron88@gmail.com>
6
+ Maintainer-email: doronz88 <doron88@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 doronz88
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/doronz88/ansilogs
30
+ Project-URL: Bug Reports, https://github.com/doronz88/ansilogs/issues
31
+ Keywords: logging,color,ansi,terminal,cli
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Programming Language :: Python :: 3.15
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Classifier: Topic :: System :: Logging
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.9
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE
48
+ Provides-Extra: test
49
+ Requires-Dist: pytest; extra == "test"
50
+ Dynamic: license-file
51
+
52
+ # ansilogs
53
+
54
+ Colored, single-line log output for command-line tools. A small, dependency-free replacement
55
+ for `coloredlogs.install()` with the same default look.
56
+
57
+ ```python
58
+ import logging
59
+
60
+ import ansilogs
61
+
62
+ ansilogs.install(logging.DEBUG)
63
+ logging.getLogger(__name__).warning("careful")
64
+ ```
65
+
66
+ ```
67
+ 2026-10-19 09:35:12 my-host my_tool.cli[4242] WARNING careful
68
+ ```
69
+
70
+ - One line per record: `<time> <host> <name>[<pid>] <LEVEL> <message>`; tracebacks follow
71
+ uncolored.
72
+ - Colors only when stderr is a terminal. `NO_COLOR` (non-empty) disables them, `FORCE_COLOR`
73
+ (non-empty) forces them. On Windows the console is switched to ANSI processing; if that
74
+ fails, output is plain.
75
+ - Records go to whatever `sys.stderr` is at the time (test capture and live progress displays
76
+ that redirect stderr keep working).
77
+
78
+ ## API
79
+
80
+ - `install(level=logging.INFO, colors=None)`: attach the handler to the root logger (once) and
81
+ show records from `level` up. `colors=None` means `supports_color()`.
82
+ - `set_level(level)`: change the shown level (installs the handler if needed).
83
+ - `supports_color(stream=None)`: the color decision used by `install()`.
84
+ - `ColoredFormatter(colors=True, hostname=None)` and `StderrHandler` for custom setups.
85
+
86
+ ## Migrating from coloredlogs
87
+
88
+ ```diff
89
+ -import coloredlogs
90
+ -coloredlogs.install(level=logging.DEBUG)
91
+ +import ansilogs
92
+ +ansilogs.install(logging.DEBUG)
93
+ ```
94
+
95
+ `coloredlogs.set_level(level)` becomes `ansilogs.set_level(level)`. `COLOREDLOGS_*` environment
96
+ variables and custom field/level styles are not supported.
@@ -0,0 +1,20 @@
1
+ .gitignore
2
+ .pre-commit-config.yaml
3
+ LICENSE
4
+ README.md
5
+ pyproject.toml
6
+ .github/workflows/python-app.yml
7
+ .github/workflows/python-publish.yml
8
+ ansilogs/__init__.py
9
+ ansilogs/_version.py
10
+ ansilogs/py.typed
11
+ ansilogs.egg-info/PKG-INFO
12
+ ansilogs.egg-info/SOURCES.txt
13
+ ansilogs.egg-info/dependency_links.txt
14
+ ansilogs.egg-info/requires.txt
15
+ ansilogs.egg-info/scm_file_list.json
16
+ ansilogs.egg-info/scm_version.json
17
+ ansilogs.egg-info/top_level.txt
18
+ tests/console_child.py
19
+ tests/test_ansilogs.py
20
+ tests/test_windows_console.py
@@ -0,0 +1,3 @@
1
+
2
+ [test]
3
+ pytest
@@ -0,0 +1,16 @@
1
+ {
2
+ "files": [
3
+ ".github/workflows/python-app.yml",
4
+ ".github/workflows/python-publish.yml",
5
+ ".gitignore",
6
+ ".pre-commit-config.yaml",
7
+ "LICENSE",
8
+ "README.md",
9
+ "ansilogs/__init__.py",
10
+ "ansilogs/py.typed",
11
+ "pyproject.toml",
12
+ "tests/console_child.py",
13
+ "tests/test_ansilogs.py",
14
+ "tests/test_windows_console.py"
15
+ ]
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.0",
3
+ "distance": 0,
4
+ "node": "gaa8e027c29ab9ce72f33445b198a6c6f71d53b2a",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-19"
8
+ }
@@ -0,0 +1,2 @@
1
+ ansilogs
2
+ dist
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "ansilogs"
3
+ description = "Colored, single-line log output for command-line tools: a dependency-free coloredlogs.install() replacement"
4
+ readme = "README.md"
5
+ requires-python = ">=3.9"
6
+ license = { file = "LICENSE" }
7
+ keywords = ["logging", "color", "ansi", "terminal", "cli"]
8
+ authors = [
9
+ { name = "doronz88", email = "doron88@gmail.com" }
10
+ ]
11
+ maintainers = [
12
+ { name = "doronz88", email = "doron88@gmail.com" }
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Programming Language :: Python :: 3.15",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Topic :: System :: Logging",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = []
30
+ dynamic = ["version"]
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest"]
34
+
35
+ [project.urls]
36
+ "Homepage" = "https://github.com/doronz88/ansilogs"
37
+ "Bug Reports" = "https://github.com/doronz88/ansilogs/issues"
38
+
39
+ [tool.setuptools.packages.find]
40
+ exclude = ["docs*", "tests*"]
41
+
42
+ [tool.setuptools.package-data]
43
+ ansilogs = ["py.typed"]
44
+
45
+ [tool.setuptools_scm]
46
+ version_file = "ansilogs/_version.py"
47
+
48
+ [build-system]
49
+ requires = ["setuptools>=64", "setuptools_scm>=8", "wheel"]
50
+ build-backend = "setuptools.build_meta"
51
+
52
+ [tool.ruff]
53
+ target-version = "py39"
54
+ line-length = 120
55
+ fix = true
56
+
57
+ [tool.ruff.lint]
58
+ select = ["YTT", "B", "A", "C4", "T10", "SIM", "I", "E", "W", "F", "PGH", "UP", "RUF", "TRY"]
59
+
60
+ [tool.pyright]
61
+ pythonVersion = "3.9"
62
+ typeCheckingMode = "strict"
63
+ include = ["ansilogs", "tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,77 @@
1
+ """
2
+ Run inside a fresh Windows console (``CREATE_NEW_CONSOLE``) by ``test_windows_console.py``.
3
+
4
+ Turns ANSI processing off, writes a raw escape sequence (the control: it must land on screen
5
+ literally), then lets ``ansilogs.install()`` turn processing on and logs a warning (which must
6
+ render without escape codes). Reports the console modes and the screen text as JSON.
7
+ """
8
+
9
+ import ctypes
10
+ import json
11
+ import logging
12
+ import sys
13
+ from ctypes import wintypes
14
+
15
+ import ansilogs
16
+
17
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
18
+ STD_ERROR_HANDLE = -12
19
+ GENERIC_READ_WRITE = 0x80000000 | 0x40000000
20
+ FILE_SHARE_READ_WRITE = 0x1 | 0x2
21
+ OPEN_EXISTING = 3
22
+
23
+
24
+ class COORD(ctypes.Structure):
25
+ _fields_ = [("X", wintypes.SHORT), ("Y", wintypes.SHORT)] # noqa: RUF012 (ctypes API)
26
+
27
+
28
+ class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
29
+ _fields_ = [ # noqa: RUF012 (ctypes API)
30
+ ("dwSize", COORD),
31
+ ("dwCursorPosition", COORD),
32
+ ("wAttributes", wintypes.WORD),
33
+ ("srWindow", wintypes.SMALL_RECT),
34
+ ("dwMaximumWindowSize", COORD),
35
+ ]
36
+
37
+
38
+ def main() -> None:
39
+ assert sys.platform == "win32"
40
+ kernel32 = ctypes.windll.kernel32
41
+ kernel32.CreateFileW.restype = wintypes.HANDLE
42
+ report = {}
43
+ stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE)
44
+ mode = wintypes.DWORD()
45
+ report["stderr_is_console"] = bool(kernel32.GetConsoleMode(stderr_handle, ctypes.byref(mode)))
46
+ if report["stderr_is_console"]:
47
+ kernel32.SetConsoleMode(stderr_handle, mode.value & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING)
48
+ sys.stderr.write("\x1b[33mCONTROL\x1b[0m\n")
49
+ sys.stderr.flush()
50
+
51
+ ansilogs.install(logging.INFO)
52
+ kernel32.GetConsoleMode(stderr_handle, ctypes.byref(mode))
53
+ report["vt_after_install"] = bool(mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
54
+ (handler,) = [h for h in logging.getLogger().handlers if isinstance(h, ansilogs.StderrHandler)]
55
+ formatter = handler.formatter
56
+ report["colors"] = isinstance(formatter, ansilogs.ColoredFormatter) and formatter.colors
57
+ logging.getLogger("ansilogs.console").warning("hello from a console")
58
+
59
+ screen = kernel32.CreateFileW(
60
+ "CONOUT$", GENERIC_READ_WRITE, FILE_SHARE_READ_WRITE, None, OPEN_EXISTING, 0, None
61
+ )
62
+ info = CONSOLE_SCREEN_BUFFER_INFO()
63
+ kernel32.GetConsoleScreenBufferInfo(screen, ctypes.byref(info))
64
+ width = info.dwSize.X
65
+ rows = []
66
+ for y in range(info.dwCursorPosition.Y + 1):
67
+ buffer = ctypes.create_unicode_buffer(width)
68
+ read = wintypes.DWORD()
69
+ kernel32.ReadConsoleOutputCharacterW(screen, buffer, width, COORD(0, y), ctypes.byref(read))
70
+ rows.append(buffer.value[: read.value].rstrip())
71
+ report["screen"] = rows
72
+ with open(sys.argv[1], "w", encoding="utf-8") as f:
73
+ json.dump(report, f)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,159 @@
1
+ import io
2
+ import logging
3
+ import re
4
+ import sys
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ import pytest
9
+
10
+ import ansilogs
11
+ from ansilogs import ColoredFormatter, StderrHandler
12
+
13
+ LINE = re.compile(r"^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d host ansilogs\.tests\[\d+\] (\w+) (.*)$")
14
+
15
+
16
+ def _record(level: int, msg: str, *args: Any, exc_info: Any = None) -> logging.LogRecord:
17
+ return logging.LogRecord("ansilogs.tests", level, __file__, 1, msg, args, exc_info)
18
+
19
+
20
+ @pytest.fixture(autouse=True)
21
+ def clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
22
+ monkeypatch.delenv("NO_COLOR", raising=False)
23
+ monkeypatch.delenv("FORCE_COLOR", raising=False)
24
+
25
+
26
+ @pytest.fixture
27
+ def root(monkeypatch: pytest.MonkeyPatch) -> Iterator[logging.Logger]:
28
+ root = logging.getLogger()
29
+ monkeypatch.setattr(root, "handlers", [])
30
+ monkeypatch.setattr(root, "level", logging.WARNING)
31
+ yield root
32
+
33
+
34
+ def _ours(root: logging.Logger) -> list[StderrHandler]:
35
+ # pytest attaches its own capture handlers to the root logger while a test runs.
36
+ return [h for h in root.handlers if isinstance(h, StderrHandler)]
37
+
38
+
39
+ def test_plain_layout_is_one_line() -> None:
40
+ line = ColoredFormatter(colors=False, hostname="host").format(_record(logging.INFO, "sent %d bytes", 5))
41
+ match = LINE.match(line)
42
+ assert match is not None
43
+ assert match.groups() == ("INFO", "sent 5 bytes")
44
+
45
+
46
+ def test_colored_layout_matches_coloredlogs_defaults() -> None:
47
+ record = _record(logging.WARNING, "careful")
48
+ line = ColoredFormatter(colors=True, hostname="host").format(record)
49
+ timestamp = ColoredFormatter(colors=False).formatTime(record, ansilogs.DATE_FORMAT)
50
+ assert line == (
51
+ f"\x1b[32m{timestamp}\x1b[0m \x1b[35mhost\x1b[0m \x1b[34mansilogs.tests[{record.process}]\x1b[0m "
52
+ "\x1b[1;30mWARNING\x1b[0m \x1b[33mcareful\x1b[0m"
53
+ )
54
+
55
+
56
+ @pytest.mark.parametrize(
57
+ ("level", "code"),
58
+ [(logging.DEBUG, "32"), (logging.WARNING, "33"), (logging.ERROR, "31"), (logging.CRITICAL, "1;31")],
59
+ )
60
+ def test_message_takes_its_level_color(level: int, code: str) -> None:
61
+ line = ColoredFormatter(colors=True).format(_record(level, "msg"))
62
+ assert line.endswith(f"\x1b[{code}mmsg\x1b[0m")
63
+
64
+
65
+ def test_info_message_is_uncolored_and_record_is_left_untouched() -> None:
66
+ record = _record(logging.INFO, "msg")
67
+ assert ColoredFormatter(colors=True).format(record).endswith("\x1b[0m msg")
68
+ warning = _record(logging.WARNING, "msg")
69
+ ColoredFormatter(colors=True).format(warning)
70
+ assert warning.message == "msg"
71
+
72
+
73
+ def test_hostname_with_percent_is_literal() -> None:
74
+ line = ColoredFormatter(colors=False, hostname="a%sb").format(_record(logging.INFO, "msg"))
75
+ assert " a%sb " in line
76
+
77
+
78
+ def test_traceback_is_appended_uncolored() -> None:
79
+ def boom() -> None:
80
+ raise ValueError("boom")
81
+
82
+ with pytest.raises(ValueError) as excinfo:
83
+ boom()
84
+ record = _record(logging.ERROR, "failed", exc_info=(excinfo.type, excinfo.value, excinfo.tb))
85
+ lines = ColoredFormatter(colors=True).format(record).splitlines()
86
+ assert lines[0].endswith("\x1b[31mfailed\x1b[0m")
87
+ assert lines[1] == "Traceback (most recent call last):"
88
+ assert lines[-1] == "ValueError: boom"
89
+
90
+
91
+ def test_handler_writes_to_the_current_stderr(monkeypatch: pytest.MonkeyPatch) -> None:
92
+ handler = StderrHandler()
93
+ handler.setFormatter(ColoredFormatter(colors=False))
94
+ first, second = io.StringIO(), io.StringIO()
95
+ monkeypatch.setattr(sys, "stderr", first)
96
+ handler.handle(_record(logging.INFO, "one"))
97
+ monkeypatch.setattr(sys, "stderr", second)
98
+ handler.handle(_record(logging.INFO, "two"))
99
+ assert first.getvalue().endswith(" INFO one\n")
100
+ assert second.getvalue().endswith(" INFO two\n")
101
+
102
+
103
+ def test_handler_tolerates_missing_stderr(monkeypatch: pytest.MonkeyPatch) -> None:
104
+ monkeypatch.setattr(sys, "stderr", None)
105
+ StderrHandler().handle(_record(logging.INFO, "dropped"))
106
+
107
+
108
+ class _Tty(io.StringIO):
109
+ def isatty(self) -> bool:
110
+ if self.closed:
111
+ raise ValueError
112
+ return True
113
+
114
+
115
+ def test_supports_color_requires_a_terminal() -> None:
116
+ assert not ansilogs.supports_color(io.StringIO())
117
+ if sys.platform != "win32":
118
+ assert ansilogs.supports_color(_Tty())
119
+
120
+
121
+ def test_supports_color_rejects_a_closed_stream() -> None:
122
+ stream = _Tty()
123
+ stream.close()
124
+ assert not ansilogs.supports_color(stream)
125
+
126
+
127
+ def test_no_color_and_force_color(monkeypatch: pytest.MonkeyPatch) -> None:
128
+ monkeypatch.setenv("FORCE_COLOR", "1")
129
+ assert ansilogs.supports_color(io.StringIO())
130
+ monkeypatch.setenv("NO_COLOR", "1")
131
+ assert not ansilogs.supports_color(_Tty())
132
+ monkeypatch.setenv("NO_COLOR", "")
133
+ assert ansilogs.supports_color(io.StringIO())
134
+
135
+
136
+ def test_install_is_idempotent_and_relaxes_the_root_level(root: logging.Logger) -> None:
137
+ ansilogs.install(logging.INFO, colors=False)
138
+ ansilogs.install(logging.INFO, colors=False)
139
+ (handler,) = _ours(root)
140
+ assert handler.level == logging.INFO
141
+ assert root.level == logging.INFO
142
+ ansilogs.set_level("DEBUG")
143
+ assert handler.level == logging.DEBUG
144
+ assert root.level == logging.DEBUG
145
+
146
+
147
+ def test_set_level_installs_when_needed(root: logging.Logger) -> None:
148
+ ansilogs.set_level(logging.DEBUG)
149
+ (handler,) = _ours(root)
150
+ assert handler.level == logging.DEBUG
151
+
152
+
153
+ def test_install_colors_follow_stderr(root: logging.Logger, monkeypatch: pytest.MonkeyPatch) -> None:
154
+ monkeypatch.setattr(sys, "stderr", io.StringIO())
155
+ ansilogs.install()
156
+ (handler,) = _ours(root)
157
+ formatter = handler.formatter
158
+ assert isinstance(formatter, ColoredFormatter)
159
+ assert not formatter.colors
@@ -0,0 +1,30 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="needs a Windows console")
9
+
10
+
11
+ def test_install_turns_on_ansi_processing_in_a_real_console(tmp_path: Path) -> None:
12
+ report_path = tmp_path / "report.json"
13
+ child = Path(__file__).with_name("console_child.py")
14
+ subprocess.run(
15
+ [sys.executable, str(child), str(report_path)],
16
+ creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0,
17
+ check=True,
18
+ timeout=60,
19
+ )
20
+ report = json.loads(report_path.read_text(encoding="utf-8"))
21
+ assert report["stderr_is_console"], report
22
+ screen = report["screen"]
23
+ # Control: with processing off, the escape sequence lands on screen literally.
24
+ assert any("[33mCONTROL" in row for row in screen), screen
25
+ assert report["vt_after_install"], report
26
+ assert report["colors"], report
27
+ (line,) = [row for row in screen if "hello from a console" in row]
28
+ assert "WARNING hello from a console" in line
29
+ assert "\x1b" not in line
30
+ assert "[0m" not in line