diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a20718cf..060d3328 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,7 +14,7 @@ - Operating system and version: [e.g. Ubuntu 20.04 LTS / Windows 10 Build 19043.1110] - Terminal emulator and version: [e.g. GNOME Terminal 3.36.2 / Windows Terminal 1.8.1521.0] -- Python version: [e.g. `3.7.2` / `3.9.6`] +- Python version: [e.g. `3.8.2` / `3.9.6`] - NVML version (driver version): [e.g. `460.84`] - `nvitop` version or commit: [e.g. `0.10.0` / `0.10.1.dev7+ga083321` / `main@75ae3c`] - `python-ml-py` version: [e.g. `11.450.51`] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 69a6215a..eb5d4335 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -50,7 +50,7 @@ jobs: id: py uses: actions/setup-python@v5 with: - python-version: "3.7 - 3.13" + python-version: "3.8 - 3.13" update-environment: true - name: Upgrade build dependencies @@ -129,7 +129,7 @@ jobs: uses: actions/setup-python@v5 if: startsWith(github.ref, 'refs/tags/') with: - python-version: "3.7 - 3.13" + python-version: "3.8 - 3.13" update-environment: true - name: Upgrade pip diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index c2f4bb63..1098844f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -27,7 +27,7 @@ jobs: id: py uses: actions/setup-python@v5 with: - python-version: "3.7 - 3.13" + python-version: "3.8 - 3.13" update-environment: true - name: Upgrade pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c27acf93..755b3eb1 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.3 + rev: v0.9.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -34,7 +34,7 @@ repos: rev: v3.19.1 hooks: - id: pyupgrade - args: [--py37-plus] # sync with requires-python + args: [--py38-plus] # sync with requires-python - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bfa2b4..1a7f5511 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 -- +- Drop Python 3.7 support by [@XuehaiPan](https://github.com/XuehaiPan) in [#150](https://github.com/XuehaiPan/nvitop/pull/150). ------ diff --git a/README.md b/README.md index 594d86ba..e3ce6ee4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -![Python 3.7+](https://img.shields.io/badge/Python-3.7%2B-brightgreen) +![Python 3.8+](https://img.shields.io/badge/Python-3.8%2B-brightgreen) [![PyPI](https://img.shields.io/pypi/v/nvitop?label=pypi&logo=pypi)](https://pypi.org/project/nvitop) [![conda-forge](https://img.shields.io/conda/vn/conda-forge/nvitop?label=conda&logo=condaforge)](https://anaconda.org/conda-forge/nvitop) [![Documentation Status](https://img.shields.io/readthedocs/nvitop?label=docs&logo=readthedocs)](https://nvitop.readthedocs.io) @@ -111,7 +111,7 @@ An interactive NVIDIA-GPU process viewer and beyond, the one-stop solution for G ## Requirements -- Python 3.7+ +- Python 3.8+ - NVIDIA Management Library (NVML) - nvidia-ml-py - psutil diff --git a/docs/source/index.rst b/docs/source/index.rst index de65a325..7c4d10fa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,7 +13,7 @@ An interactive NVIDIA-GPU process viewer and beyond, the one-stop solution for G .. |GitHub| image:: https://img.shields.io/badge/GitHub-Homepage-blue?logo=github .. _GitHub: https://github.com/XuehaiPan/nvitop -.. |Python Version| image:: https://img.shields.io/badge/Python-3.7%2B-brightgreen +.. |Python Version| image:: https://img.shields.io/badge/Python-3.8%2B-brightgreen .. _Python Version: https://pypi.org/project/nvitop .. |PyPI Package| image:: https://img.shields.io/pypi/v/nvitop?label=pypi&logo=pypi @@ -56,7 +56,7 @@ Install from PyPI (|PyPI Package|_): .. note:: - Python 3.7+ is required, and Python versions lower than 3.7 is not supported. + Python 3.8+ is required, and Python versions lower than 3.8 is not supported. Install from conda-forge (|Conda-forge Package|_): diff --git a/nvitop-exporter/pyproject.toml b/nvitop-exporter/pyproject.toml index 95d257b0..7f3c7bde 100644 --- a/nvitop-exporter/pyproject.toml +++ b/nvitop-exporter/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "nvitop-exporter" description = "Prometheus exporter built on top of `nvitop`." readme = "README.md" -requires-python = ">= 3.7" +requires-python = ">= 3.8" authors = [{ name = "Xuehai Pan", email = "XuehaiPan@pku.edu.cn" }] license = { text = "Apache License, Version 2.0 (Apache-2.0)" } keywords = [ @@ -27,7 +27,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/nvitop/api/caching.py b/nvitop/api/caching.py index d2e82507..ff19c564 100644 --- a/nvitop/api/caching.py +++ b/nvitop/api/caching.py @@ -123,6 +123,7 @@ def __init__( @overload def ttl_cache( maxsize: int | None = 128, + *, ttl: float = 600.0, timer: Callable[[], float] = time.monotonic, typed: bool = False, @@ -132,6 +133,7 @@ def ttl_cache( @overload def ttl_cache( maxsize: Callable[_P, _T], + *, ttl: float = 600.0, timer: Callable[[], float] = time.monotonic, typed: bool = False, @@ -141,6 +143,7 @@ def ttl_cache( # 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, @@ -165,7 +168,7 @@ def ttl_cache( 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]: + 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() diff --git a/nvitop/api/collector.py b/nvitop/api/collector.py index e58bfdec..0bd7b5a9 100644 --- a/nvitop/api/collector.py +++ b/nvitop/api/collector.py @@ -52,7 +52,7 @@ class SnapshotResult(NamedTuple): # pylint: disable=missing-class-docstring _T = TypeVar('_T') -def _unique(iterable: Iterable[_T]) -> list[_T]: +def _unique(iterable: Iterable[_T], /) -> list[_T]: return list(OrderedDict.fromkeys(iterable).keys()) @@ -399,6 +399,7 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes def __init__( self, devices: Iterable[Device] | None = None, + *, root_pids: Iterable[int] | None = None, interval: float = 1.0, ) -> None: @@ -778,6 +779,7 @@ def __init__( self, tag: str, collector: ResourceMetricCollector, + *, prev: _MetricBuffer | None = None, ) -> None: self.collector: ResourceMetricCollector = collector diff --git a/nvitop/api/device.py b/nvitop/api/device.py index e3c5f69d..b5e8ae8c 100644 --- a/nvitop/api/device.py +++ b/nvitop/api/device.py @@ -1589,6 +1589,7 @@ def nvlink_throughput(self, interval: float | None = None) -> list[ThroughputInf return [] def query_nvlink_throughput_counters() -> tuple[tuple[int | NaType, int]]: + assert self._handle is not None return tuple( # type: ignore[return-value] libnvml.nvmlQueryFieldValues( self._handle, @@ -3149,7 +3150,7 @@ def _parse_cuda_visible_devices( ) -> list[str]: ... -@functools.lru_cache() +@functools.lru_cache def _parse_cuda_visible_devices( # pylint: disable=too-many-branches,too-many-statements cuda_visible_devices: str | None = None, format: Literal['index', 'uuid'] = 'index', # pylint: disable=redefined-builtin diff --git a/nvitop/api/libcuda.py b/nvitop/api/libcuda.py index 50b50c48..47132272 100644 --- a/nvitop/api/libcuda.py +++ b/nvitop/api/libcuda.py @@ -277,7 +277,7 @@ def __reduce__(self) -> tuple[type[CUDAError], tuple[int]]: return CUDAError, (self.value,) # pylint: disable=no-member -def cudaExceptionClass(cudaErrorCode: int) -> type[CUDAError]: +def cudaExceptionClass(cudaErrorCode: int, /) -> type[CUDAError]: """Map value to a proper subclass of :class:`CUDAError`. Raises: @@ -348,7 +348,7 @@ def __new__(cls) -> CUDAError: # type: ignore[misc,empty-body] del _extract_cuda_errors_as_classes -def _cudaCheckReturn(ret: _Any) -> _Any: +def _cudaCheckReturn(ret: _Any, /) -> _Any: if ret != CUDA_SUCCESS: raise CUDAError(ret) return ret diff --git a/nvitop/api/libcudart.py b/nvitop/api/libcudart.py index 2e7f3fc1..bff1e506 100644 --- a/nvitop/api/libcudart.py +++ b/nvitop/api/libcudart.py @@ -325,7 +325,7 @@ def __reduce__(self) -> tuple[type[cudaError], tuple[int]]: return cudaError, (self.value,) # pylint: disable=no-member -def cudaExceptionClass(cudaErrorCode: int) -> type[cudaError]: +def cudaExceptionClass(cudaErrorCode: int, /) -> type[cudaError]: """Map value to a proper subclass of :class:`cudaError`. Raises: @@ -399,7 +399,7 @@ def __new__(cls) -> cudaError: # type: ignore[misc,empty-body] del _extract_cuda_errors_as_classes -def _cudaCheckReturn(ret: _Any) -> _Any: +def _cudaCheckReturn(ret: _Any, /) -> _Any: if ret != cudaSuccess: raise cudaError(ret) return ret @@ -412,7 +412,7 @@ def _cudaCheckReturn(ret: _Any) -> _Any: __cudaGetFunctionPointer_cache: dict[str, _ctypes._CFuncPtr] = {} # type: ignore[name-defined] -def __cudaGetFunctionPointer(name: str) -> _ctypes._CFuncPtr: # type: ignore[name-defined] +def __cudaGetFunctionPointer(name: str, /) -> _ctypes._CFuncPtr: # type: ignore[name-defined] """Get the function pointer from the CUDA Runtime library. Raises: diff --git a/nvitop/api/libnvml.py b/nvitop/api/libnvml.py index cc73848f..eca2f9a0 100644 --- a/nvitop/api/libnvml.py +++ b/nvitop/api/libnvml.py @@ -174,7 +174,14 @@ # 5. Add explicit references to appease linters # pylint: disable=no-member -c_nvmlDevice_t: _TypeAlias = _pynvml.c_nvmlDevice_t # noqa: PYI042 +if _TYPE_CHECKING: + # pylint: disable-next=missing-class-docstring,too-few-public-methods,function-redefined + class c_nvmlDevice_t(_ctypes.c_void_p): + pass + +else: + c_nvmlDevice_t: _TypeAlias = _pynvml.c_nvmlDevice_t # type: ignore[no-redef] # noqa: PYI042 + c_nvmlFieldValue_t: _TypeAlias = _pynvml.c_nvmlFieldValue_t # noqa: PYI042 NVML_SUCCESS: int = _pynvml.NVML_SUCCESS NVML_ERROR_INSUFFICIENT_SIZE: int = _pynvml.NVML_ERROR_INSUFFICIENT_SIZE @@ -377,6 +384,7 @@ def nvmlShutdown() -> None: # pylint: disable=function-redefined def nvmlQuery( func: _Callable[..., _Any] | str, + /, *args: _Any, default: _Any = NA, ignore_errors: bool = True, @@ -513,7 +521,7 @@ def nvmlQueryFieldValues( return values_with_timestamps -def nvmlCheckReturn(retval: _Any, types: type | tuple[type, ...] | None = None) -> bool: +def nvmlCheckReturn(retval: _Any, types: type | tuple[type, ...] | None = None, /) -> bool: """Check whether the return value is not :const:`nvitop.NA` and is one of the given types.""" if types is None: return retval != NA @@ -643,6 +651,7 @@ def lookup(symbol: str) -> _Any | None: def __nvml_device_get_running_processes( func: str, + /, handle: c_nvmlDevice_t, ) -> list[c_nvmlProcessInfo_t]: """Helper function for :func:`nvmlDeviceGet{Compute,Graphics,MPSCompute}RunningProcesses`. diff --git a/nvitop/api/process.py b/nvitop/api/process.py index e6397d02..fa69e9ab 100644 --- a/nvitop/api/process.py +++ b/nvitop/api/process.py @@ -114,9 +114,9 @@ def auto_garbage_clean( raises an exception when falls. """ - def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: + def wrapper(func: Callable[..., Any], /) -> Callable[..., Any]: @functools.wraps(func) - def wrapped(self: GpuProcess, *args: Any, **kwargs: Any) -> Any: + def wrapped(self: GpuProcess, /, *args: Any, **kwargs: Any) -> Any: try: return func(self, *args, **kwargs) except host.PsutilError as ex: diff --git a/nvitop/api/termcolor.py b/nvitop/api/termcolor.py index 5783d387..03a97eb4 100644 --- a/nvitop/api/termcolor.py +++ b/nvitop/api/termcolor.py @@ -209,6 +209,7 @@ def _can_do_color( # pylint: disable-next=too-many-arguments def colored( text: Any, + /, color: Color | None = None, on_color: Highlight | None = None, attrs: Iterable[Attribute] | None = None, @@ -258,6 +259,7 @@ def colored( # pylint: disable-next=too-many-arguments def cprint( text: object, + /, color: Color | None = None, on_color: Highlight | None = None, attrs: Iterable[Attribute] | None = None, diff --git a/nvitop/api/utils.py b/nvitop/api/utils.py index 9551a833..2399c375 100644 --- a/nvitop/api/utils.py +++ b/nvitop/api/utils.py @@ -29,7 +29,7 @@ import sys import time from collections.abc import KeysView -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, Callable, TypeVar, final from nvitop.api import termcolor @@ -79,6 +79,7 @@ def set_color(value: bool) -> None: def colored( text: Any, + /, color: termcolor.Color | None = None, on_color: termcolor.Highlight | None = None, attrs: Iterable[termcolor.Attribute] | None = None, @@ -103,6 +104,7 @@ def colored( return str(text) +@final class NaType(str): """A singleton (:const:`str: 'N/A'`) class represents a not applicable value. @@ -133,8 +135,7 @@ class NaType(str): nan """ - # NOTE: Decorate this class with `@final` and remove `noqa` when we drop Python 3.7 support. - def __new__(cls) -> NaType: # noqa: PYI034 + def __new__(cls) -> NaType: """Get the singleton instance (:const:`nvitop.NA`).""" if not hasattr(cls, '_instance'): cls._instance = super().__new__(cls, 'N/A') @@ -510,6 +511,7 @@ def __format__(self, format_spec: str) -> str: # pylint: disable-next=too-many-return-statements,too-many-branches def bytes2human( b: int | float | NaType, # noqa: PYI041 + /, *, min_unit: int = 1, ) -> str: @@ -546,7 +548,7 @@ def bytes2human( return f'{round(b / PiB, 1):.1f}PiB' -def human2bytes(s: int | str) -> int: +def human2bytes(s: int | str, /) -> int: """Convert a human readable size string (*case insensitive*) to bytes. Raises: @@ -582,6 +584,7 @@ def human2bytes(s: int | str) -> int: def timedelta2human( dt: int | float | datetime.timedelta | NaType, # noqa: PYI041 + /, *, round: bool = False, # pylint: disable=redefined-builtin ) -> str: @@ -601,7 +604,7 @@ def timedelta2human( return '{:d}:{:02d}'.format(*divmod(seconds, 60)) -def utilization2string(utilization: int | float | NaType) -> str: # noqa: PYI041 +def utilization2string(utilization: int | float | NaType, /) -> str: # noqa: PYI041 """Convert a utilization rate to string.""" if utilization != NA: if isinstance(utilization, int): @@ -611,7 +614,7 @@ def utilization2string(utilization: int | float | NaType) -> str: # noqa: PYI04 return NA -def boolify(string: str, default: Any = None) -> bool: +def boolify(string: str, /, default: Any = None) -> bool: """Convert the given value, usually a string, to boolean.""" if string.lower() in {'true', 'yes', 'on', 'enabled', '1'}: return True @@ -703,7 +706,7 @@ def keys(self) -> Iterable[str]: # Modified from psutil (https://github.com/giampaolo/psutil) -def memoize_when_activated(method: Method) -> Method: +def memoize_when_activated(method: Method, /) -> Method: """A memoize decorator which is disabled by default. It can be activated and deactivated on request. For efficiency reasons it can be used only @@ -711,7 +714,7 @@ def memoize_when_activated(method: Method) -> Method: """ @functools.wraps(method) - def wrapped(self: object, *args: Any, **kwargs: Any) -> Any: + def wrapped(self: object, /, *args: Any, **kwargs: Any) -> Any: try: # case 1: we previously entered oneshot() ctx # pylint: disable-next=protected-access diff --git a/pyproject.toml b/pyproject.toml index eb897115..9acea406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "nvitop" description = "An interactive NVIDIA-GPU process viewer and beyond, the one-stop solution for GPU process management." readme = "README.md" -requires-python = ">= 3.7" +requires-python = ">= 3.8" authors = [{ name = "Xuehai Pan", email = "XuehaiPan@pku.edu.cn" }] license = { text = "Apache License, Version 2.0 (Apache-2.0) & GNU General Public License, Version 3 (GPL-3.0)" } keywords = [ @@ -24,7 +24,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -96,7 +95,7 @@ module = ['nvitop.callbacks.*', 'nvitop.tui.*'] ignore_errors = true [tool.pylint] -main.py-version = "3.7" +main.py-version = "3.8" basic.good-names = ["x", "y", "dx", "dy", "p", "s", "fg", "bg", "n", "ui", "tx", "rx"] format.max-line-length = 120 "messages control".disable = ["consider-using-f-string", "duplicate-code", "wrong-import-order"] @@ -108,7 +107,7 @@ ignore-words = "docs/source/spelling_wordlist.txt" [tool.ruff] # Sync with requires-python -target-version = "py37" +target-version = "py38" line-length = 100 output-format = "full" src = ["nvitop", "nvitop-exporter/nvitop_exporter"]