kspl 0.1.0__tar.gz

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.
kspl-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2023 Cuinixam
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
kspl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.1
2
+ Name: kspl
3
+ Version: 0.1.0
4
+ Summary: KConfig GUI for Software Product Lines with multiple variants.
5
+ Home-page: https://github.com/cuinixam/kspl
6
+ License: MIT
7
+ Author: Cuinixam
8
+ Author-email: cuinixam@me.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Dist: customtkinter (>=5.2.0,<6.0.0)
20
+ Requires-Dist: kconfiglib (>=14.1.0,<15.0.0)
21
+ Requires-Dist: py-app-dev (>=1.0.0,<2.0.0)
22
+ Project-URL: Bug Tracker, https://github.com/cuinixam/kspl/issues
23
+ Project-URL: Changelog, https://github.com/cuinixam/kspl/blob/main/CHANGELOG.md
24
+ Project-URL: Documentation, https://kspl.readthedocs.io
25
+ Project-URL: Repository, https://github.com/cuinixam/kspl
26
+ Description-Content-Type: text/markdown
27
+
28
+ # SPL KConfig GUI
29
+
30
+ <p align="center">
31
+ <a href="https://github.com/cuinixam/kspl/actions/workflows/ci.yml?query=branch%3Amain">
32
+ <img src="https://img.shields.io/github/actions/workflow/status/cuinixam/kspl/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
33
+ </a>
34
+ <a href="https://kspl.readthedocs.io">
35
+ <img src="https://img.shields.io/readthedocs/kspl.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
36
+ </a>
37
+ <a href="https://codecov.io/gh/cuinixam/kspl">
38
+ <img src="https://img.shields.io/codecov/c/github/cuinixam/kspl.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
39
+ </a>
40
+ </p>
41
+ <p align="center">
42
+ <a href="https://python-poetry.org/">
43
+ <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
44
+ </a>
45
+ <a href="https://github.com/ambv/black">
46
+ <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
47
+ </a>
48
+ <a href="https://github.com/pre-commit/pre-commit">
49
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
50
+ </a>
51
+ </p>
52
+ <p align="center">
53
+ <a href="https://pypi.org/project/kspl/">
54
+ <img src="https://img.shields.io/pypi/v/kspl.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
55
+ </a>
56
+ <img src="https://img.shields.io/pypi/pyversions/kspl.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
57
+ <img src="https://img.shields.io/pypi/l/kspl.svg?style=flat-square" alt="License">
58
+ </p>
59
+
60
+ KConfig GUI for Software Product Lines with multiple variants.
61
+
62
+ ## Installation
63
+
64
+ Install this via pip (or your favourite package manager):
65
+
66
+ `pip install kspl`
67
+
68
+ ## Start developing
69
+
70
+ The project uses Poetry for dependencies management and packaging.
71
+ If you do not have Poetry installed, you can run the `boostrap.ps1` script.
72
+ This will install Python and Poetry as configured in `scoopfile.json`.
73
+
74
+ ```powershell
75
+ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force
76
+ .\bootstrap.ps1
77
+ ```
78
+
79
+ To install the development dependencies in a virtual environment, type:
80
+
81
+ ```shell
82
+ poetry install
83
+ ```
84
+
85
+ This will also generate a `poetry.lock` file, you should track this file in version control. To execute the test suite, call pytest inside Poetry's virtual environment via `poetry run`:
86
+
87
+ ```shell
88
+ poetry run pytest
89
+ ```
90
+
91
+ Check out the Poetry documentation for more information on the available commands.
92
+
93
+ For those using [VS Code](https://code.visualstudio.com/) there are tasks defined for the most common commands:
94
+
95
+ - install development dependencies
96
+ - run tests
97
+ - run all checks configured for pre-commit
98
+ - generate documentation
99
+
100
+ See the `.vscode/tasks.json` for more details.
101
+
102
+ ## Committing changes
103
+
104
+ This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en).
105
+
106
+ ## Contributors ✨
107
+
108
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
109
+
110
+ <!-- prettier-ignore-start -->
111
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
112
+ <!-- markdownlint-disable -->
113
+ <!-- markdownlint-enable -->
114
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
115
+ <!-- prettier-ignore-end -->
116
+
117
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
118
+
119
+ ## Credits
120
+
121
+ This package was created with
122
+ [Copier](https://copier.readthedocs.io/) and the
123
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
124
+ project template.
125
+
kspl-0.1.0/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # SPL KConfig GUI
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/cuinixam/kspl/actions/workflows/ci.yml?query=branch%3Amain">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/cuinixam/kspl/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
6
+ </a>
7
+ <a href="https://kspl.readthedocs.io">
8
+ <img src="https://img.shields.io/readthedocs/kspl.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
9
+ </a>
10
+ <a href="https://codecov.io/gh/cuinixam/kspl">
11
+ <img src="https://img.shields.io/codecov/c/github/cuinixam/kspl.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
12
+ </a>
13
+ </p>
14
+ <p align="center">
15
+ <a href="https://python-poetry.org/">
16
+ <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
17
+ </a>
18
+ <a href="https://github.com/ambv/black">
19
+ <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
20
+ </a>
21
+ <a href="https://github.com/pre-commit/pre-commit">
22
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
23
+ </a>
24
+ </p>
25
+ <p align="center">
26
+ <a href="https://pypi.org/project/kspl/">
27
+ <img src="https://img.shields.io/pypi/v/kspl.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
28
+ </a>
29
+ <img src="https://img.shields.io/pypi/pyversions/kspl.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
30
+ <img src="https://img.shields.io/pypi/l/kspl.svg?style=flat-square" alt="License">
31
+ </p>
32
+
33
+ KConfig GUI for Software Product Lines with multiple variants.
34
+
35
+ ## Installation
36
+
37
+ Install this via pip (or your favourite package manager):
38
+
39
+ `pip install kspl`
40
+
41
+ ## Start developing
42
+
43
+ The project uses Poetry for dependencies management and packaging.
44
+ If you do not have Poetry installed, you can run the `boostrap.ps1` script.
45
+ This will install Python and Poetry as configured in `scoopfile.json`.
46
+
47
+ ```powershell
48
+ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force
49
+ .\bootstrap.ps1
50
+ ```
51
+
52
+ To install the development dependencies in a virtual environment, type:
53
+
54
+ ```shell
55
+ poetry install
56
+ ```
57
+
58
+ This will also generate a `poetry.lock` file, you should track this file in version control. To execute the test suite, call pytest inside Poetry's virtual environment via `poetry run`:
59
+
60
+ ```shell
61
+ poetry run pytest
62
+ ```
63
+
64
+ Check out the Poetry documentation for more information on the available commands.
65
+
66
+ For those using [VS Code](https://code.visualstudio.com/) there are tasks defined for the most common commands:
67
+
68
+ - install development dependencies
69
+ - run tests
70
+ - run all checks configured for pre-commit
71
+ - generate documentation
72
+
73
+ See the `.vscode/tasks.json` for more details.
74
+
75
+ ## Committing changes
76
+
77
+ This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en).
78
+
79
+ ## Contributors ✨
80
+
81
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
82
+
83
+ <!-- prettier-ignore-start -->
84
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
85
+ <!-- markdownlint-disable -->
86
+ <!-- markdownlint-enable -->
87
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
88
+ <!-- prettier-ignore-end -->
89
+
90
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
91
+
92
+ ## Credits
93
+
94
+ This package was created with
95
+ [Copier](https://copier.readthedocs.io/) and the
96
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
97
+ project template.
@@ -0,0 +1,113 @@
1
+ [tool.poetry]
2
+ name = "kspl"
3
+ version = "0.1.0"
4
+ description = "KConfig GUI for Software Product Lines with multiple variants."
5
+ authors = ["Cuinixam <cuinixam@me.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/cuinixam/kspl"
9
+ documentation = "https://kspl.readthedocs.io"
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Natural Language :: English",
14
+ "Operating System :: OS Independent",
15
+ "Topic :: Software Development :: Libraries",
16
+ ]
17
+ packages = [
18
+ { include = "kspl", from = "src" },
19
+ ]
20
+
21
+ [tool.poetry.scripts]
22
+ kspl = "kspl.main:main"
23
+
24
+ [tool.poetry.urls]
25
+ "Bug Tracker" = "https://github.com/cuinixam/kspl/issues"
26
+ "Changelog" = "https://github.com/cuinixam/kspl/blob/main/CHANGELOG.md"
27
+
28
+ [tool.poetry.dependencies]
29
+ python = "^3.10"
30
+ customtkinter = "^5.2.0"
31
+ kconfiglib = "^14.1.0"
32
+ py-app-dev = "^1.0.0"
33
+
34
+ [tool.poetry.group.dev.dependencies]
35
+ pytest = "^7.0"
36
+ pytest-cov = "^4.0"
37
+ black = "^23.1.0"
38
+ pre-commit = "^3.1.1"
39
+
40
+ [tool.poetry.group.docs]
41
+ optional = true
42
+
43
+ [tool.poetry.group.docs.dependencies]
44
+ myst-parser = ">=0.16"
45
+ sphinx = ">=4.0"
46
+ sphinx-rtd-theme = ">=1.0"
47
+ m2r = "^0.3.1"
48
+ sphinxcontrib-mermaid = "^0.8.1"
49
+
50
+ [tool.semantic_release]
51
+ branch = "main"
52
+ version_toml = "pyproject.toml:tool.poetry.version"
53
+ version_variable = "src/kspl/__init__.py:__version__"
54
+ build_command = "pip install poetry && poetry build"
55
+
56
+ [tool.semantic_release.changelog]
57
+ exclude_commit_patterns = [
58
+ "chore*",
59
+ "ci*",
60
+ ]
61
+
62
+ [tool.semantic_release.changelog.environment]
63
+ keep_trailing_newline = true
64
+
65
+ [tool.pytest.ini_options]
66
+ addopts = "-v -Wdefault --cov=kspl --cov-report=term-missing:skip-covered -s"
67
+ pythonpath = ["src"]
68
+
69
+ [tool.coverage.run]
70
+ branch = true
71
+
72
+ [tool.coverage.report]
73
+ exclude_lines = [
74
+ "pragma: no cover",
75
+ "@overload",
76
+ "if TYPE_CHECKING",
77
+ "raise NotImplementedError",
78
+ 'if __name__ == "__main__":',
79
+ ]
80
+
81
+ [tool.isort]
82
+ profile = "black"
83
+ known_first_party = ["kspl", "tests"]
84
+
85
+ [tool.mypy]
86
+ check_untyped_defs = true
87
+ disallow_any_generics = true
88
+ disallow_incomplete_defs = true
89
+ disallow_untyped_defs = true
90
+ mypy_path = "src/"
91
+ no_implicit_optional = true
92
+ show_error_codes = true
93
+ warn_unreachable = true
94
+ warn_unused_ignores = true
95
+ exclude = [
96
+ 'docs/.*',
97
+ 'setup.py',
98
+ ]
99
+
100
+ [[tool.mypy.overrides]]
101
+ module = "tests.*"
102
+ allow_untyped_defs = true
103
+
104
+ [[tool.mypy.overrides]]
105
+ module = "docs.*"
106
+ ignore_errors = true
107
+
108
+ [tool.codespell]
109
+ skip = '*.lock'
110
+
111
+ [build-system]
112
+ requires = ["poetry-core>=1.0.0"]
113
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ """Used to run main from the command line when run from this repository.
2
+ This is required because kspl module is not visible when running
3
+ from the repository."""
4
+ import runpy
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, Path(__file__).parent.parent.absolute().as_posix())
9
+ runpy.run_module("kspl.main", run_name="__main__")
@@ -0,0 +1,194 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from argparse import ArgumentParser, Namespace
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ import kconfiglib
9
+ from mashumaro import DataClassDictMixin
10
+ from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
11
+ from py_app_dev.core.logging import logger, time_it
12
+
13
+ from kspl.kconfig import ConfigElementType, ConfigurationData, KConfig, TriState
14
+
15
+
16
+ class GeneratedFile:
17
+ def __init__(
18
+ self, path: Path, content: str = "", skip_writing_if_unchanged: bool = False
19
+ ) -> None:
20
+ self.path = path
21
+
22
+ self.content = content
23
+
24
+ self.skip_writing_if_unchanged = skip_writing_if_unchanged
25
+
26
+ def to_string(self) -> str:
27
+ return self.content
28
+
29
+ def to_file(self) -> None:
30
+ """Only write to file if the content has changed.
31
+ The directory of the file is created if it does not exist."""
32
+
33
+ content = self.to_string()
34
+
35
+ if (
36
+ not self.path.exists()
37
+ or not self.skip_writing_if_unchanged
38
+ or self.path.read_text() != content
39
+ ):
40
+ self.path.parent.mkdir(parents=True, exist_ok=True)
41
+ self.path.write_text(content)
42
+
43
+
44
+ class FileWriter(ABC):
45
+ """- writes the ConfigurationData to a file"""
46
+
47
+ def __init__(self, output_file: Path):
48
+ self.output_file = output_file
49
+
50
+ def write(self, configuration_data: ConfigurationData) -> None:
51
+ """- writes the ConfigurationData to a file
52
+ The file shall not be modified if the content is the same as the existing one"""
53
+ content = self.generate_content(configuration_data)
54
+ GeneratedFile(
55
+ self.output_file, content, skip_writing_if_unchanged=True
56
+ ).to_file()
57
+
58
+ @abstractmethod
59
+ def generate_content(self, configuration_data: ConfigurationData) -> str:
60
+ """- generates the content of the file from the ConfigurationData"""
61
+
62
+
63
+ class HeaderWriter(FileWriter):
64
+ """Writes the ConfigurationData as pre-processor defines in a C Header file"""
65
+
66
+ config_prefix = "CONFIG_" # Prefix for all configuration defines
67
+
68
+ def generate_content(self, configuration_data: ConfigurationData) -> str:
69
+ """This method does exactly what the kconfiglib.write_autoconf() method does.
70
+ We had to implemented here because we refactor the file writers to use the ConfigurationData
71
+ instead of the KConfig configuration. ConfigurationData has variable substitution already done.
72
+ """
73
+ result: List[str] = [
74
+ "/** @file */",
75
+ "#ifndef __autoconf_h__",
76
+ "#define __autoconf_h__",
77
+ "",
78
+ ]
79
+
80
+ def add_define(define_decl: str, description: str) -> None:
81
+ result.append(f"/** {description} */")
82
+ result.append(define_decl)
83
+
84
+ for element in configuration_data.elements:
85
+ val = element.value
86
+ if element.type in [ConfigElementType.BOOL, ConfigElementType.TRISTATE]:
87
+ if val == TriState.Y:
88
+ add_define(
89
+ f"#define {self.config_prefix}{element.name} 1",
90
+ element.name,
91
+ )
92
+ elif val == TriState.M:
93
+ add_define(
94
+ "#define {}{}_MODULE 1".format(
95
+ self.config_prefix, element.name
96
+ ),
97
+ element.name,
98
+ )
99
+
100
+ elif element.type is ConfigElementType.STRING:
101
+ add_define(
102
+ '#define {}{} "{}"'.format(
103
+ self.config_prefix, element.name, kconfiglib.escape(val)
104
+ ),
105
+ element.name,
106
+ )
107
+
108
+ else: # element.type in [INT, HEX]:
109
+ if element.type is ConfigElementType.HEX:
110
+ val = hex(val)
111
+ add_define(
112
+ f"#define {self.config_prefix}{element.name} {val}",
113
+ element.name,
114
+ )
115
+ result.extend(["", "#endif /* __autoconf_h__ */", ""])
116
+ return "\n".join(result)
117
+
118
+
119
+ class JsonWriter(FileWriter):
120
+ """Writes the ConfigurationData in json format"""
121
+
122
+ def generate_content(self, configuration_data: ConfigurationData) -> str:
123
+ result = {}
124
+ for element in configuration_data.elements:
125
+ if element.type is ConfigElementType.BOOL:
126
+ result[element.name] = True if element.value == TriState.Y else False
127
+ else:
128
+ result[element.name] = element.value
129
+ return json.dumps({"features": result}, indent=4)
130
+
131
+
132
+ class CMakeWriter(FileWriter):
133
+ """Writes the ConfigurationData as CMake variables"""
134
+
135
+ def generate_content(self, configuration_data: ConfigurationData) -> str:
136
+ result: List[str] = []
137
+ add = result.append
138
+ for element in configuration_data.elements:
139
+ val = element.value
140
+ if element.type is ConfigElementType.BOOL:
141
+ val = True if element.value == TriState.Y else False
142
+ add(f'set({element.name} "{val}")')
143
+
144
+ return "\n".join(result)
145
+
146
+
147
+ @dataclass
148
+ class GenerateCommandConfig(DataClassDictMixin):
149
+ kconfig_model_file: Path = field(metadata={"help": "KConfig model file (KConfig)."})
150
+ kconfig_config_file: Optional[Path] = field(
151
+ default=None, metadata={"help": "KConfig user configuration file (config.txt)."}
152
+ )
153
+ out_header_file: Optional[Path] = field(
154
+ default=None, metadata={"help": "File to write the configuration as C header."}
155
+ )
156
+ out_json_file: Optional[Path] = field(
157
+ default=None,
158
+ metadata={"help": "File to write the configuration in JSON format."},
159
+ )
160
+ out_cmake_file: Optional[Path] = field(
161
+ default=None,
162
+ metadata={"help": "File to write the configuration in CMake format."},
163
+ )
164
+
165
+ @classmethod
166
+ def from_namespace(cls, namespace: Namespace) -> "GenerateCommandConfig":
167
+ return cls.from_dict(vars(namespace))
168
+
169
+
170
+ class GenerateCommand(Command):
171
+ def __init__(self) -> None:
172
+ super().__init__(
173
+ "generate", "Generate the KConfig configuration in the specified formats."
174
+ )
175
+ self.logger = logger.bind()
176
+
177
+ @time_it("Build")
178
+ def run(self, args: Namespace) -> int:
179
+ self.logger.info(f"Running {self.name} with args {args}")
180
+ cmd_config = GenerateCommandConfig.from_namespace(args)
181
+ config = KConfig(
182
+ cmd_config.kconfig_model_file, cmd_config.kconfig_config_file
183
+ ).config
184
+
185
+ if cmd_config.out_header_file:
186
+ HeaderWriter(cmd_config.out_header_file).write(config)
187
+ if cmd_config.out_json_file:
188
+ JsonWriter(cmd_config.out_json_file).write(config)
189
+ if cmd_config.out_cmake_file:
190
+ CMakeWriter(cmd_config.out_cmake_file).write(config)
191
+ return 0
192
+
193
+ def _register_arguments(self, parser: ArgumentParser) -> None:
194
+ register_arguments_for_config_dataclass(parser, GenerateCommandConfig)
@@ -0,0 +1,338 @@
1
+ import tkinter
2
+ from abc import abstractmethod
3
+ from argparse import ArgumentParser, Namespace
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum, auto
6
+ from pathlib import Path
7
+ from tkinter import simpledialog, ttk
8
+ from typing import Any, Dict, List
9
+
10
+ import customtkinter
11
+ from mashumaro import DataClassDictMixin
12
+ from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
13
+ from py_app_dev.core.logging import logger, time_it
14
+ from py_app_dev.mvp.event_manager import EventID, EventManager
15
+ from py_app_dev.mvp.presenter import Presenter
16
+ from py_app_dev.mvp.view import View
17
+
18
+ from kspl.kconfig import KConfig
19
+
20
+
21
+ class ConfigElementType(Enum):
22
+ MENU = auto()
23
+ CONFIG = auto()
24
+
25
+
26
+ @dataclass
27
+ class ConfigElementViewData:
28
+ name: str
29
+ level: int
30
+ type: ConfigElementType
31
+
32
+
33
+ @dataclass
34
+ class VariantViewData:
35
+ """A variant is a set of configuration values for a KConfig model."""
36
+
37
+ name: str
38
+ config_dict: Dict[str, int | str | bool]
39
+
40
+
41
+ class KSplEvents(EventID):
42
+ EDIT = auto()
43
+
44
+
45
+ class CTkView(View):
46
+ @abstractmethod
47
+ def mainloop(self) -> None:
48
+ pass
49
+
50
+
51
+ class MainView(CTkView):
52
+ def __init__(
53
+ self,
54
+ event_manager: EventManager,
55
+ elements: List[ConfigElementViewData],
56
+ variants: List[VariantViewData],
57
+ ) -> None:
58
+ self.event_manager = event_manager
59
+ self.elements = elements
60
+ self.variants = variants
61
+ self.root = customtkinter.CTk()
62
+
63
+ # Configure the main window
64
+ self.root.title("K-SPL")
65
+ self.root.geometry(f"{1080}x{580}")
66
+
67
+ # ========================================================
68
+ # create tabview and populate with frames
69
+ tabview = customtkinter.CTkTabview(self.root)
70
+ self.tree = self.create_tree_view(tabview.add("Configuration"))
71
+ self.tree["columns"] = tuple(variant.name for variant in self.variants)
72
+ self.tree.heading("#0", text="Configuration")
73
+ for variant in self.variants:
74
+ self.tree.heading(variant.name, text=variant.name)
75
+ # Keep track of the mapping between the tree view items and the config elements
76
+ self.tree_view_items_mapping = self.populate_tree_view()
77
+ self.tree.pack(fill="both", expand=True)
78
+ self.tree.bind("<Double-1>", self.double_click_handler)
79
+
80
+ # ========================================================
81
+ # put all together
82
+ self.root.grid_columnconfigure(0, weight=1)
83
+ self.root.grid_rowconfigure(0, weight=1)
84
+ tabview.grid(row=0, column=0, sticky="nsew")
85
+
86
+ def mainloop(self) -> None:
87
+ self.root.mainloop()
88
+
89
+ def create_tree_view(self, frame: customtkinter.CTkFrame) -> ttk.Treeview:
90
+ frame.grid_rowconfigure(0, weight=10)
91
+ frame.grid_rowconfigure(1, weight=1)
92
+ frame.grid_columnconfigure(0, weight=1)
93
+
94
+ columns = [var.name for var in self.variants]
95
+
96
+ style = ttk.Style()
97
+ style.configure(
98
+ "mystyle.Treeview", highlightthickness=0, bd=0, font=("Calibri", 14)
99
+ ) # Modify the font of the body
100
+ style.configure(
101
+ "mystyle.Treeview.Heading", font=("Calibri", 14, "bold")
102
+ ) # Modify the font of the headings
103
+
104
+ # create a Treeview widget
105
+ config_treeview = ttk.Treeview(
106
+ frame,
107
+ columns=columns,
108
+ show="tree headings",
109
+ style="mystyle.Treeview",
110
+ )
111
+ config_treeview.grid(row=0, column=0, sticky="nsew")
112
+ return config_treeview
113
+
114
+ def populate_tree_view(self) -> Dict[str, str]:
115
+ """
116
+ Populates the tree view with the configuration elements.
117
+ :return: a mapping between the tree view items and the configuration elements
118
+ """
119
+ stack = [] # To keep track of the parent items
120
+ last_level = -1
121
+ mapping: Dict[str, str] = {}
122
+
123
+ for element in self.elements:
124
+ values = self.collect_values_for_element(element)
125
+ if element.level == 0:
126
+ # Insert at the root level
127
+ item_id = self.tree.insert("", "end", text=element.name, values=values)
128
+ stack = [item_id] # Reset the stack with the root item
129
+ elif element.level > last_level:
130
+ # Insert as a child of the last inserted item
131
+ item_id = self.tree.insert(
132
+ stack[-1], "end", text=element.name, values=values
133
+ )
134
+ stack.append(item_id)
135
+ elif element.level == last_level:
136
+ # Insert at the same level as the last item
137
+ item_id = self.tree.insert(
138
+ stack[-2], "end", text=element.name, values=values
139
+ )
140
+ stack[-1] = item_id # Replace the top item in the stack
141
+ else:
142
+ # Go up in the hierarchy and insert at the appropriate level
143
+ item_id = self.tree.insert(
144
+ stack[element.level - 1], "end", text=element.name, values=values
145
+ )
146
+ stack = stack[: element.level] + [item_id]
147
+
148
+ last_level = element.level
149
+ mapping[item_id] = element.name
150
+ return mapping
151
+
152
+ def collect_values_for_element(
153
+ self, element: ConfigElementViewData
154
+ ) -> List[int | str]:
155
+ return (
156
+ [
157
+ self.prepare_value_to_be_displayed(
158
+ variant.config_dict.get(element.name, None)
159
+ )
160
+ for variant in self.variants
161
+ ]
162
+ if element.type == ConfigElementType.CONFIG
163
+ else []
164
+ )
165
+
166
+ def prepare_value_to_be_displayed(self, value: Any) -> str:
167
+ if value is None:
168
+ return "N/A"
169
+ elif isinstance(value, bool):
170
+ return "✅" if value else "⛔"
171
+ else:
172
+ return str(value)
173
+
174
+ def double_click_handler(self, event: tkinter.Event) -> None: # type: ignore
175
+ current_selection = self.tree.selection()
176
+ if not current_selection:
177
+ return
178
+
179
+ selected_item = current_selection[0]
180
+ elem_name = self.tree_view_items_mapping[selected_item]
181
+
182
+ variant_idx_str = self.tree.identify_column(event.x) # Get the clicked column
183
+ variant_idx = (
184
+ int(variant_idx_str.split("#")[-1]) - 1
185
+ ) # Convert to 0-based index
186
+
187
+ if variant_idx < 0 or variant_idx >= len(self.variants):
188
+ return
189
+
190
+ selected_variant = self.variants[variant_idx]
191
+ selected_value = selected_variant.config_dict[elem_name]
192
+
193
+ if selected_value is not None:
194
+ new_value = selected_value
195
+ if isinstance(selected_value, bool):
196
+ # Toggle the boolean value
197
+ new_value = not selected_value
198
+ elif isinstance(selected_value, int):
199
+ tmp_int_value = simpledialog.askinteger(
200
+ "Enter new value", "Enter new value", initialvalue=selected_value
201
+ )
202
+ if tmp_int_value is not None:
203
+ new_value = tmp_int_value
204
+ elif isinstance(selected_value, str):
205
+ # Prompt the user to enter a new string value using messagebox
206
+ tmp_str_value = simpledialog.askstring(
207
+ "Enter new value", "Enter new value", initialvalue=selected_value
208
+ )
209
+ if tmp_str_value is not None:
210
+ new_value = tmp_str_value
211
+
212
+ selected_variant.config_dict[elem_name] = new_value
213
+
214
+ # Update the Treeview
215
+ values = list(
216
+ self.tree.item(selected_item, "values")
217
+ ) # Get the current values of the selected item
218
+ values[variant_idx] = self.prepare_value_to_be_displayed(new_value)
219
+ self.tree.item(selected_item, values=values)
220
+
221
+
222
+ @dataclass
223
+ class VariantData:
224
+ name: str
225
+ config: KConfig
226
+
227
+
228
+ class SPLKConfigData:
229
+ def __init__(self, project_root_dir: Path) -> None:
230
+ self.project_root_dir = project_root_dir.absolute()
231
+ variant_config_files = self._search_variant_config_file(self.project_root_dir)
232
+ self.model = KConfig(self.kconfig_model_file)
233
+ if variant_config_files:
234
+ self.variant_configs: List[VariantData] = [
235
+ VariantData(
236
+ self._get_variant_name(file), KConfig(self.kconfig_model_file, file)
237
+ )
238
+ for file in variant_config_files
239
+ ]
240
+ else:
241
+ self.variant_configs = [VariantData("Default", self.model)]
242
+
243
+ @property
244
+ def kconfig_model_file(self) -> Path:
245
+ return self.project_root_dir / "KConfig"
246
+
247
+ def get_elements(self) -> List[ConfigElementViewData]:
248
+ elements = []
249
+ for elem in self.model.elements:
250
+ elements.append(
251
+ ConfigElementViewData(
252
+ elem.name,
253
+ elem.level,
254
+ ConfigElementType.MENU
255
+ if elem.is_menu
256
+ else ConfigElementType.CONFIG,
257
+ )
258
+ )
259
+ return elements
260
+
261
+ def get_variants(self) -> List[VariantViewData]:
262
+ variants = []
263
+
264
+ for variant in self.variant_configs:
265
+ variants.append(
266
+ VariantViewData(
267
+ variant.name,
268
+ {
269
+ config_elem.name: config_elem.raw_value
270
+ for config_elem in variant.config.elements
271
+ if not config_elem.is_menu
272
+ },
273
+ )
274
+ )
275
+ return variants
276
+
277
+ def _get_variant_name(self, file: Path) -> str:
278
+ return file.relative_to(self.project_root_dir / "variants").parent.name
279
+
280
+ def _search_variant_config_file(self, project_dir: Path) -> List[Path]:
281
+ """
282
+ Finds all files called 'config.txt' in the variants directory
283
+ and returns a list with their paths.
284
+ """
285
+ return list((project_dir / "variants").glob("**/config.txt"))
286
+
287
+
288
+ class KSPL(Presenter):
289
+ def __init__(self, event_manager: EventManager, project_dir: Path) -> None:
290
+ self.event_manager = event_manager
291
+ self.event_manager.subscribe(KSplEvents.EDIT, self.edit)
292
+ self.counter = 0
293
+ self.logger = logger.bind()
294
+ self.kconfig_data = SPLKConfigData(project_dir)
295
+ self.view = MainView(
296
+ self.event_manager,
297
+ self.kconfig_data.get_elements(),
298
+ self.kconfig_data.get_variants(),
299
+ )
300
+
301
+ def edit(self) -> None:
302
+ self.counter += 1
303
+ self.logger.info(f"Counter: {self.counter}")
304
+
305
+ def run(self) -> None:
306
+ self.view.mainloop()
307
+
308
+
309
+ @dataclass
310
+ class GuiCommandConfig(DataClassDictMixin):
311
+ project_dir: Path = field(
312
+ default=Path(".").absolute(),
313
+ metadata={
314
+ "help": "Project root directory. "
315
+ "Defaults to the current directory if not specified."
316
+ },
317
+ )
318
+
319
+ @classmethod
320
+ def from_namespace(cls, namespace: Namespace) -> "GuiCommandConfig":
321
+ return cls.from_dict(vars(namespace))
322
+
323
+
324
+ class GuiCommand(Command):
325
+ def __init__(self) -> None:
326
+ super().__init__("gui", "Start the GUI for SPL configuration.")
327
+ self.logger = logger.bind()
328
+
329
+ @time_it("Build")
330
+ def run(self, args: Namespace) -> int:
331
+ self.logger.info(f"Running {self.name} with args {args}")
332
+ config = GuiCommandConfig.from_namespace(args)
333
+ event_manager = EventManager()
334
+ KSPL(event_manager, config.project_dir.absolute()).run()
335
+ return 0
336
+
337
+ def _register_arguments(self, parser: ArgumentParser) -> None:
338
+ register_arguments_for_config_dataclass(parser, GuiCommandConfig)
@@ -0,0 +1,221 @@
1
+ import os
2
+ import re
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass
5
+ from enum import Enum, auto
6
+ from pathlib import Path
7
+ from typing import Any, Generator, List, Optional
8
+
9
+ import kconfiglib
10
+ from kconfiglib import MenuNode
11
+
12
+
13
+ class TriState(Enum):
14
+ Y = auto()
15
+ M = auto()
16
+ N = auto()
17
+
18
+
19
+ class ConfigElementType(Enum):
20
+ UNKNOWN = auto()
21
+ BOOL = auto()
22
+ TRISTATE = auto()
23
+ STRING = auto()
24
+ INT = auto()
25
+ HEX = auto()
26
+ MENU = auto()
27
+
28
+
29
+ @dataclass
30
+ class ConfigElement:
31
+ type: ConfigElementType
32
+ name: str
33
+ value: Any
34
+
35
+ @property
36
+ def is_menu(self) -> bool:
37
+ return self.type == ConfigElementType.MENU
38
+
39
+ @property
40
+ def raw_value(self) -> int | str | bool:
41
+ value = self.value
42
+ if self.type == ConfigElementType.BOOL:
43
+ value = self.value == TriState.Y
44
+ elif self.type == ConfigElementType.TRISTATE:
45
+ value = str(self.value)
46
+ elif self.type == ConfigElementType.HEX:
47
+ value = hex(self.value)
48
+ return value
49
+
50
+
51
+ @dataclass
52
+ class EditableConfigElement(ConfigElement):
53
+ original_value: Any
54
+
55
+ #: The level of the menu this element is in. 0 is the top level.
56
+ level: int = 0
57
+ #: Is determined when the value is calculated. This is a hidden function call due to property magic.
58
+ write_to_conf: bool = True
59
+
60
+ @property
61
+ def id(self) -> str:
62
+ return self.name
63
+
64
+ @property
65
+ def has_been_changed(self) -> bool:
66
+ return self.original_value != self.value
67
+
68
+
69
+ @dataclass
70
+ class ConfigurationData:
71
+ """Holds the variant configuration data which is relevant for the code generation
72
+ Requires no variable substitution (this should have been already done)"""
73
+
74
+ elements: List[ConfigElement]
75
+
76
+
77
+ @contextmanager
78
+ def working_directory(some_directory: Path) -> Generator[None, Any, None]:
79
+ current_directory = Path().absolute()
80
+ try:
81
+ os.chdir(some_directory)
82
+ yield
83
+ finally:
84
+ os.chdir(current_directory)
85
+
86
+
87
+ class KConfig:
88
+ def __init__(
89
+ self,
90
+ k_config_model_file: Path,
91
+ k_config_file: Optional[Path] = None,
92
+ k_config_root_directory: Optional[Path] = None,
93
+ ):
94
+ """
95
+ :param k_config_model_file: Feature model definition (KConfig format)
96
+ :param k_config_file: User feature selection configuration file
97
+ :param k_config_root_directory: all paths for the included configuration paths shall be relative to this folder
98
+ """
99
+ if not k_config_model_file.is_file():
100
+ raise FileNotFoundError(f"File {k_config_model_file} does not exist.")
101
+ with working_directory(k_config_root_directory or k_config_model_file.parent):
102
+ self._config = kconfiglib.Kconfig(k_config_model_file.absolute().as_posix())
103
+ if k_config_file:
104
+ if not k_config_file.is_file():
105
+ raise FileNotFoundError(f"File {k_config_file} does not exist.")
106
+ self._config.load_config(k_config_file, replace=False)
107
+ self.elements = self._collect_elements()
108
+ self.config = self._create_config_data()
109
+
110
+ def _create_config_data(self) -> ConfigurationData:
111
+ """- creates the ConfigurationData from the KConfig configuration"""
112
+ elements = self.elements
113
+ elements_dict = {element.id: element for element in elements}
114
+
115
+ # replace text in KConfig with referenced variables (string type only)
116
+ # KConfig variables get replaced like: ${VARIABLE_NAME}, e.g. ${CONFIG_FOO}
117
+ for element in elements:
118
+ if element.type == ConfigElementType.STRING:
119
+ element.value = re.sub(
120
+ r"\$\{([A-Za-z0-9_]+)\}",
121
+ lambda m: str(elements_dict[m.group(1)].value),
122
+ element.value,
123
+ )
124
+ element.value = re.sub(
125
+ r"\$\{ENV:([A-Za-z0-9_]+)\}",
126
+ lambda m: str(os.environ.get(m.group(1), "")),
127
+ element.value,
128
+ )
129
+
130
+ return ConfigurationData(
131
+ [
132
+ ConfigElement(elem.type, elem.name, elem.value)
133
+ for elem in elements
134
+ if elem.type != ConfigElementType.MENU
135
+ ]
136
+ )
137
+
138
+ def _collect_elements(self) -> List[EditableConfigElement]:
139
+ elements: List[EditableConfigElement] = []
140
+
141
+ def convert_to_element(
142
+ node: MenuNode, level: int
143
+ ) -> Optional[EditableConfigElement]:
144
+ # TODO: Symbols like 'choice' and 'comment' shall be ignored.
145
+ element = None
146
+ sym = node.item
147
+ if isinstance(sym, kconfiglib.Symbol):
148
+ if sym.config_string:
149
+ val = sym.str_value
150
+ type = ConfigElementType.STRING
151
+ if sym.type in [kconfiglib.BOOL, kconfiglib.TRISTATE]:
152
+ val = getattr(TriState, str(val).upper())
153
+ type = (
154
+ ConfigElementType.BOOL
155
+ if sym.type == kconfiglib.BOOL
156
+ else ConfigElementType.TRISTATE
157
+ )
158
+ elif sym.type == kconfiglib.HEX:
159
+ val = int(str(val), 16)
160
+ type = ConfigElementType.HEX
161
+ elif sym.type == kconfiglib.INT:
162
+ val = int(val)
163
+ type = ConfigElementType.INT
164
+ element = EditableConfigElement(
165
+ type=type,
166
+ name=sym.name,
167
+ value=val,
168
+ original_value=val,
169
+ level=level,
170
+ write_to_conf=sym._write_to_conf,
171
+ )
172
+ else:
173
+ if isinstance(node, kconfiglib.MenuNode):
174
+ element = EditableConfigElement(
175
+ type=ConfigElementType.MENU,
176
+ name=node.prompt[0],
177
+ value=None,
178
+ original_value=None,
179
+ level=level,
180
+ write_to_conf=False,
181
+ )
182
+ return element
183
+
184
+ def _shown_full_nodes(node: MenuNode) -> List[MenuNode]:
185
+ # Returns the list of menu nodes shown in 'menu' (a menu node for a menu)
186
+ # for full-tree mode. A tricky detail is that invisible items need to be
187
+ # shown if they have visible children.
188
+
189
+ def rec(node: MenuNode) -> List[MenuNode]:
190
+ res = []
191
+
192
+ while node:
193
+ res.append(node)
194
+ if node.list and isinstance(node.item, kconfiglib.Symbol):
195
+ # Nodes from menu created from dependencies
196
+ res += rec(node.list)
197
+ node = node.next
198
+
199
+ return res
200
+
201
+ return rec(node.list)
202
+
203
+ def create_elements_tree(
204
+ node: MenuNode, collected_nodes: List[EditableConfigElement], level: int = 0
205
+ ) -> None:
206
+ # Updates the tree starting from menu.list, in full-tree mode. To speed
207
+ # things up, only open menus are updated. The menu-at-a-time logic here is
208
+ # to deal with invisible items that can show up outside show-all mode (see
209
+ # _shown_full_nodes()).
210
+
211
+ for node in _shown_full_nodes(node):
212
+ element = convert_to_element(node, level)
213
+ if element:
214
+ collected_nodes.append(element)
215
+ # _shown_full_nodes() includes nodes from menus rooted at symbols, so
216
+ # we only need to check "real" menus/choices here
217
+ if node.list and not isinstance(node.item, kconfiglib.Symbol):
218
+ create_elements_tree(node, collected_nodes, level + 1)
219
+
220
+ create_elements_tree(self._config.top_node, elements)
221
+ return elements
@@ -0,0 +1,38 @@
1
+ import sys
2
+ from argparse import ArgumentParser
3
+ from sys import argv
4
+
5
+ from py_app_dev.core.cmd_line import CommandLineHandlerBuilder
6
+ from py_app_dev.core.exceptions import UserNotificationException
7
+ from py_app_dev.core.logging import logger, setup_logger
8
+
9
+ from kspl import __version__
10
+ from kspl.generate import GenerateCommand
11
+ from kspl.gui import GuiCommand
12
+
13
+
14
+ def do_run() -> None:
15
+ parser = ArgumentParser(
16
+ prog="kspl", description="kconfig for SPL", exit_on_error=False
17
+ )
18
+ parser.add_argument(
19
+ "-v", "--version", action="version", version=f"%(prog)s {__version__}"
20
+ )
21
+ builder = CommandLineHandlerBuilder(parser)
22
+ builder.add_commands([GuiCommand(), GenerateCommand()])
23
+ handler = builder.create()
24
+ handler.run(argv[1:])
25
+
26
+
27
+ def main() -> int:
28
+ try:
29
+ setup_logger()
30
+ do_run()
31
+ except UserNotificationException as e:
32
+ logger.error(f"{e}")
33
+ return 1
34
+ return 0
35
+
36
+
37
+ if __name__ == "__main__":
38
+ sys.exit(main())
File without changes