From aa6b407fe72692cd00041f4d49f53a3e6128d191 Mon Sep 17 00:00:00 2001 From: Siarhei Siniak Date: Thu, 11 Sep 2025 13:51:35 +0300 Subject: [PATCH] [+] update pr34 1. add -U to UV_ARGS, ignore it in venv; 2. generate .whl; 3. update m.py for pr34; --- python/m.py | 361 +++++++++++++++--- .../pr34/commands_typed/cli_bootstrap.py | 4 +- ...ne_fxreader_pr34-0.1.5.28-py3-none-any.whl | 3 + 3 files changed, 317 insertions(+), 51 deletions(-) create mode 100644 releases/whl/online_fxreader_pr34-0.1.5.28-py3-none-any.whl diff --git a/python/m.py b/python/m.py index 99da03a..a98e0c8 100755 --- a/python/m.py +++ b/python/m.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import glob +import importlib +import json import io import tempfile import dataclasses @@ -8,15 +10,21 @@ import sys import subprocess import os import logging +import typing from typing import ( Optional, Any, + cast, + Type, + TypeVar, + Callable, ) from typing_extensions import ( Self, BinaryIO, + overload, ) logger = logging.getLogger(__name__) @@ -24,17 +32,23 @@ logger = logging.getLogger(__name__) def toml_load(f: BinaryIO) -> Any: try: - import tomllib + tomllib = importlib.import_module('tomllib') - return tomllib.load(f) - except: + return cast( + Callable[[Any], Any], + getattr( + tomllib, + 'load', + ), + )(f) + except ModuleNotFoundError: pass try: import tomli return tomli.load(f) - except: + except ModuleNotFoundError: pass raise NotImplementedError @@ -42,14 +56,136 @@ def toml_load(f: BinaryIO) -> Any: @dataclasses.dataclass class PyProject: + @dataclasses.dataclass + class Module: + name: str + meson: Optional[pathlib.Path] = None + tool: dict[str, Any] = dataclasses.field(default_factory=lambda: dict()) + path: pathlib.Path dependencies: dict[str, list[str]] early_features: Optional[list[str]] = None pip_find_links: Optional[list[pathlib.Path]] = None runtime_libdirs: Optional[list[pathlib.Path]] = None runtime_preload: Optional[list[pathlib.Path]] = None + + @dataclasses.dataclass + class ThirdPartyRoot: + package: Optional[str] = None + module_root: Optional[str] = None + path: Optional[str] = None + + third_party_roots: list[ThirdPartyRoot] = dataclasses.field( + default_factory=lambda: [], + ) requirements: dict[str, pathlib.Path] = dataclasses.field(default_factory=lambda: dict()) + modules: list[Module] = dataclasses.field( + default_factory=lambda: [], + ) + + tool: dict[str, Any] = dataclasses.field( + default_factory=lambda: dict(), + ) + + +Key = TypeVar('Key') +Value = TypeVar('Value') + + +@overload +def check_dict( + value: Any, + KT: Type[Key], + VT: Type[Value], +) -> dict[Key, Value]: ... + + +@overload +def check_dict( + value: Any, + KT: Type[Key], +) -> dict[Key, Any]: ... + + +def check_dict( + value: Any, + KT: Type[Key], + VT: Optional[Type[Value]] = None, +) -> dict[Key, Value]: + assert isinstance(value, dict) + value2 = cast(dict[Any, Any], value) + + VT_class: Optional[type[Any]] = None + + if not VT is None: + if not typing.get_origin(VT) is None: + VT_class = cast(type[Any], typing.get_origin(VT)) + else: + VT_class = VT + + assert all([isinstance(k, KT) and (VT_class is None or isinstance(v, VT_class)) for k, v in value2.items()]) + + if VT is None: + return cast( + dict[Key, Any], + value, + ) + else: + return cast( + dict[Key, Value], + value, + ) + + +@overload +def check_list( + value: Any, + VT: Type[Value], +) -> list[Value]: ... + + +@overload +def check_list( + value: Any, +) -> list[Any]: ... + + +def check_list( + value: Any, + VT: Optional[Type[Value]] = None, +) -> list[Value] | list[Any]: + assert isinstance(value, list) + value2 = cast(list[Any], value) + + assert all([(VT is None or isinstance(o, VT)) for o in value2]) + + if VT is None: + return cast( + list[Any], + value, + ) + else: + return cast( + list[Value], + value, + ) + + +def check_type( + value: Any, + VT: Type[Value], + attribute_name: Optional[str] = None, +) -> Value: + if attribute_name: + attribute_value = getattr(value, attribute_name) + assert isinstance(attribute_value, VT) + return attribute_value + else: + assert isinstance(value, VT) + + return value + def pyproject_load( d: pathlib.Path, @@ -66,9 +202,21 @@ def pyproject_load( if 'optional-dependencies' in content['project']: assert isinstance(content['project']['optional-dependencies'], dict) - for k, v in content['project']['optional-dependencies'].items(): - assert isinstance(v, list) - assert isinstance(k, str) + for k, v in check_dict( + check_dict( + check_dict( + content, + str, + # Any, + )['project'], + str, + # Any, + )['optional-dependencies'], + str, + list[Any], + ).items(): + # assert isinstance(v, list) + # assert isinstance(k, str) dependencies[k] = v @@ -79,36 +227,88 @@ def pyproject_load( tool_name = 'online.fxreader.pr34'.replace('.', '-') + if 'tool' in content: + res.tool = check_dict( + content['tool'], + str, + ) + if 'tool' in content and isinstance(content['tool'], dict) and tool_name in content['tool'] and isinstance(content['tool'][tool_name], dict): - if 'early_features' in content['tool'][tool_name]: - res.early_features = content['tool'][tool_name]['early_features'] + pr34_tool = check_dict( + check_dict( + content['tool'], + str, + )[tool_name], + str, + ) - if 'pip_find_links' in content['tool'][tool_name]: - res.pip_find_links = [d.parent / pathlib.Path(o) for o in content['tool'][tool_name]['pip_find_links']] + if 'early_features' in pr34_tool: + res.early_features = pr34_tool['early_features'] - if 'runtime_libdirs' in content['tool'][tool_name]: + if 'pip_find_links' in pr34_tool: + res.pip_find_links = [d.parent / pathlib.Path(o) for o in pr34_tool['pip_find_links']] + + if 'runtime_libdirs' in pr34_tool: res.runtime_libdirs = [ d.parent / pathlib.Path(o) # pathlib.Path(o) - for o in content['tool'][tool_name]['runtime_libdirs'] + for o in check_list(pr34_tool['runtime_libdirs'], str) ] - if 'runtime_preload' in content['tool'][tool_name]: + if 'runtime_preload' in pr34_tool: res.runtime_preload = [ d.parent / pathlib.Path(o) # pathlib.Path(o) - for o in content['tool'][tool_name]['runtime_preload'] + for o in check_list(pr34_tool['runtime_preload'], str) ] - if 'requirements' in content['tool'][tool_name]: - assert isinstance(content['tool'][tool_name]['requirements'], dict) + if 'third_party_roots' in pr34_tool: + for o in check_list(pr34_tool['third_party_roots']): + o2 = check_dict(o, str, str) + assert all([k in {'package', 'module_root', 'path'} for k in o2]) + res.third_party_roots.append( + PyProject.ThirdPartyRoot( + package=o2.get('package'), + module_root=o2.get('module_root'), + path=o2.get('path'), + ) + ) + + if 'requirements' in pr34_tool: res.requirements = { k: d.parent / pathlib.Path(v) # pathlib.Path(o) - for k, v in content['tool'][tool_name]['requirements'].items() + for k, v in check_dict(pr34_tool['requirements'], str, str).items() } + if 'modules' in pr34_tool: + modules = check_list(pr34_tool['modules']) + # res.modules = [] + + for o in modules: + assert isinstance(o, dict) + assert 'name' in o and isinstance(o['name'], str) + + module = PyProject.Module( + name=o['name'], + ) + + if 'meson' in o: + assert 'meson' in o and isinstance(o['meson'], str) + + module.meson = pathlib.Path(o['meson']) + + if 'tool' in o: + module.tool.update( + check_dict( + o['tool'], + str, + ) + ) + + res.modules.append(module) + return res @@ -127,10 +327,13 @@ class BootstrapSettings: ), ).strip() ) + pip_check_conflicts: Optional[bool] = dataclasses.field( + default_factory=lambda: os.environ.get('PIP_CHECK_CONFLICTS', json.dumps(True)) in [json.dumps(True)], + ) uv_args: list[str] = dataclasses.field( default_factory=lambda: os.environ.get( 'UV_ARGS', - '--offline', + '--offline -U', ).split(), ) @@ -142,7 +345,12 @@ class BootstrapSettings: if base_dir is None: base_dir = pathlib.Path.cwd() - env_path = base_dir / '.venv' + env_path: Optional[pathlib.Path] = None + if 'ENV_PATH' in os.environ: + env_path = pathlib.Path(os.environ['ENV_PATH']) + else: + env_path = base_dir / '.venv' + python_path = env_path / 'bin' / 'python3' return cls( @@ -152,6 +360,47 @@ class BootstrapSettings: ) +class requirements_name_get_t: + @dataclasses.dataclass + class res_t: + not_compiled: pathlib.Path + compiled: pathlib.Path + name: str + + +def requirements_name_get( + source_dir: pathlib.Path, + python_version: Optional[str], + features: list[str], + requirements: dict[str, pathlib.Path], +) -> requirements_name_get_t.res_t: + requirements_python_version: Optional[str] = None + if not python_version is None: + requirements_python_version = python_version.replace('.', '_') + + requirements_name = '_'.join(sorted(features)) + + if requirements_python_version: + requirements_name += '_' + requirements_python_version + + requirements_path: Optional[pathlib.Path] = None + + if requirements_name in requirements: + requirements_path = requirements[requirements_name] + else: + requirements_path = source_dir / 'requirements.txt' + + requirements_path_in = requirements_path.parent / (requirements_path.stem + '.in') + + requirements_in: list[str] = [] + + return requirements_name_get_t.res_t( + not_compiled=requirements_path_in, + compiled=requirements_path, + name=requirements_name, + ) + + def env_bootstrap( bootstrap_settings: BootstrapSettings, pyproject: PyProject, @@ -169,7 +418,7 @@ def env_bootstrap( ] for o in pip_find_links ], - [], + cast(list[str], []), ) features: list[str] = [] @@ -177,31 +426,24 @@ def env_bootstrap( if pyproject.early_features: features.extend(pyproject.early_features) - requirements_python_version: Optional[str] = None - if not bootstrap_settings.python_version is None: - requirements_python_version = bootstrap_settings.python_version.replace('.', '_') - - requirements_name = '_'.join(sorted(features)) - - if requirements_python_version: - requirements_name += '_' + requirements_python_version - - requirements_path: Optional[pathlib.Path] = None - - if requirements_name in pyproject.requirements: - requirements_path = pyproject.requirements[requirements_name] - else: - requirements_path = pyproject.path.parent / 'requirements.txt' + requirements_name_get_res = requirements_name_get( + python_version=bootstrap_settings.python_version, + features=features, + requirements=pyproject.requirements, + source_dir=pyproject.path.parent, + ) + requirements_path = requirements_name_get_res.compiled requirements_in: list[str] = [] requirements_in.extend(['uv', 'pip', 'build', 'setuptools', 'meson-python', 'pybind11']) if pyproject.early_features: - early_dependencies = sum([pyproject.dependencies[o] for o in pyproject.early_features], []) + early_dependencies = sum([pyproject.dependencies[o] for o in pyproject.early_features], cast(list[str], [])) logger.info( dict( + requirements_name_get_res=requirements_name_get_res, early_dependencies=early_dependencies, ) ) @@ -218,6 +460,25 @@ def env_bootstrap( # *early_dependencies, # ]) + uv_python_version: list[str] = [] + venv_python_version: list[str] = [] + + if not bootstrap_settings.python_version is None: + uv_python_version.extend( + [ + # '-p', + '--python-version', + bootstrap_settings.python_version, + ] + ) + venv_python_version.extend( + [ + '-p', + # '--python-version', + bootstrap_settings.python_version, + ] + ) + if not requirements_path.exists(): with tempfile.NamedTemporaryFile( mode='w', @@ -232,6 +493,7 @@ def env_bootstrap( 'uv', 'pip', 'compile', + *uv_python_version, '--generate-hashes', *pip_find_links_args, # '-p', @@ -243,24 +505,14 @@ def env_bootstrap( ] ) - uv_python_version: list[str] = [] - - if not bootstrap_settings.python_version is None: - uv_python_version.extend( - [ - '-p', - bootstrap_settings.python_version, - ] - ) - subprocess.check_call( [ 'uv', + *[o for o in bootstrap_settings.uv_args if not o in ['-U', '--upgrade']], 'venv', - *uv_python_version, + *venv_python_version, *pip_find_links_args, # '--seed', - *bootstrap_settings.uv_args, str(bootstrap_settings.env_path), ] ) @@ -270,6 +522,7 @@ def env_bootstrap( 'uv', 'pip', 'install', + *uv_python_version, *pip_find_links_args, '-p', bootstrap_settings.python_path, @@ -280,6 +533,16 @@ def env_bootstrap( ] ) + if bootstrap_settings.pip_check_conflicts: + subprocess.check_call( + [ + bootstrap_settings.python_path, + '-m', + 'online.fxreader.pr34.commands', + 'pip_check_conflicts', + ] + ) + def paths_equal(a: pathlib.Path | str, b: pathlib.Path | str) -> bool: return os.path.abspath(str(a)) == os.path.abspath(str(b)) diff --git a/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py b/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py index ca9cc64..7a95032 100644 --- a/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py +++ b/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py @@ -333,7 +333,7 @@ class BootstrapSettings: uv_args: list[str] = dataclasses.field( default_factory=lambda: os.environ.get( 'UV_ARGS', - '--offline', + '--offline -U', ).split(), ) @@ -508,11 +508,11 @@ def env_bootstrap( subprocess.check_call( [ 'uv', + *[o for o in bootstrap_settings.uv_args if not o in ['-U', '--upgrade']], 'venv', *venv_python_version, *pip_find_links_args, # '--seed', - *bootstrap_settings.uv_args, str(bootstrap_settings.env_path), ] ) diff --git a/releases/whl/online_fxreader_pr34-0.1.5.28-py3-none-any.whl b/releases/whl/online_fxreader_pr34-0.1.5.28-py3-none-any.whl new file mode 100644 index 0000000..e1397f2 --- /dev/null +++ b/releases/whl/online_fxreader_pr34-0.1.5.28-py3-none-any.whl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dedd1d53da278e01078df915f3b574aa81bd5af39bad5f815cd2b332bd2cfe75 +size 74933