deps(cachetools): remove third-party dependency cachetools (#147)

This commit is contained in:
Xuehai Pan 2025-01-24 16:52:16 +08:00 committed by GitHub
parent 0bcb5e0260
commit 652859c84b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 352 additions and 22 deletions

View file

@ -25,7 +25,7 @@ repos:
- id: debug-statements - id: debug-statements
- id: double-quote-string-fixer - id: double-quote-string-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.2 rev: v0.9.3
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]
@ -53,7 +53,7 @@ repos:
^docs/source/conf.py$ ^docs/source/conf.py$
) )
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.3.0 rev: v2.4.0
hooks: hooks:
- id: codespell - id: codespell
additional_dependencies: [".[toml]"] additional_dependencies: [".[toml]"]

View file

@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- - Remove third-party dependency `cachetools` by [@XuehaiPan](https://github.com/XuehaiPan) in [#147](https://github.com/XuehaiPan/nvitop/pull/147).
------ ------

View file

@ -115,7 +115,6 @@ An interactive NVIDIA-GPU process viewer and beyond, the one-stop solution for G
- NVIDIA Management Library (NVML) - NVIDIA Management Library (NVML)
- nvidia-ml-py - nvidia-ml-py
- psutil - psutil
- cachetools
- termcolor - termcolor
- curses<sup>[*](#curses)</sup> (with `libncursesw`) - curses<sup>[*](#curses)</sup> (with `libncursesw`)

View file

@ -0,0 +1,10 @@
nvitop.caching module
---------------------
.. currentmodule:: nvitop
.. autosummary::
ttl_cache
.. autofunction:: nvitop.ttl_cache

View file

@ -179,6 +179,7 @@ Please refer to section `More than a Monitor <https://github.com/XuehaiPan/nvito
api/libnvml api/libnvml
api/libcuda api/libcuda
api/libcudart api/libcudart
api/caching
api/utils api/utils
select select
callbacks callbacks

View file

@ -155,3 +155,6 @@ api
utils utils
GpuStatsLogger GpuStatsLogger
hostname hostname
len
maxsize
reentrant

View file

@ -20,7 +20,17 @@ import sys
from nvitop import api from nvitop import api
from nvitop.api import * # noqa: F403 from nvitop.api import * # noqa: F403
from nvitop.api import collector, device, host, libcuda, libcudart, libnvml, process, utils from nvitop.api import (
caching,
collector,
device,
host,
libcuda,
libcudart,
libnvml,
process,
utils,
)
from nvitop.select import select_devices from nvitop.select import select_devices
from nvitop.version import __version__ from nvitop.version import __version__
@ -28,7 +38,7 @@ from nvitop.version import __version__
__all__ = [*api.__all__, 'select_devices'] __all__ = [*api.__all__, 'select_devices']
# Add submodules to the top-level namespace # Add submodules to the top-level namespace
for submodule in (collector, device, host, libcuda, libcudart, libnvml, process, utils): for submodule in (caching, collector, device, host, libcuda, libcudart, libnvml, process, utils):
sys.modules[f'{__name__}.{submodule.__name__.rpartition(".")[-1]}'] = submodule sys.modules[f'{__name__}.{submodule.__name__.rpartition(".")[-1]}'] = submodule
# Remove the nvitop.select module from sys.modules # Remove the nvitop.select module from sys.modules

View file

@ -16,7 +16,18 @@
# ============================================================================== # ==============================================================================
"""The core APIs of nvitop.""" """The core APIs of nvitop."""
from nvitop.api import collector, device, host, libcuda, libcudart, libnvml, process, utils from nvitop.api import (
caching,
collector,
device,
host,
libcuda,
libcudart,
libnvml,
process,
utils,
)
from nvitop.api.caching import ttl_cache
from nvitop.api.collector import ResourceMetricCollector, collect_in_background, take_snapshots from nvitop.api.collector import ResourceMetricCollector, collect_in_background, take_snapshots
from nvitop.api.device import ( from nvitop.api.device import (
CudaDevice, CudaDevice,
@ -76,6 +87,8 @@ __all__ = [
'take_snapshots', 'take_snapshots',
'collect_in_background', 'collect_in_background',
'ResourceMetricCollector', 'ResourceMetricCollector',
# nvitop.api.caching
'ttl_cache',
# nvitop.api.utils # nvitop.api.utils
'NA', 'NA',
'NaType', 'NaType',

279
nvitop/api/caching.py Normal file
View file

@ -0,0 +1,279 @@
# 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.
# ==============================================================================
"""Caching utilities."""
from __future__ import annotations
import builtins
import functools
import time
from threading import RLock
from typing import TYPE_CHECKING, Any, NamedTuple, overload
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
_P = ParamSpec('_P')
_T = TypeVar('_T')
__all__ = ['ttl_cache']
class _CacheInfo(NamedTuple):
"""A named tuple representing the cache statistics."""
hits: int
misses: int
maxsize: int
currsize: int
try:
from functools import _make_key
except ImportError:
class _HashedSeq(list):
"""This class guarantees that hash() will be called no more than once per element."""
__slots__ = ('__hashvalue',)
def __init__(
self,
seq: tuple[Any, ...],
hash: Callable[[Any], int] = builtins.hash, # pylint: disable=redefined-builtin
) -> None:
"""Initialize the hashed sequence."""
self[:] = seq
self.__hashvalue = hash(seq)
def __hash__(self) -> int: # type: ignore[override]
"""Return the hash value of the hashed sequence."""
return self.__hashvalue
_KWD_MARK = object()
# pylint: disable-next=too-many-arguments
def _make_key( # type: ignore[misc]
args: tuple[Hashable, ...],
kwds: dict[str, Hashable],
typed: bool,
*,
kwd_mark: tuple[object, ...] = (_KWD_MARK,),
fasttypes: AbstractSet[type] = frozenset({int, str}),
tuple: type[tuple] = builtins.tuple, # pylint: disable=redefined-builtin
type: type[type] = builtins.type, # pylint: disable=redefined-builtin
len: Callable[[Sized], int] = builtins.len, # pylint: disable=redefined-builtin
) -> Hashable:
"""Make a cache key from optionally typed positional and keyword arguments."""
key = args
if kwds:
key += kwd_mark
for item in kwds.items():
key += item
if typed:
key += tuple(type(v) for v in args)
if kwds:
key += tuple(type(v) for v in kwds.values())
elif len(key) == 1 and type(key[0]) in fasttypes:
return key[0]
return _HashedSeq(key)
class _TTLCacheLink: # pylint: disable=too-few-public-methods
__slots__ = ('expires', 'key', 'next', 'prev', 'value')
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
def __init__(
self,
prev: Self | None,
next: Self | None, # pylint: disable=redefined-builtin
key: Hashable,
value: Any,
expires: float | None,
) -> None:
self.prev: Self = prev # type: ignore[assignment]
self.next: Self = next # type: ignore[assignment]
self.key: Hashable = key
self.value: Any = value
self.expires: float = expires # type: ignore[assignment]
@overload
def ttl_cache(
maxsize: int | None = 128,
ttl: float = 600.0,
timer: Callable[[], float] = time.monotonic,
typed: bool = False,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ...
@overload
def ttl_cache(
maxsize: Callable[_P, _T],
ttl: float = 600.0,
timer: Callable[[], float] = time.monotonic,
typed: bool = False,
) -> Callable[_P, _T]: ...
# pylint: disable-next=too-many-statements
def ttl_cache(
maxsize: int | Callable[_P, _T] | None = 128,
ttl: float = 600.0,
timer: Callable[[], float] = time.monotonic,
typed: bool = False,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]] | Callable[_P, _T]:
"""Time aware cache decorator."""
if isinstance(maxsize, int):
# Negative maxsize is treated as 0
maxsize = max(0, maxsize)
elif callable(maxsize) and isinstance(typed, bool):
# The user_function was passed in directly via the maxsize argument
func, maxsize = maxsize, 128
return ttl_cache(maxsize, ttl=ttl, timer=timer, typed=typed)(func)
elif maxsize is not None:
raise TypeError('Expected first argument to be an integer, a callable, or None')
if ttl < 0.0:
raise ValueError('TTL must be a non-negative number')
if not callable(timer):
raise TypeError('Timer must be a callable')
if maxsize == 0 or maxsize is None:
return functools.lru_cache(maxsize=maxsize, typed=typed) # type: ignore[return-value]
# pylint: disable-next=too-many-statements,too-many-locals
def wrapper(func: Callable[_P, _T]) -> Callable[_P, _T]:
cache: dict[Any, _TTLCacheLink] = {}
cache_get = cache.get # bound method to lookup a key or return None
cache_len = cache.__len__ # get cache size without calling len()
lock = RLock() # because linked-list updates aren't thread-safe
root = _TTLCacheLink(*((None,) * 5)) # root of the circular doubly linked list
root.prev = root.next = root # initialize by pointing to self
hits = misses = 0
full = False
def unlink(link: _TTLCacheLink) -> _TTLCacheLink:
with lock:
link_prev, link_next = link.prev, link.next
link_next.prev, link_prev.next = link_prev, link_next
return link_next
def append(link: _TTLCacheLink) -> _TTLCacheLink:
with lock:
last = root.prev
last.next = root.prev = link
link.prev, link.next = last, root
return link
def move_to_end(link: _TTLCacheLink) -> _TTLCacheLink:
with lock:
unlink(link)
append(link)
return link
def expire() -> None:
nonlocal full
with lock:
now = timer()
front = root.next
while front is not root and front.expires < now:
del cache[front.key]
front = unlink(front)
full = cache_len() >= maxsize
@functools.wraps(func)
def wrapped(*args: _P.args, **kwargs: _P.kwargs) -> _T:
# Size limited time aware caching
nonlocal root, hits, misses, full
key = _make_key(args, kwargs, typed)
with lock:
link = cache_get(key)
if link is not None:
if timer() < link.expires:
hits += 1
return link.value
expire()
misses += 1
result = func(*args, **kwargs)
expires = timer() + ttl
with lock:
if key in cache:
# Getting here means that this same key was added to the cache while the lock
# was released or the key was expired. Move the link to the front of the
# circular queue.
link = move_to_end(cache[key])
# We need only update the expiration time.
link.value = result
link.expires = expires
else:
if full:
expire()
if full:
# Use the old root to store the new key and result.
root.key = key
root.value = result
root.expires = expires
# Empty the oldest link and make it the new root.
# Keep a reference to the old key and old result to prevent their ref counts
# from going to zero during the update. That will prevent potentially
# arbitrary object clean-up code (i.e. __del__) from running while we're
# still adjusting the links.
front = root.next
old_key = front.key
front.key = front.value = front.expires = None # type: ignore[assignment]
# Now update the cache dictionary.
del cache[old_key]
# Save the potentially reentrant cache[key] assignment for last, after the
# root and links have been put in a consistent state.
cache[key], root = root, front
else:
# Put result in a new link at the front of the queue.
cache[key] = append(_TTLCacheLink(None, None, key, result, expires))
full = cache_len() >= maxsize
return result
def cache_info() -> _CacheInfo:
"""Report cache statistics."""
with lock:
expire()
return _CacheInfo(hits, misses, maxsize, cache_len())
def cache_clear() -> None:
"""Clear the cache and cache statistics."""
nonlocal hits, misses, full
with lock:
cache.clear()
root.prev = root.next = root
root.key = root.value = root.expires = None # type: ignore[assignment]
hits = misses = 0
full = False
wrapped.cache_info = cache_info # type: ignore[attr-defined]
wrapped.cache_clear = cache_clear # type: ignore[attr-defined]
wrapped.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed} # type: ignore[attr-defined]
return wrapped
return wrapper

View file

@ -39,5 +39,6 @@ from nvitop.gui.library.utils import (
cut_string, cut_string,
make_bar, make_bar,
set_color, set_color,
ttl_cache,
) )
from nvitop.gui.library.widestring import WideString, wcslen from nvitop.gui.library.widestring import WideString, wcslen

View file

@ -3,9 +3,7 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
from cachetools.func import ttl_cache from nvitop.api import NA, libnvml, ttl_cache, utilization2string
from nvitop.api import NA, libnvml, utilization2string
from nvitop.api import MigDevice as MigDeviceBase from nvitop.api import MigDevice as MigDeviceBase
from nvitop.api import PhysicalDevice as DeviceBase from nvitop.api import PhysicalDevice as DeviceBase
from nvitop.gui.library.process import GpuProcess from nvitop.gui.library.process import GpuProcess

View file

@ -3,7 +3,6 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
from nvitop.api import ( from nvitop.api import (
NA, NA,
GiB, GiB,

View file

@ -7,10 +7,25 @@ import contextlib
import math import math
import os import os
from nvitop.api import NA, colored, host, set_color # noqa: F401 # pylint: disable=unused-import from nvitop.api import NA, colored, host, set_color, ttl_cache
from nvitop.gui.library.widestring import WideString from nvitop.gui.library.widestring import WideString
__all__ = [
'NA',
'USERNAME',
'HOSTNAME',
'SUPERUSER',
'USERCONTEXT',
'LARGE_INTEGER',
'ttl_cache',
'colored',
'set_color',
'cut_string',
'make_bar',
]
USERNAME = 'N/A' USERNAME = 'N/A'
with contextlib.suppress(ImportError, OSError): with contextlib.suppress(ImportError, OSError):
USERNAME = host.getuser() USERNAME = host.getuser()

View file

@ -6,9 +6,16 @@
import threading import threading
import time import time
from cachetools.func import ttl_cache from nvitop.gui.library import (
NA,
from nvitop.gui.library import NA, Device, Displayable, colored, cut_string, host, make_bar Device,
Displayable,
colored,
cut_string,
host,
make_bar,
ttl_cache,
)
from nvitop.version import __version__ from nvitop.version import __version__

View file

@ -11,8 +11,6 @@ import time
from operator import attrgetter, xor from operator import attrgetter, xor
from typing import TYPE_CHECKING, Any, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple
from cachetools.func import ttl_cache
from nvitop.gui.library import ( from nvitop.gui.library import (
HOSTNAME, HOSTNAME,
LARGE_INTEGER, LARGE_INTEGER,
@ -27,6 +25,7 @@ from nvitop.gui.library import (
colored, colored,
cut_string, cut_string,
host, host,
ttl_cache,
wcslen, wcslen,
) )

View file

@ -9,8 +9,6 @@ from collections import deque
from functools import partial from functools import partial
from itertools import islice from itertools import islice
from cachetools.func import ttl_cache
from nvitop.gui.library import ( from nvitop.gui.library import (
NA, NA,
SUPERUSER, SUPERUSER,
@ -22,6 +20,7 @@ from nvitop.gui.library import (
WideString, WideString,
host, host,
send_signal, send_signal,
ttl_cache,
) )

View file

@ -49,7 +49,6 @@ dependencies = [
# Sync with nvitop/version.py and requirements.txt # Sync with nvitop/version.py and requirements.txt
"nvidia-ml-py >= 11.450.51, < 12.561.0a0", "nvidia-ml-py >= 11.450.51, < 12.561.0a0",
"psutil >= 5.6.6", "psutil >= 5.6.6",
"cachetools >= 1.0.1",
"termcolor >= 1.0.0", "termcolor >= 1.0.0",
"colorama >= 0.4.0; platform_system == 'Windows'", "colorama >= 0.4.0; platform_system == 'Windows'",
"windows-curses >= 2.2.0; platform_system == 'Windows'", "windows-curses >= 2.2.0; platform_system == 'Windows'",
@ -204,7 +203,6 @@ ignore = [
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["nvitop", "nvitop_exporter"] known-first-party = ["nvitop", "nvitop_exporter"]
known-local-folder = ["nvitop", "nvitop-exporter"]
extra-standard-library = ["typing_extensions"] extra-standard-library = ["typing_extensions"]
lines-after-imports = 2 lines-after-imports = 2

View file

@ -1,7 +1,6 @@
# Sync with pyproject.toml and nvitop/version.py # Sync with pyproject.toml and nvitop/version.py
nvidia-ml-py >= 11.450.51, < 12.561.0a0 nvidia-ml-py >= 11.450.51, < 12.561.0a0
psutil >= 5.6.6 psutil >= 5.6.6
cachetools >= 1.0.1
termcolor >= 1.0.0 termcolor >= 1.0.0
colorama >= 0.4.0; platform_system == 'Windows' colorama >= 0.4.0; platform_system == 'Windows'
windows-curses >= 2.2.0; platform_system == 'Windows' windows-curses >= 2.2.0; platform_system == 'Windows'