mposcli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mposcli/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ mposcli
3
+ CLI helper for MicroPythonOS: https://github.com/MicroPythonOS/MicroPythonOS
4
+ """
5
+
6
+ # See https://packaging.python.org/en/latest/specifications/version-specifiers/
7
+ __version__ = '0.1.0'
8
+ __author__ = 'Jens Diemer <cookiecutter_templates@jensdiemer.de>'
mposcli/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Allow mposcli to be executable
3
+ through `python -m mposcli`.
4
+ """
5
+
6
+ from mposcli.cli_app import main
7
+
8
+
9
+ if __name__ == '__main__':
10
+ main()
@@ -0,0 +1,41 @@
1
+ """
2
+ CLI for usage
3
+ """
4
+
5
+ import logging
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from cli_base.autodiscover import import_all_files
10
+ from cli_base.cli_tools.version_info import print_version
11
+ from rich import print # noqa
12
+ from tyro.extras import SubcommandApp
13
+
14
+ import mposcli
15
+ from mposcli import constants
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ app = SubcommandApp()
21
+
22
+ # Register all CLI commands, just by import all files in this package:
23
+ import_all_files(package=__package__, init_file=__file__)
24
+
25
+
26
+ @app.command
27
+ def version():
28
+ """Print version and exit"""
29
+ # Pseudo command, because the version always printed on every CLI call ;)
30
+ sys.exit(0)
31
+
32
+
33
+ def main(args: Sequence[str] | None = None):
34
+ print_version(mposcli)
35
+ app.cli(
36
+ prog='mposcli', # Enforce "pipx" usage
37
+ description=constants.CLI_EPILOG,
38
+ use_underscores=False, # use hyphens instead of underscores
39
+ sort_subcommands=True,
40
+ args=args,
41
+ )
@@ -0,0 +1,46 @@
1
+ import logging
2
+ from typing import Annotated, Literal
3
+
4
+ import tyro
5
+ from bx_py_utils.path import assert_is_file
6
+ from cli_base.cli_tools.subprocess_utils import verbose_check_call
7
+ from cli_base.cli_tools.verbosity import setup_logging
8
+ from cli_base.tyro_commands import TyroVerbosityArgType
9
+ from rich import print # noqa
10
+
11
+ from mposcli.cli_app import app
12
+ from mposcli.mpos_utils import get_mpos_path
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @app.command
19
+ def build(
20
+ target: Annotated[
21
+ Literal['esp32', 'esp32s3', 'unix', 'macOS'],
22
+ tyro.conf.arg(
23
+ help='Target platform to build for.',
24
+ ),
25
+ ] = 'unix',
26
+ verbosity: TyroVerbosityArgType = 1,
27
+ ):
28
+ """
29
+ Build MicroPythonOS by calling: ./scripts/build_mpos.sh <target>
30
+ see: https://docs.micropythonos.com/os-development/
31
+ """
32
+ setup_logging(verbosity=verbosity)
33
+
34
+ mpos_path = get_mpos_path()
35
+
36
+ script_path = mpos_path / 'scripts' / 'build_mpos.sh'
37
+ assert_is_file(script_path)
38
+
39
+ verbose_check_call(
40
+ script_path,
41
+ target,
42
+ verbose=True,
43
+ cwd=mpos_path,
44
+ timeout=None,
45
+ text=None,
46
+ )
@@ -0,0 +1,105 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import tyro
9
+ from bx_py_utils.path import assert_is_file
10
+ from cli_base.cli_tools.subprocess_utils import verbose_check_call
11
+ from cli_base.cli_tools.verbosity import setup_logging
12
+ from cli_base.tyro_commands import TyroVerbosityArgType
13
+ from rich import print # noqa
14
+
15
+ from mposcli.cli_app import app
16
+ from mposcli.fs_utils import list_executables
17
+ from mposcli.mpos_utils import get_mpos_path
18
+ from mposcli.user_input import file_chooser
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @app.command
25
+ def run_desktop(
26
+ heapsize: Annotated[
27
+ int,
28
+ tyro.conf.arg(
29
+ help='Heap size in MB (default: 8, same as PSRAM on many ESP32-S3 boards)',
30
+ ),
31
+ ] = 8,
32
+ script: Annotated[
33
+ str | None,
34
+ tyro.conf.arg(help='Script file (.py) or app name to run. If omitted, starts normally.'),
35
+ ] = None,
36
+ binary: Annotated[
37
+ str | None,
38
+ tyro.conf.arg(
39
+ help='Optional name of the binary to start.'
40
+ ' If omitted, shows a file chooser to select one from the lvgl_micropython build directory.'
41
+ ),
42
+ ] = None,
43
+ verbosity: TyroVerbosityArgType = 1,
44
+ ):
45
+ """
46
+ Run MicroPythonOS on desktop.
47
+ see: https://docs.micropythonos.com/getting-started/running/#running-on-desktop
48
+ """
49
+ setup_logging(verbosity=verbosity)
50
+
51
+ mpos_path = get_mpos_path()
52
+
53
+ internal_fs = mpos_path / 'internal_filesystem'
54
+ lvgl_micropython_build_path = mpos_path / 'lvgl_micropython' / 'build'
55
+
56
+ executables = list_executables(lvgl_micropython_build_path)
57
+ if not executables:
58
+ print(f'Error: No executable found in {lvgl_micropython_build_path}.')
59
+ print('Hint: Download or build the lvgl_micropython binary first.')
60
+ print('Hint 2: Forgotten to make the binary executable? e.g.: chmod +x <binary>')
61
+ sys.exit(1)
62
+
63
+ if binary:
64
+ binary = lvgl_micropython_build_path / binary
65
+ else:
66
+ binary = file_chooser(executables)
67
+ assert_is_file(binary)
68
+
69
+ os.environ['HEAPSIZE'] = f'{heapsize}M'
70
+
71
+ # Change to internal_filesystem dir
72
+ os.chdir(internal_fs)
73
+
74
+ popenargs = [binary]
75
+
76
+ if script and script.endswith('.py') and Path(script).is_file():
77
+ # Run script file directly
78
+ script_path = str(Path(script).resolve())
79
+ popenargs += ('-v', '-i', script_path)
80
+ else:
81
+ if script:
82
+ # Treat as app name
83
+ config_file = Path('data/com.micropythonos.settings/config.json')
84
+ config_file.parent.mkdir(parents=True, exist_ok=True)
85
+ if config_file.exists():
86
+ with config_file.open('r', encoding='utf-8') as f:
87
+ try:
88
+ config = json.load(f)
89
+ except Exception:
90
+ config = {}
91
+ else:
92
+ config = {}
93
+ config['auto_start_app'] = script
94
+ with config_file.open('w', encoding='utf-8') as f:
95
+ json.dump(config, f)
96
+ popenargs += ('-X', f'heapsize={os.environ["HEAPSIZE"]}', '-v', '-i', '-c', Path('main.py').read_text())
97
+
98
+ verbose_check_call(
99
+ *popenargs,
100
+ env=os.environ,
101
+ verbose=True,
102
+ cwd=internal_fs,
103
+ timeout=None,
104
+ text=None,
105
+ )
@@ -0,0 +1,35 @@
1
+ import logging
2
+
3
+ from cli_base.cli_tools.subprocess_utils import verbose_check_call
4
+ from cli_base.cli_tools.verbosity import setup_logging
5
+ from cli_base.tyro_commands import TyroVerbosityArgType
6
+ from rich import print # noqa
7
+
8
+ from mposcli.cli_app import app
9
+ from mposcli.mpos_utils import get_mpos_path
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @app.command
16
+ def update_submodules(verbosity: TyroVerbosityArgType = 1):
17
+ """
18
+ Update MicroPythonOS repository and all submodules
19
+ see: https://docs.micropythonos.com/os-development/linux/#optional-updating-the-code
20
+ """
21
+ setup_logging(verbosity=verbosity)
22
+ mpos_path = get_mpos_path()
23
+
24
+ commands = (
25
+ ('git', 'submodule', 'foreach', '--recursive', 'git', 'clean', '-f', ';', 'git', 'checkout', '.'),
26
+ ('git', 'pull', '--recurse-submodules'),
27
+ )
28
+ for popenargs in commands:
29
+ verbose_check_call(
30
+ *popenargs,
31
+ verbose=True,
32
+ cwd=mpos_path,
33
+ timeout=None,
34
+ text=None,
35
+ )
@@ -0,0 +1,70 @@
1
+ """
2
+ CLI for development
3
+ """
4
+
5
+ import importlib
6
+ import logging
7
+ import sys
8
+ from collections.abc import Sequence
9
+
10
+ from bx_py_utils.path import assert_is_file
11
+ from cli_base.autodiscover import import_all_files
12
+ from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli
13
+ from cli_base.cli_tools.version_info import print_version
14
+ from typeguard import install_import_hook
15
+ from tyro.extras import SubcommandApp
16
+
17
+ import mposcli
18
+ from mposcli import constants
19
+
20
+
21
+ # Check type annotations via typeguard in all tests.
22
+ # Sadly we must activate this here and can't do this in ./tests/__init__.py
23
+ install_import_hook(packages=('mposcli',))
24
+
25
+ # reload the module, after the typeguard import hook is activated:
26
+ importlib.reload(mposcli)
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ PACKAGE_ROOT = constants.BASE_PATH.parent
33
+ assert_is_file(PACKAGE_ROOT / 'pyproject.toml') # Exists only in cloned git repo
34
+
35
+
36
+ app = SubcommandApp()
37
+
38
+
39
+ # Register all CLI commands, just by import all files in this package:
40
+ import_all_files(package=__package__, init_file=__file__)
41
+
42
+
43
+ @app.command
44
+ def version():
45
+ """Print version and exit"""
46
+ # Pseudo command, because the version always printed on every CLI call ;)
47
+ sys.exit(0)
48
+
49
+
50
+ def main(args: Sequence[str] | None = None):
51
+ print_version(mposcli)
52
+
53
+ if len(sys.argv) >= 2:
54
+ # Check if we can just pass a command call to origin CLI:
55
+ command = sys.argv[1]
56
+ command_map = {
57
+ 'test': run_unittest_cli,
58
+ 'nox': run_nox,
59
+ 'coverage': run_coverage,
60
+ }
61
+ if real_func := command_map.get(command):
62
+ real_func(argv=sys.argv, exit_after_run=True)
63
+
64
+ app.cli(
65
+ prog='./dev-cli.py',
66
+ description=constants.CLI_EPILOG,
67
+ use_underscores=False, # use hyphens instead of underscores
68
+ sort_subcommands=True,
69
+ args=args,
70
+ )
@@ -0,0 +1,10 @@
1
+ """
2
+ Allow mposcli to be executable
3
+ through `python -m mposcli.cli_dev`.
4
+ """
5
+
6
+ from mposcli.cli_dev import main
7
+
8
+
9
+ if __name__ == '__main__':
10
+ main()
@@ -0,0 +1,12 @@
1
+ from cli_base.cli_tools.code_style import assert_code_style
2
+ from cli_base.tyro_commands import TyroVerbosityArgType
3
+
4
+ from mposcli.cli_dev import PACKAGE_ROOT, app
5
+
6
+
7
+ @app.command
8
+ def lint(verbosity: TyroVerbosityArgType = 1):
9
+ """
10
+ Check/fix code style by run: "ruff check --fix"
11
+ """
12
+ assert_code_style(package_root=PACKAGE_ROOT, verbose=bool(verbosity), sys_exit=True)
@@ -0,0 +1,62 @@
1
+ import logging
2
+
3
+ from cli_base.cli_tools.dev_tools import run_unittest_cli
4
+ from cli_base.cli_tools.subprocess_utils import ToolsExecutor
5
+ from cli_base.cli_tools.verbosity import setup_logging
6
+ from cli_base.run_pip_audit import run_pip_audit
7
+ from cli_base.tyro_commands import TyroVerbosityArgType
8
+ from manageprojects.utilities.publish import publish_package
9
+
10
+ import mposcli
11
+ from mposcli.cli_dev import PACKAGE_ROOT, app
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @app.command
18
+ def install():
19
+ """
20
+ Install requirements and 'mposcli' via pip as editable.
21
+ """
22
+ tools_executor = ToolsExecutor(cwd=PACKAGE_ROOT)
23
+ tools_executor.verbose_check_call('uv', 'sync')
24
+ tools_executor.verbose_check_call('uv', 'pip', 'install', '--no-deps', '-e', '.')
25
+
26
+
27
+ @app.command
28
+ def pip_audit(verbosity: TyroVerbosityArgType):
29
+ """
30
+ Run pip-audit check against current requirements files
31
+ """
32
+ setup_logging(verbosity=verbosity)
33
+ run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity)
34
+
35
+
36
+ @app.command
37
+ def update(verbosity: TyroVerbosityArgType):
38
+ """
39
+ Update dependencies (uv.lock) and git pre-commit hooks
40
+ """
41
+ setup_logging(verbosity=verbosity)
42
+
43
+ tools_executor = ToolsExecutor(cwd=PACKAGE_ROOT)
44
+ tools_executor.verbose_check_call('uv', 'lock', '--upgrade')
45
+
46
+ run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity)
47
+
48
+ # Install new dependencies in current .venv:
49
+ tools_executor.verbose_check_call('uv', 'sync')
50
+
51
+ # Update git pre-commit hooks:
52
+ tools_executor.verbose_check_call('pre-commit', 'autoupdate')
53
+
54
+
55
+ @app.command
56
+ def publish():
57
+ """
58
+ Build and upload this project to PyPi
59
+ """
60
+ run_unittest_cli(verbose=False, exit_after_run=False) # Don't publish a broken state
61
+
62
+ publish_package(module=mposcli, package_path=PACKAGE_ROOT)
@@ -0,0 +1,23 @@
1
+ import logging
2
+
3
+ from cli_base.cli_tools.shell_completion import setup_tyro_shell_completion
4
+ from cli_base.cli_tools.verbosity import setup_logging
5
+ from cli_base.tyro_commands import TyroVerbosityArgType
6
+ from rich import print # noqa
7
+
8
+ from mposcli.cli_dev import app
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @app.command
15
+ def shell_completion(verbosity: TyroVerbosityArgType = 1, remove: bool = False) -> None:
16
+ """
17
+ Setup shell completion for this CLI (Currently only for bash shell)
18
+ """
19
+ setup_logging(verbosity=verbosity)
20
+ setup_tyro_shell_completion(
21
+ prog_name='mposcli_dev_cli',
22
+ remove=remove,
23
+ )
@@ -0,0 +1,52 @@
1
+ from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli
2
+ from cli_base.cli_tools.subprocess_utils import verbose_check_call
3
+ from cli_base.cli_tools.test_utils.snapshot import UpdateTestSnapshotFiles
4
+ from cli_base.tyro_commands import TyroVerbosityArgType
5
+
6
+ from mposcli.cli_dev import PACKAGE_ROOT, app
7
+
8
+
9
+ @app.command
10
+ def mypy(verbosity: TyroVerbosityArgType):
11
+ """Run Mypy (configured in pyproject.toml)"""
12
+ verbose_check_call('mypy', '.', cwd=PACKAGE_ROOT, verbose=verbosity > 0, exit_on_error=True)
13
+
14
+
15
+ @app.command
16
+ def update_test_snapshot_files(verbosity: TyroVerbosityArgType):
17
+ """
18
+ Update all test snapshot files (by remove and recreate all snapshot files)
19
+ """
20
+ with UpdateTestSnapshotFiles(root_path=PACKAGE_ROOT, verbose=verbosity > 0):
21
+ # Just recreate them by running tests:
22
+ run_unittest_cli(
23
+ extra_env=dict(
24
+ RAISE_SNAPSHOT_ERRORS='0', # Recreate snapshot files without error
25
+ ),
26
+ verbose=verbosity > 1,
27
+ exit_after_run=False,
28
+ )
29
+
30
+
31
+ @app.command # Dummy command
32
+ def test():
33
+ """
34
+ Run unittests
35
+ """
36
+ run_unittest_cli()
37
+
38
+
39
+ @app.command # Dummy command
40
+ def coverage():
41
+ """
42
+ Run tests and show coverage report.
43
+ """
44
+ run_coverage()
45
+
46
+
47
+ @app.command # Dummy "nox" command
48
+ def nox():
49
+ """
50
+ Run nox
51
+ """
52
+ run_nox()
@@ -0,0 +1,33 @@
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from cli_base.cli_tools import git_history
6
+ from cli_base.cli_tools.verbosity import setup_logging
7
+ from cli_base.tyro_commands import TyroVerbosityArgType
8
+ from rich import print # noqa
9
+
10
+ from mposcli.cli_dev import app
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @app.command
17
+ def update_readme_history(verbosity: TyroVerbosityArgType):
18
+ """
19
+ Update project history base on git commits/tags in README.md
20
+
21
+ Will be exited with 1 if the README.md was updated otherwise with 0.
22
+
23
+ Also, callable via e.g.:
24
+ python -m cli_base update-readme-history -v
25
+ """
26
+ setup_logging(verbosity=verbosity)
27
+
28
+ logger.debug('%s called. CWD: %s', __name__, Path.cwd())
29
+ updated = git_history.update_readme_history(verbosity=verbosity)
30
+ exit_code = 1 if updated else 0
31
+ if verbosity:
32
+ print(f'{exit_code=}')
33
+ sys.exit(exit_code)
mposcli/constants.py ADDED
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ import mposcli
4
+
5
+
6
+ CLI_EPILOG = 'Project Homepage: https://github.com/jedie/mposcli'
7
+
8
+ BASE_PATH = Path(mposcli.__file__).parent
mposcli/fs_utils.py ADDED
@@ -0,0 +1,6 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ def list_executables(directory: Path) -> list[Path]:
6
+ return [f for f in directory.iterdir() if f.is_file() and os.access(f, os.X_OK)]
mposcli/mpos_utils.py ADDED
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from rich import print # noqa
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def get_mpos_path() -> Path:
12
+ """
13
+ Get the path to the MicroPythonOS project root directory.
14
+ Assume that mposcli is only called from the root directory of a MicroPythonOS project.
15
+ """
16
+ current_path = Path().cwd().resolve()
17
+ logger.info('Current working directory: %s', current_path)
18
+
19
+ for dir_name in ('internal_filesystem', Path('lvgl_micropython', 'build')):
20
+ dir_path = current_path / dir_name
21
+ logger.debug('Checking for directory: %s', dir_path)
22
+ if not dir_path.is_dir():
23
+ print(f"Error: Directory '{dir_name}' not found in {current_path}.")
24
+ print('Hint: Call "mposcli" only in the root directory of a MicroPythonOS project!')
25
+ sys.exit(1)
26
+
27
+ mpos_path = current_path
28
+ return mpos_path
@@ -0,0 +1,36 @@
1
+ import os
2
+ import unittest.util
3
+ from pathlib import Path
4
+
5
+ from bx_py_utils.test_utils.deny_requests import deny_any_real_request
6
+ from cli_base.cli_tools.verbosity import MAX_LOG_LEVEL, setup_logging
7
+ from rich import print # noqa
8
+ from typeguard import install_import_hook
9
+
10
+
11
+ # Check type annotations via typeguard in all tests:
12
+ install_import_hook(packages=('mposcli',))
13
+
14
+
15
+ def pre_configure_tests() -> None:
16
+ print(f'Configure unittests via "load_tests Protocol" from {Path(__file__).relative_to(Path.cwd())}')
17
+
18
+ # Hacky way to display more "assert"-Context in failing tests:
19
+ _MIN_MAX_DIFF = unittest.util._MAX_LENGTH - unittest.util._MIN_DIFF_LEN
20
+ unittest.util._MAX_LENGTH = int(os.environ.get('UNITTEST_MAX_LENGTH', 2000))
21
+ unittest.util._MIN_DIFF_LEN = unittest.util._MAX_LENGTH - _MIN_MAX_DIFF
22
+
23
+ # Deny any request via docket/urllib3 because tests they should mock all requests:
24
+ deny_any_real_request()
25
+
26
+ # Display DEBUG logs in tests:
27
+ setup_logging(verbosity=MAX_LOG_LEVEL)
28
+
29
+
30
+ def load_tests(loader, tests, pattern):
31
+ """
32
+ Use unittest "load_tests Protocol" as a hook to setup test environment before running tests.
33
+ https://docs.python.org/3/library/unittest.html#load-tests-protocol
34
+ """
35
+ pre_configure_tests()
36
+ return loader.discover(start_dir=Path(__file__).parent, pattern=pattern)
@@ -0,0 +1,10 @@
1
+ from bx_py_utils.test_utils.unittest_utils import BaseDocTests
2
+
3
+ import mposcli
4
+
5
+
6
+ class DocTests(BaseDocTests):
7
+ def test_doctests(self):
8
+ self.run_doctests(
9
+ modules=(mposcli,),
10
+ )
@@ -0,0 +1,46 @@
1
+ import subprocess
2
+ from unittest import TestCase
3
+
4
+ from bx_py_utils.path import assert_is_file
5
+ from cli_base.cli_tools.code_style import assert_code_style
6
+ from cli_base.cli_tools.subprocess_utils import ToolsExecutor
7
+ from manageprojects.test_utils.project_setup import check_editor_config, get_py_max_line_length
8
+ from packaging.version import Version
9
+
10
+ from mposcli import __version__
11
+ from mposcli.cli_dev import PACKAGE_ROOT
12
+
13
+
14
+ class ProjectSetupTestCase(TestCase):
15
+ def test_version(self):
16
+ self.assertIsNotNone(__version__)
17
+
18
+ version = Version(__version__) # Will raise InvalidVersion() if wrong formatted
19
+ self.assertEqual(str(version), __version__)
20
+
21
+ cli_bin = PACKAGE_ROOT / 'cli.py'
22
+ assert_is_file(cli_bin)
23
+
24
+ output = subprocess.check_output([cli_bin, 'version'], text=True)
25
+ self.assertIn(f'mposcli v{__version__}', output)
26
+
27
+ dev_cli_bin = PACKAGE_ROOT / 'dev-cli.py'
28
+ assert_is_file(dev_cli_bin)
29
+
30
+ output = subprocess.check_output([dev_cli_bin, 'version'], text=True)
31
+ self.assertIn(f'mposcli v{__version__}', output)
32
+
33
+ def test_code_style(self):
34
+ return_code = assert_code_style(package_root=PACKAGE_ROOT)
35
+ self.assertEqual(return_code, 0, 'Code style error, see output above!')
36
+
37
+ def test_check_editor_config(self):
38
+ check_editor_config(package_root=PACKAGE_ROOT)
39
+
40
+ max_line_length = get_py_max_line_length(package_root=PACKAGE_ROOT)
41
+ self.assertEqual(max_line_length, 119)
42
+
43
+ def test_pre_commit_hooks(self):
44
+ executor = ToolsExecutor(cwd=PACKAGE_ROOT)
45
+ for command in ('migrate-config', 'validate-config', 'validate-manifest'):
46
+ executor.verbose_check_call('pre-commit', command, exit_on_error=True)
@@ -0,0 +1,88 @@
1
+ from bx_py_utils.auto_doc import assert_readme_block
2
+ from bx_py_utils.path import assert_is_file
3
+ from cli_base.cli_tools.test_utils.assertion import assert_in
4
+ from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich, invoke
5
+ from manageprojects.tests.base import BaseTestCase
6
+
7
+ from mposcli import constants
8
+ from mposcli.cli_dev import PACKAGE_ROOT
9
+
10
+
11
+ def assert_cli_help_in_readme(text_block: str, marker: str):
12
+ README_PATH = PACKAGE_ROOT / 'README.md'
13
+ assert_is_file(README_PATH)
14
+
15
+ text_block = text_block.replace(constants.CLI_EPILOG, '')
16
+ text_block = f'```\n{text_block.strip()}\n```'
17
+ assert_readme_block(
18
+ readme_path=README_PATH,
19
+ text_block=text_block,
20
+ start_marker_line=f'[comment]: <> (✂✂✂ auto generated {marker} start ✂✂✂)',
21
+ end_marker_line=f'[comment]: <> (✂✂✂ auto generated {marker} end ✂✂✂)',
22
+ )
23
+
24
+
25
+ class ReadmeTestCase(BaseTestCase):
26
+
27
+ def test_main_help(self):
28
+ with NoColorEnvRich():
29
+ stdout = invoke(
30
+ cli_bin=PACKAGE_ROOT / 'cli.py',
31
+ args=['--help'],
32
+ strip_line_prefix='usage: ',
33
+ )
34
+ assert_in(
35
+ content=stdout,
36
+ parts=(
37
+ 'usage: mposcli [-h]',
38
+ ' version ',
39
+ 'Print version and exit',
40
+ constants.CLI_EPILOG,
41
+ ),
42
+ )
43
+ assert_cli_help_in_readme(text_block=stdout, marker='main help')
44
+
45
+ def test_dev_help(self):
46
+ with NoColorEnvRich():
47
+ stdout = invoke(
48
+ cli_bin=PACKAGE_ROOT / 'dev-cli.py',
49
+ args=['--help'],
50
+ strip_line_prefix='usage: ',
51
+ )
52
+ assert_in(
53
+ content=stdout,
54
+ parts=(
55
+ 'usage: ./dev-cli.py [-h]',
56
+ ' lint ',
57
+ ' coverage ',
58
+ ' update-readme-history ',
59
+ ' publish ',
60
+ constants.CLI_EPILOG,
61
+ ),
62
+ )
63
+ assert_cli_help_in_readme(text_block=stdout, marker='dev help')
64
+
65
+ def test_cli_commands(self):
66
+ from mposcli.cli_app import app
67
+
68
+ # Dynamically build command list from app object
69
+ # tyro SubcommandApp stores subcommands in _subcommands dict
70
+ commands = set(command.replace('_', '-') for command in app._subcommands.keys())
71
+
72
+ commands.discard('version') # version is pseudo command, because the version always printed on every CLI call
73
+ commands = sorted(commands)
74
+ self.assertEqual(commands, ['build', 'run-desktop', 'update-submodules'])
75
+
76
+ for command in commands:
77
+ with self.subTest(command):
78
+ with NoColorEnvRich():
79
+ stdout = invoke(
80
+ cli_bin=PACKAGE_ROOT / 'cli.py',
81
+ args=[command, '--help'],
82
+ strip_line_prefix='usage: ',
83
+ )
84
+ assert_in(
85
+ content=stdout,
86
+ parts=(f'usage: mposcli {command} [-h]',),
87
+ )
88
+ assert_cli_help_in_readme(text_block=stdout, marker=command)
@@ -0,0 +1,9 @@
1
+ from unittest import TestCase
2
+
3
+ from cli_base.cli_tools.git_history import update_readme_history
4
+
5
+
6
+ class ReadmeHistoryTestCase(TestCase):
7
+ def test_readme_history(self):
8
+ with self.assertLogs():
9
+ update_readme_history(raise_update_error=True)
mposcli/user_input.py ADDED
@@ -0,0 +1,37 @@
1
+ import datetime
2
+ import sys
3
+
4
+ from rich import print
5
+
6
+
7
+ def file_chooser(paths):
8
+ """
9
+ Display a numbered list of files sorted by modification time (newest first).
10
+ Show mtime and file name. Input number, ENTER = 1. Return selected Path.
11
+ """
12
+
13
+ files = [(p, p.stat().st_mtime) for p in paths if p.is_file()]
14
+ if not files:
15
+ print('[red]No files found.[/red]')
16
+ return None
17
+ files.sort(key=lambda x: x[1], reverse=True)
18
+
19
+ print('[bold]Choose a file:[/bold]')
20
+ for idx, (p, mtime) in enumerate(files):
21
+ dt = datetime.datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
22
+ print(f'[{idx}] {dt} {p.name}')
23
+
24
+ number = input('Enter number (ENTER = 0 - the newest file): ').strip() or 0
25
+ try:
26
+ number = int(number)
27
+ except Exception as err:
28
+ print(f'[red]Invalid input: {err}[/red]')
29
+ sys.exit(1)
30
+
31
+ try:
32
+ selection = files[number][0]
33
+ except IndexError:
34
+ print(f'[red]Invalid selection: {number}[/red]')
35
+ sys.exit(1)
36
+
37
+ return selection
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: mposcli
3
+ Version: 0.1.0
4
+ Summary: CLI helper for MicroPythonOS: https://github.com/MicroPythonOS/MicroPythonOS
5
+ Project-URL: Documentation, https://github.com/jedie/mposcli
6
+ Project-URL: Source, https://github.com/jedie/mposcli
7
+ Author-email: Jens Diemer <cookiecutter_templates@jensdiemer.de>
8
+ License: GPL-3.0-or-later
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: bx-py-utils
11
+ Requires-Dist: cli-base-utilities>=0.27.4
12
+ Requires-Dist: rich
13
+ Requires-Dist: tyro
14
+ Description-Content-Type: text/markdown
15
+
16
+ # mposcli
17
+
18
+ [![tests](https://github.com/jedie/mposcli/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/jedie/mposcli/actions/workflows/tests.yml)
19
+ [![codecov](https://codecov.io/github/jedie/mposcli/branch/main/graph/badge.svg)](https://app.codecov.io/github/jedie/mposcli)
20
+ [![mposcli @ PyPi](https://img.shields.io/pypi/v/mposcli?label=mposcli%20%40%20PyPi)](https://pypi.org/project/mposcli/)
21
+ [![Python Versions](https://img.shields.io/pypi/pyversions/mposcli)](https://github.com/jedie/mposcli/blob/main/pyproject.toml)
22
+ [![License GPL-3.0-or-later](https://img.shields.io/pypi/l/mposcli)](https://github.com/jedie/mposcli/blob/main/LICENSE)
23
+
24
+ Experimental CLI helper for MicroPythonOS: https://github.com/MicroPythonOS/MicroPythonOS
25
+
26
+ Main Idea: Install it via pipx (see below) and use `mposcli` command in MicroPythonOS repository path.
27
+
28
+ ## CLI
29
+
30
+ [comment]: <> (✂✂✂ auto generated main help start ✂✂✂)
31
+ ```
32
+ usage: mposcli [-h] {build,run-desktop,update-submodules,version}
33
+
34
+
35
+
36
+ ╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
37
+ │ -h, --help show this help message and exit │
38
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
39
+ ╭─ subcommands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮
40
+ │ (required) │
41
+ │ • build Build MicroPythonOS by calling: ./scripts/build_mpos.sh <target> see: │
42
+ │ https://docs.micropythonos.com/os-development/ │
43
+ │ • run-desktop Run MicroPythonOS on desktop. see: │
44
+ │ https://docs.micropythonos.com/getting-started/running/#running-on-desktop │
45
+ │ • update-submodules Update MicroPythonOS repository and all submodules see: │
46
+ │ https://docs.micropythonos.com/os-development/linux/#optional-updating-the-code │
47
+ │ • version Print version and exit │
48
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
49
+ ```
50
+ [comment]: <> (✂✂✂ auto generated main help end ✂✂✂)
51
+
52
+
53
+ ## CLI - build
54
+
55
+ [comment]: <> (✂✂✂ auto generated build start ✂✂✂)
56
+ ```
57
+ usage: mposcli build [-h] [--target {esp32,esp32s3,unix,macOS}] [-v]
58
+
59
+ Build MicroPythonOS by calling: ./scripts/build_mpos.sh <target> see: https://docs.micropythonos.com/os-development/
60
+
61
+ ╭─ options ────────────────────────────────────────────────────────────────╮
62
+ │ -h, --help show this help message and exit │
63
+ │ --target {esp32,esp32s3,unix,macOS} │
64
+ │ Target platform to build for. (default: unix) │
65
+ │ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │
66
+ ╰──────────────────────────────────────────────────────────────────────────╯
67
+ ```
68
+ [comment]: <> (✂✂✂ auto generated build end ✂✂✂)
69
+
70
+
71
+
72
+ ## CLI - run-desktop
73
+
74
+
75
+ [comment]: <> (✂✂✂ auto generated run-desktop start ✂✂✂)
76
+ ```
77
+ usage: mposcli run-desktop [-h] [--heapsize INT] [--script {None}|STR] [--binary {None}|STR] [-v]
78
+
79
+ Run MicroPythonOS on desktop. see: https://docs.micropythonos.com/getting-started/running/#running-on-desktop
80
+
81
+ ╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
82
+ │ -h, --help show this help message and exit │
83
+ │ --heapsize INT Heap size in MB (default: 8, same as PSRAM on many ESP32-S3 boards) (default: 8) │
84
+ │ --script {None}|STR Script file (.py) or app name to run. If omitted, starts normally. (default: None) │
85
+ │ --binary {None}|STR Optional name of the binary to start. If omitted, shows a file chooser to select one from the │
86
+ │ lvgl_micropython build directory. (default: None) │
87
+ │ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │
88
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
89
+ ```
90
+ [comment]: <> (✂✂✂ auto generated run-desktop end ✂✂✂)
91
+
92
+
93
+ ## CLI - update-submodules
94
+
95
+
96
+ [comment]: <> (✂✂✂ auto generated update-submodules start ✂✂✂)
97
+ ```
98
+ usage: mposcli update-submodules [-h] [-v]
99
+
100
+ Update MicroPythonOS repository and all submodules see: https://docs.micropythonos.com/os-development/linux/#optional-updating-the-code
101
+
102
+ ╭─ options ────────────────────────────────────────────────────────────────╮
103
+ │ -h, --help show this help message and exit │
104
+ │ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │
105
+ ╰──────────────────────────────────────────────────────────────────────────╯
106
+ ```
107
+ [comment]: <> (✂✂✂ auto generated update-submodules end ✂✂✂)
108
+
109
+
110
+
111
+
112
+
113
+
114
+ ## start development
115
+
116
+ At least `uv` is needed. Install e.g.: via pipx:
117
+ ```bash
118
+ apt-get install pipx
119
+ pipx install uv
120
+ ```
121
+
122
+ Clone the project and just start the CLI help commands.
123
+ A virtual environment will be created/updated automatically.
124
+
125
+ ```bash
126
+ ~$ git clone https://github.com/jedie/mposcli.git
127
+ ~$ cd mposcli
128
+ ~/mposcli$ ./cli.py --help
129
+ ~/mposcli$ ./dev-cli.py --help
130
+ ```
131
+
132
+ [comment]: <> (✂✂✂ auto generated dev help start ✂✂✂)
133
+ ```
134
+ usage: ./dev-cli.py [-h] {coverage,install,lint,mypy,nox,pip-audit,publish,shell-completion,test,update,update-readme-history,update-test-snapshot-files,version}
135
+
136
+
137
+
138
+ ╭─ options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
139
+ │ -h, --help show this help message and exit │
140
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
141
+ ╭─ subcommands ────────────────────────────────────────────────────────────────────────────────────────────────────────╮
142
+ │ (required) │
143
+ │ • coverage Run tests and show coverage report. │
144
+ │ • install Install requirements and 'mposcli' via pip as editable. │
145
+ │ • lint Check/fix code style by run: "ruff check --fix" │
146
+ │ • mypy Run Mypy (configured in pyproject.toml) │
147
+ │ • nox Run nox │
148
+ │ • pip-audit Run pip-audit check against current requirements files │
149
+ │ • publish Build and upload this project to PyPi │
150
+ │ • shell-completion │
151
+ │ Setup shell completion for this CLI (Currently only for bash shell) │
152
+ │ • test Run unittests │
153
+ │ • update Update dependencies (uv.lock) and git pre-commit hooks │
154
+ │ • update-readme-history │
155
+ │ Update project history base on git commits/tags in README.md Will be exited with 1 if the README.md │
156
+ │ was updated otherwise with 0. │
157
+ │ │
158
+ │ Also, callable via e.g.: │
159
+ │ python -m cli_base update-readme-history -v │
160
+ │ • update-test-snapshot-files │
161
+ │ Update all test snapshot files (by remove and recreate all snapshot files) │
162
+ │ • version Print version and exit │
163
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
164
+ ```
165
+ [comment]: <> (✂✂✂ auto generated dev help end ✂✂✂)
166
+
167
+
168
+ ## History
169
+
170
+ [comment]: <> (✂✂✂ auto generated history start ✂✂✂)
171
+
172
+ * [**dev**](https://github.com/jedie/mposcli/compare/1695026...main)
173
+ * 2026-02-16 - Add "update-submodules" command
174
+ * 2026-02-16 - Add "build" command
175
+ * 2026-02-16 - CLI command: "run-desktop"
176
+ * 2026-02-16 - first commit
177
+
178
+ [comment]: <> (✂✂✂ auto generated history end ✂✂✂)
@@ -0,0 +1,26 @@
1
+ mposcli/__init__.py,sha256=xJiMZ56FfdXOY73tYd5S2cxUMBXmiwtzl7kkbcEFdPI,270
2
+ mposcli/__main__.py,sha256=Xyf-B30u54m12uBqpL1HDMdFtD64a-rhFPgmRZlevjU,150
3
+ mposcli/constants.py,sha256=26abB8No0nNvCd--M7Z6sGgAPG_OloyUk5ugJGR6D6I,152
4
+ mposcli/fs_utils.py,sha256=_S2TtgG5iY0ZHctKFZ-9J49WrV0QChbyU-JqpZjsBJg,175
5
+ mposcli/mpos_utils.py,sha256=DJ9LHL0Dw0nFe7Kmqattc0kxoQIk6u8tiwFkv_GR5co,899
6
+ mposcli/user_input.py,sha256=eUuRiGCHb7OxorDB-dyG6pbXKDeDIPc8VFcDLT7AWSg,1048
7
+ mposcli/cli_app/__init__.py,sha256=ngvmI2LD2Qtc18TNxORzv4yvQT1QrflD3kj5EoOlyjs,966
8
+ mposcli/cli_app/build.py,sha256=7fjxPYT5jTXgrqouMYlvnY5R0whd2H6JzuFmfFMlISU,1141
9
+ mposcli/cli_app/run_deskop.py,sha256=cBl5-JMd6SK6dAKJ-ttjGmb8CXvciMfW2bO9svXaWqU,3330
10
+ mposcli/cli_app/update.py,sha256=J2cAfITOa_GhmOzvsACSuqzKRVvDfD1XpAT8awb8GKo,1023
11
+ mposcli/cli_dev/__init__.py,sha256=cPMttKS3TNZdP-CTSKCNSEIsquaUi8K-8VDxQ9Ey8xQ,1873
12
+ mposcli/cli_dev/__main__.py,sha256=Yx-Cc_oeUQo_M1uPV37Evz0PvKy5gUsUeEH19oEzSvM,158
13
+ mposcli/cli_dev/code_style.py,sha256=5kBHRqAPmAg27R00TWXE4EdPlTsOLkw65RKn4qEVkx8,382
14
+ mposcli/cli_dev/packaging.py,sha256=bZH7DLIF9lVWwdKJjpe69VgGmTLGylSfU9i_WUlm0PY,1798
15
+ mposcli/cli_dev/shell_completion.py,sha256=y96MQ8L0PLFYop9l8QTvRKs3q-aSC_5IrVf5AiSixqo,636
16
+ mposcli/cli_dev/testing.py,sha256=j4yj6aMQLlEYDUoel3uJnfkFKArW0YtiSGPK7BPMxfM,1381
17
+ mposcli/cli_dev/update_readme_history.py,sha256=k0wVSHMIRyQxiQdxHWwVxxQAYSqMWuEogEkdlWP0fBQ,900
18
+ mposcli/tests/__init__.py,sha256=LsOCUKW7i2biT8mo4eGTiNqS6d_G0p7801FrrDJQku8,1338
19
+ mposcli/tests/test_doctests.py,sha256=Gfv3EbactIO1JgDroQVp-sunSDWhasgEtElwc_ddL3Q,209
20
+ mposcli/tests/test_project_setup.py,sha256=dSqtKBQo6lUFTnX22LU7buY_jWkUiGrM_rXfslYtwCY,1759
21
+ mposcli/tests/test_readme.py,sha256=I9H5uxhASMCzxllPBShHQKS14ZdRZWiyvXOpYc4uLf8,3173
22
+ mposcli/tests/test_readme_history.py,sha256=PbxF_yRspyRwkVCQosZXevIHrsla3WFBTUVeLI0bb-s,263
23
+ mposcli-0.1.0.dist-info/METADATA,sha256=9TTGJ634Iun41T0m5w-XH8egoPx50z5IiEmvDSA0q30,13266
24
+ mposcli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
+ mposcli-0.1.0.dist-info/entry_points.txt,sha256=N3mHDYWJTw7GQa9Kakr3fcyDpRIXRC04TllScaURHN4,50
26
+ mposcli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mposcli = mposcli.__main__:main