flamapy 2.6.0.dev5__py3-none-any.whl → 2.6.0.dev11__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.
@@ -1,6 +1,8 @@
1
1
  import argparse
2
2
  import os
3
+ import shutil
3
4
  import sys
5
+ import textwrap
4
6
  from functools import wraps
5
7
  import inspect
6
8
  from pathlib import Path
@@ -13,6 +15,26 @@ from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
13
15
  # List to store registered commands and their arguments
14
16
  MANUAL_COMMANDS: List[Tuple[str, str, FunctionType, Tuple[Any, ...]]] = []
15
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
+
16
38
 
17
39
  def command(name, description, *args): # type: ignore
18
40
  def decorator(func): # type: ignore
@@ -27,26 +49,30 @@ def command(name, description, *args): # type: ignore
27
49
  return decorator
28
50
 
29
51
 
30
- def extract_commands(cls: type) -> List[Tuple[str, str, FunctionType, List[inspect.Parameter]]]:
52
+ def extract_commands(
53
+ cls: type,
54
+ ) -> List[Tuple[str, str, FunctionType, List[inspect.Parameter], Any]]:
31
55
  commands = []
32
56
  for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
33
57
  if name.startswith("_"):
34
58
  continue
59
+ if getattr(method, "_facade_kind", "operation") != "operation":
60
+ continue # producers/transformers return models; Python-API only for now
35
61
  docstring: Optional[str] = method.__doc__
36
62
  signature = inspect.signature(method)
37
63
  # Exclude 'self' from parameters
38
64
  parameters = list(signature.parameters.values())[1:] # Skip 'self'
39
- commands.append((name, docstring or "", method, parameters))
65
+ descriptor = getattr(method, "_facade_descriptor", None)
66
+ commands.append((name, docstring or "", method, parameters, descriptor))
40
67
  return commands
41
68
 
42
69
 
43
70
  @command(
44
71
  "generate_plugin",
45
- """This command generates a new plugin to implement your
46
- cusom operations. To execute it you should set yourself in the path of the
47
- flamapy src directory""",
48
- ("name", str, "The pluggins name"),
49
- ("extension", str, "The extansion to be registered with the flamapy ecosystem"),
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"),
50
76
  ("path", str, "The path to generate it"),
51
77
  )
52
78
  def generate_plugin(args): # type: ignore
@@ -107,8 +133,8 @@ def resolve_annotation_type(annotation: Any) -> type:
107
133
 
108
134
 
109
135
  def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
110
- for name, docstring, method, parameters in dynamic_commands:
111
- subparser = subparsers.add_parser(name, help=docstring)
136
+ for name, docstring, method, parameters, _ in dynamic_commands:
137
+ subparser = subparsers.add_parser(name, help=_summary(docstring), description=docstring)
112
138
  subparser.add_argument("model_path", type=str, help="Path to the feature model file")
113
139
  for param in parameters:
114
140
  arg_name = param.name
@@ -135,7 +161,7 @@ def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
135
161
 
136
162
  def setup_manual_commands(subparsers, manual_commands): # type: ignore
137
163
  for name, description, func, args in manual_commands:
138
- subparser = subparsers.add_parser(name, help=description)
164
+ subparser = subparsers.add_parser(name, help=_summary(description), description=description)
139
165
  for arg in args:
140
166
  arg_name, arg_type, arg_help = arg
141
167
  subparser.add_argument(arg_name, type=arg_type, help=arg_help)
@@ -170,9 +196,51 @@ def execute_command(args: argparse.Namespace) -> None:
170
196
  print(f"Attribute error: {attr_error}")
171
197
 
172
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
+
173
241
  def flamapy_cli() -> None:
174
242
  parser = argparse.ArgumentParser(description="FLAMA Feature Model CLI")
175
- subparsers = parser.add_subparsers(dest="command")
243
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND", title="commands")
176
244
 
177
245
  dynamic_commands = extract_commands(FLAMAFeatureModel)
178
246
  setup_dynamic_commands(subparsers, dynamic_commands)
@@ -183,10 +251,4 @@ def flamapy_cli() -> None:
183
251
  if args.command:
184
252
  execute_command(args)
185
253
  else:
186
- print("Feature model operations:")
187
- for name, docstring, _, _ in dynamic_commands:
188
- print(f" {name}: {docstring}")
189
- print("Framework developers operations:")
190
- for name, description, _, _ in MANUAL_COMMANDS:
191
- print(f" {name}: {description}")
192
- print("Execute flamapy --help for more information")
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}')
@@ -1,18 +1,34 @@
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
1
12
  import logging
2
13
  from enum import Enum
3
- from typing import Any, Dict, List, Optional, Tuple, Union
14
+ from typing import Any, Dict, Optional, Union
15
+
4
16
  from flamapy.core.discover import DiscoverMetamodels
5
- from flamapy.core.models import VariabilityModel
6
- from flamapy.metamodels.fm_metamodel.models import FeatureModel
7
17
  from flamapy.core.exceptions import FlamaException
