[+] update pr34

1. update dependencies handling
    during venv creation;
  2. update cli and cli_boostrap modules;
This commit is contained in:
Siarhei Siniak 2024-12-22 20:45:46 +03:00
parent adc3fd0205
commit 9c8b554acc
6 changed files with 447 additions and 14 deletions

120
python/cli.py Normal file

@ -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()

@ -1,10 +1,210 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import glob
import io
import dataclasses
import pathlib import pathlib
import sys
import subprocess
import os
import logging
import tomllib
sys.path.append( from typing import (Self, Optional, Any,)
str(pathlib.Path.cwd())
)
import _m logger = logging.getLogger(__name__)
_m.run()
@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',
)

@ -54,6 +54,16 @@ class CLI(abc.ABC):
def dependencies(self) -> dict[str, Dependency]: def dependencies(self) -> dict[str, Dependency]:
raise NotImplementedError raise NotImplementedError
def mypy(
self,
argv: list[str]
) -> None:
from . import mypy as _mypy
_mypy.run(
argv,
)
def deploy_fetch_dist( def deploy_fetch_dist(
self, self,
force: bool, force: bool,
@ -134,6 +144,7 @@ class CLI(abc.ABC):
output_dir: Optional[pathlib.Path] = None, output_dir: Optional[pathlib.Path] = None,
force: Optional[bool] = None, force: Optional[bool] = None,
env: Optional[dict[str, str]] = None, env: Optional[dict[str, str]] = None,
mypy: bool = False,
) -> None: ) -> None:
project = self.projects[project_name] project = self.projects[project_name]
@ -151,6 +162,9 @@ class CLI(abc.ABC):
force=force, force=force,
) )
if mypy:
self.mypy()
if env is None: if env is None:
env = dict() env = dict()

@ -1,16 +1,72 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import glob import glob
import io
import dataclasses import dataclasses
import pathlib import pathlib
import sys import sys
import subprocess import subprocess
import os import os
import logging import logging
import tomllib
from typing import (Self, Optional,) from typing import (Self, Optional, Any,)
logger = logging.getLogger(__name__) 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 @dataclasses.dataclass
class BootstrapSettings: class BootstrapSettings:
env_path: pathlib.Path env_path: pathlib.Path
@ -32,9 +88,10 @@ class BootstrapSettings:
python_path=python_path, python_path=python_path,
) )
def env_bootstrap() -> None: def env_bootstrap(
bootstrap_settings = BootstrapSettings.get() bootstrap_settings: BootstrapSettings,
pyproject: PyProject,
) -> None:
subprocess.check_call([ subprocess.check_call([
'uv', 'venv', '--seed', '--offline', 'uv', 'venv', '--seed', '--offline',
str(bootstrap_settings.env_path) str(bootstrap_settings.env_path)
@ -63,6 +120,7 @@ def env_bootstrap() -> None:
pathlib.Path(__file__).parent / 'deps' / 'dist' / 'early' / '*.whl' pathlib.Path(__file__).parent / 'deps' / 'dist' / 'early' / '*.whl'
) )
) )
if len(early_wheels) > 0: if len(early_wheels) > 0:
subprocess.check_call([ subprocess.check_call([
bootstrap_settings.python_path, bootstrap_settings.python_path,
@ -72,6 +130,24 @@ def env_bootstrap() -> None:
*early_wheels, *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( def paths_equal(
a: pathlib.Path | str, a: pathlib.Path | str,
b: pathlib.Path | str b: pathlib.Path | str
@ -81,13 +157,29 @@ def paths_equal(
os.path.abspath(str(b)) 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() bootstrap_settings = BootstrapSettings.get()
pyproject : PyProject = pyproject_load(
d
)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
if not bootstrap_settings.env_path.exists(): 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]) logger.info([sys.executable, sys.argv, bootstrap_settings.python_path])
@ -105,12 +197,11 @@ def run() -> None:
[ [
str(bootstrap_settings.python_path), str(bootstrap_settings.python_path),
str( str(
pathlib.Path(__file__).parent / 'cli.py' cli_path
), ),
*sys.argv[1:], *sys.argv[1:],
] ]
) )
if __name__ == '__main__': if __name__ == '__main__':
run() run()

@ -10,6 +10,14 @@ dependencies = [
'pydantic-settings', 'pydantic-settings',
] ]
[project.optional-dependencies]
early = [
'numpy'
]
[tool.online-fxreader-pr34]
early_features = ['default', 'early',]
[build-system] [build-system]
requires = ['setuptools'] requires = ['setuptools']
build-backend = 'setuptools.build_meta' build-backend = 'setuptools.build_meta'