1#!/usr/bin/env python3 2""" 3# Created: Mon Aug 7 14:41:01 2023 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8from typing import Any 9 10def assert_py_versions_match(config_file: str, toml_data: dict[str, Any]) -> None: 11 import re 12 13 def tuplify_version_str(version_str: str) -> tuple[int, int, int]: 14 assert isinstance(version_str, str) 15 version = list(map(int, version_str.split('.'))) 16 while len(version) < 3: 17 version.append(0) 18 # type checkers complain: 19 # 20 # Incompatible return value type (got "Tuple[int, ...]", expected 21 # "Tuple[int, int, int]") [return-value] 22 # 23 # but we know that version is of length 3, so we can safely ignore this 24 return tuple(version) # type: ignore[return-value] 25 26 project_data = toml_data['project'] 27 py_version_match = re.search(r'([\d.]+)', project_data['requires-python']) 28 assert py_version_match is not None # pacify type checkers 29 min_py_version = tuplify_version_str(py_version_match.group(1)) 30 31 def assert_version_match(tool_name: str, version_str: str) -> None: 32 tup = tuplify_version_str(version_str) 33 if tup != min_py_version: 34 raise ValueError( 35 f'{tool_name} minimum python version {tup} in {config_file} does not match the projects ' 36 f'minimum {min_py_version}. Likely you have bumped up the project version and forgotten ' 37 'to update this one to match!' 38 ) 39 return 40 41 assert_version_match('mypy', toml_data['tool']['mypy']['python_version']) 42 assert_version_match('vermin', str(toml_data['vermin']['targets'])) 43 return 44 45def assert_requirements_match(toml_file: str, toml_data: dict[str, Any], req_file: str, req_lines: list[str]) -> None: 46 toml_reqs = toml_data['project']['dependencies'] 47 if len(toml_reqs) != len(req_lines): 48 raise RuntimeError(f'Number of requirements don\'t match. {toml_file}::[project][dependencies] has {len(toml_reqs)}, {req_file} has {len(req_lines)}') 49 assert len(toml_reqs) == len(req_lines), f'' 50 for toml_req, req_req in zip(toml_reqs, req_lines): 51 assert toml_req == req_req, f'{toml_file}: {toml_req} != {req_file}: {req_req}' 52 return 53 54def load_toml_data(toml_path: str) -> dict[str, Any]: 55 try: 56 # since 3.11 57 # novermin 58 import tomllib # type: ignore[import] 59 except (ModuleNotFoundError, ImportError): 60 try: 61 import tomli as tomllib # type: ignore[import] 62 except (ModuleNotFoundError, ImportError): 63 try: 64 from pip._vendor import tomli as tomllib 65 except (ModuleNotFoundError, ImportError) as mnfe: 66 raise RuntimeError( 67 f'No package installed to read the {toml_path} file! Install tomli via ' 68 'python3 -m pip install tomli' 69 ) from mnfe 70 71 with open(toml_path, 'rb') as fd: 72 toml_data: dict[str, Any] = tomllib.load(fd) 73 return toml_data 74 75def load_requirements_data(req_path: str) -> list[str]: 76 ret: list[str] = [] 77 with open(req_path) as fd: 78 for line in fd.readlines(): 79 if lstrp := line.strip(): 80 if not lstrp.startswith('#'): 81 ret.append(lstrp) 82 return ret 83 84def main() -> None: 85 import os 86 87 par_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) 88 # open ./../pyproject.toml 89 toml_path = os.path.join(par_dir, 'pyproject.toml') 90 toml_data = load_toml_data(toml_path) 91 # open ./../requirements.txt 92 req_path = os.path.join(par_dir, 'requirements.txt') 93 req_data = load_requirements_data(req_path) 94 assert_py_versions_match(toml_path, toml_data) 95 assert_requirements_match(toml_path, toml_data, req_path, req_data) 96 return 97 98if __name__ == '__main__': 99 try: 100 main() 101 except Exception as exc: 102 import sys 103 104 print('=' * 90) 105 print('ERROR:', str(exc)) 106 print('=' * 90) 107 sys.exit(1) 108