kspl 1.1.1__py3-none-any.whl → 1.2.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.
- kspl/__init__.py +1 -1
- kspl/_run.py +13 -9
- kspl/config_slurper.py +71 -85
- kspl/edit.py +71 -84
- kspl/generate.py +22 -43
- kspl/gui.py +204 -79
- kspl/kconfig.py +228 -242
- kspl/main.py +35 -39
- kspl-1.2.0.dist-info/METADATA +99 -0
- kspl-1.2.0.dist-info/RECORD +14 -0
- {kspl-1.1.1.dist-info → kspl-1.2.0.dist-info}/WHEEL +1 -1
- kspl-1.1.1.dist-info/METADATA +0 -127
- kspl-1.1.1.dist-info/RECORD +0 -14
- {kspl-1.1.1.dist-info → kspl-1.2.0.dist-info}/LICENSE +0 -0
- {kspl-1.1.1.dist-info → kspl-1.2.0.dist-info}/entry_points.txt +0 -0
kspl/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.2.0"
|
kspl/_run.py
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
-
"""
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
"""
|
2
|
+
Used to run main from the command line when run from this repository.
|
3
|
+
|
4
|
+
This is required because kspl module is not visible when running
|
5
|
+
from the repository.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import runpy
|
9
|
+
import sys
|
10
|
+
from pathlib import Path
|
11
|
+
|
12
|
+
sys.path.insert(0, Path(__file__).parent.parent.absolute().as_posix())
|
13
|
+
runpy.run_module("kspl.main", run_name="__main__")
|
kspl/config_slurper.py
CHANGED
@@ -1,85 +1,71 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
from py_app_dev.core.exceptions import UserNotificationException
|
6
|
-
from py_app_dev.core.logging import logger
|
7
|
-
|
8
|
-
from kspl.kconfig import EditableConfigElement, KConfig
|
9
|
-
|
10
|
-
|
11
|
-
@dataclass
|
12
|
-
class VariantViewData:
|
13
|
-
"""A variant is a set of configuration values for a KConfig model."""
|
14
|
-
|
15
|
-
name: str
|
16
|
-
config_dict:
|
17
|
-
|
18
|
-
|
19
|
-
@dataclass
|
20
|
-
class VariantData:
|
21
|
-
name: str
|
22
|
-
config: KConfig
|
23
|
-
|
24
|
-
def find_element(self, element_name: str) ->
|
25
|
-
return self.config.find_element(element_name)
|
26
|
-
|
27
|
-
|
28
|
-
class SPLKConfigData:
|
29
|
-
def __init__(self, project_root_dir: Path) -> None:
|
30
|
-
self.project_root_dir = project_root_dir.absolute()
|
31
|
-
variant_config_files = self._search_variant_config_file(self.project_root_dir)
|
32
|
-
if not self.kconfig_model_file.is_file():
|
33
|
-
raise UserNotificationException(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
self.variant_configs
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
self.
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
return file.relative_to(self.project_root_dir / "variants").parent.as_posix()
|
73
|
-
|
74
|
-
def _search_variant_config_file(self, project_dir: Path) -> List[Path]:
|
75
|
-
"""
|
76
|
-
Finds all files called 'config.txt' in the variants directory
|
77
|
-
and returns a list with their paths.
|
78
|
-
"""
|
79
|
-
return list((project_dir / "variants").glob("**/config.txt"))
|
80
|
-
|
81
|
-
def find_variant_config(self, variant_name: str) -> Optional[VariantData]:
|
82
|
-
for variant in self.variant_configs:
|
83
|
-
if variant.name == variant_name:
|
84
|
-
return variant
|
85
|
-
return None
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
6
|
+
from py_app_dev.core.logging import logger
|
7
|
+
|
8
|
+
from kspl.kconfig import EditableConfigElement, KConfig
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class VariantViewData:
|
13
|
+
"""A variant is a set of configuration values for a KConfig model."""
|
14
|
+
|
15
|
+
name: str
|
16
|
+
config_dict: dict[str, Any]
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class VariantData:
|
21
|
+
name: str
|
22
|
+
config: KConfig
|
23
|
+
|
24
|
+
def find_element(self, element_name: str) -> EditableConfigElement | None:
|
25
|
+
return self.config.find_element(element_name)
|
26
|
+
|
27
|
+
|
28
|
+
class SPLKConfigData:
|
29
|
+
def __init__(self, project_root_dir: Path) -> None:
|
30
|
+
self.project_root_dir = project_root_dir.absolute()
|
31
|
+
variant_config_files = self._search_variant_config_file(self.project_root_dir)
|
32
|
+
if not self.kconfig_model_file.is_file():
|
33
|
+
raise UserNotificationException(f"File {self.kconfig_model_file} does not exist.")
|
34
|
+
self.model = KConfig(self.kconfig_model_file)
|
35
|
+
if variant_config_files:
|
36
|
+
self.variant_configs: list[VariantData] = [VariantData(self._get_variant_name(file), KConfig(self.kconfig_model_file, file)) for file in variant_config_files]
|
37
|
+
else:
|
38
|
+
self.variant_configs = [VariantData("Default", self.model)]
|
39
|
+
self.logger = logger.bind()
|
40
|
+
|
41
|
+
@property
|
42
|
+
def kconfig_model_file(self) -> Path:
|
43
|
+
return self.project_root_dir / "KConfig"
|
44
|
+
|
45
|
+
def get_elements(self) -> list[EditableConfigElement]:
|
46
|
+
return self.model.elements
|
47
|
+
|
48
|
+
def get_variants(self) -> list[VariantViewData]:
|
49
|
+
variants = []
|
50
|
+
|
51
|
+
for variant in self.variant_configs:
|
52
|
+
variants.append(
|
53
|
+
VariantViewData(
|
54
|
+
variant.name,
|
55
|
+
{config_elem.name: config_elem.value for config_elem in variant.config.elements if not config_elem.is_menu},
|
56
|
+
)
|
57
|
+
)
|
58
|
+
return variants
|
59
|
+
|
60
|
+
def _get_variant_name(self, file: Path) -> str:
|
61
|
+
return file.relative_to(self.project_root_dir / "variants").parent.as_posix()
|
62
|
+
|
63
|
+
def _search_variant_config_file(self, project_dir: Path) -> list[Path]:
|
64
|
+
"""Finds all files called 'config.txt' in the variants directory and returns a list with their paths."""
|
65
|
+
return list((project_dir / "variants").glob("**/config.txt"))
|
66
|
+
|
67
|
+
def find_variant_config(self, variant_name: str) -> VariantData | None:
|
68
|
+
for variant in self.variant_configs:
|
69
|
+
if variant.name == variant_name:
|
70
|
+
return variant
|
71
|
+
return None
|
kspl/edit.py
CHANGED
@@ -1,84 +1,71 @@
|
|
1
|
-
from argparse import ArgumentParser, Namespace
|
2
|
-
from dataclasses import dataclass, field
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import
|
5
|
-
|
6
|
-
from mashumaro import DataClassDictMixin
|
7
|
-
from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
|
8
|
-
from py_app_dev.core.logging import logger, time_it
|
9
|
-
|
10
|
-
from .config_slurper import SPLKConfigData
|
11
|
-
from .kconfig import KConfig
|
12
|
-
|
13
|
-
|
14
|
-
@dataclass
|
15
|
-
class EditCommandConfig(DataClassDictMixin):
|
16
|
-
project_dir: Path = field(
|
17
|
-
default=Path(".").absolute(),
|
18
|
-
metadata={
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
break
|
73
|
-
except (ValueError, IndexError):
|
74
|
-
self.logger.error(
|
75
|
-
"Invalid input. Please try again or press CTRL+C to abort."
|
76
|
-
)
|
77
|
-
# In case of KeyboardInterrupt (CTRL+C) we want to abort
|
78
|
-
except KeyboardInterrupt:
|
79
|
-
self.logger.warning("Aborted by user.")
|
80
|
-
break
|
81
|
-
return selected_variant
|
82
|
-
|
83
|
-
def _register_arguments(self, parser: ArgumentParser) -> None:
|
84
|
-
register_arguments_for_config_dataclass(parser, EditCommandConfig)
|
1
|
+
from argparse import ArgumentParser, Namespace
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from mashumaro import DataClassDictMixin
|
7
|
+
from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
|
8
|
+
from py_app_dev.core.logging import logger, time_it
|
9
|
+
|
10
|
+
from .config_slurper import SPLKConfigData
|
11
|
+
from .kconfig import KConfig
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class EditCommandConfig(DataClassDictMixin):
|
16
|
+
project_dir: Path = field(
|
17
|
+
default=Path(".").absolute(),
|
18
|
+
metadata={"help": "Project root directory. Defaults to the current directory if not specified."},
|
19
|
+
)
|
20
|
+
kconfig_model_file: Optional[Path] = field(default=None, metadata={"help": "KConfig model file (KConfig)."})
|
21
|
+
kconfig_config_file: Optional[Path] = field(default=None, metadata={"help": "KConfig user configuration file (config.txt)."})
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def from_namespace(cls, namespace: Namespace) -> "EditCommandConfig":
|
25
|
+
return cls.from_dict(vars(namespace))
|
26
|
+
|
27
|
+
|
28
|
+
class EditCommand(Command):
|
29
|
+
def __init__(self) -> None:
|
30
|
+
super().__init__("edit", "Edit KConfig configuration.")
|
31
|
+
self.logger = logger.bind()
|
32
|
+
|
33
|
+
@time_it("Build")
|
34
|
+
def run(self, args: Namespace) -> int:
|
35
|
+
self.logger.info(f"Running {self.name} with args {args}")
|
36
|
+
cmd_config = EditCommandConfig.from_namespace(args)
|
37
|
+
if cmd_config.kconfig_model_file is None:
|
38
|
+
kconfig_data = SPLKConfigData(cmd_config.project_dir)
|
39
|
+
variants = kconfig_data.get_variants()
|
40
|
+
variant_names = [variant.name for variant in variants]
|
41
|
+
selected_variant = self._select_variant(variant_names)
|
42
|
+
if selected_variant is not None:
|
43
|
+
variant_data = kconfig_data.find_variant_config(selected_variant)
|
44
|
+
if variant_data is not None:
|
45
|
+
variant_data.config.menu_config()
|
46
|
+
else:
|
47
|
+
self.logger.error(f"Variant {selected_variant} not found.")
|
48
|
+
else:
|
49
|
+
KConfig(cmd_config.kconfig_model_file, cmd_config.kconfig_config_file).menu_config()
|
50
|
+
return 0
|
51
|
+
|
52
|
+
def _select_variant(self, variant_names: list[str]) -> str | None:
|
53
|
+
"""Print the list of variants to choose from and let the user choose one."""
|
54
|
+
selected_variant = None
|
55
|
+
self.logger.info("Select a variant:")
|
56
|
+
for index, variant_name in enumerate(variant_names):
|
57
|
+
self.logger.info(f" [{index + 1}] {variant_name}")
|
58
|
+
while True:
|
59
|
+
try:
|
60
|
+
selected_variant = variant_names[int(input(f"Select a variant (1-{len(variant_names)}): ")) - 1]
|
61
|
+
break
|
62
|
+
except (ValueError, IndexError):
|
63
|
+
self.logger.error("Invalid input. Please try again or press CTRL+C to abort.")
|
64
|
+
# In case of KeyboardInterrupt (CTRL+C) we want to abort
|
65
|
+
except KeyboardInterrupt:
|
66
|
+
self.logger.warning("Aborted by user.")
|
67
|
+
break
|
68
|
+
return selected_variant
|
69
|
+
|
70
|
+
def _register_arguments(self, parser: ArgumentParser) -> None:
|
71
|
+
register_arguments_for_config_dataclass(parser, EditCommandConfig)
|
kspl/generate.py
CHANGED
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
|
|
3
3
|
from argparse import ArgumentParser, Namespace
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import
|
6
|
+
from typing import Optional
|
7
7
|
|
8
8
|
import kconfiglib
|
9
9
|
from mashumaro import DataClassDictMixin
|
@@ -14,9 +14,7 @@ from kspl.kconfig import ConfigElementType, ConfigurationData, KConfig, TriState
|
|
14
14
|
|
15
15
|
|
16
16
|
class GeneratedFile:
|
17
|
-
def __init__(
|
18
|
-
self, path: Path, content: str = "", skip_writing_if_unchanged: bool = False
|
19
|
-
) -> None:
|
17
|
+
def __init__(self, path: Path, content: str = "", skip_writing_if_unchanged: bool = False) -> None:
|
20
18
|
self.path = path
|
21
19
|
|
22
20
|
self.content = content
|
@@ -27,50 +25,43 @@ class GeneratedFile:
|
|
27
25
|
return self.content
|
28
26
|
|
29
27
|
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
|
-
|
28
|
+
"""Only write to file if the content has changed. The directory of the file is created if it does not exist."""
|
33
29
|
content = self.to_string()
|
34
30
|
|
35
|
-
if (
|
36
|
-
not self.path.exists()
|
37
|
-
or not self.skip_writing_if_unchanged
|
38
|
-
or self.path.read_text() != content
|
39
|
-
):
|
31
|
+
if not self.path.exists() or not self.skip_writing_if_unchanged or self.path.read_text() != content:
|
40
32
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
41
33
|
self.path.write_text(content)
|
42
34
|
|
43
35
|
|
44
36
|
class FileWriter(ABC):
|
45
|
-
"""
|
37
|
+
"""Writes the ConfigurationData to a file."""
|
46
38
|
|
47
39
|
def __init__(self, output_file: Path):
|
48
40
|
self.output_file = output_file
|
49
41
|
|
50
42
|
def write(self, configuration_data: ConfigurationData) -> None:
|
51
|
-
"""
|
52
|
-
The file shall not be modified if the content is the same as the existing one"""
|
43
|
+
"""Writes the ConfigurationData to a file. The file shall not be modified if the content is the same as the existing one."""
|
53
44
|
content = self.generate_content(configuration_data)
|
54
|
-
GeneratedFile(
|
55
|
-
self.output_file, content, skip_writing_if_unchanged=True
|
56
|
-
).to_file()
|
45
|
+
GeneratedFile(self.output_file, content, skip_writing_if_unchanged=True).to_file()
|
57
46
|
|
58
47
|
@abstractmethod
|
59
48
|
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
60
|
-
"""- generates the content of the file from the ConfigurationData"""
|
49
|
+
"""- generates the content of the file from the ConfigurationData."""
|
61
50
|
|
62
51
|
|
63
52
|
class HeaderWriter(FileWriter):
|
64
|
-
"""Writes the ConfigurationData as pre-processor defines in a C Header file"""
|
53
|
+
"""Writes the ConfigurationData as pre-processor defines in a C Header file."""
|
65
54
|
|
66
55
|
config_prefix = "CONFIG_" # Prefix for all configuration defines
|
67
56
|
|
68
57
|
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
69
|
-
"""
|
58
|
+
"""
|
59
|
+
Does exactly what the kconfiglib.write_autoconf() method does.
|
60
|
+
|
70
61
|
We had to implemented here because we refactor the file writers to use the ConfigurationData
|
71
62
|
instead of the KConfig configuration. ConfigurationData has variable substitution already done.
|
72
63
|
"""
|
73
|
-
result:
|
64
|
+
result: list[str] = [
|
74
65
|
"/** @file */",
|
75
66
|
"#ifndef __autoconf_h__",
|
76
67
|
"#define __autoconf_h__",
|
@@ -91,17 +82,13 @@ class HeaderWriter(FileWriter):
|
|
91
82
|
)
|
92
83
|
elif val == TriState.M:
|
93
84
|
add_define(
|
94
|
-
"#define {}{}_MODULE 1"
|
95
|
-
self.config_prefix, element.name
|
96
|
-
),
|
85
|
+
f"#define {self.config_prefix}{element.name}_MODULE 1",
|
97
86
|
element.name,
|
98
87
|
)
|
99
88
|
|
100
89
|
elif element.type is ConfigElementType.STRING:
|
101
90
|
add_define(
|
102
|
-
'#define {}{} "{}"'
|
103
|
-
self.config_prefix, element.name, kconfiglib.escape(val)
|
104
|
-
),
|
91
|
+
f'#define {self.config_prefix}{element.name} "{kconfiglib.escape(val)}"',
|
105
92
|
element.name,
|
106
93
|
)
|
107
94
|
|
@@ -117,7 +104,7 @@ class HeaderWriter(FileWriter):
|
|
117
104
|
|
118
105
|
|
119
106
|
class JsonWriter(FileWriter):
|
120
|
-
"""Writes the ConfigurationData in json format"""
|
107
|
+
"""Writes the ConfigurationData in json format."""
|
121
108
|
|
122
109
|
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
123
110
|
result = {}
|
@@ -130,10 +117,10 @@ class JsonWriter(FileWriter):
|
|
130
117
|
|
131
118
|
|
132
119
|
class CMakeWriter(FileWriter):
|
133
|
-
"""Writes the ConfigurationData as CMake variables"""
|
120
|
+
"""Writes the ConfigurationData as CMake variables."""
|
134
121
|
|
135
122
|
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
136
|
-
result:
|
123
|
+
result: list[str] = []
|
137
124
|
add = result.append
|
138
125
|
for element in configuration_data.elements:
|
139
126
|
val = element.value
|
@@ -147,12 +134,8 @@ class CMakeWriter(FileWriter):
|
|
147
134
|
@dataclass
|
148
135
|
class GenerateCommandConfig(DataClassDictMixin):
|
149
136
|
kconfig_model_file: Path = field(metadata={"help": "KConfig model file (KConfig)."})
|
150
|
-
kconfig_config_file: Optional[Path] = field(
|
151
|
-
|
152
|
-
)
|
153
|
-
out_header_file: Optional[Path] = field(
|
154
|
-
default=None, metadata={"help": "File to write the configuration as C header."}
|
155
|
-
)
|
137
|
+
kconfig_config_file: Optional[Path] = field(default=None, metadata={"help": "KConfig user configuration file (config.txt)."})
|
138
|
+
out_header_file: Optional[Path] = field(default=None, metadata={"help": "File to write the configuration as C header."})
|
156
139
|
out_json_file: Optional[Path] = field(
|
157
140
|
default=None,
|
158
141
|
metadata={"help": "File to write the configuration in JSON format."},
|
@@ -169,18 +152,14 @@ class GenerateCommandConfig(DataClassDictMixin):
|
|
169
152
|
|
170
153
|
class GenerateCommand(Command):
|
171
154
|
def __init__(self) -> None:
|
172
|
-
super().__init__(
|
173
|
-
"generate", "Generate the KConfig configuration in the specified formats."
|
174
|
-
)
|
155
|
+
super().__init__("generate", "Generate the KConfig configuration in the specified formats.")
|
175
156
|
self.logger = logger.bind()
|
176
157
|
|
177
158
|
@time_it("Build")
|
178
159
|
def run(self, args: Namespace) -> int:
|
179
160
|
self.logger.info(f"Running {self.name} with args {args}")
|
180
161
|
cmd_config = GenerateCommandConfig.from_namespace(args)
|
181
|
-
config = KConfig(
|
182
|
-
cmd_config.kconfig_model_file, cmd_config.kconfig_config_file
|
183
|
-
).collect_config_data()
|
162
|
+
config = KConfig(cmd_config.kconfig_model_file, cmd_config.kconfig_config_file).collect_config_data()
|
184
163
|
|
185
164
|
if cmd_config.out_header_file:
|
186
165
|
HeaderWriter(cmd_config.out_header_file).write(config)
|