From 37b95d859edadc39b809d491dace08e28873d0c7 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 24 Jan 2025 18:24:47 +0800 Subject: [PATCH] deps(termcolor): vendor third-party dependency `termcolor` (#148) --- CHANGELOG.md | 1 + README.md | 1 - docs/source/spelling_wordlist.txt | 2 + nvitop-exporter/nvitop_exporter/cli.py | 2 +- nvitop/__init__.py | 14 +- nvitop/api/__init__.py | 1 + nvitop/api/caching.py | 5 +- nvitop/api/libnvml.py | 4 +- nvitop/api/termcolor.py | 283 +++++++++++++++++++++++++ nvitop/api/utils.py | 38 +--- nvitop/cli.py | 2 +- pyproject.toml | 1 - requirements.txt | 1 - 13 files changed, 315 insertions(+), 40 deletions(-) create mode 100644 nvitop/api/termcolor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f04f23c..c6b88bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- Vendor third-party dependency `termcolor` by [@XuehaiPan](https://github.com/XuehaiPan) in [#148](https://github.com/XuehaiPan/nvitop/pull/148). - Remove third-party dependency `cachetools` by [@XuehaiPan](https://github.com/XuehaiPan) in [#147](https://github.com/XuehaiPan/nvitop/pull/147). ------ diff --git a/README.md b/README.md index 1291ed0..48d491e 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ An interactive NVIDIA-GPU process viewer and beyond, the one-stop solution for G - NVIDIA Management Library (NVML) - nvidia-ml-py - psutil -- termcolor - curses[*](#curses) (with `libncursesw`) **NOTE:** The [NVIDIA Management Library (*NVML*)](https://developer.nvidia.com/nvidia-management-library-nvml) is a C-based programmatic interface for monitoring and managing various states. The runtime version of the NVML library ships with the NVIDIA display driver (available at [Download Drivers | NVIDIA](https://www.nvidia.com/Download/index.aspx)), or can be downloaded as part of the NVIDIA CUDA Toolkit (available at [CUDA Toolkit | NVIDIA Developer](https://developer.nvidia.com/cuda-downloads)). The lists of OS platforms and NVIDIA-GPUs supported by the NVML library can be found in the [NVML API Reference](https://docs.nvidia.com/deploy/nvml-api/nvml-api-reference.html). diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index c1726d1..bdacf22 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -158,3 +158,5 @@ hostname len maxsize reentrant +env +tty diff --git a/nvitop-exporter/nvitop_exporter/cli.py b/nvitop-exporter/nvitop_exporter/cli.py index 1616815..1806b45 100644 --- a/nvitop-exporter/nvitop_exporter/cli.py +++ b/nvitop-exporter/nvitop_exporter/cli.py @@ -42,7 +42,7 @@ def cprint(text: str = '', *, file: TextIO | None = None) -> None: if text.startswith(prefix): text = text.replace( prefix.rstrip(), - colored(prefix.rstrip(), color=color, attrs=('bold',)), + colored(prefix.rstrip(), color=color, attrs=('bold',)), # type: ignore[arg-type] 1, ) print(text, file=file) diff --git a/nvitop/__init__.py b/nvitop/__init__.py index dcddd4d..333b722 100644 --- a/nvitop/__init__.py +++ b/nvitop/__init__.py @@ -29,6 +29,7 @@ from nvitop.api import ( libcudart, libnvml, process, + termcolor, utils, ) from nvitop.select import select_devices @@ -38,7 +39,18 @@ from nvitop.version import __version__ __all__ = [*api.__all__, 'select_devices'] # Add submodules to the top-level namespace -for submodule in (caching, collector, device, host, libcuda, libcudart, libnvml, process, utils): +for submodule in ( + caching, + collector, + device, + host, + libcuda, + libcudart, + libnvml, + process, + termcolor, + utils, +): sys.modules[f'{__name__}.{submodule.__name__.rpartition(".")[-1]}'] = submodule # Remove the nvitop.select module from sys.modules diff --git a/nvitop/api/__init__.py b/nvitop/api/__init__.py index ad1d3c4..6866145 100644 --- a/nvitop/api/__init__.py +++ b/nvitop/api/__init__.py @@ -25,6 +25,7 @@ from nvitop.api import ( libcudart, libnvml, process, + termcolor, utils, ) from nvitop.api.caching import ttl_cache diff --git a/nvitop/api/caching.py b/nvitop/api/caching.py index 6f62b9e..d2e8250 100644 --- a/nvitop/api/caching.py +++ b/nvitop/api/caching.py @@ -29,7 +29,10 @@ if TYPE_CHECKING: from collections.abc import Callable, Hashable, Sized from collections.abc import Set as AbstractSet from typing import TypeVar - from typing_extensions import ParamSpec, Self + from typing_extensions import ( + ParamSpec, # Python 3.10+ + Self, # Python 3.11+ + ) _P = ParamSpec('_P') _T = TypeVar('_T') diff --git a/nvitop/api/libnvml.py b/nvitop/api/libnvml.py index 4e1fbb6..cc73848 100644 --- a/nvitop/api/libnvml.py +++ b/nvitop/api/libnvml.py @@ -323,7 +323,7 @@ def nvmlInitWithFlags(flags: int) -> None: # pylint: disable=function-redefined ('https://developer.nvidia.com/cuda-downloads', None, ('underline',)), ('https://docs.nvidia.com/deploy/nvml-api', None, ('underline',)), ): - message = message.replace(text, __colored(text, color=color, attrs=attrs)) + message = message.replace(text, __colored(text, color=color, attrs=attrs)) # type: ignore[arg-type] LOGGER.critical(message) raise @@ -340,7 +340,7 @@ def nvmlInitWithFlags(flags: int) -> None: # pylint: disable=function-redefined ('pynvml', None, ('bold',)), ('nvitop', None, ('bold',)), ): - message = message.replace(text, __colored(text, color=color, attrs=attrs), 1) + message = message.replace(text, __colored(text, color=color, attrs=attrs), 1) # type: ignore[arg-type] LOGGER.critical(message) raise diff --git a/nvitop/api/termcolor.py b/nvitop/api/termcolor.py new file mode 100644 index 0000000..5783d38 --- /dev/null +++ b/nvitop/api/termcolor.py @@ -0,0 +1,283 @@ +# This file is part of nvitop, the interactive NVIDIA-GPU process viewer. +# +# Copyright 2021-2025 Xuehai Pan. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +# pylint: disable=wrong-spelling-in-comment +# Vendored from the `termcolor` package: https://github.com/termcolor/termcolor +# ============================================================================== +# Copyright (c) 2008-2011 Volvox Development Team +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# Author: Konstantin Lepa +# ============================================================================== +"""ANSI color formatting for output in terminal.""" + +from __future__ import annotations + +import io +import os +import sys +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing_extensions import Literal # Python 3.8+ + + Attribute = Literal[ + 'bold', + 'dark', + 'underline', + 'blink', + 'reverse', + 'concealed', + 'strike', + ] + Highlight = Literal[ + 'on_black', + 'on_grey', + 'on_red', + 'on_green', + 'on_yellow', + 'on_blue', + 'on_magenta', + 'on_cyan', + 'on_light_grey', + 'on_dark_grey', + 'on_light_red', + 'on_light_green', + 'on_light_yellow', + 'on_light_blue', + 'on_light_magenta', + 'on_light_cyan', + 'on_white', + ] + Color = Literal[ + 'black', + 'grey', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'light_grey', + 'dark_grey', + 'light_red', + 'light_green', + 'light_yellow', + 'light_blue', + 'light_magenta', + 'light_cyan', + 'white', + ] + + +__all__ = ['colored', 'cprint'] + + +if os.name == 'nt': # Windows + try: + from colorama import init + except ImportError: + pass + else: + init() + + +ATTRIBUTES: dict[Attribute, int] = { + 'bold': 1, + 'dark': 2, + 'underline': 4, + 'blink': 5, + 'reverse': 7, + 'concealed': 8, + 'strike': 9, +} + +HIGHLIGHTS: dict[Highlight, int] = { + 'on_black': 40, + 'on_grey': 40, # Actually black but kept for backwards compatibility + 'on_red': 41, + 'on_green': 42, + 'on_yellow': 43, + 'on_blue': 44, + 'on_magenta': 45, + 'on_cyan': 46, + 'on_light_grey': 47, + 'on_dark_grey': 100, + 'on_light_red': 101, + 'on_light_green': 102, + 'on_light_yellow': 103, + 'on_light_blue': 104, + 'on_light_magenta': 105, + 'on_light_cyan': 106, + 'on_white': 107, +} + +COLORS: dict[Color, int] = { + 'black': 30, + 'grey': 30, # Actually black but kept for backwards compatibility + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'light_grey': 37, + 'dark_grey': 90, + 'light_red': 91, + 'light_green': 92, + 'light_yellow': 93, + 'light_blue': 94, + 'light_magenta': 95, + 'light_cyan': 96, + 'white': 97, +} + + +RESET = '\033[0m' + + +# pylint: disable-next=too-many-return-statements +def _can_do_color( + *, + no_color: bool | None = None, + force_color: bool | None = None, +) -> bool: + """Check env vars and for tty/dumb terminal.""" + # First check overrides: + # "User-level configuration files and per-instance command-line arguments should + # override $NO_COLOR. A user should be able to export $NO_COLOR in their shell + # configuration file as a default, but configure a specific program in its + # configuration file to specifically enable color." + # https://no-color.org + if no_color is not None and no_color: + return False + if force_color is not None and force_color: + return True + + # Then check env vars: + if 'ANSI_COLORS_DISABLED' in os.environ: + return False + if 'NO_COLOR' in os.environ: + return False + if 'FORCE_COLOR' in os.environ: + return True + + # Then check system: + if os.environ.get('TERM') == 'dumb': + return False + if not hasattr(sys.stdout, 'fileno'): + return False + + try: + return os.isatty(sys.stdout.fileno()) + except io.UnsupportedOperation: + return sys.stdout.isatty() + + +# pylint: disable-next=too-many-arguments +def colored( + text: Any, + color: Color | None = None, + on_color: Highlight | None = None, + attrs: Iterable[Attribute] | None = None, + *, + no_color: bool | None = None, + force_color: bool | None = None, +) -> str: + """Colorize text. + + Available text colors: + black, red, green, yellow, blue, magenta, cyan, white, + light_grey, dark_grey, light_red, light_green, light_yellow, light_blue, + light_magenta, light_cyan. + + Available text highlights: + on_black, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white, + on_light_grey, on_dark_grey, on_light_red, on_light_green, on_light_yellow, + on_light_blue, on_light_magenta, on_light_cyan. + + Available attributes: + bold, dark, underline, blink, reverse, concealed. + + Example: + colored('Hello, World!', 'red', 'on_black', ['bold', 'blink']) + colored('Hello, World!', 'green') + """ + result = str(text) + if not _can_do_color(no_color=no_color, force_color=force_color): + return result + + fmt_str = '\033[%dm%s' + if color is not None: + result = fmt_str % (COLORS[color], result) + + if on_color is not None: + result = fmt_str % (HIGHLIGHTS[on_color], result) + + if attrs is not None: + for attr in attrs: + result = fmt_str % (ATTRIBUTES[attr], result) + + result += RESET + + return result + + +# pylint: disable-next=too-many-arguments +def cprint( + text: object, + color: Color | None = None, + on_color: Highlight | None = None, + attrs: Iterable[Attribute] | None = None, + *, + no_color: bool | None = None, + force_color: bool | None = None, + **kwargs: Any, +) -> None: + """Print colorized text. + + It accepts arguments of print function. + """ + print( + colored( + text, + color, + on_color, + attrs, + no_color=no_color, + force_color=force_color, + ), + **kwargs, + ) diff --git a/nvitop/api/utils.py b/nvitop/api/utils.py index 87ea6a9..9551a83 100644 --- a/nvitop/api/utils.py +++ b/nvitop/api/utils.py @@ -31,7 +31,7 @@ import time from collections.abc import KeysView from typing import TYPE_CHECKING, Any, Callable, TypeVar -from psutil import WINDOWS +from nvitop.api import termcolor if TYPE_CHECKING: @@ -62,30 +62,6 @@ __all__ = [ ] -if WINDOWS: - try: - from colorama import init - except ImportError: - pass - else: - init() - -try: - from termcolor import colored as _colored -except ImportError: - - def _colored( # type: ignore[misc] # pylint: disable=unused-argument,too-many-arguments - text: str, - color: str | None = None, - on_color: str | None = None, - attrs: Iterable[str] | None = None, - *, - no_color: bool | None = None, - force_color: bool | None = None, - ) -> str: - return text - - COLOR: bool = sys.stdout.isatty() @@ -102,10 +78,10 @@ def set_color(value: bool) -> None: def colored( - text: str, - color: str | None = None, - on_color: str | None = None, - attrs: Iterable[str] | None = None, + text: Any, + color: termcolor.Color | None = None, + on_color: termcolor.Highlight | None = None, + attrs: Iterable[termcolor.Attribute] | None = None, ) -> str: """Colorize text with ANSI color escape codes. @@ -123,8 +99,8 @@ def colored( >>> colored('Hello, World!', 'green') """ if COLOR: - return _colored(text, color=color, on_color=on_color, attrs=attrs) # type: ignore[arg-type] - return text + return termcolor.colored(text, color=color, on_color=on_color, attrs=attrs) + return str(text) class NaType(str): diff --git a/nvitop/cli.py b/nvitop/cli.py index 14eb3d9..59411a7 100644 --- a/nvitop/cli.py +++ b/nvitop/cli.py @@ -425,7 +425,7 @@ def main() -> int: if message.startswith(prefix): message = message.replace( prefix, - colored(prefix, color=color, attrs=('bold',)), + colored(prefix, color=color, attrs=('bold',)), # type: ignore[arg-type] 1, ) break diff --git a/pyproject.toml b/pyproject.toml index a684bf2..e31f262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ dependencies = [ # Sync with nvitop/version.py and requirements.txt "nvidia-ml-py >= 11.450.51, < 12.561.0a0", "psutil >= 5.6.6", - "termcolor >= 1.0.0", "colorama >= 0.4.0; platform_system == 'Windows'", "windows-curses >= 2.2.0; platform_system == 'Windows'", ] diff --git a/requirements.txt b/requirements.txt index 87929fa..baec469 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ # Sync with pyproject.toml and nvitop/version.py nvidia-ml-py >= 11.450.51, < 12.561.0a0 psutil >= 5.6.6 -termcolor >= 1.0.0 colorama >= 0.4.0; platform_system == 'Windows' windows-curses >= 2.2.0; platform_system == 'Windows'