8
- from flamapy.core.operations import OptimizationGoal
18
+ from flamapy.core.models import VariabilityModel
19
+ from flamapy.core.operations import OperationDescriptor
9
20
  from flamapy.metamodels.configuration_metamodel.models import Configuration
21
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel
10
22
 
11
23
  logger = logging.getLogger(__name__)
12
24
 
13
25
 
14
26
  class Backend(str, Enum):
15
- """Analysis backend (plugin) used to run a facade operation."""
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
+ """
16
32
 
17
33
  SAT = "sat"
18
34
  BDD = "bdd"
@@ -20,781 +36,179 @@ class Backend(str, Enum):
20
36
  SHARPSAT = "sharpsat"
21
37
 
22
38
 
39
+ # Facade backend name -> fm-to-backend transformation extension (the only non-identity mapping).
40
+ _BACKEND_ALIAS = {"sat": "pysat"}
41
+
42
+
23
43
  class FLAMAFeatureModel:
24
- # Backend -> (lazy transformation method, attribute caching the model)
25
- _BACKENDS = {
26
- Backend.SAT: ("_transform_to_sat", "sat_model"),
27
- Backend.BDD: ("_transform_to_bdd", "bdd_model"),
28
- Backend.Z3: ("_transform_to_z3", "z3_model"),
29
- Backend.SHARPSAT: ("_transform_to_sharpsat", "sharpsat_model"),
30
- }
31
-
32
- def __init__(self, model_path: str, backend: Optional[str] = None):
33
- """
34
- This is the path in the filesystem where the model is located.
35
- Any model in UVL, FaMaXML or FeatureIDE format are accepted.
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
+ """
36
49
 
37
- ``backend`` optionally sets a workspace-wide default backend
38
- ("sat", "bdd" or "z3") for every operation that supports more than
39
- one. When left as ``None`` each operation keeps its own historical
40
- default, so existing code behaves exactly as before.
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.
41
56
  """
42
57
  self.model_path = model_path
43
58
  self.discover_metamodel = DiscoverMetamodels()
44
59
  self.fm_model = self._read(model_path)
45
- self.sat_model = None
46
- self.bdd_model = None
47
- self.z3_model = None
48
- self.sharpsat_model = None
49
- self.diagnosis_model = None
50
60
  self.backend = Backend(backend) if backend is not None else None
61
+ self._backend_models: Dict[str, VariabilityModel] = {}
51
62
 
52
63
  def _read(self, model_path: str) -> FeatureModel:
53
64
  return self.discover_metamodel.use_transformation_t2m(model_path, "fm")
54
65
 
55
- def _select_backend(
56
- self,
57
- backend: Optional[str],
58
- default: Backend,
59
- ) -> VariabilityModel:
60
- """Resolve the backend to use, lazily transform to it and return its model.
61
-
62
- Resolution order: explicit per-call ``backend`` > workspace-wide
63
- ``self.backend`` > the operation's historical ``default``. All
64
- backends (sat, bdd, z3) are accepted; if the chosen one does not
65
- implement the requested operation a FlamaException is raised when it
66
- runs.
67
- """
68
- try:
69
- chosen = Backend(backend) if backend is not None else (self.backend or default)
70
- except ValueError:
71
- raise FlamaException(
72
- f"Unknown backend '{backend}'. "
73
- f"Choose from {[b.value for b in Backend]}."
74
- )
75
- transform, attr = self._BACKENDS[chosen]
76
- getattr(self, transform)()
77
- return getattr(self, attr)
78
-
79
- def _transform_to_sat(self) -> None:
80
- if self.sat_model is None:
81
- self.sat_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "pysat")
82
-
83
- def _transform_to_bdd(self) -> None:
84
- if self.bdd_model is None:
85
- self.bdd_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "bdd")
86
-
87
- def _transform_to_z3(self) -> None:
88
- if self.z3_model is None:
89
- self.z3_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "z3")
90
-
91
- def _transform_to_sharpsat(self) -> None:
92
- if self.sharpsat_model is None:
93
- # Requires the optional flamapy-sharpsat plugin. use_transformation_m2m raises a
94
- # FlamaException if it is not installed, which the operations surface to the caller.
95
- self.sharpsat_model = self.discover_metamodel.use_transformation_m2m(
96
- self.fm_model, "sharpsat"
97
- )
98
-
99
- def _transform_to_diagnosis(self) -> None:
100
- if self.diagnosis_model is None:
101
- self.diagnosis_model = self.discover_metamodel.use_transformation_m2m(
102
- self.fm_model, "pysat_diagnosis"
103
- )
104
-
105
66
  def _as_configuration(
106
67
  self, configuration: Union[str, Dict[str, Any], Configuration]
107
68
  ) -> Configuration:
108
69
  """Resolve a configuration argument into a Configuration model.
109
70
 
110
- Accepts a path to a configuration file (read via the configuration
111
- transformation), a ``{feature: value}`` mapping, or an already-built
112
- Configuration. This lets callers that already hold a configuration in
113
- memory (e.g. an interactive UI) pass it directly without writing a file.
71
+ Accepts a path to a configuration file (read via the configuration transformation), a
72
+ ``{feature: value}`` mapping, or an already-built Configuration.
114
73
  """
