flamapy 2.6.0.dev4__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.
- flamapy/commands/__init__.py +104 -29
- flamapy/interfaces/python/_stub.py +76 -0
- flamapy/interfaces/python/flamapy_feature_model.py +169 -671
- flamapy/interfaces/python/flamapy_feature_model.pyi +63 -0
- {flamapy-2.6.0.dev4.dist-info → flamapy-2.6.0.dev11.dist-info}/METADATA +6 -6
- flamapy-2.6.0.dev11.dist-info/RECORD +11 -0
- flamapy-2.6.0.dev4.dist-info/RECORD +0 -9
- {flamapy-2.6.0.dev4.dist-info → flamapy-2.6.0.dev11.dist-info}/WHEEL +0 -0
- {flamapy-2.6.0.dev4.dist-info → flamapy-2.6.0.dev11.dist-info}/entry_points.txt +0 -0
- {flamapy-2.6.0.dev4.dist-info → flamapy-2.6.0.dev11.dist-info}/licenses/LICENSE +0 -0
- {flamapy-2.6.0.dev4.dist-info → flamapy-2.6.0.dev11.dist-info}/top_level.txt +0 -0
flamapy/commands/__init__.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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
|
|
7
9
|
from shutil import copytree
|
|
8
|
-
from typing import Any, List, Tuple, Optional
|
|
10
|
+
from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
|
|
9
11
|
from types import FunctionType
|
|
10
12
|
|
|
11
13
|
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
("
|
|
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
|
|
@@ -92,37 +118,50 @@ def generate_plugin(args): # type: ignore
|
|
|
92
118
|
print("Plugin generated!")
|
|
93
119
|
|
|
94
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
|
+
|
|
95
135
|
def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
|
|
96
|
-
for name, docstring, method, parameters in dynamic_commands:
|
|
97
|
-
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)
|
|
98
138
|
subparser.add_argument("model_path", type=str, help="Path to the feature model file")
|
|
99
|
-
if "configuration_path" in [param.name for param in parameters]:
|
|
100
|
-
subparser.add_argument(
|
|
101
|
-
"--configuration_path",
|
|
102
|
-
type=str,
|
|
103
|
-
help="Path to the configuration file",
|
|
104
|
-
required=False,
|
|
105
|
-
)
|
|
106
139
|
for param in parameters:
|
|
107
140
|
arg_name = param.name
|
|
108
141
|
if arg_name not in ["model_path"]: # Avoid duplicates
|
|
142
|
+
arg_type = resolve_annotation_type(param.annotation)
|
|
109
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)
|
|
110
146
|
subparser.add_argument(
|
|
111
|
-
arg_name,
|
|
147
|
+
f"--{arg_name}",
|
|
148
|
+
action=argparse.BooleanOptionalAction,
|
|
149
|
+
default=param.default,
|
|
150
|
+
help="Optional bool",
|
|
112
151
|
)
|
|
113
152
|
else: # Optional argument
|
|
114
153
|
subparser.add_argument(
|
|
115
154
|
f"--{arg_name}",
|
|
116
|
-
type=
|
|
155
|
+
type=arg_type,
|
|
117
156
|
default=param.default,
|
|
118
|
-
help=f"Optional {
|
|
157
|
+
help=f"Optional {arg_type.__name__}",
|
|
119
158
|
)
|
|
120
159
|
subparser.set_defaults(func=method, method_name=name, parameters=parameters)
|
|
121
160
|
|
|
122
161
|
|
|
123
162
|
def setup_manual_commands(subparsers, manual_commands): # type: ignore
|
|
124
163
|
for name, description, func, args in manual_commands:
|
|
125
|
-
subparser = subparsers.add_parser(name, help=description)
|
|
164
|
+
subparser = subparsers.add_parser(name, help=_summary(description), description=description)
|
|
126
165
|
for arg in args:
|
|
127
166
|
arg_name, arg_type, arg_help = arg
|
|
128
167
|
subparser.add_argument(arg_name, type=arg_type, help=arg_help)
|
|
@@ -157,9 +196,51 @@ def execute_command(args: argparse.Namespace) -> None:
|
|
|
157
196
|
print(f"Attribute error: {attr_error}")
|
|
158
197
|
|
|
159
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
|
+
|
|
160
241
|
def flamapy_cli() -> None:
|
|
161
242
|
parser = argparse.ArgumentParser(description="FLAMA Feature Model CLI")
|
|
162
|
-
subparsers = parser.add_subparsers(dest="command")
|
|
243
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND", title="commands")
|
|
163
244
|
|
|
164
245
|
dynamic_commands = extract_commands(FLAMAFeatureModel)
|
|
165
246
|
setup_dynamic_commands(subparsers, dynamic_commands)
|
|
@@ -170,10 +251,4 @@ def flamapy_cli() -> None:
|
|
|
170
251
|
if args.command:
|
|
171
252
|
execute_command(args)
|
|
172
253
|
else:
|
|
173
|
-
|
|
174
|
-
for name, docstring, _, _ in dynamic_commands:
|
|
175
|
-
print(f" {name}: {docstring}")
|
|
176
|
-
print("Framework developers operations:")
|
|
177
|
-
for name, description, _, _ in MANUAL_COMMANDS:
|
|
178
|
-
print(f" {name}: {description}")
|
|
179
|
-
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,716 +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
|
|
1
12
|
import logging
|
|
2
13
|
from enum import Enum
|
|
3
|
-
from typing import Any, Dict,
|
|
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
|
|
18
|
+
from flamapy.core.models import VariabilityModel
|
|
19
|
+
from flamapy.core.operations import OperationDescriptor
|
|
8
20
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
21
|
+
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
9
22
|
|
|
10
23
|
logger = logging.getLogger(__name__)
|
|
11
24
|
|
|
12
25
|
|
|
13
26
|
class Backend(str, Enum):
|
|
14
|
-
"""Analysis backend
|
|
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
|
+
"""
|
|
15
32
|
|
|
16
33
|
SAT = "sat"
|
|
17
34
|
BDD = "bdd"
|
|
18
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"}
|
|
19
41
|
|
|
20
42
|
|
|
21
43
|
class FLAMAFeatureModel:
|
|
22
|
-
|
|
23
|
-
_BACKENDS = {
|
|
24
|
-
Backend.SAT: ("_transform_to_sat", "sat_model"),
|
|
25
|
-
Backend.BDD: ("_transform_to_bdd", "bdd_model"),
|
|
26
|
-
Backend.Z3: ("_transform_to_z3", "z3_model"),
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
def __init__(self, model_path: str, backend: Optional[str] = None):
|
|
30
|
-
"""
|
|
31
|
-
This is the path in the filesystem where the model is located.
|
|
32
|
-
Any model in UVL, FaMaXML or FeatureIDE format are accepted.
|
|
44
|
+
"""A feature model together with the analysis operations discovered from the installed plugins.
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
38
56
|
"""
|
|
39
57
|
self.model_path = model_path
|
|
40
58
|
self.discover_metamodel = DiscoverMetamodels()
|
|
41
59
|
self.fm_model = self._read(model_path)
|
|
42
|
-
self.sat_model = None
|
|
43
|
-
self.bdd_model = None
|
|
44
|
-
self.z3_model = None
|
|
45
|
-
self.diagnosis_model = None
|
|
46
60
|
self.backend = Backend(backend) if backend is not None else None
|
|
61
|
+
self._backend_models: Dict[str, VariabilityModel] = {}
|
|
47
62
|
|
|
48
63
|
def _read(self, model_path: str) -> FeatureModel:
|
|
49
64
|
return self.discover_metamodel.use_transformation_t2m(model_path, "fm")
|
|
50
65
|
|
|
51
|
-
def _select_backend(
|
|
52
|
-
self,
|
|
53
|
-
backend: Optional[str],
|
|
54
|
-
default: Backend,
|
|
55
|
-
) -> VariabilityModel:
|
|
56
|
-
"""Resolve the backend to use, lazily transform to it and return its model.
|
|
57
|
-
|
|
58
|
-
Resolution order: explicit per-call ``backend`` > workspace-wide
|
|
59
|
-
``self.backend`` > the operation's historical ``default``. All
|
|
60
|
-
backends (sat, bdd, z3) are accepted; if the chosen one does not
|
|
61
|
-
implement the requested operation a FlamaException is raised when it
|
|
62
|
-
runs.
|
|
63
|
-
"""
|
|
64
|
-
try:
|
|
65
|
-
chosen = Backend(backend) if backend is not None else (self.backend or default)
|
|
66
|
-
except ValueError:
|
|
67
|
-
raise FlamaException(
|
|
68
|
-
f"Unknown backend '{backend}'. "
|
|
69
|
-
f"Choose from {[b.value for b in Backend]}."
|
|
70
|
-
)
|
|
71
|
-
transform, attr = self._BACKENDS[chosen]
|
|
72
|
-
getattr(self, transform)()
|
|
73
|
-
return getattr(self, attr)
|
|
74
|
-
|
|
75
|
-
def _transform_to_sat(self) -> None:
|
|
76
|
-
if self.sat_model is None:
|
|
77
|
-
self.sat_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "pysat")
|
|
78
|
-
|
|
79
|
-
def _transform_to_bdd(self) -> None:
|
|
80
|
-
if self.bdd_model is None:
|
|
81
|
-
self.bdd_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "bdd")
|
|
82
|
-
|
|
83
|
-
def _transform_to_z3(self) -> None:
|
|
84
|
-
if self.z3_model is None:
|
|
85
|
-
self.z3_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "z3")
|
|
86
|
-
|
|
87
|
-
def _transform_to_diagnosis(self) -> None:
|
|
88
|
-
if self.diagnosis_model is None:
|
|
89
|
-
self.diagnosis_model = self.discover_metamodel.use_transformation_m2m(
|
|
90
|
-
self.fm_model, "pysat_diagnosis"
|
|
91
|
-
)
|
|
92
|
-
|
|
93
66
|
def _as_configuration(
|
|
94
67
|
self, configuration: Union[str, Dict[str, Any], Configuration]
|
|
95
68
|
) -> Configuration:
|
|
96
69
|
"""Resolve a configuration argument into a Configuration model.
|
|
97
70
|
|
|
98
|
-
Accepts a path to a configuration file (read via the configuration
|
|
99
|
-
|
|
100
|
-
Configuration. This lets callers that already hold a configuration in
|
|
101
|
-
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.
|
|
102
73
|
"""
|
|
103
74
|
if isinstance(configuration, Configuration):
|
|
104
75
|
return configuration
|
|
105
76
|
if isinstance(configuration, dict):
|
|
106
77
|
return Configuration(configuration)
|
|
107
|
-
return self.discover_metamodel.use_transformation_t2m(
|
|
108
|
-
configuration, "configuration"
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
def atomic_sets(self) -> Union[None, List[List[Any]]]:
|
|
112
|
-
"""
|
|
113
|
-
This operation is used to find the atomic sets in a model:
|
|
114
|
-
It returns the atomic sets if they are found in the model.
|
|
115
|
-
If the model does not follow the UVL specification, an
|
|
116
|
-
exception is raised and the operation returns False.
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
# Try to use the Find operation, which returns the atomic sets if they are found
|
|
120
|
-
try:
|
|
121
|
-
atomic_sets = self.discover_metamodel.use_operation(
|
|
122
|
-
self.fm_model, "FMAtomicSets"
|
|
123
|
-
).get_result()
|
|
124
|
-
result = []
|
|
125
|
-
for atomic_set in atomic_sets:
|
|
126
|
-
partial_set = []
|
|
127
|
-
for feature in atomic_set:
|
|
128
|
-
partial_set.append(feature.name)
|
|
129
|
-
result.append(partial_set)
|
|
130
|
-
return result
|
|
131
|
-
except FlamaException as exception:
|
|
132
|
-
logger.error("Error: %s", exception)
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
def language_level(self) -> Union[None, Any]:
|
|
136
|
-
"""Return the UVL language level."""
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
result = self.discover_metamodel.use_operation(
|
|
140
|
-
self.fm_model, "FMLanguageLevel"
|
|
141
|
-
).get_result()
|
|
142
|
-
return result
|
|
143
|
-
except FlamaException as exception:
|
|
144
|
-
print(f"Error: {exception}")
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
def average_branching_factor(self) -> Union[None, float]:
|
|
148
|
-
"""
|
|
149
|
-
This refers to the average number of child features that a parent feature has in a
|
|
150
|
-
feature model. It's calculated by dividing the total number of child features by the
|
|
151
|
-
total number of parent features. A high average branching factor indicates a complex
|
|
152
|
-
feature model with many options, while a low average branching factor indicates a
|
|
153
|
-
simpler model.
|
|
154
|
-
"""
|
|
155
|
-
|
|
156
|
-
# Try to use the Find operation, which returns the atomic sets if they are found
|
|
157
|
-
try:
|
|
158
|
-
result = self.discover_metamodel.use_operation(
|
|
159
|
-
self.fm_model, "FMAverageBranchingFactor"
|
|
160
|
-
).get_result()
|
|
161
|
-
return result
|
|
162
|
-
except FlamaException as exception:
|
|
163
|
-
logger.error("Error: %s", exception)
|
|
164
|
-
return None
|
|
165
|
-
|
|
166
|
-
def count_leafs(self) -> Union[None, int]:
|
|
167
|
-
"""
|
|
168
|
-
This operation counts the number of leaf features in a feature model. Leaf features
|
|
169
|
-
are those that do not have any child features. They represent the most specific
|
|
170
|
-
options in a product line.
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
# Try to use the Find operation, which returns the atomic sets if they are found
|
|
174
|
-
try:
|
|
175
|
-
result = self.discover_metamodel.use_operation(
|
|
176
|
-
self.fm_model, "FMCountLeafs"
|
|
177
|
-
).get_result()
|
|
178
|
-
return result
|
|
179
|
-
except FlamaException as exception:
|
|
180
|
-
logger.error("Error: %s", exception)
|
|
181
|
-
return None
|
|
182
|
-
|
|
183
|
-
def estimated_number_of_configurations(self) -> Union[None, int]:
|
|
184
|
-
"""
|
|
185
|
-
This is an estimate of the total number of different products that can be produced
|
|
186
|
-
from a feature model. It's calculated by considering all possible combinations of
|
|
187
|
-
features. This can be a simple multiplication if all features are independent, but
|
|
188
|
-
in most cases, constraints and dependencies between features need to be taken
|
|
189
|
-
into account.
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
# Try to use the Find operation, which returns the atomic sets if they are found
|
|
193
|
-
try:
|
|
194
|
-
result = self.discover_metamodel.use_operation(
|
|
195
|
-
self.fm_model, "FMEstimatedConfigurationsNumber"
|
|
196
|
-
).get_result()
|
|
197
|
-
return result
|
|
198
|
-
except FlamaException as exception:
|
|
199
|
-
logger.error("Error: %s", exception)
|
|
200
|
-
return None
|
|
201
|
-
|
|
202
|
-
def feature_ancestors(self, feature_name: str) -> Union[None, List[str]]:
|
|
203
|
-
"""
|
|
204
|
-
These are the features that are directly or indirectly the parent of a given feature in
|
|
205
|
-
a feature model. Ancestors of a feature are found by traversing up the feature hierarchy.
|
|
206
|
-
This information can be useful to understand the context and dependencies of a feature.
|
|
207
|
-
"""
|
|
208
|
-
# Try to use the Find operation, which returns the atomic sets if they are found
|
|
209
|
-
try:
|
|
210
|
-
operation = self.discover_metamodel.get_operation(self.fm_model, "FMFeatureAncestors")
|
|
211
|
-
operation.set_feature(self.fm_model.get_feature_by_name(feature_name))
|
|
212
|
-
operation.execute(self.fm_model)
|
|
213
|
-
flama_result = operation.get_result()
|
|
214
|
-
result = []
|
|
215
|
-
for res in flama_result:
|
|
216
|
-
result.append(res.name)
|
|
217
|
-
return result
|
|
218
|
-
except FlamaException as exception:
|
|
219
|
-
logger.error("Error: %s", exception)
|
|
220
|
-
return None
|
|
221
|
-
|
|
222
|
-
def leaf_features(self) -> Union[None, List[str]]:
|
|
223
|
-
"""
|
|
224
|
-
This operation is used to find leaf features in a model:
|
|
225
|
-
It returns the leaf features if they are found in the model.
|
|
226
|
-
If the model does not follow the UVL specification, an
|
|
227
|
-
exception is raised and the operation returns False.
|
|
228
|
-
|
|
229
|
-
Traditionally you would use the flama tool by
|
|
230
|
-
features = discover_metamodel.use_operation_from_file('OperationString', model)
|
|
231
|
-
however, in this tool we know that this operation is from the fm metamodel,
|
|
232
|
-
so we avoid to execute the transformation if possible
|
|
233
|
-
"""
|
|
78
|
+
return self.discover_metamodel.use_transformation_t2m(configuration, "configuration")
|
|
234
79
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
These are features that, due to the constraints and dependencies in the
|
|
284
|
-
feature model, cannot be included in any valid product. Dead features are usually
|
|
285
|
-
a sign of an error in the feature model.
|
|
286
|
-
|
|
287
|
-
``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
|
|
288
|
-
"""
|
|
289
|
-
try:
|
|
290
|
-
model = self._select_backend(backend, Backend.SAT)
|
|
291
|
-
return self.discover_metamodel.use_operation(model, "DeadFeatures").get_result()
|
|
292
|
-
except FlamaException as exception:
|
|
293
|
-
logger.error("Error: %s", exception)
|
|
294
|
-
return None
|
|
295
|
-
|
|
296
|
-
def false_optional_features(self, backend: Optional[str] = None) -> Union[None, List[str]]:
|
|
297
|
-
"""
|
|
298
|
-
These are features that appear to be optional in the feature model, but due to the
|
|
299
|
-
constraints and dependencies, must be included in every valid product. Like dead features,
|
|
300
|
-
false optional features are usually a sign of an error in the feature model.
|
|
301
|
-
|
|
302
|
-
``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to sat.
|
|
303
|
-
"""
|
|
304
|
-
try:
|
|
305
|
-
model = self._select_backend(backend, Backend.SAT)
|
|
306
|
-
return self.discover_metamodel.use_operation(
|
|
307
|
-
model, "FalseOptionalFeatures"
|
|
308
|
-
).get_result()
|
|
309
|
-
except FlamaException as exception:
|
|
310
|
-
logger.error("Error: %s", exception)
|
|
311
|
-
return None
|
|
312
|
-
|
|
313
|
-
def filter(self, configuration_path: str) -> Union[None, List[Configuration]]:
|
|
314
|
-
"""
|
|
315
|
-
This operation selects a subset of the products of a product line based on certain
|
|
316
|
-
criteria. For example, you might filter the products to only include those that
|
|
317
|
-
contain a certain feature.
|
|
318
|
-
|
|
319
|
-
``configuration_path`` accepts a configuration file path, a ``{feature: value}``
|
|
320
|
-
mapping, or a Configuration object.
|
|
321
|
-
"""
|
|
322
|
-
try:
|
|
323
|
-
self._transform_to_sat()
|
|
324
|
-
configuration = self._as_configuration(configuration_path)
|
|
325
|
-
operation = self.discover_metamodel.get_operation(self.sat_model, "PySATFilter")
|
|
326
|
-
operation.set_configuration(configuration)
|
|
327
|
-
operation.execute(self.sat_model)
|
|
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)
|
|
328
128
|
result = operation.get_result()
|
|
129
|
+
if descriptor.result_adapter is not None:
|
|
130
|
+
result = descriptor.result_adapter(result)
|
|
329
131
|
return result
|
|
330
132
|
except FlamaException as exception:
|
|
331
133
|
logger.error("Error: %s", exception)
|
|
332
134
|
return None
|
|
333
135
|
|
|
334
|
-
def configurations_number(self, backend: Optional[str] = None) -> Union[None, int]:
|
|
335
|
-
"""
|
|
336
|
-
This is the total number of different full configurations that can be
|
|
337
|
-
produced from a feature model. It's calculated by considering all possible
|
|
338
|
-
combinations of features, taking into account the constraints and
|
|
339
|
-
dependencies between features.
|
|
340
|
-
|
|
341
|
-
``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to bdd.
|
|
342
|
-
"""
|
|
343
|
-
try:
|
|
344
|
-
model = self._select_backend(backend, Backend.BDD)
|
|
345
|
-
return self.discover_metamodel.use_operation(
|
|
346
|
-
model, "ConfigurationsNumber"
|
|
347
|
-
).get_result()
|
|
348
|
-
except FlamaException as exception:
|
|
349
|
-
logger.error("Error: %s", exception)
|
|
350
|
-
return None
|
|
351
|
-
|
|
352
|
-
def configurations(self, backend: Optional[str] = None) -> Union[None, List[Configuration]]:
|
|
353
|
-
"""
|
|
354
|
-
These are the individual outcomes that can be produced from a feature model. Each product
|
|
355
|
-
is a combination of features that satisfies all the constraints and dependencies in the
|
|
356
|
-
feature model.
|
|
357
|
-
|
|
358
|
-
``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to bdd.
|
|
359
|
-
"""
|
|
360
|
-
try:
|
|
361
|
-
model = self._select_backend(backend, Backend.BDD)
|
|
362
|
-
return self.discover_metamodel.use_operation(model, "Configurations").get_result()
|
|
363
|
-
except FlamaException as exception:
|
|
364
|
-
logger.error("Error: %s", exception)
|
|
365
|
-
return None
|
|
366
|
-
|
|
367
|
-
def commonality(self, configuration_path: str) -> Union[None, float]:
|
|
368
|
-
"""
|
|
369
|
-
This is a measure of how often a feature appears in the products of a
|
|
370
|
-
product line. It's usually expressed as a percentage. A feature with
|
|
371
|
-
100 per cent commonality is a core feature, as it appears in all products.
|
|
372
|
-
|
|
373
|
-
``configuration_path`` accepts a configuration file path, a ``{feature: value}``
|
|
374
|
-
mapping, or a Configuration object.
|
|
375
|
-
"""
|
|
376
|
-
try:
|
|
377
|
-
self._transform_to_sat()
|
|
378
|
-
configuration = self._as_configuration(configuration_path)
|
|
379
|
-
|
|
380
|
-
operation = self.discover_metamodel.get_operation(self.sat_model, "PySATCommonality")
|
|
381
|
-
operation.set_configuration(configuration)
|
|
382
|
-
operation.execute(self.sat_model)
|
|
383
|
-
return operation.get_result()
|
|
384
|
-
except FlamaException as exception:
|
|
385
|
-
logger.error("Error: %s", exception)
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
def satisfiable_configuration(
|
|
389
|
-
self,
|
|
390
|
-
configuration_path: str,
|
|
391
|
-
full_configuration: bool = False,
|
|
392
|
-
backend: Optional[str] = None,
|
|
393
|
-
) -> Union[None, bool]:
|
|
394
|
-
"""
|
|
395
|
-
This is a product that is produced from a valid configuration of features. A valid
|
|
396
|
-
product satisfies all the constraints and dependencies in the feature model.
|
|
397
136
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
""
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
except FlamaException as exception:
|
|
477
|
-
logger.error("Error: %s", exception)
|
|
478
|
-
return None
|
|
479
|
-
|
|
480
|
-
# BDD-based operations
|
|
481
|
-
|
|
482
|
-
def homogeneity(self) -> Union[None, float]:
|
|
483
|
-
"""
|
|
484
|
-
Measures how similar the products of the feature model are to each other. It is
|
|
485
|
-
computed as the average commonality factor across all features. A value of 1.0 means
|
|
486
|
-
all products are identical; lower values indicate more diversity.
|
|
487
|
-
"""
|
|
488
|
-
try:
|
|
489
|
-
self._transform_to_bdd()
|
|
490
|
-
return self.discover_metamodel.use_operation(
|
|
491
|
-
self.bdd_model, "BDDHomogeneity"
|
|
492
|
-
).get_result()
|
|
493
|
-
except FlamaException as exception:
|
|
494
|
-
logger.error("Error: %s", exception)
|
|
495
|
-
return None
|
|
496
|
-
|
|
497
|
-
def product_distribution(self) -> Union[None, List[int]]:
|
|
498
|
-
"""
|
|
499
|
-
Returns the distribution of products by number of activated features. The value at
|
|
500
|
-
index i is the count of valid configurations that have exactly i features selected.
|
|
501
|
-
"""
|
|
502
|
-
try:
|
|
503
|
-
self._transform_to_bdd()
|
|
504
|
-
return self.discover_metamodel.use_operation(
|
|
505
|
-
self.bdd_model, "BDDProductDistribution"
|
|
506
|
-
).get_result()
|
|
507
|
-
except FlamaException as exception:
|
|
508
|
-
logger.error("Error: %s", exception)
|
|
509
|
-
return None
|
|
510
|
-
|
|
511
|
-
def variability(self) -> Union[None, Tuple[float, float]]:
|
|
512
|
-
"""
|
|
513
|
-
Returns a tuple of (total_variability, partial_variability). Total variability is the
|
|
514
|
-
ratio of valid configurations to the theoretical maximum; partial variability is the
|
|
515
|
-
ratio relative to only the variant features.
|
|
516
|
-
"""
|
|
517
|
-
try:
|
|
518
|
-
self._transform_to_bdd()
|
|
519
|
-
return self.discover_metamodel.use_operation(
|
|
520
|
-
self.bdd_model, "BDDVariability"
|
|
521
|
-
).get_result()
|
|
522
|
-
except FlamaException as exception:
|
|
523
|
-
logger.error("Error: %s", exception)
|
|
524
|
-
return None
|
|
525
|
-
|
|
526
|
-
def variant_features(self) -> Union[None, List[str]]:
|
|
527
|
-
"""
|
|
528
|
-
Returns the features that are neither core nor dead — they appear in some but not all
|
|
529
|
-
valid configurations. These are the features that actually vary across products.
|
|
530
|
-
"""
|
|
531
|
-
try:
|
|
532
|
-
self._transform_to_bdd()
|
|
533
|
-
return self.discover_metamodel.use_operation(
|
|
534
|
-
self.bdd_model, "BDDVariantFeatures"
|
|
535
|
-
).get_result()
|
|
536
|
-
except FlamaException as exception:
|
|
537
|
-
logger.error("Error: %s", exception)
|
|
538
|
-
return None
|
|
539
|
-
|
|
540
|
-
def unique_features(self) -> Union[None, List[str]]:
|
|
541
|
-
"""
|
|
542
|
-
Returns features that appear in exactly one valid configuration. These features
|
|
543
|
-
uniquely identify a single product in the product line.
|
|
544
|
-
"""
|
|
545
|
-
try:
|
|
546
|
-
self._transform_to_bdd()
|
|
547
|
-
return self.discover_metamodel.use_operation(
|
|
548
|
-
self.bdd_model, "BDDUniqueFeatures"
|
|
549
|
-
).get_result()
|
|
550
|
-
except FlamaException as exception:
|
|
551
|
-
logger.error("Error: %s", exception)
|
|
552
|
-
return None
|
|
553
|
-
|
|
554
|
-
def pure_optional_features(self) -> Union[None, List[str]]:
|
|
555
|
-
"""
|
|
556
|
-
Returns features with a feature inclusion probability of exactly 0.5, meaning they
|
|
557
|
-
are selected in exactly half of the valid configurations. These are the most
|
|
558
|
-
unconstrained optional features.
|
|
559
|
-
"""
|
|
560
|
-
try:
|
|
561
|
-
self._transform_to_bdd()
|
|
562
|
-
return self.discover_metamodel.use_operation(
|
|
563
|
-
self.bdd_model, "BDDPureOptionalFeatures"
|
|
564
|
-
).get_result()
|
|
565
|
-
except FlamaException as exception:
|
|
566
|
-
logger.error("Error: %s", exception)
|
|
567
|
-
return None
|
|
568
|
-
|
|
569
|
-
def feature_inclusion_probability(self) -> Union[None, Dict[str, float]]:
|
|
570
|
-
"""
|
|
571
|
-
Returns a mapping from each feature name to its inclusion probability: the fraction
|
|
572
|
-
of valid configurations in which that feature is selected. Core features have
|
|
573
|
-
probability 1.0 and dead features have probability 0.0.
|
|
574
|
-
"""
|
|
575
|
-
try:
|
|
576
|
-
self._transform_to_bdd()
|
|
577
|
-
return self.discover_metamodel.use_operation(
|
|
578
|
-
self.bdd_model, "BDDFeatureInclusionProbability"
|
|
579
|
-
).get_result()
|
|
580
|
-
except FlamaException as exception:
|
|
581
|
-
logger.error("Error: %s", exception)
|
|
582
|
-
return None
|
|
583
|
-
|
|
584
|
-
def configurations_with_n_features(self, n: int) -> Union[None, List[Configuration]]:
|
|
585
|
-
"""
|
|
586
|
-
Returns all valid configurations that have exactly n features selected.
|
|
587
|
-
"""
|
|
588
|
-
try:
|
|
589
|
-
self._transform_to_bdd()
|
|
590
|
-
operation = self.discover_metamodel.get_operation(
|
|
591
|
-
self.bdd_model, "BDDConfigurationsWithNFeatures"
|
|
592
|
-
)
|
|
593
|
-
operation.set_n_features(n)
|
|
594
|
-
operation.execute(self.bdd_model)
|
|
595
|
-
return list(operation.get_result())
|
|
596
|
-
except FlamaException as exception:
|
|
597
|
-
logger.error("Error: %s", exception)
|
|
598
|
-
return None
|
|
599
|
-
|
|
600
|
-
def sampling(
|
|
601
|
-
self, size: int, with_replacement: bool = False, backend: Optional[str] = None
|
|
602
|
-
) -> Union[None, List[Configuration]]:
|
|
603
|
-
"""
|
|
604
|
-
Returns a random sample of valid configurations of the given size. When
|
|
605
|
-
with_replacement is True, the same configuration may appear more than once.
|
|
606
|
-
|
|
607
|
-
``backend`` selects the analysis plugin; defaults to bdd. Only sat and bdd
|
|
608
|
-
implement sampling.
|
|
609
|
-
"""
|
|
610
|
-
try:
|
|
611
|
-
model = self._select_backend(backend, Backend.BDD)
|
|
612
|
-
operation = self.discover_metamodel.get_operation(model, "Sampling")
|
|
613
|
-
operation.set_sample_size(size)
|
|
614
|
-
operation.set_with_replacement(with_replacement)
|
|
615
|
-
operation.execute(model)
|
|
616
|
-
return operation.get_result()
|
|
617
|
-
except FlamaException as exception:
|
|
618
|
-
logger.error("Error: %s", exception)
|
|
619
|
-
return None
|
|
620
|
-
|
|
621
|
-
# Z3-based operations
|
|
622
|
-
|
|
623
|
-
def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]:
|
|
624
|
-
"""
|
|
625
|
-
Returns the value bounds for all typed (Integer, Real, String) attribute features in
|
|
626
|
-
the model. Each entry maps a feature name to a dict with 'min', 'max', and 'bounded'
|
|
627
|
-
keys. Requires the z3 metamodel plugin.
|
|
628
|
-
"""
|
|
629
|
-
try:
|
|
630
|
-
self._transform_to_z3()
|
|
631
|
-
return self.discover_metamodel.use_operation(
|
|
632
|
-
self.z3_model, "Z3AllFeatureBounds"
|
|
633
|
-
).get_result()
|
|
634
|
-
except FlamaException as exception:
|
|
635
|
-
logger.error("Error: %s", exception)
|
|
636
|
-
return None
|
|
637
|
-
|
|
638
|
-
def feature_bounds(self, variable_name: str) -> Union[None, Dict[str, Any]]:
|
|
639
|
-
"""
|
|
640
|
-
Returns the value bounds for a specific typed attribute feature. The result dict
|
|
641
|
-
contains 'min', 'max', and 'bounded' keys. Requires the z3 metamodel plugin.
|
|
642
|
-
"""
|
|
643
|
-
try:
|
|
644
|
-
self._transform_to_z3()
|
|
645
|
-
operation = self.discover_metamodel.get_operation(self.z3_model, "Z3FeatureBounds")
|
|
646
|
-
operation.set_variable_name(variable_name)
|
|
647
|
-
operation.execute(self.z3_model)
|
|
648
|
-
return operation.get_result()
|
|
649
|
-
except FlamaException as exception:
|
|
650
|
-
logger.error("Error: %s", exception)
|
|
651
|
-
return None
|
|
652
|
-
|
|
653
|
-
# Diagnosis-based operations
|
|
654
|
-
|
|
655
|
-
def diagnosis(
|
|
656
|
-
self,
|
|
657
|
-
configuration_path: str,
|
|
658
|
-
test_case_path: Optional[str] = None,
|
|
659
|
-
max_diagnoses: Optional[int] = None,
|
|
660
|
-
) -> Union[None, List[str]]:
|
|
661
|
-
"""
|
|
662
|
-
Returns a list of diagnoses explaining why a configuration is not valid against the
|
|
663
|
-
feature model: each diagnosis is a minimal set of constraints whose removal would
|
|
664
|
-
resolve the inconsistency. Requires the pysat_diagnosis plugin.
|
|
665
|
-
|
|
666
|
-
``configuration_path`` accepts a configuration file path, a ``{feature: value}``
|
|
667
|
-
mapping, or a Configuration object. ``test_case_path`` is optional and, when given,
|
|
668
|
-
diagnoses against that expected outcome instead; it accepts the same input types.
|
|
669
|
-
"""
|
|
670
|
-
try:
|
|
671
|
-
self._transform_to_diagnosis()
|
|
672
|
-
operation = self.discover_metamodel.get_operation(
|
|
673
|
-
self.diagnosis_model, "PySATDiagnosis"
|
|
674
|
-
)
|
|
675
|
-
if configuration_path is not None:
|
|
676
|
-
operation.set_configuration(self._as_configuration(configuration_path))
|
|
677
|
-
if test_case_path is not None:
|
|
678
|
-
operation.set_test_case(self._as_configuration(test_case_path))
|
|
679
|
-
if max_diagnoses is not None:
|
|
680
|
-
operation.set_max_diagnoses(max_diagnoses)
|
|
681
|
-
operation.execute(self.diagnosis_model)
|
|
682
|
-
return operation.get_result()
|
|
683
|
-
except FlamaException as exception:
|
|
684
|
-
logger.error("Error: %s", exception)
|
|
685
|
-
return None
|
|
686
|
-
|
|
687
|
-
def conflict(
|
|
688
|
-
self,
|
|
689
|
-
configuration_path: str,
|
|
690
|
-
test_case_path: Optional[str] = None,
|
|
691
|
-
max_conflicts: Optional[int] = None,
|
|
692
|
-
) -> Union[None, List[str]]:
|
|
693
|
-
"""
|
|
694
|
-
Returns a list of conflict sets: minimal subsets of the model constraints that are
|
|
695
|
-
inconsistent with the given configuration. Requires the pysat_diagnosis plugin.
|
|
696
|
-
|
|
697
|
-
``configuration_path`` accepts a configuration file path, a ``{feature: value}``
|
|
698
|
-
mapping, or a Configuration object. ``test_case_path`` is optional and, when given,
|
|
699
|
-
looks for conflicts against that expected outcome instead; same input types.
|
|
700
|
-
"""
|
|
701
|
-
try:
|
|
702
|
-
self._transform_to_diagnosis()
|
|
703
|
-
operation = self.discover_metamodel.get_operation(
|
|
704
|
-
self.diagnosis_model, "PySATConflict"
|
|
705
|
-
)
|
|
706
|
-
if configuration_path is not None:
|
|
707
|
-
operation.set_configuration(self._as_configuration(configuration_path))
|
|
708
|
-
if test_case_path is not None:
|
|
709
|
-
operation.set_test_case(self._as_configuration(test_case_path))
|
|
710
|
-
if max_conflicts is not None:
|
|
711
|
-
operation.set_max_conflicts(max_conflicts)
|
|
712
|
-
operation.execute(self.diagnosis_model)
|
|
713
|
-
return operation.get_result()
|
|
714
|
-
except FlamaException as exception:
|
|
715
|
-
logger.error("Error: %s", exception)
|
|
716
|
-
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.
|
|
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,11 +8,11 @@ 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.
|
|
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.
|
|
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"
|
|
@@ -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=S9KJB8u4Ww5_6KQYs70iwQFbtDm08J7UvE7vxAB6DeE,6609
|
|
2
|
-
flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
|
|
3
|
-
flamapy/interfaces/python/flamapy_feature_model.py,sha256=ojxMc3XtAvnBs9T7ETLo1rFpt1g0XRMc8MRdHf1HJ2w,30598
|
|
4
|
-
flamapy-2.6.0.dev4.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
5
|
-
flamapy-2.6.0.dev4.dist-info/METADATA,sha256=jbaO1-9RFf15ynsn6ftjbaoy0eV8Mp3JjwAKSowwD3o,1265
|
|
6
|
-
flamapy-2.6.0.dev4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
-
flamapy-2.6.0.dev4.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
|
|
8
|
-
flamapy-2.6.0.dev4.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
|
|
9
|
-
flamapy-2.6.0.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|