flamapy 2.5.0.dev0__tar.gz → 2.6.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.
Files changed (30) hide show
  1. flamapy-2.6.0/PKG-INFO +36 -0
  2. flamapy-2.6.0/README.md +13 -0
  3. flamapy-2.6.0/flamapy/commands/__init__.py +254 -0
  4. flamapy-2.6.0/flamapy/interfaces/python/_stub.py +76 -0
  5. flamapy-2.6.0/flamapy/interfaces/python/flamapy_feature_model.py +214 -0
  6. flamapy-2.6.0/flamapy/interfaces/python/flamapy_feature_model.pyi +63 -0
  7. flamapy-2.6.0/flamapy.egg-info/PKG-INFO +36 -0
  8. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/flamapy.egg-info/SOURCES.txt +6 -1
  9. flamapy-2.6.0/flamapy.egg-info/requires.txt +12 -0
  10. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/pyproject.toml +16 -8
  11. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/tests/test_basic_units.py +18 -1
  12. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/tests/test_cli_basic_units.py +27 -0
  13. flamapy-2.6.0/tests/test_descriptors_cover_facade.py +154 -0
  14. flamapy-2.6.0/tests/test_documentation_examples.py +356 -0
  15. flamapy-2.6.0/tests/test_optimization_and_sampling.py +124 -0
  16. flamapy-2.5.0.dev0/PKG-INFO +0 -104
  17. flamapy-2.5.0.dev0/README.md +0 -82
  18. flamapy-2.5.0.dev0/flamapy/commands/__init__.py +0 -179
  19. flamapy-2.5.0.dev0/flamapy/interfaces/python/flamapy_feature_model.py +0 -381
  20. flamapy-2.5.0.dev0/flamapy.egg-info/PKG-INFO +0 -104
  21. flamapy-2.5.0.dev0/flamapy.egg-info/requires.txt +0 -11
  22. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/LICENSE +0 -0
  23. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/flamapy/interfaces/python/__init__.py +0 -0
  24. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/flamapy.egg-info/dependency_links.txt +0 -0
  25. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/flamapy.egg-info/entry_points.txt +0 -0
  26. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/flamapy.egg-info/top_level.txt +0 -0
  27. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/setup.cfg +0 -0
  28. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/setup.py +0 -0
  29. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/tests/test_fama_suite_bdd.py +0 -0
  30. {flamapy-2.5.0.dev0 → flamapy-2.6.0}/tests/test_fama_suite_sat.py +0 -0