115
74
  if isinstance(configuration, Configuration):
116
75
  return configuration
117
76
  if isinstance(configuration, dict):
118
77
  return Configuration(configuration)
119
- return self.discover_metamodel.use_transformation_t2m(
120
- configuration, "configuration"
121
- )
122
-
123
- def atomic_sets(self) -> Union[None, List[List[Any]]]:
124
- """
125
- This operation is used to find the atomic sets in a model:
126
- It returns the atomic sets if they are found in the model.
127
- If the model does not follow the UVL specification, an
128
- exception is raised and the operation returns False.
129
- """
130
-
131
- # Try to use the Find operation, which returns the atomic sets if they are found
132
- try:
133
- atomic_sets = self.discover_metamodel.use_operation(
134
- self.fm_model, "FMAtomicSets"
135
- ).get_result()
136
- result = []
137
- for atomic_set in atomic_sets:
138
- partial_set = []
139
- for feature in atomic_set:
140
- partial_set.append(feature.name)
141
- result.append(partial_set)
142
- return result
143
- except FlamaException as exception:
144
- logger.error("Error: %s", exception)
145
- return None
146
-
147
- def language_level(self) -> Union[None, Any]:
148
- """Return the UVL language level."""
149
-
150
- try:
151
- result = self.discover_metamodel.use_operation(
152
- self.fm_model, "FMLanguageLevel"
153
- ).get_result()
154
- return result
155
- except FlamaException as exception:
156
- print(f"Error: {exception}")
157
- return None
158
-
159
- def average_branching_factor(self) -> Union[None, float]:
160
- """
161
- This refers to the average number of child features that a parent feature has in a
162
- feature model. It's calculated by dividing the total number of child features by the
163
- total number of parent features. A high average branching factor indicates a complex
164
- feature model with many options, while a low average branching factor indicates a
165
- simpler model.
166
- """
167
-
168
- # Try to use the Find operation, which returns the atomic sets if they are found
169
- try:
170
- result = self.discover_metamodel.use_operation(
171
- self.fm_model, "FMAverageBranchingFactor"
172
- ).get_result()
173
- return result
174
- except FlamaException as exception:
175
- logger.error("Error: %s", exception)
176
- return None
177
-
178
- def count_leafs(self) -> Union[None, int]:
179
- """
180
- This operation counts the number of leaf features in a feature model. Leaf features
181
- are those that do not have any child features. They represent the most specific
182
- options in a product line.
183
- """
184
-
185
- # Try to use the Find operation, which returns the atomic sets if they are found
186
- try:
187
- result = self.discover_metamodel.use_operation(
188
- self.fm_model, "FMCountLeafs"
189
- ).get_result()
190
- return result
191
- except FlamaException as exception:
192
- logger.error("Error: %s", exception)
193
- return None
194
-
195
- def estimated_number_of_configurations(self) -> Union[None, int]:
196
- """
197
- This is an estimate of the total number of different products that can be produced
198
- from a feature model. It's calculated by considering all possible combinations of
199
- features. This can be a simple multiplication if all features are independent, but
200
- in most cases, constraints and dependencies between features need to be taken
201
- into account.
202
- """
203
-
204
- # Try to use the Find operation, which returns the atomic sets if they are found
205
- try:
206
- result = self.discover_metamodel.use_operation(
207
- self.fm_model, "FMEstimatedConfigurationsNumber"
208
- ).get_result()
209
- return result
210
- except FlamaException as exception:
211
- logger.error("Error: %s", exception)
212
- return None
213
-
214
- def feature_ancestors(self, feature_name: str) -> Union[None, List[str]]:
215
- """
216
- These are the features that are directly or indirectly the parent of a given feature in
217
- a feature model. Ancestors of a feature are found by traversing up the feature hierarchy.
218
- This information can be useful to understand the context and dependencies of a feature.
219
- """
220
- # Try to use the Find operation, which returns the atomic sets if they are found
221
- try:
222
- operation = self.discover_metamodel.get_operation(self.fm_model, "FMFeatureAncestors")
223
- operation.set_feature(self.fm_model.get_feature_by_name(feature_name))
224
- operation.execute(self.fm_model)
225
- flama_result = operation.get_result()
226
- result = []
227
- for res in flama_result:
228
- result.append(res.name)
229
- return result
230
- except FlamaException as exception:
231
- logger.error("Error: %s", exception)
232
- return None
233
-
234
- def leaf_features(self) -> Union[None, List[str]]:
235
- """
236
- This operation is used to find leaf features in a model:
237
- It returns the leaf features if they are found in the model.
238
- If the model does not follow the UVL specification, an
239
- exception is raised and the operation returns False.
240
-
241
- Traditionally you would use the flama tool by
242
- features = discover_metamodel.use_operation_from_file('OperationString', model)
243
- however, in this tool we know that this operation is from the fm metamodel,
244
- so we avoid to execute the transformation if possible
245
- """
246
-
247
- # Try to use the operation, which returns the leaf features if they are found
248
- try:
249
- features = self.discover_metamodel.use_operation(
250
- self.fm_model, "FMLeafFeatures"
251
- ).get_result()
252
- leaf_features = []
253
- for feature in features:
254
- leaf_features.append(feature.name)
255
- return leaf_features
256
- except FlamaException as exception:
257
- logger.error("Error: %s", exception)
258
- return None
259
-
260
- def max_depth(self) -> Union[None, int]:
261
- """
262
- This operation is used to find the max depth of the tree in a model:
263
- It returns the max depth of the tree.
264
- If the model does not follow the UVL specification, an
265
- exception is raised and the operation returns False.
266
- """
267
-
268
- # Try to use the Find operation, which returns the max depth of the tree
269
- try:
270
- return self.discover_metamodel.use_operation(
271
- self.fm_model, "FMMaxDepthTree"
272
- ).get_result()
273
- except FlamaException as exception:
274
- logger.error("Error: %s", exception)
275
- return None
276
-
277
- # The methods above rely on sat to be executed.
278
- def core_features(self, backend: Optional[str] = None) -> Union[None, List[str]]:
279
- """
280
- These are the features that are present in all products of a product line.
281
- In a feature model, they are the features that are mandatory and not optional.
282
- Core features define the commonality among all products in a product line.
283
-
284
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
285
- """
286
- try:
287
- model = self._select_backend(backend, Backend.SAT)
288
- return self.discover_metamodel.use_operation(model, "CoreFeatures").get_result()
289
- except FlamaException as exception:
290
- logger.error("Error: %s", exception)
291
- return None
292
-
293
- def dead_features(self, backend: Optional[str] = None) -> Union[None, List[str]]:
294
- """
295
- These are features that, due to the constraints and dependencies in the
296
- feature model, cannot be included in any valid product. Dead features are usually
297
- a sign of an error in the feature model.
298
-
299
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
300
- """
301
- try:
302
- model = self._select_backend(backend, Backend.SAT)
303
- return self.discover_metamodel.use_operation(model, "DeadFeatures").get_result()
304
- except FlamaException as exception:
305
- logger.error("Error: %s", exception)
306
- return None
307
-
308
- def false_optional_features(self, backend: Optional[str] = None) -> Union[None, List[str]]:
309
- """
310
- These are features that appear to be optional in the feature model, but due to the
311
- constraints and dependencies, must be included in every valid product. Like dead features,
312
- false optional features are usually a sign of an error in the feature model.
313
-
314
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
315
- """
316
- try:
317
- model = self._select_backend(backend, Backend.SAT)
318
- return self.discover_metamodel.use_operation(
319
- model, "FalseOptionalFeatures"
320
- ).get_result()
321
- except FlamaException as exception:
322
- logger.error("Error: %s", exception)
323
- return None
324
-
325
- def filter(self, configuration_path: str) -> Union[None, List[Configuration]]:
326
- """
327
- This operation selects a subset of the products of a product line based on certain
328
- criteria. For example, you might filter the products to only include those that
329
- contain a certain feature.
330
-
331
- ``configuration_path`` accepts a configuration file path, a ``{feature: value}``
332
- mapping, or a Configuration object.
333
- """
334
- try:
335
- self._transform_to_sat()
336
- configuration = self._as_configuration(configuration_path)
337
- operation = self.discover_metamodel.get_operation(self.sat_model, "PySATFilter")
338
- operation.set_configuration(configuration)
339
- operation.execute(self.sat_model)
340
- result = operation.get_result()
341
- return result
342
- except FlamaException as exception:
343
- logger.error("Error: %s", exception)
344
- return None
345
-
346
- def configurations_number(self, backend: Optional[str] = None) -> Union[None, int]:
347
- """
348
- This is the total number of different full configurations that can be
349
- produced from a feature model. It's calculated by considering all possible
350
- combinations of features, taking into account the constraints and
351
- dependencies between features.
352
-
353
- ``backend`` selects the analysis plugin ("sat", "bdd", "z3", or "sharpsat" for a
354
- scalable approximate count via the optional flamapy-sharpsat plugin); defaults to bdd.
355
- """
356
- try:
357
- model = self._select_backend(backend, Backend.BDD)
358
- return self.discover_metamodel.use_operation(
359
- model, "ConfigurationsNumber"
360
- ).get_result()
361
- except FlamaException as exception:
362
- logger.error("Error: %s", exception)
363
- return None
364
-
365
- def configurations(self, backend: Optional[str] = None) -> Union[None, List[Configuration]]:
366
- """
367
- These are the individual outcomes that can be produced from a feature model. Each product
368
- is a combination of features that satisfies all the constraints and dependencies in the
369
- feature model.
370
-
371
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to bdd.
372
- """
373
- try:
374
- model = self._select_backend(backend, Backend.BDD)
375
- return self.discover_metamodel.use_operation(model, "Configurations").get_result()
376
- except FlamaException as exception:
377
- logger.error("Error: %s", exception)
378
- return None
379
-
380
- def commonality(self, configuration_path: str) -> Union[None, float]:
381
- """
382
- This is a measure of how often a feature appears in the products of a
383
- product line. It's usually expressed as a percentage. A feature with
384
- 100 per cent commonality is a core feature, as it appears in all products.
385
-
386
- ``configuration_path`` accepts a configuration file path, a ``{feature: value}``
387
- mapping, or a Configuration object.
388
- """
389
- try:
390
- self._transform_to_sat()
391
- configuration = self._as_configuration(configuration_path)
392
-
393
- operation = self.discover_metamodel.get_operation(self.sat_model, "PySATCommonality")
394
- operation.set_configuration(configuration)
395
- operation.execute(self.sat_model)
396
- return operation.get_result()
397
- except FlamaException as exception:
398
- logger.error("Error: %s", exception)
399
- return None
400
-
401
- def satisfiable_configuration(
402
- self,
403
- configuration_path: str,
404
- full_configuration: bool = False,
405
- backend: Optional[str] = None,
406
- ) -> Union[None, bool]:
407
- """
408
- This is a product that is produced from a valid configuration of features. A valid
409
- product satisfies all the constraints and dependencies in the feature model.
410
-
411
- ``configuration_path`` accepts a configuration file path, a ``{feature: value}``
412
- mapping, or a Configuration object.
413
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
414
- """
415
- try:
416
- model = self._select_backend(backend, Backend.SAT)
417
- configuration = self._as_configuration(configuration_path)
418
- operation = self.discover_metamodel.get_operation(
419
- model, "SatisfiableConfiguration"
420
- )
421
-
422
- configuration.is_full = bool(full_configuration)
423
-
424
- operation.set_configuration(configuration)
425
- operation.execute(model)
426
- return operation.get_result()
427
- except FlamaException as exception:
428
- logger.error("Error: %s", exception)
429
- return None
78
+ return self.discover_metamodel.use_transformation_t2m(configuration, "configuration")
430
79
 
