diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 01b95b0..a5a6a88 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,7 +25,7 @@ repos:
- id: debug-statements
- id: double-quote-string-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.9.2
+ rev: v0.9.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
@@ -53,7 +53,7 @@ repos:
^docs/source/conf.py$
)
- repo: https://github.com/codespell-project/codespell
- rev: v2.3.0
+ rev: v2.4.0
hooks:
- id: codespell
additional_dependencies: [".[toml]"]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70d9ea1..f04f23c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
--
+- 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 f8f0b25..1291ed0 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
-- cachetools
- termcolor
- curses[*](#curses) (with `libncursesw`)
diff --git a/docs/source/api/caching.rst b/docs/source/api/caching.rst
new file mode 100644
index 0000000..b9f269b
--- /dev/null
+++ b/docs/source/api/caching.rst
@@ -0,0 +1,10 @@
+nvitop.caching module
+---------------------
+
+.. currentmodule:: nvitop
+
+.. autosummary::
+
+ ttl_cache
+
+.. autofunction:: nvitop.ttl_cache
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a6e6085..12e9e64 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -179,6 +179,7 @@ Please refer to section `More than a Monitor 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
diff --git a/nvitop/gui/library/__init__.py b/nvitop/gui/library/__init__.py
index 1c1c98e..9dba057 100644
--- a/nvitop/gui/library/__init__.py
+++ b/nvitop/gui/library/__init__.py
@@ -39,5 +39,6 @@ from nvitop.gui.library.utils import (
cut_string,
make_bar,
set_color,
+ ttl_cache,
)
from nvitop.gui.library.widestring import WideString, wcslen
diff --git a/nvitop/gui/library/device.py b/nvitop/gui/library/device.py
index d29cfc8..a147b80 100644
--- a/nvitop/gui/library/device.py
+++ b/nvitop/gui/library/device.py
@@ -3,9 +3,7 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
-from cachetools.func import ttl_cache
-
-from nvitop.api import NA, libnvml, utilization2string
+from nvitop.api import NA, libnvml, ttl_cache, utilization2string
from nvitop.api import MigDevice as MigDeviceBase
from nvitop.api import PhysicalDevice as DeviceBase
from nvitop.gui.library.process import GpuProcess
diff --git a/nvitop/gui/library/process.py b/nvitop/gui/library/process.py
index 05d6a94..e12fbd0 100644
--- a/nvitop/gui/library/process.py
+++ b/nvitop/gui/library/process.py
@@ -3,7 +3,6 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
-
from nvitop.api import (
NA,
GiB,
diff --git a/nvitop/gui/library/utils.py b/nvitop/gui/library/utils.py
index 5ab64a7..d97ea48 100644
--- a/nvitop/gui/library/utils.py
+++ b/nvitop/gui/library/utils.py
@@ -7,10 +7,25 @@ import contextlib
import math
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
+__all__ = [
+ 'NA',
+ 'USERNAME',
+ 'HOSTNAME',
+ 'SUPERUSER',
+ 'USERCONTEXT',
+ 'LARGE_INTEGER',
+ 'ttl_cache',
+ 'colored',
+ 'set_color',
+ 'cut_string',
+ 'make_bar',
+]
+
+
USERNAME = 'N/A'
with contextlib.suppress(ImportError, OSError):
USERNAME = host.getuser()
diff --git a/nvitop/gui/screens/main/device.py b/nvitop/gui/screens/main/device.py
index c47babb..fddeaff 100644
--- a/nvitop/gui/screens/main/device.py
+++ b/nvitop/gui/screens/main/device.py
@@ -6,9 +6,16 @@
import threading
import time
-from cachetools.func import ttl_cache
-
-from nvitop.gui.library import NA, Device, Displayable, colored, cut_string, host, make_bar
+from nvitop.gui.library import (
+ NA,
+ Device,
+ Displayable,
+ colored,
+ cut_string,
+ host,
+ make_bar,
+ ttl_cache,
+)
from nvitop.version import __version__
diff --git a/nvitop/gui/screens/main/process.py b/nvitop/gui/screens/main/process.py
index d5e9c34..d91aee8 100644
--- a/nvitop/gui/screens/main/process.py
+++ b/nvitop/gui/screens/main/process.py
@@ -11,8 +11,6 @@ import time
from operator import attrgetter, xor
from typing import TYPE_CHECKING, Any, NamedTuple
-from cachetools.func import ttl_cache
-
from nvitop.gui.library import (
HOSTNAME,
LARGE_INTEGER,
@@ -27,6 +25,7 @@ from nvitop.gui.library import (
colored,
cut_string,
host,
+ ttl_cache,
wcslen,
)
diff --git a/nvitop/gui/screens/treeview.py b/nvitop/gui/screens/treeview.py
index e6a9f9b..cb7a5cd 100644
--- a/nvitop/gui/screens/treeview.py
+++ b/nvitop/gui/screens/treeview.py
@@ -9,8 +9,6 @@ from collections import deque
from functools import partial
from itertools import islice
-from cachetools.func import ttl_cache
-
from nvitop.gui.library import (
NA,
SUPERUSER,
@@ -22,6 +20,7 @@ from nvitop.gui.library import (
WideString,
host,
send_signal,
+ ttl_cache,
)
diff --git a/pyproject.toml b/pyproject.toml
index 539651a..a684bf2 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",
- "cachetools >= 1.0.1",
"termcolor >= 1.0.0",
"colorama >= 0.4.0; platform_system == 'Windows'",
"windows-curses >= 2.2.0; platform_system == 'Windows'",
@@ -204,7 +203,6 @@ ignore = [
[tool.ruff.lint.isort]
known-first-party = ["nvitop", "nvitop_exporter"]
-known-local-folder = ["nvitop", "nvitop-exporter"]
extra-standard-library = ["typing_extensions"]
lines-after-imports = 2
diff --git a/requirements.txt b/requirements.txt
index b5bb43c..87929fa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
# Sync with pyproject.toml and nvitop/version.py
nvidia-ml-py >= 11.450.51, < 12.561.0a0
psutil >= 5.6.6
-cachetools >= 1.0.1
termcolor >= 1.0.0
colorama >= 0.4.0; platform_system == 'Windows'
windows-curses >= 2.2.0; platform_system == 'Windows'