diff --git a/m b/m.py similarity index 100% rename from m rename to m.py diff --git a/python/cli.py b/python/cli.py new file mode 100644 index 0000000..738b0e8 --- /dev/null +++ b/python/cli.py @@ -0,0 +1,120 @@ +import sys +import shutil +import glob +import io +import copy +import subprocess +import pathlib +import logging +import enum +import argparse +import dataclasses + +from typing import (Optional, override,) + +from online.fxreader.pr34.commands_typed.logging import setup as logging_setup + +from online.fxreader.pr34.commands_typed import cli as _cli + +from online.fxreader.pr34.commands_typed import cli_bootstrap + +logging_setup() + +logger = logging.getLogger(__name__) + + +class Command(enum.StrEnum): + mypy = 'mypy' + deploy_wheel = 'deploy:wheel' + +@dataclasses.dataclass +class Settings( + _cli.DistSettings, +): + base_dir: pathlib.Path = pathlib.Path(__file__).parent.parent + build_dir: pathlib.Path = base_dir / 'tmp' / 'build' + wheel_dir: pathlib.Path = base_dir / 'deps' / 'dist' + env_path: pathlib.Path = cli_bootstrap.BootstrapSettings.get(base_dir).env_path + python_path: pathlib.Path = cli_bootstrap.BootstrapSettings.get(base_dir).python_path + + +class CLI(_cli.CLI): + def __init__(self) -> None: + self.settings = Settings() + self._projects: dict[str, _cli.Project] = { + 'online.fxreader.pr34': _cli.Project( + source_dir=self.settings.base_dir / 'python', + build_dir=self.settings.base_dir / 'tmp' / 'online' / 'fxreader' / 'pr34' / 'build', + dest_dir=self.settings.base_dir / 'tmp' / 'online' / 'fxreader' / 'pr34' / 'install', + ) + } + + self._dependencies : dict[str, _cli.Dependency] = dict() + + @override + @property + def dist_settings(self) -> _cli.DistSettings: + return self.settings + + @override + @property + def projects(self) -> dict[str, _cli.Project]: + return self._projects + + @override + @property + def dependencies(self) -> dict[str, _cli.Dependency]: + return self._dependencies + + def run(self, argv: Optional[list[str]] = None) -> None: + if argv is None: + argv = copy.deepcopy(sys.argv) + + parser = argparse.ArgumentParser() + parser.add_argument( + 'command', + choices=[ + o.value + for o in Command + ] + ) + parser.add_argument( + '-p', '--project', + choices=[ + o + for o in self.projects + ] + ) + parser.add_argument( + '-o', '--output_dir', + default=None, + help='wheel output dir for deploy:wheel', + ) + parser.add_argument( + '-f', '--force', + default=False, + action='store_true', + help='remove install dir, before installing, default = false', + ) + + options, args = parser.parse_known_args(argv[1:]) + + options.command = Command(options.command) + + if options.command is Command.deploy_wheel: + assert not options.project is None + + self.deploy_wheel( + project_name=options.project, + argv=args, + output_dir=options.output_dir, + ) + elif options.command is Command.mypy: + self.mypy( + argv=args, + ) + else: + raise NotImplementedError + +if __name__ == '__main__': + CLI().run() \ No newline at end of file diff --git a/python/m.py b/python/m.py index e92a601..bb5a4c8 100755 --- a/python/m.py +++ b/python/m.py @@ -1,10 +1,210 @@ #!/usr/bin/env python3 -import sys +import glob +import io +import dataclasses import pathlib +import sys +import subprocess +import os +import logging +import tomllib -sys.path.append( - str(pathlib.Path.cwd()) -) +from typing import (Self, Optional, Any,) -import _m -_m.run() +logger = logging.getLogger(__name__) + +@dataclasses.dataclass +class PyProject: + dependencies: dict[str, list[str]] + early_features: Optional[list[str]] = None + +def pyproject_load( + d: pathlib.Path, +) -> PyProject: + with io.open(d, 'rb') as f: + content = tomllib.load(f) + + assert isinstance(content, dict) + + dependencies : dict[str, list[str]] = dict() + + dependencies['default'] = content['project']['dependencies'] + + 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) + + dependencies[k] = v + + + res = PyProject( + dependencies=dependencies, + ) + + tool_name = 'online.fxreader.pr34'.replace('.', '-') + + 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'] + + return res + +@dataclasses.dataclass +class BootstrapSettings: + env_path: pathlib.Path + python_path: pathlib.Path + + @classmethod + def get( + cls, + base_dir: Optional[pathlib.Path] = None, + ) -> Self: + if base_dir is None: + base_dir = pathlib.Path.cwd() + + env_path = base_dir / '.venv' + python_path = env_path / 'bin' / 'python3' + + return cls( + env_path=env_path, + python_path=python_path, + ) + +def env_bootstrap( + bootstrap_settings: BootstrapSettings, + pyproject: PyProject, +) -> None: + subprocess.check_call([ + 'uv', 'venv', '--seed', '--offline', + str(bootstrap_settings.env_path) + ]) + + subprocess.check_call([ + 'uv', + 'pip', + 'install', + '-p', + bootstrap_settings.python_path, + '--offline', + 'uv', + ]) + + subprocess.check_call([ + bootstrap_settings.python_path, + '-m', + 'uv', 'pip', 'install', + '--offline', + 'build', 'setuptools', 'meson-python', 'pybind11', + ]) + + early_wheels = glob.glob( + str( + pathlib.Path(__file__).parent / 'deps' / 'dist' / 'early' / '*.whl' + ) + ) + + if len(early_wheels) > 0: + subprocess.check_call([ + bootstrap_settings.python_path, + '-m', + 'uv', 'pip', 'install', + '--offline', + *early_wheels, + ]) + + if pyproject.early_features: + early_dependencies = sum([ + pyproject.dependencies[o] + for o in pyproject.early_features + ], []) + + logger.info(dict( + early_dependencies=early_dependencies, + )) + if len(early_dependencies) > 0: + subprocess.check_call([ + bootstrap_settings.python_path, + '-m', + 'uv', 'pip', 'install', + '--offline', + *early_dependencies, + ]) + +def paths_equal( + a: pathlib.Path | str, + b: pathlib.Path | str +) -> bool: + return ( + os.path.abspath(str(a)) == + os.path.abspath(str(b)) + ) + +def run( + d: Optional[pathlib.Path] = None, + cli_path: Optional[pathlib.Path] = None, +) -> None: + if cli_path is None: + cli_path = pathlib.Path(__file__).parent / 'cli.py' + + if d is None: + d = pathlib.Path(__file__).parent / 'pyproject.toml' + + bootstrap_settings = BootstrapSettings.get() + + pyproject : PyProject = pyproject_load( + d + ) + + logging.basicConfig(level=logging.INFO) + + if not bootstrap_settings.env_path.exists(): + env_bootstrap( + bootstrap_settings=bootstrap_settings, + pyproject=pyproject, + ) + + logger.info([sys.executable, sys.argv, bootstrap_settings.python_path]) + + if not paths_equal(sys.executable, bootstrap_settings.python_path): + os.execv( + str(bootstrap_settings.python_path), + [ + str(bootstrap_settings.python_path), + *sys.argv, + ] + ) + + os.execv( + str(bootstrap_settings.python_path), + [ + str(bootstrap_settings.python_path), + str( + cli_path + ), + *sys.argv[1:], + ] + ) + +if __name__ == '__main__': + run( + d=pathlib.Path(__file__).parent / 'python' / 'pyproject.toml', + cli_path=pathlib.Path(__file__).parent / 'python' / 'cli.py', + ) \ No newline at end of file diff --git a/python/online/fxreader/pr34/commands_typed/cli.py b/python/online/fxreader/pr34/commands_typed/cli.py index fac6d48..9742ef2 100644 --- a/python/online/fxreader/pr34/commands_typed/cli.py +++ b/python/online/fxreader/pr34/commands_typed/cli.py @@ -54,6 +54,16 @@ class CLI(abc.ABC): def dependencies(self) -> dict[str, Dependency]: raise NotImplementedError + def mypy( + self, + argv: list[str] + ) -> None: + from . import mypy as _mypy + + _mypy.run( + argv, + ) + def deploy_fetch_dist( self, force: bool, @@ -134,6 +144,7 @@ class CLI(abc.ABC): output_dir: Optional[pathlib.Path] = None, force: Optional[bool] = None, env: Optional[dict[str, str]] = None, + mypy: bool = False, ) -> None: project = self.projects[project_name] @@ -151,6 +162,9 @@ class CLI(abc.ABC): force=force, ) + if mypy: + self.mypy() + if env is None: env = dict() diff --git a/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py b/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py index 04c4d83..026cbba 100644 --- a/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py +++ b/python/online/fxreader/pr34/commands_typed/cli_bootstrap.py @@ -1,16 +1,72 @@ #!/usr/bin/env python3 import glob +import io import dataclasses import pathlib import sys import subprocess import os import logging +import tomllib -from typing import (Self, Optional,) +from typing import (Self, Optional, Any,) logger = logging.getLogger(__name__) +@dataclasses.dataclass +class PyProject: + dependencies: dict[str, list[str]] + early_features: Optional[list[str]] = None + +def pyproject_load( + d: pathlib.Path, +) -> PyProject: + with io.open(d, 'rb') as f: + content = tomllib.load(f) + + assert isinstance(content, dict) + + dependencies : dict[str, list[str]] = dict() + + dependencies['default'] = content['project']['dependencies'] + + 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) + + dependencies[k] = v + + + res = PyProject( + dependencies=dependencies, + ) + + tool_name = 'online.fxreader.pr34'.replace('.', '-') + + 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'] + + return res + @dataclasses.dataclass class BootstrapSettings: env_path: pathlib.Path @@ -32,9 +88,10 @@ class BootstrapSettings: python_path=python_path, ) -def env_bootstrap() -> None: - bootstrap_settings = BootstrapSettings.get() - +def env_bootstrap( + bootstrap_settings: BootstrapSettings, + pyproject: PyProject, +) -> None: subprocess.check_call([ 'uv', 'venv', '--seed', '--offline', str(bootstrap_settings.env_path) @@ -63,6 +120,7 @@ def env_bootstrap() -> None: pathlib.Path(__file__).parent / 'deps' / 'dist' / 'early' / '*.whl' ) ) + if len(early_wheels) > 0: subprocess.check_call([ bootstrap_settings.python_path, @@ -72,6 +130,24 @@ def env_bootstrap() -> None: *early_wheels, ]) + if pyproject.early_features: + early_dependencies = sum([ + pyproject.dependencies[o] + for o in pyproject.early_features + ], []) + + logger.info(dict( + early_dependencies=early_dependencies, + )) + if len(early_dependencies) > 0: + subprocess.check_call([ + bootstrap_settings.python_path, + '-m', + 'uv', 'pip', 'install', + '--offline', + *early_dependencies, + ]) + def paths_equal( a: pathlib.Path | str, b: pathlib.Path | str @@ -81,13 +157,29 @@ def paths_equal( os.path.abspath(str(b)) ) -def run() -> None: +def run( + d: Optional[pathlib.Path] = None, + cli_path: Optional[pathlib.Path] = None, +) -> None: + if cli_path is None: + cli_path = pathlib.Path(__file__).parent / 'cli.py' + + if d is None: + d = pathlib.Path(__file__).parent / 'pyproject.toml' + bootstrap_settings = BootstrapSettings.get() + pyproject : PyProject = pyproject_load( + d + ) + logging.basicConfig(level=logging.INFO) if not bootstrap_settings.env_path.exists(): - env_bootstrap() + env_bootstrap( + bootstrap_settings=bootstrap_settings, + pyproject=pyproject, + ) logger.info([sys.executable, sys.argv, bootstrap_settings.python_path]) @@ -105,12 +197,11 @@ def run() -> None: [ str(bootstrap_settings.python_path), str( - pathlib.Path(__file__).parent / 'cli.py' + cli_path ), *sys.argv[1:], ] ) - if __name__ == '__main__': run() \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index b2314e5..bc3c56a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -10,6 +10,14 @@ dependencies = [ 'pydantic-settings', ] +[project.optional-dependencies] +early = [ + 'numpy' +] + +[tool.online-fxreader-pr34] +early_features = ['default', 'early',] + [build-system] requires = ['setuptools'] build-backend = 'setuptools.build_meta'