431
- def satisfiable(self, backend: Optional[str] = None) -> Union[None, bool]:
432
- """
433
- In the context of feature models, this usually refers to whether the feature model itself
434
- satisfies all the constraints and dependencies. A a valid feature model is one that
435
- does encodes at least a single valid product.
436
-
437
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
438
- """
439
- try:
440
- model = self._select_backend(backend, Backend.SAT)
441
- return self.discover_metamodel.use_operation(model, "Satisfiable").get_result()
442
- except FlamaException as exception:
443
- logger.error("Error: %s", exception)
444
- return None
445
-
446
- # FM-based operations
447
-
448
- def metrics(self) -> Union[None, List[Dict[str, Any]]]:
449
- """
450
- Returns a collection of structural metrics for the feature model, such as number of
451
- features, constraints, relations, and complexity indicators.
452
- """
453
- try:
454
- return self.discover_metamodel.use_operation(
455
- self.fm_model, "FMMetrics"
456
- ).get_result()
457
- except FlamaException as exception:
458
- logger.error("Error: %s", exception)
459
- return None
460
-
461
- def variation_points(self) -> Union[None, Dict[str, List[str]]]:
462
- """
463
- Returns the variation points of the feature model. A variation point is a feature that
464
- has at least one non-mandatory child, representing a decision point in the model.
465
- The result maps each variation point name to the list of its variant feature names.
466
- """
467
- try:
468
- raw = self.discover_metamodel.use_operation(
469
- self.fm_model, "FMVariationPoints"
470
- ).get_result()
471
- return {vp.name: [v.name for v in variants] for vp, variants in raw.items()}
472
- except FlamaException as exception:
473
- logger.error("Error: %s", exception)
474
- return None
475
-
476
- # SAT-based operations
477
-
478
- def backbone(self) -> Union[None, Dict[str, List[Any]]]:
479
- """
480
- Returns the backbone of the feature model: the set of features that must always be
481
- selected (core) and those that must never be selected (dead) across all valid
482
- configurations, grouped under 'core' and 'dead' keys.
483
- """
484
- try:
485
- self._transform_to_sat()
486
- return self.discover_metamodel.use_operation(
487
- self.sat_model, "PySATBackbone"
488
- ).get_result()
489
- except FlamaException as exception:
490
- logger.error("Error: %s", exception)
491
- return None
492
-
493
- # BDD-based operations
494
-
495
- def homogeneity(self) -> Union[None, float]:
496
- """
497
- Measures how similar the products of the feature model are to each other. It is
498
- computed as the average commonality factor across all features. A value of 1.0 means
499
- all products are identical; lower values indicate more diversity.
500
- """
501
- try:
502
- self._transform_to_bdd()
503
- return self.discover_metamodel.use_operation(
504
- self.bdd_model, "BDDHomogeneity"
505
- ).get_result()
506
- except FlamaException as exception:
507
- logger.error("Error: %s", exception)
508
- return None
509
-
510
- def product_distribution(self) -> Union[None, List[int]]:
511
- """
512
- Returns the distribution of products by number of activated features. The value at
513
- index i is the count of valid configurations that have exactly i features selected.
514
- """
515
- try:
516
- self._transform_to_bdd()
517
- return self.discover_metamodel.use_operation(
518
- self.bdd_model, "BDDProductDistribution"
519
- ).get_result()
520
- except FlamaException as exception:
521
- logger.error("Error: %s", exception)
522
- return None
523
-
524
- def variability(self) -> Union[None, Tuple[float, float]]:
525
- """
526
- Returns a tuple of (total_variability, partial_variability). Total variability is the
527
- ratio of valid configurations to the theoretical maximum; partial variability is the
528
- ratio relative to only the variant features.
529
- """
530
- try:
531
- self._transform_to_bdd()
532
- return self.discover_metamodel.use_operation(
533
- self.bdd_model, "BDDVariability"
534
- ).get_result()
535
- except FlamaException as exception:
536
- logger.error("Error: %s", exception)
537
- return None
538
-
539
- def variant_features(self) -> Union[None, List[str]]:
540
- """
541
- Returns the features that are neither core nor dead — they appear in some but not all
542
- valid configurations. These are the features that actually vary across products.
543
- """
544
- try:
545
- self._transform_to_bdd()
546
- return self.discover_metamodel.use_operation(
547
- self.bdd_model, "BDDVariantFeatures"
548
- ).get_result()
549
- except FlamaException as exception:
550
- logger.error("Error: %s", exception)
551
- return None
552
-
553
- def unique_features(self) -> Union[None, List[str]]:
554
- """
555
- Returns features that appear in exactly one valid configuration. These features
556
- uniquely identify a single product in the product line.
557
- """
558
- try:
559
- self._transform_to_bdd()
560
- return self.discover_metamodel.use_operation(
561
- self.bdd_model, "BDDUniqueFeatures"
562
- ).get_result()
563
- except FlamaException as exception:
564
- logger.error("Error: %s", exception)
565
- return None
566
-
567
- def pure_optional_features(self) -> Union[None, List[str]]:
568
- """
569
- Returns features with a feature inclusion probability of exactly 0.5, meaning they
570
- are selected in exactly half of the valid configurations. These are the most
571
- unconstrained optional features.
572
- """
573
- try:
574
- self._transform_to_bdd()
575
- return self.discover_metamodel.use_operation(
576
- self.bdd_model, "BDDPureOptionalFeatures"
577
- ).get_result()
578
- except FlamaException as exception:
579
- logger.error("Error: %s", exception)
580
- return None
581
-
582
- def feature_inclusion_probability(self) -> Union[None, Dict[str, float]]:
583
- """
584
- Returns a mapping from each feature name to its inclusion probability: the fraction
585
- of valid configurations in which that feature is selected. Core features have
586
- probability 1.0 and dead features have probability 0.0.
587
- """
588
- try:
589
- self._transform_to_bdd()
590
- return self.discover_metamodel.use_operation(
591
- self.bdd_model, "BDDFeatureInclusionProbability"
592
- ).get_result()
593
- except FlamaException as exception:
594
- logger.error("Error: %s", exception)
595
- return None
596
-
597
- def configurations_with_n_features(self, n: int) -> Union[None, List[Configuration]]:
598
- """
599
- Returns all valid configurations that have exactly n features selected.
600
- """
601
- try:
602
- self._transform_to_bdd()
603
- operation = self.discover_metamodel.get_operation(
604
- self.bdd_model, "BDDConfigurationsWithNFeatures"
605
- )
606
- operation.set_n_features(n)
607
- operation.execute(self.bdd_model)
608
- return list(operation.get_result())
609
- except FlamaException as exception:
610
- logger.error("Error: %s", exception)
611
- return None
612
-
613
- def sampling(
614
- self, size: int, with_replacement: bool = False, backend: Optional[str] = None
615
- ) -> Union[None, List[Configuration]]:
616
- """
617
- Returns a random sample of valid configurations of the given size. When
618
- with_replacement is True, the same configuration may appear more than once.
619
-
620
- ``backend`` selects the analysis plugin; defaults to bdd. sat and bdd implement
621
- (deterministic/enumerating) sampling; "sharpsat" provides almost-uniform sampling
622
- via the optional flamapy-sharpsat plugin.
623
- """
624
- try:
625
- model = self._select_backend(backend, Backend.BDD)
626
- operation = self.discover_metamodel.get_operation(model, "Sampling")
627
- operation.set_sample_size(size)
628
- operation.set_with_replacement(with_replacement)
629
- operation.execute(model)
630
- return operation.get_result()
631
- except FlamaException as exception:
632
- logger.error("Error: %s", exception)
633
- return None
634
-
635
- def t_wise_sampling(
636
- self, t: int = 2, backend: Optional[str] = None
637
- ) -> Union[None, List[Configuration]]:
638
- """
639
- Returns a t-wise (combinatorial) sample: a set of valid configurations that covers
640
- every satisfiable combination of ``t`` feature selections (pairwise for ``t = 2``).
641
-
642
- ``backend`` selects the analysis plugin; only "sat" implements t-wise sampling.
643
- """
644
- try:
645
- model = self._select_backend(backend, Backend.SAT)
646
- operation = self.discover_metamodel.get_operation(model, "PySATTWiseSampling")
647
- operation.set_t(t)
648
- operation.execute(model)
649
- return operation.get_result()
650
- except FlamaException as exception:
651
- logger.error("Error: %s", exception)
652
- return None
653
-
654
- def minimum_configuration(
655
- self, backend: Optional[str] = None
656
- ) -> Union[None, Configuration]:
657
- """
658
- Returns a valid configuration with the fewest selected features (the minimum
659
- working configuration).
660
-
661
- ``backend`` selects the analysis plugin; only "sat" implements this operation.
662
- """
663
- try:
664
- model = self._select_backend(backend, Backend.SAT)
665
- operation = self.discover_metamodel.get_operation(model, "PySATMinimumConfiguration")
666
- operation.execute(model)
667
- return operation.get_result()
668
- except FlamaException as exception:
669
- logger.error("Error: %s", exception)
670
- return None
671
-
672
- def attribute_optimization(
673
- self, objectives: Any, backend: Optional[str] = None
674
- ) -> Union[None, List[Configuration]]:
675
- """
676
- Returns the configuration(s) that optimize one or more numeric feature attributes.
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.
677
84
 
678
- ``objectives`` is either a single attribute name (defaults to minimizing it) or a
679
- mapping ``{attribute_name: "Minimize"|"Maximize"}``. ``backend`` selects the plugin:
680
- "sat" performs single-objective MaxSAT optimization; "z3" additionally supports
681
- typed attributes and multi-objective (Pareto) optimization. Defaults to "sat".
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.
682
90
  """