flamapy-2.6.0/PKG-INFO ADDED
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy
3
+ Version: 2.6.0
4
+ Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
5
+ Author-email: Flamapy <flamapy@us.es>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flamapy-fw~=2.6.0
12
+ Requires-Dist: flamapy-fm~=2.6.0
13
+ Requires-Dist: flamapy-sat~=2.6.0
14
+ Requires-Dist: flamapy-bdd~=2.6.0
15
+ Requires-Dist: flamapy-z3~=2.6.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: pytest-mock; extra == "dev"
19
+ Requires-Dist: prospector; extra == "dev"
20
+ Requires-Dist: mypy; extra == "dev"
21
+ Requires-Dist: coverage; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # flamapy
25
+
26
+ The flamapy feature-model distribution: a meta-package that installs the
27
+ framework and the feature-model analysis plugins, and provides the command-line
28
+ interface and the easy-to-use Python facade (`FLAMAFeatureModel`).
29
+
30
+ **Documentation:** https://docs.flamapy.org
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install flamapy
36
+ ```
@@ -0,0 +1,13 @@
1
+ # flamapy
2
+
3
+ The flamapy feature-model distribution: a meta-package that installs the
4
+ framework and the feature-model analysis plugins, and provides the command-line
5
+ interface and the easy-to-use Python facade (`FLAMAFeatureModel`).
6
+
7
+ **Documentation:** https://docs.flamapy.org
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install flamapy
13
+ ```
@@ -0,0 +1,254 @@
1
+ import argparse
2
+ import os
3
+ import shutil
4
+ import sys
5
+ import textwrap
6
+ from functools import wraps
7
+ import inspect
8
+ from pathlib import Path
9
+ from shutil import copytree
10
+ from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
11
+ from types import FunctionType
12
+
13
+ from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
14
+
15
+ # List to store registered commands and their arguments
16
+ MANUAL_COMMANDS: List[Tuple[str, str, FunctionType, Tuple[Any, ...]]] = []
17
+
18
+ # Section headings for the no-args listing, keyed by the operations' default backend
19
+ # (None => the operation runs directly on the feature model tree). Backends not listed
20
+ # here (future plugins) get their own section headed by the backend name.
21
+ BACKEND_SECTIONS: List[Tuple[Optional[str], str]] = [
22
+ (None, "Feature model operations (no solver required)"),
23
+ ("sat", "SAT-based operations (pysat metamodel)"),
24
+ ("bdd", "BDD-based operations (bdd metamodel)"),
25
+ ("z3", "SMT-based operations (z3 metamodel)"),
26
+ ("pysat_diagnosis", "Diagnosis operations (pysat_diagnosis metamodel)"),
27
+ ]
28
+
29
+
30
+ def _summary(doc: str) -> str:
31
+ """One-line summary of a docstring: whitespace collapsed, first sentence only."""
32
+ collapsed = " ".join(doc.split())
33
+ if not collapsed:
34
+ return "(no description)"
35
+ end = collapsed.find(". ")
36
+ return collapsed[: end + 1] if end != -1 else collapsed
37
+
38
+
39
+ def command(name, description, *args): # type: ignore
40
+ def decorator(func): # type: ignore
41
+ MANUAL_COMMANDS.append((name, description, func, args))
42
+
43
+ @wraps(func)
44
+ def wrapper(*func_args, **func_kwargs): # type: ignore
45
+ return func(*func_args, **func_kwargs)
46
+
47
+ return wrapper
48
+
49
+ return decorator
50
+
51
+
52
+ def extract_commands(
53
+ cls: type,
54
+ ) -> List[Tuple[str, str, FunctionType, List[inspect.Parameter], Any]]:
55
+ commands = []
56
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
57
+ if name.startswith("_"):
58
+ continue
59
+ if getattr(method, "_facade_kind", "operation") != "operation":
60
+ continue # producers/transformers return models; Python-API only for now
61
+ docstring: Optional[str] = method.__doc__
62
+ signature = inspect.signature(method)
63
+ # Exclude 'self' from parameters
64
+ parameters = list(signature.parameters.values())[1:] # Skip 'self'
65
+ descriptor = getattr(method, "_facade_descriptor", None)
66
+ commands.append((name, docstring or "", method, parameters, descriptor))
67
+ return commands
68
+
69
+
70
+ @command(
71
+ "generate_plugin",
72
+ "Generates a new plugin skeleton to implement your custom operations. "
73
+ "Run it from the path of the flamapy src directory.",
74
+ ("name", str, "The plugin's name"),
75
+ ("extension", str, "The extension to be registered with the flamapy ecosystem"),
76
+ ("path", str, "The path to generate it"),
77
+ )
78
+ def generate_plugin(args): # type: ignore
79
+ name = args.name
80
+ ext = args.extension
81
+ dst = args.path
82
+ src = "skel_metamodel/"
83
+
84
+ # Check DST exist
85
+ if not os.path.isdir(dst):
86
+ print(f"Folder {dst} not exist")
87
+ sys.exit()
88
+
89
+ # Check DST is empty
90
+ if len(os.listdir(dst)) != 0:
91
+ print(f"Folder {dst} is not empty")
92
+ sys.exit()
93
+
94
+ # Check DST has permissions to WRITE
95
+ if not os.access(dst, os.W_OK):
96
+ print(f"Folder {dst} has not write permissions")
97
+ sys.exit()
98
+
99
+ # Generating structure
100
+ print("Generating structure ...")
101
+
102
+ copy_files = copytree(src, dst, dirs_exist_ok=True)
103
+
104
+ for copy_file in Path(copy_files).glob("**/*"):
105
+ if copy_file.is_dir():
106
+ continue
107
+ with open(copy_file, "r", encoding="utf-8") as file:
108
+ lines = file.readlines()
109
+ with open(copy_file, "w", encoding="utf-8") as filewrite:
110
+ for line in lines:
111
+ out_line = line.replace("__NAME__", name.capitalize()).replace("__EXT__", ext)
112
+ filewrite.write(out_line)
113
+
114
+ os.rename(
115
+ os.path.join(dst, "flamapy/metamodels/__NAME__"),
116
+ os.path.join(dst, f"flamapy/metamodels/{name}"),
117
+ )
118
+ print("Plugin generated!")
119
+
120
+
121
+ def resolve_annotation_type(annotation: Any) -> type:
122
+ """Map a parameter annotation to a concrete type usable as an argparse 'type'.
123
+
124
+ Unwraps Optional[X]/Union[X, None] to X; falls back to str for anything
125
+ that is not a plain class (argparse requires a callable).
126
+ """
127
+ if annotation is inspect.Parameter.empty:
128
+ return str
129
+ if get_origin(annotation) is Union:
130
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
131
+ annotation = args[0] if args else str
132
+ return annotation if isinstance(annotation, type) else str
133
+
134
+
135
+ def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
136
+ for name, docstring, method, parameters, _ in dynamic_commands:
137
+ subparser = subparsers.add_parser(name, help=_summary(docstring), description=docstring)
138
+ subparser.add_argument("model_path", type=str, help="Path to the feature model file")
139
+ for param in parameters:
140
+ arg_name = param.name
141
+ if arg_name not in ["model_path"]: # Avoid duplicates
142
+ arg_type = resolve_annotation_type(param.annotation)
143
+ if param.default == param.empty: # Positional argument
144
+ subparser.add_argument(arg_name, type=arg_type, help=arg_type.__name__)
145
+ elif arg_type is bool: # Boolean flag (type=bool would parse 'False' as True)
146
+ subparser.add_argument(
147
+ f"--{arg_name}",
148
+ action=argparse.BooleanOptionalAction,
149
+ default=param.default,
150
+ help="Optional bool",
151
+ )
152
+ else: # Optional argument
153
+ subparser.add_argument(
154
+ f"--{arg_name}",
155
+ type=arg_type,
156
+ default=param.default,
157
+ help=f"Optional {arg_type.__name__}",
158
+ )
159
+ subparser.set_defaults(func=method, method_name=name, parameters=parameters)
160
+
161
+
162
+ def setup_manual_commands(subparsers, manual_commands): # type: ignore
163
+ for name, description, func, args in manual_commands:
164
+ subparser = subparsers.add_parser(name, help=_summary(description), description=description)
165
+ for arg in args:
166
+ arg_name, arg_type, arg_help = arg
167
+ subparser.add_argument(arg_name, type=arg_type, help=arg_help)
168
+ subparser.set_defaults(func=func)
169
+
170
+
171
+ def execute_command(args: argparse.Namespace) -> None:
172
+ try:
173
+ if hasattr(args, "method_name"):
174
+ cls_instance = FLAMAFeatureModel(args.model_path)
175
+ method_parameters = [param.name for param in args.parameters]
176
+ command_args = {k: v for k, v in vars(args).items() if k in method_parameters}
177
+ method = getattr(cls_instance, args.method_name)
178
+ result = method(**command_args)
179
+ if result is not None:
180
+ print(result)
181
+ else:
182
+ func = args.func
183
+ command_args = {k: v for k, v in vars(args).items() if k != "func"}
184
+ result = func(args)
185
+ if result is not None:
186
+ print(result)
187
+ except FileNotFoundError as fnf_error:
188
+ print(f"File not found error: {fnf_error}")
189
+ except TypeError as type_error:
190
+ print(f"Type error: {type_error}")
191
+ except ValueError as value_error:
192
+ print(f"Value error: {value_error}")
193
+ except KeyError as key_error:
194
+ print(f"Key error: {key_error}")
195
+ except AttributeError as attr_error:
196
+ print(f"Attribute error: {attr_error}")
197
+
198
+
199
+ def _print_section(title: str, rows: List[Tuple[str, str]], column: int, width: int) -> None:
200
+ print(f"{title}:")
201
+ for label, summary in sorted(rows):
202
+ lead = f" {label:<{column}} "
203
+ wrapped = textwrap.wrap(summary, width=max(width - len(lead), 20)) or [""]
204
+ print(lead + wrapped[0])
205
+ for line in wrapped[1:]:
206
+ print(" " * len(lead) + line)
207
+
208
+
209
+ def print_command_overview(dynamic_commands) -> None: # type: ignore
210
+ width = min(shutil.get_terminal_size().columns, 100)
211
+ rows_by_backend: dict[Optional[str], List[Tuple[str, str]]] = {}
212
+ for name, docstring, _, _, descriptor in dynamic_commands:
213
+ backend = getattr(descriptor, "default_backend", None)
214
+ label = name + (" *" if getattr(descriptor, "selectable_backend", False) else "")
215
+ rows_by_backend.setdefault(backend, []).append((label, _summary(docstring)))
216
+ manual_rows = [(name, _summary(description)) for name, description, _, _ in MANUAL_COMMANDS]
217
+
218
+ all_rows = [row for rows in rows_by_backend.values() for row in rows] + manual_rows
219
+ column = max((len(label) for label, _ in all_rows), default=0)
220
+
221
+ sections = list(BACKEND_SECTIONS)
222
+ known = {backend for backend, _ in sections}
223
+ sections += [(backend, f"Operations on the {backend} backend")
224
+ for backend in sorted(rows_by_backend.keys() - known, key=str)]
225
+ first = True
226
+ for backend, title in sections:
227
+ rows = rows_by_backend.get(backend)
228
+ if not rows:
229
+ continue
230
+ if not first:
231
+ print()
232
+ _print_section(title, rows, column, width)
233
+ first = False
234
+ print()
235
+ _print_section("Framework developers operations", manual_rows, column, width)
236
+ print()
237
+ print("Operations marked with * accept a --backend option to pick the solver.")
238
+ print("Run 'flamapy <command> --help' for details, or 'flamapy --help' for the full list.")
239
+
240
+
241
+ def flamapy_cli() -> None:
242
+ parser = argparse.ArgumentParser(description="FLAMA Feature Model CLI")
243
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND", title="commands")
244
+
245
+ dynamic_commands = extract_commands(FLAMAFeatureModel)
246
+ setup_dynamic_commands(subparsers, dynamic_commands)
247
+ setup_manual_commands(subparsers, MANUAL_COMMANDS)
248
+
249
+ args = parser.parse_args()
250
+
251
+ if args.command:
252
+ execute_command(args)
253
+ else:
254
+ print_command_overview(dynamic_commands)
@@ -0,0 +1,76 @@
1
+ """Generator for ``flamapy_feature_model.pyi`` — static typing for the descriptor-generated facade.
2
+
3
+ The facade synthesises its operation methods at runtime, so type checkers and IDEs cannot see them.
4
+ This module renders a ``.pyi`` stub from the same OperationDescriptors, giving autocomplete and
5
+ mypy the real signatures + return types. Run ``python -m flamapy.interfaces.python._stub`` to
6
+ regenerate; a test asserts the shipped stub matches this output so it never drifts.
7
+ """
8
+ from typing import Any
9
+
10
+ from flamapy.core.discover import DiscoverMetamodels
11
+ from flamapy.core.operations import Input, OperationDescriptor
12
+
13
+ _HEADER = '''\
14
+ # Auto-generated by flamapy.interfaces.python._stub — do not edit by hand.
15
+ # Regenerate with: python -m flamapy.interfaces.python._stub
16
+ from enum import Enum
17
+ from typing import Any, Dict, List, Optional, Tuple, Union
18
+
19
+ from flamapy.metamodels.configuration_metamodel.models import Configuration
20
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel
21
+
22
+
23
+ class Backend(str, Enum):
24
+ SAT = ...
25
+ BDD = ...
26
+ Z3 = ...
27
+ SHARPSAT = ...
28
+
29
+
30
+ class FLAMAFeatureModel:
31
+ model_path: str
32
+ fm_model: FeatureModel
33
+ backend: Optional[Backend]
34
+ def __init__(self, model_path: str, backend: Optional[str] = ...) -> None: ...
35
+ '''
36
+
37
+
38
+ def _input_annotation(input_spec: Input) -> str:
39
+ base = 'Any' if input_spec.type is Any else getattr(input_spec.type, '__name__', 'Any')
40
+ if not input_spec.required and input_spec.default is None:
41
+ return f'Optional[{base}]'
42
+ return base
43
+
44
+
45
+ def _method_signature(descriptor: OperationDescriptor) -> str:
46
+ params = [] if descriptor.kind == 'producer' else ['self']
47
+ for input_spec in descriptor.inputs:
48
+ annotation = _input_annotation(input_spec)
49
+ if input_spec.required:
50
+ params.append(f'{input_spec.name}: {annotation}')
51
+ else:
52
+ params.append(f'{input_spec.name}: {annotation} = ...')
53
+ if descriptor.selectable_backend:
54
+ params.append('backend: Optional[str] = ...')
55
+ return ', '.join(params)
56
+
57
+
58
+ def render_stub(operations: 'dict[str, OperationDescriptor] | None' = None) -> str:
59
+ if operations is None:
60
+ operations = DiscoverMetamodels().available_operations()
61
+ lines = [_HEADER.rstrip('\n')]
62
+ for name in sorted(operations):
63
+ descriptor = operations[name]
64
+ if descriptor.kind == 'producer':
65
+ lines.append(' @staticmethod')
66
+ lines.append(
67
+ f' def {name}({_method_signature(descriptor)}) -> {descriptor.returns}: ...')
68
+ return '\n'.join(lines) + '\n'
69
+
70
+
71
+ if __name__ == '__main__':
72
+ import os
73
+ target = os.path.join(os.path.dirname(__file__), 'flamapy_feature_model.pyi')
74
+ with open(target, 'w', encoding='utf-8') as handle:
75
+ handle.write(render_stub())
76
+ print(f'wrote {target}')
@@ -0,0 +1,214 @@
1
+ """Descriptor-driven facade over the flamapy analysis operations.
2
+
3
+ The public operation methods are not written by hand. At import time :func:`_install_operations`
4
+ reads the :class:`OperationDescriptor`s discovered from the installed plugins
5
+ (:meth:`DiscoverMetamodels.available_operations`) and synthesises one method per operation, each
6
+ with a real signature and ``__doc__`` so the CLI and REST layers can keep introspecting the class.
7
+ Dropping a new operation into a plugin therefore extends the Python facade, the CLI and the REST API
8
+ for free. Backend resolution is dynamic (by fm-to-backend transformation extension), so there is no
9
+ per-operation or per-backend code here. Static typing is provided by a generated ``.pyi`` stub.
10
+ """
11
+ import inspect
12
+ import logging
13
+ from enum import Enum
14
+ from typing import Any, Dict, Optional, Union
15
+
16
+ from flamapy.core.discover import DiscoverMetamodels
17
+ from flamapy.core.exceptions import FlamaException
18
+ from flamapy.core.models import VariabilityModel
19
+ from flamapy.core.operations import OperationDescriptor
20
+ from flamapy.metamodels.configuration_metamodel.models import Configuration
21
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class Backend(str, Enum):
27
+ """Analysis backend identifier.
28
+
29
+ Retained so the REST layer can offer a backend dropdown; the facade itself no longer switches on
30
+ it — it resolves the backend dynamically to a transformation extension.
31
+ """
32
+
33
+ SAT = "sat"
34
+ BDD = "bdd"
35
+ Z3 = "z3"
36
+ SHARPSAT = "sharpsat"
37
+
38
+
39
+ # Facade backend name -> fm-to-backend transformation extension (the only non-identity mapping).
40
+ _BACKEND_ALIAS = {"sat": "pysat"}
41
+
42
+
43
+ class FLAMAFeatureModel:
44
+ """A feature model together with the analysis operations discovered from the installed plugins.
45
+
46
+ Operation methods are generated from their OperationDescriptors (see the module docstring); call
47
+ them as normal methods, e.g. ``FLAMAFeatureModel(path).satisfiable()``.
48
+ """
49
+
50
+ def __init__(self, model_path: str, backend: Optional[str] = None) -> None:
51
+ """``model_path`` is the filesystem path to the model (UVL, FaMaXML or FeatureIDE).
52
+
53
+ ``backend`` optionally sets a workspace-wide default backend ("sat", "bdd" or "z3") for
54
+ every operation that supports more than one. When left as ``None`` each operation keeps its
55
+ historical default, so existing code behaves exactly as before.
56
+ """
57
+ self.model_path = model_path
58
+ self.discover_metamodel = DiscoverMetamodels()
59
+ self.fm_model = self._read(model_path)
60
+ self.backend = Backend(backend) if backend is not None else None
61
+ self._backend_models: Dict[str, VariabilityModel] = {}
62
+
63
+ def _read(self, model_path: str) -> FeatureModel:
64
+ return self.discover_metamodel.use_transformation_t2m(model_path, "fm")
65
+
66
+ def _as_configuration(
67
+ self, configuration: Union[str, Dict[str, Any], Configuration]
68
+ ) -> Configuration:
69
+ """Resolve a configuration argument into a Configuration model.
70
+
71
+ Accepts a path to a configuration file (read via the configuration transformation), a
72
+ ``{feature: value}`` mapping, or an already-built Configuration.
73
+ """
74
+ if isinstance(configuration, Configuration):
75
+ return configuration
76
+ if isinstance(configuration, dict):
77
+ return Configuration(configuration)
78
+ return self.discover_metamodel.use_transformation_t2m(configuration, "configuration")
79
+
80
+ def _model_for(
81
+ self, descriptor: OperationDescriptor, backend_arg: Optional[str]
82
+ ) -> VariabilityModel:
83
+ """Resolve the operation's target model, lazily transforming (and caching) fm->backend.
84
+
85
+ The backend is the per-call one > workspace default > the operation's default (for
86
+ backend-selectable ops), or simply the fixed default. When it resolves to ``None`` (an
87
+ fm-level op, or a selectable op with no backend requested) the operation runs on the feature
88
+ model directly. Otherwise the name is mapped to a transformation extension (only
89
+ ``sat -> pysat`` differs) and cached per extension.
90
+ """
91
+ if descriptor.selectable_backend:
92
+ resolved = backend_arg or self.backend or descriptor.default_backend
93
+ else:
94
+ resolved = descriptor.default_backend
95
+ if resolved is None:
96
+ return self.fm_model
97
+ name = resolved.value if isinstance(resolved, Backend) else str(resolved)
98
+ extension = _BACKEND_ALIAS.get(name, name)
99
+ if extension not in self._backend_models:
100
+ self._backend_models[extension] = self.discover_metamodel.use_transformation_m2m(
101
+ self.fm_model, extension)
102
+ return self._backend_models[extension]
103
+
104
+ def _run_operation(
105
+ self, descriptor: OperationDescriptor, call_kwargs: Dict[str, Any]
106
+ ) -> Any:
107
+ """Resolve model + operation, wire inputs, execute, and marshal the result.
108
+
109
+ Preserves the facade's historical contract: any :class:`FlamaException` is logged and the
110
+ call returns ``None``.
111
+ """
112
+ try:
113
+ backend_arg = call_kwargs.pop('backend', None)
114
+ model = self._model_for(descriptor, backend_arg)
115
+ operation = self.discover_metamodel.get_operation(model, descriptor.operation)
116
+ if descriptor.input_adapter is not None:
117
+ descriptor.input_adapter(operation, self, call_kwargs)
118
+ else:
119
+ for input_spec in descriptor.inputs:
120
+ value = call_kwargs.get(input_spec.name)
121
+ if value is None:
122
+ continue
123
+ if input_spec.kind == 'configuration':
124
+ value = self._as_configuration(value)
125
+ if input_spec.setter is not None:
126
+ getattr(operation, input_spec.setter)(value)
127
+ operation.execute(model)
128
+ result = operation.get_result()
129
+ if descriptor.result_adapter is not None:
130
+ result = descriptor.result_adapter(result)
131
+ return result
132
+ except FlamaException as exception:
133
+ logger.error("Error: %s", exception)
134
+ return None
135
+
136
+
137
+ # One discovery instance, reused to install the operations and to run producers (which have no
138
+ # per-facade instance to hold their own).
139
+ _DISCOVERY = DiscoverMetamodels()
140
+
141
+
142
+ def _run_producer(descriptor: OperationDescriptor, call_kwargs: Dict[str, Any]) -> Any:
143
+ """Run a 'producer' operation — one that *creates* a model instead of analysing one.
144
+
145
+ Producers take no input model, so the operation is instantiated directly and executed with no
146
+ model. Same FlamaException -> None contract as :meth:`FLAMAFeatureModel._run_operation`.
147
+ """
148
+ try:
149
+ operation_class = next(
150
+ (op for op in _DISCOVERY.get_operations() if op.__name__ == descriptor.operation), None)
151
+ if operation_class is None:
152
+ raise FlamaException(f"Operation '{descriptor.operation}' was not discovered")
153
+ operation = operation_class()
154
+ for input_spec in descriptor.inputs:
155
+ value = call_kwargs.get(input_spec.name)
156
+ if value is not None and input_spec.setter is not None:
157
+ getattr(operation, input_spec.setter)(value)
158
+ operation.execute(None)
159
+ result = operation.get_result()
160
+ if descriptor.result_adapter is not None:
161
+ result = descriptor.result_adapter(result)
162
+ return result
163
+ except FlamaException as exception:
164
+ logger.error("Error: %s", exception)
165
+ return None
166
+
167
+
168
+ def _make_operation_method(descriptor: OperationDescriptor) -> Any:
169
+ """Build a real callable from a descriptor.
170
+
171
+ Operations become instance methods ``(self, *inputs[, backend])`` dispatching via
172
+ ``_run_operation``; producers become plain functions ``(*inputs)`` (wrapped as a
173
+ ``staticmethod`` by the installer) dispatching via ``_run_producer``.
174
+ """
175
+ producer = descriptor.kind == 'producer'
176
+ parameters = [] if producer else [
177
+ inspect.Parameter('self', inspect.Parameter.POSITIONAL_OR_KEYWORD)]
178
+ for input_spec in descriptor.inputs:
179
+ default = inspect.Parameter.empty if input_spec.required else input_spec.default
180
+ parameters.append(inspect.Parameter(
181
+ input_spec.name, inspect.Parameter.POSITIONAL_OR_KEYWORD,
182
+ default=default, annotation=input_spec.type))
183
+ if descriptor.selectable_backend:
184
+ parameters.append(inspect.Parameter(
185
+ 'backend', inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None,
186
+ annotation=Optional[str]))
187
+ signature = inspect.Signature(parameters)
188
+
189
+ def method(*args: Any, **kwargs: Any) -> Any:
190
+ bound = signature.bind(*args, **kwargs)
191
+ bound.apply_defaults()
192
+ call_kwargs = dict(bound.arguments)
193
+ if producer:
194
+ return _run_producer(descriptor, call_kwargs)
195
+ instance = call_kwargs.pop('self')
196
+ return instance._run_operation(descriptor, call_kwargs)
197
+
198
+ method.__name__ = descriptor.name
199
+ method.__qualname__ = f'{FLAMAFeatureModel.__name__}.{descriptor.name}'
200
+ method.__doc__ = descriptor.doc
201
+ method.__signature__ = signature # type: ignore[attr-defined]
202
+ # Let the CLI/REST/tests tell analysis ops from producers/transformers without re-discovering.
203
+ setattr(method, '_facade_kind', descriptor.kind)
204
+ setattr(method, '_facade_descriptor', descriptor)
205
+ return method
206
+
207
+
208
+ def _install_operations(cls: type) -> None:
209
+ for name, descriptor in _DISCOVERY.available_operations().items():
210
+ method = _make_operation_method(descriptor)
211
+ setattr(cls, name, staticmethod(method) if descriptor.kind == 'producer' else method)
212
+
213
+
214
+ _install_operations(FLAMAFeatureModel)
@@ -0,0 +1,63 @@
1
+ # Auto-generated by flamapy.interfaces.python._stub — do not edit by hand.
2
+ # Regenerate with: python -m flamapy.interfaces.python._stub
3
+ from enum import Enum
4
+ from typing import Any, Dict, List, Optional, Tuple, Union
5
+
6
+ from flamapy.metamodels.configuration_metamodel.models import Configuration
7
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel
8
+
9
+
10
+ class Backend(str, Enum):
11
+ SAT = ...
12
+ BDD = ...
13
+ Z3 = ...
14
+ SHARPSAT = ...
15
+
16
+
17
+ class FLAMAFeatureModel:
18
+ model_path: str
19
+ fm_model: FeatureModel
20
+ backend: Optional[Backend]
21
+ def __init__(self, model_path: str, backend: Optional[str] = ...) -> None: ...
22
+ def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]: ...
23
+ def atomic_sets(self) -> Union[None, List[List[Any]]]: ...
24
+ def attribute_optimization(self, objectives: Any, backend: Optional[str] = ...) -> Union[None, List[Configuration]]: ...
25
+ def average_branching_factor(self) -> Union[None, float]: ...
26
+ def backbone(self) -> Union[None, Dict[str, List[Any]]]: ...
27
+ def commonality(self, configuration_path: str) -> Union[None, float]: ...
28
+ def configuration_conflict(self, configuration_path: str) -> Union[None, List[Any]]: ...
29
+ def configuration_repair(self, configuration_path: str) -> Union[None, List[Any]]: ...
30
+ def configurations(self, backend: Optional[str] = ...) -> Union[None, List[Configuration]]: ...
31
+ def configurations_number(self, backend: Optional[str] = ...) -> Union[None, int]: ...
32
+ def configurations_with_n_features(self, n: int) -> Union[None, List[Configuration]]: ...
33
+ def conflict(self, configuration_path: str, test_case_path: Optional[str] = ..., max_conflicts: Optional[int] = ...) -> Union[None, List[str]]: ...
34
+ def core_features(self, backend: Optional[str] = ...) -> Union[None, List[str]]: ...
35
+ def count_leafs(self) -> Union[None, int]: ...
36
+ def dead_features(self, backend: Optional[str] = ...) -> Union[None, List[str]]: ...
37
+ def diagnosis(self, configuration_path: str, test_case_path: Optional[str] = ..., max_diagnoses: Optional[int] = ...) -> Union[None, List[str]]: ...
38
+ def estimated_number_of_configurations(self) -> Union[None, int]: ...
39
+ def false_optional_features(self, backend: Optional[str] = ...) -> Union[None, List[str]]: ...
40
+ def feature_ancestors(self, feature_name: str) -> Union[None, List[str]]: ...
41
+ def feature_bounds(self, variable_name: str) -> Union[None, Dict[str, Any]]: ...
42
+ def feature_explanation(self, configuration_path: str, feature_name: str) -> Union[None, List[Any]]: ...
43
+ def feature_inclusion_probability(self) -> Union[None, Dict[str, float]]: ...
44
+ def filter(self, configuration_path: str) -> Union[None, List[Configuration]]: ...
45
+ def generate_random_attribute(self, name: str, min_value: int = ..., max_value: int = ..., only_leaf_features: bool = ...) -> FeatureModel: ...
46
+ @staticmethod
47
+ def generate_random_feature_model(num_features: int = ..., max_constraints: int = ..., seed: int = ..., void: bool = ..., language_level: str = ...) -> FeatureModel: ...
48
+ def homogeneity(self) -> Union[None, float]: ...
49
+ def language_level(self) -> Union[None, Any]: ...
50
+ def leaf_features(self) -> Union[None, List[str]]: ...
51
+ def max_depth(self) -> Union[None, int]: ...
52
+ def metrics(self) -> Union[None, List[Dict[str, Any]]]: ...
53
+ def minimum_configuration(self, backend: Optional[str] = ...) -> Union[None, Configuration]: ...
54
+ def product_distribution(self) -> Union[None, List[int]]: ...
55
+ def pure_optional_features(self) -> Union[None, List[str]]: ...
56
+ def sampling(self, size: int, with_replacement: bool = ..., backend: Optional[str] = ...) -> Union[None, List[Configuration]]: ...
57
+ def satisfiable(self, backend: Optional[str] = ...) -> Union[None, bool]: ...
58
+ def satisfiable_configuration(self, configuration_path: str, full_configuration: bool = ..., backend: Optional[str] = ...) -> Union[None, bool]: ...
59
+ def t_wise_sampling(self, t: int = ..., backend: Optional[str] = ...) -> Union[None, List[Configuration]]: ...
60
+ def unique_features(self) -> Union[None, List[str]]: ...
61
+ def variability(self) -> Union[None, Tuple[float, float]]: ...
62
+ def variant_features(self) -> Union[None, List[str]]: ...
63
+ def variation_points(self) -> Union[None, Dict[str, List[str]]]: ...