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.
- ansilogs-1.0.0/.github/workflows/python-app.yml +44 -0
- ansilogs-1.0.0/.github/workflows/python-publish.yml +38 -0
- ansilogs-1.0.0/.gitignore +10 -0
- ansilogs-1.0.0/.pre-commit-config.yaml +9 -0
- ansilogs-1.0.0/LICENSE +21 -0
- ansilogs-1.0.0/PKG-INFO +96 -0
- ansilogs-1.0.0/README.md +45 -0
- ansilogs-1.0.0/ansilogs/__init__.py +173 -0
- ansilogs-1.0.0/ansilogs/_version.py +24 -0
- ansilogs-1.0.0/ansilogs/py.typed +0 -0
- ansilogs-1.0.0/ansilogs.egg-info/PKG-INFO +96 -0
- ansilogs-1.0.0/ansilogs.egg-info/SOURCES.txt +20 -0
- ansilogs-1.0.0/ansilogs.egg-info/dependency_links.txt +1 -0
- ansilogs-1.0.0/ansilogs.egg-info/requires.txt +3 -0
- ansilogs-1.0.0/ansilogs.egg-info/scm_file_list.json +16 -0
- ansilogs-1.0.0/ansilogs.egg-info/scm_version.json +8 -0
- ansilogs-1.0.0/ansilogs.egg-info/top_level.txt +2 -0
- ansilogs-1.0.0/pyproject.toml +63 -0
- ansilogs-1.0.0/setup.cfg +4 -0
- ansilogs-1.0.0/tests/console_child.py +77 -0
- ansilogs-1.0.0/tests/test_ansilogs.py +159 -0
- ansilogs-1.0.0/tests/test_windows_console.py +30 -0
|
@@ -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
|
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.
|
ansilogs-1.0.0/PKG-INFO
ADDED
|
@@ -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.
|
ansilogs-1.0.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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"]
|
ansilogs-1.0.0/setup.cfg
ADDED
|
@@ -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
|