683
- def _goal(value: Any) -> OptimizationGoal:
684
- return (OptimizationGoal.MINIMIZE
685
- if str(value).lower().startswith('min')
686
- else OptimizationGoal.MAXIMIZE)
687
-
688
- if isinstance(objectives, str):
689
- attributes = {objectives: OptimizationGoal.MINIMIZE}
91
+ if descriptor.selectable_backend:
92
+ resolved = backend_arg or self.backend or descriptor.default_backend
690
93
  else:
691
- attributes = {name: _goal(goal) for name, goal in dict(objectives).items()}
692
-
693
- try:
694
- model = self._select_backend(backend, Backend.SAT)
695
- operation = self.discover_metamodel.get_operation(model, "AttributeOptimization")
696
- operation.set_attributes(attributes)
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)
697
127
  operation.execute(model)
698
- # z3 returns (configuration, values) tuples; sat returns configurations.
699
- return [item[0] if isinstance(item, tuple) else item
700
- for item in operation.get_result()]
701
- except FlamaException as exception:
702
- logger.error("Error: %s", exception)
703
- return None
704
-
705
- # Z3-based operations
706
-
707
- def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]:
708
- """
709
- Returns the value bounds for all typed (Integer, Real, String) attribute features in
710
- the model. Each entry maps a feature name to a dict with 'min', 'max', and 'bounded'
711
- keys. Requires the z3 metamodel plugin.
712
- """
713
- try:
714
- self._transform_to_z3()
715
- return self.discover_metamodel.use_operation(
716
- self.z3_model, "Z3AllFeatureBounds"
717
- ).get_result()
718
- except FlamaException as exception:
719
- logger.error("Error: %s", exception)
720
- return None
721
-
722
- def feature_bounds(self, variable_name: str) -> Union[None, Dict[str, Any]]:
723
- """
724
- Returns the value bounds for a specific typed attribute feature. The result dict
725
- contains 'min', 'max', and 'bounded' keys. Requires the z3 metamodel plugin.
726
- """
727
- try:
728
- self._transform_to_z3()
729
- operation = self.discover_metamodel.get_operation(self.z3_model, "Z3FeatureBounds")
730
- operation.set_variable_name(variable_name)
731
- operation.execute(self.z3_model)
732
- return operation.get_result()
733
- except FlamaException as exception:
734
- logger.error("Error: %s", exception)
735
- return None
736
-
737
- # Diagnosis-based operations
738
-
739
- def diagnosis(
740
- self,
741
- configuration_path: str,
742
- test_case_path: Optional[str] = None,
743
- max_diagnoses: Optional[int] = None,
744
- ) -> Union[None, List[str]]:
745
- """
746
- Returns a list of diagnoses explaining why a configuration is not valid against the
747
- feature model: each diagnosis is a minimal set of constraints whose removal would
748
- resolve the inconsistency. Requires the pysat_diagnosis plugin.
749
-
750
- ``configuration_path`` accepts a configuration file path, a ``{feature: value}``
751
- mapping, or a Configuration object. ``test_case_path`` is optional and, when given,
752
- diagnoses against that expected outcome instead; it accepts the same input types.
753
- """
754
- try:
755
- self._transform_to_diagnosis()
756
- operation = self.discover_metamodel.get_operation(
757
- self.diagnosis_model, "PySATDiagnosis"
758
- )
759
- if configuration_path is not None:
760
- operation.set_configuration(self._as_configuration(configuration_path))
761
- if test_case_path is not None:
762
- operation.set_test_case(self._as_configuration(test_case_path))
763
- if max_diagnoses is not None:
764
- operation.set_max_diagnoses(max_diagnoses)
765
- operation.execute(self.diagnosis_model)
766
- return operation.get_result()
128
+ result = operation.get_result()
129
+ if descriptor.result_adapter is not None:
130
+ result = descriptor.result_adapter(result)
131
+ return result
767
132
  except FlamaException as exception:
768
133
  logger.error("Error: %s", exception)
769
134
  return None
770
135
 
771
- def conflict(
772
- self,
773
- configuration_path: str,
774
- test_case_path: Optional[str] = None,
775
- max_conflicts: Optional[int] = None,
776
- ) -> Union[None, List[str]]:
777
- """
778
- Returns a list of conflict sets: minimal subsets of the model constraints that are
779
- inconsistent with the given configuration. Requires the pysat_diagnosis plugin.
780
136
 
781
- ``configuration_path`` accepts a configuration file path, a ``{feature: value}``
782
- mapping, or a Configuration object. ``test_case_path`` is optional and, when given,
783
- looks for conflicts against that expected outcome instead; same input types.
784
- """
785
- try:
786
- self._transform_to_diagnosis()
787
- operation = self.discover_metamodel.get_operation(
788
- self.diagnosis_model, "PySATConflict"
789
- )
790
- if configuration_path is not None:
791
- operation.set_configuration(self._as_configuration(configuration_path))
792
- if test_case_path is not None:
793
- operation.set_test_case(self._as_configuration(test_case_path))
794
- if max_conflicts is not None:
795
- operation.set_max_conflicts(max_conflicts)
796
- operation.execute(self.diagnosis_model)
797
- return operation.get_result()
798
- except FlamaException as exception:
799
- logger.error("Error: %s", exception)
800
- return None
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]]]: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy
3
- Version: 2.6.0.dev5
3
+ Version: 2.6.0.dev11
4
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
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,19 +8,17 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.6.0.dev5
12
- Requires-Dist: flamapy-fm~=2.6.0.dev5
13
- Requires-Dist: flamapy-sat~=2.6.0.dev5
14
- Requires-Dist: flamapy-bdd~=2.6.0.dev5
15
- Requires-Dist: flamapy-z3~=2.6.0.dev5
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev11
12
+ Requires-Dist: flamapy-fm~=2.6.0.dev11
13
+ Requires-Dist: flamapy-sat~=2.6.0.dev11
14
+ Requires-Dist: flamapy-bdd~=2.6.0.dev11
15
+ Requires-Dist: flamapy-z3~=2.6.0.dev11
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-mock; extra == "dev"
19
19
  Requires-Dist: prospector; extra == "dev"
