1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3# Copyright(c) 2024 PANTHEON.tech s.r.o. 4# 5"""Utilities for DTS dependencies. 6 7The module can be used as an executable script, 8which verifies that the running Python version meets the version requirement of DTS. 9The script exits with the standard exit codes in this mode (0 is success, 1 is failure). 10 11The module also contains a function, get_missing_imports, 12which looks for runtime dependencies in the DTS pyproject.toml file 13and returns a list of module names used in an import statement (import packages) that are missing. 14This function is not used when the module is run as a script and is available to be imported. 15""" 16 17import configparser 18import importlib.metadata 19import importlib.util 20import os.path 21import platform 22 23from packaging.version import Version 24 25_VERSION_COMPARISON_CHARS = '^<>=' 26# The names of packages used in import statements may be different from distribution package names. 27# We get distribution package names from pyproject.toml. 28# _EXTRA_DEPS adds those import names which don't match their distribution package name. 29_EXTRA_DEPS = { 30 'invoke': {'version': '>=1.3'}, 31 'paramiko': {'version': '>=2.4'}, 32 'PyYAML': {'version': '^6.0', 'import_package': 'yaml'} 33} 34_DPDK_ROOT = os.path.dirname(os.path.dirname(__file__)) 35_DTS_DEP_FILE_PATH = os.path.join(_DPDK_ROOT, 'dts', 'pyproject.toml') 36 37 38def _get_dependencies(cfg_file_path): 39 cfg = configparser.ConfigParser() 40 with open(cfg_file_path) as f: 41 dts_deps_file_str = f.read() 42 dts_deps_file_str = dts_deps_file_str.replace("\n]", "]") 43 cfg.read_string(dts_deps_file_str) 44 45 deps_section = cfg['tool.poetry.dependencies'] 46 return {dep: {'version': deps_section[dep].strip('"\'')} for dep in deps_section} 47 48 49def get_missing_imports(): 50 """Get missing DTS import packages from third party libraries. 51 52 Scan the DTS pyproject.toml file for dependencies and find those that are not installed. 53 The dependencies in pyproject.toml are listed by their distribution package names, 54 but the function finds the associated import packages - those used in import statements. 55 56 The function is not used when the module is run as a script. It should be imported. 57 58 Returns: 59 A list of missing import packages. 60 """ 61 missing_imports = [] 62 req_deps = _get_dependencies(_DTS_DEP_FILE_PATH) 63 req_deps.pop('python') 64 65 for req_dep, dep_data in (req_deps | _EXTRA_DEPS).items(): 66 req_ver = dep_data['version'] 67 try: 68 import_package = dep_data['import_package'] 69 except KeyError: 70 import_package = req_dep 71 import_package = import_package.lower().replace('-', '_') 72 73 try: 74 req_ver = Version(req_ver.strip(_VERSION_COMPARISON_CHARS)) 75 found_dep_ver = Version(importlib.metadata.version(req_dep)) 76 if found_dep_ver < req_ver: 77 print( 78 f'The version "{found_dep_ver}" of package "{req_dep}" ' 79 f'is lower than required "{req_ver}".' 80 ) 81 except importlib.metadata.PackageNotFoundError: 82 print(f'Package "{req_dep}" not found.') 83 missing_imports.append(import_package) 84 85 return missing_imports 86 87 88if __name__ == '__main__': 89 python_version = _get_dependencies(_DTS_DEP_FILE_PATH).pop('python') 90 if python_version: 91 sys_ver = Version(platform.python_version()) 92 req_ver = Version(python_version['version'].strip(_VERSION_COMPARISON_CHARS)) 93 if sys_ver < req_ver: 94 print( 95 f'The available Python version "{sys_ver}" is lower than required "{req_ver}".' 96 ) 97 exit(1) 98