20
20
  Requires-Dist: mypy; extra == "dev"
21
21
  Requires-Dist: coverage; extra == "dev"
22
- Provides-Extra: sharpsat
23
- Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
24
22
  Dynamic: license-file
25
23
 
26
24
  # flamapy
@@ -0,0 +1,11 @@
1
+ flamapy/commands/__init__.py,sha256=rajfqLxDAa5E4glNlLfUXQbIl5ZcgkGI6kTBKM9apSM,10144
2
+ flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
3
+ flamapy/interfaces/python/_stub.py,sha256=SDlbrXrfQaJpgTzuScY-Q9IrkarGO96ad463NL0RJMY,2822
4
+ flamapy/interfaces/python/flamapy_feature_model.py,sha256=sBZffI8CA4NA7Tzg9q7OaTCDKTV-EY-tWKQ8kdEEBkY,9840
5
+ flamapy/interfaces/python/flamapy_feature_model.pyi,sha256=VdGtS3KdobqOEQfmvO_HTo2JkCNRB7UOWurKG4Xet64,4336
6
+ flamapy-2.6.0.dev11.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
7
+ flamapy-2.6.0.dev11.dist-info/METADATA,sha256=NirN5M8AxSAKjhVaN6dk-7a02UCU1odFpcdhaDDCgiE,1271
8
+ flamapy-2.6.0.dev11.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ flamapy-2.6.0.dev11.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
10
+ flamapy-2.6.0.dev11.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
11
+ flamapy-2.6.0.dev11.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- flamapy/commands/__init__.py,sha256=KKFyT5PDLYCE_FUD6RMtZGHmP0XIUvoPoTSxzsfAwks,7289
2
- flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
3
- flamapy/interfaces/python/flamapy_feature_model.py,sha256=hcP6kVVYV1nJ1vFTwTo_4rOp84h40NGsUREDo0I6zbI,34546
4
- flamapy-2.6.0.dev5.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
5
- flamapy-2.6.0.dev5.dist-info/METADATA,sha256=6c6BHMwLKgvb1Vw3jKMccouvL9vLrP_6txJ-d7B6HG8,1355
6
- flamapy-2.6.0.dev5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
- flamapy-2.6.0.dev5.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
8
- flamapy-2.6.0.dev5.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
9
- flamapy-2.6.0.dev5.dist-info/RECORD,,