flamapy 2.6.0.dev11__py3-none-any.whl → 2.9.0.dev0__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 +114 -10
- flamapy/interfaces/python/_stub.py +26 -3
- flamapy/interfaces/python/flamapy_feature_model.py +292 -33
- flamapy/interfaces/python/flamapy_feature_model.pyi +62 -41
- {flamapy-2.6.0.dev11.dist-info → flamapy-2.9.0.dev0.dist-info}/METADATA +16 -6
- flamapy-2.9.0.dev0.dist-info/RECORD +11 -0
- {flamapy-2.6.0.dev11.dist-info → flamapy-2.9.0.dev0.dist-info}/WHEEL +1 -1
- flamapy-2.6.0.dev11.dist-info/RECORD +0 -11
- {flamapy-2.6.0.dev11.dist-info → flamapy-2.9.0.dev0.dist-info}/entry_points.txt +0 -0
- {flamapy-2.6.0.dev11.dist-info → flamapy-2.9.0.dev0.dist-info}/licenses/LICENSE +0 -0
- {flamapy-2.6.0.dev11.dist-info → flamapy-2.9.0.dev0.dist-info}/top_level.txt +0 -0
flamapy/commands/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from shutil import copytree
|
|
|
10
10
|
from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
|
|
11
11
|
from types import FunctionType
|
|
12
12
|
|
|
13
|
+
from flamapy.core.exceptions import FlamaException
|
|
13
14
|
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
14
15
|
|
|
15
16
|
# List to store registered commands and their arguments
|
|
@@ -57,7 +58,8 @@ def extract_commands(
|
|
|
57
58
|
if name.startswith("_"):
|
|
58
59
|
continue
|
|
59
60
|
if getattr(method, "_facade_kind", "operation") != "operation":
|
|
60
|
-
|
|
61
|
+
# producers/transformers return models: Python API and REST (as UVL), not the CLI
|
|
62
|
+
continue
|
|
61
63
|
docstring: Optional[str] = method.__doc__
|
|
62
64
|
signature = inspect.signature(method)
|
|
63
65
|
# Exclude 'self' from parameters
|
|
@@ -67,6 +69,53 @@ def extract_commands(
|
|
|
67
69
|
return commands
|
|
68
70
|
|
|
69
71
|
|
|
72
|
+
@command(
|
|
73
|
+
"plan",
|
|
74
|
+
"Shows which backend the query planner would pick for an operation on a model. "
|
|
75
|
+
"The planner is opt-in: run operations with --backend auto (or FLAMAPY_BACKEND=auto) "
|
|
76
|
+
"to actually use it.",
|
|
77
|
+
("operation", str, "Facade operation name, e.g. configurations_number"),
|
|
78
|
+
("model_path", str, "Path to the feature model file"),
|
|
79
|
+
)
|
|
80
|
+
def plan_command(args): # type: ignore
|
|
81
|
+
fm = FLAMAFeatureModel(args.model_path)
|
|
82
|
+
try:
|
|
83
|
+
chosen = fm.plan(args.operation)
|
|
84
|
+
except FlamaException as error:
|
|
85
|
+
print(f"No compatible backend: {error}")
|
|
86
|
+
return None
|
|
87
|
+
data = chosen.to_dict(fm.fm_model.get_extension())
|
|
88
|
+
print(f"operation: {args.operation}")
|
|
89
|
+
print(f"chosen backend: {data['plugin']} (extension '{data['backend']}')")
|
|
90
|
+
print(f"transformation chain: {' -> '.join(data['transformation_chain'])}")
|
|
91
|
+
print(f"estimated cost: {data['estimated_cost_ms']} ms (heuristic)")
|
|
92
|
+
print(f"rationale: {data['rationale']}")
|
|
93
|
+
if data['alternatives']:
|
|
94
|
+
print(f"alternatives: {', '.join(data['alternatives'])}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@command(
|
|
99
|
+
"cache",
|
|
100
|
+
"Inspects or clears the compilation-artifact cache (opt-in: set FLAMAPY_CACHE=1 to "
|
|
101
|
+
"cache compiled backends such as BDDs across runs).",
|
|
102
|
+
("action", str, "'info' or 'clear'"),
|
|
103
|
+
)
|
|
104
|
+
def cache_command(args): # type: ignore
|
|
105
|
+
from flamapy.core import cache as artifact_cache # noqa: PLC0415
|
|
106
|
+
|
|
107
|
+
if args.action == "info":
|
|
108
|
+
stats = artifact_cache.info()
|
|
109
|
+
print(f"enabled: {stats['enabled']} (set FLAMAPY_CACHE=1 to enable)")
|
|
110
|
+
print(f"directory: {stats['directory']}")
|
|
111
|
+
print(f"artifacts: {stats['artifacts']} ({stats['size_mb']} MB)")
|
|
112
|
+
elif args.action == "clear":
|
|
113
|
+
removed = artifact_cache.clear()
|
|
114
|
+
print(f"removed {removed} artifact(s)")
|
|
115
|
+
else:
|
|
116
|
+
print(f"Unknown cache action '{args.action}'; use 'info' or 'clear'.")
|
|
117
|
+
|
|
118
|
+
|
|
70
119
|
@command(
|
|
71
120
|
"generate_plugin",
|
|
72
121
|
"Generates a new plugin skeleton to implement your custom operations. "
|
|
@@ -108,7 +157,9 @@ def generate_plugin(args): # type: ignore
|
|
|
108
157
|
lines = file.readlines()
|
|
109
158
|
with open(copy_file, "w", encoding="utf-8") as filewrite:
|
|
110
159
|
for line in lines:
|
|
111
|
-
out_line = line.replace("
|
|
160
|
+
out_line = (line.replace("__NAME_LOWER__", name)
|
|
161
|
+
.replace("__NAME__", name.capitalize())
|
|
162
|
+
.replace("__EXT__", ext))
|
|
112
163
|
filewrite.write(out_line)
|
|
113
164
|
|
|
114
165
|
os.rename(
|
|
@@ -156,6 +207,12 @@ def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
|
|
|
156
207
|
default=param.default,
|
|
157
208
|
help=f"Optional {arg_type.__name__}",
|
|
158
209
|
)
|
|
210
|
+
subparser.add_argument(
|
|
211
|
+
"-v", "--verbose", action="store_true",
|
|
212
|
+
help="Also print the result's provenance (plugin, backend, exact, wall time)")
|
|
213
|
+
subparser.add_argument(
|
|
214
|
+
"--timeout", type=float, default=None, metavar="SECONDS",
|
|
215
|
+
help="Abort if the operation exceeds this wall-clock time (exit code 124)")
|
|
159
216
|
subparser.set_defaults(func=method, method_name=name, parameters=parameters)
|
|
160
217
|
|
|
161
218
|
|
|
@@ -168,19 +225,66 @@ def setup_manual_commands(subparsers, manual_commands): # type: ignore
|
|
|
168
225
|
subparser.set_defaults(func=func)
|
|
169
226
|
|
|
170
227
|
|
|
228
|
+
def _run_with_timeout(cls_instance, method_name, command_args, timeout): # type: ignore
|
|
229
|
+
"""Run the operation asynchronously and enforce the wall-clock deadline (exit 124)."""
|
|
230
|
+
from flamapy.core.exceptions import OperationTimeout # noqa: PLC0415
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
job = cls_instance.run_async(method_name, **command_args)
|
|
234
|
+
result = job.result(timeout=timeout)
|
|
235
|
+
except OperationTimeout as error:
|
|
236
|
+
print(f"Timeout: {error}", file=sys.stderr)
|
|
237
|
+
sys.exit(124)
|
|
238
|
+
except FlamaException as error:
|
|
239
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
240
|
+
return None
|
|
241
|
+
cls_instance.last_result = job.envelope()
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _print_provenance(envelope) -> None: # type: ignore
|
|
246
|
+
print("--- provenance ---", file=sys.stderr)
|
|
247
|
+
print(f"operation: {envelope.operation} plugin: {envelope.plugin} "
|
|
248
|
+
f"backend: {envelope.backend}", file=sys.stderr)
|
|
249
|
+
print(f"exact: {envelope.exact} wall_time: {envelope.wall_time:.6f}s "
|
|
250
|
+
f"cache_hit: {envelope.cache_hit}", file=sys.stderr)
|
|
251
|
+
if envelope.planner is not None:
|
|
252
|
+
print(f"planner: {envelope.planner}", file=sys.stderr)
|
|
253
|
+
print(f"model_hash: {envelope.model_hash}", file=sys.stderr)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _execute_dynamic(args: argparse.Namespace) -> None:
|
|
257
|
+
cls_instance = FLAMAFeatureModel(args.model_path)
|
|
258
|
+
method_parameters = [param.name for param in args.parameters]
|
|
259
|
+
command_args = {k: v for k, v in vars(args).items() if k in method_parameters}
|
|
260
|
+
descriptor = getattr(
|
|
261
|
+
getattr(type(cls_instance), args.method_name), "_facade_descriptor", None)
|
|
262
|
+
timeout = getattr(args, "timeout", None)
|
|
263
|
+
if timeout is not None:
|
|
264
|
+
result = _run_with_timeout(cls_instance, args.method_name, command_args, timeout)
|
|
265
|
+
elif (getattr(descriptor, "streaming", False)
|
|
266
|
+
and args.method_name == "configurations"
|
|
267
|
+
and command_args.get("backend") == "sat"):
|
|
268
|
+
# Explicitly-chosen streaming backend: print each configuration as the
|
|
269
|
+
# solver produces it (the default bdd output is unchanged).
|
|
270
|
+
for configuration in cls_instance.iter_configurations(backend="sat"):
|
|
271
|
+
print(configuration)
|
|
272
|
+
result = None
|
|
273
|
+
else:
|
|
274
|
+
method = getattr(cls_instance, args.method_name)
|
|
275
|
+
result = method(**command_args)
|
|
276
|
+
if result is not None:
|
|
277
|
+
print(result)
|
|
278
|
+
if getattr(args, "verbose", False) and cls_instance.last_result is not None:
|
|
279
|
+
_print_provenance(cls_instance.last_result)
|
|
280
|
+
|
|
281
|
+
|
|
171
282
|
def execute_command(args: argparse.Namespace) -> None:
|
|
172
283
|
try:
|
|
173
284
|
if hasattr(args, "method_name"):
|
|
174
|
-
|
|
175
|
-
method_parameters = [param.name for param in args.parameters]
|
|
176
|
-
command_args = {k: v for k, v in vars(args).items() if k in method_parameters}
|
|
177
|
-
method = getattr(cls_instance, args.method_name)
|
|
178
|
-
result = method(**command_args)
|
|
179
|
-
if result is not None:
|
|
180
|
-
print(result)
|
|
285
|
+
_execute_dynamic(args)
|
|
181
286
|
else:
|
|
182
287
|
func = args.func
|
|
183
|
-
command_args = {k: v for k, v in vars(args).items() if k != "func"}
|
|
184
288
|
result = func(args)
|
|
185
289
|
if result is not None:
|
|
186
290
|
print(result)
|
|
@@ -5,6 +5,7 @@ This module renders a ``.pyi`` stub from the same OperationDescriptors, giving a
|
|
|
5
5
|
mypy the real signatures + return types. Run ``python -m flamapy.interfaces.python._stub`` to
|
|
6
6
|
regenerate; a test asserts the shipped stub matches this output so it never drifts.
|
|
7
7
|
"""
|
|
8
|
+
import re
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
from flamapy.core.discover import DiscoverMetamodels
|
|
@@ -14,8 +15,10 @@ _HEADER = '''\
|
|
|
14
15
|
# Auto-generated by flamapy.interfaces.python._stub — do not edit by hand.
|
|
15
16
|
# Regenerate with: python -m flamapy.interfaces.python._stub
|
|
16
17
|
from enum import Enum
|
|
17
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple, Union # noqa: F401
|
|
18
19
|
|
|
20
|
+
from flamapy.core.operations import OperationResult
|
|
21
|
+
from flamapy.core.planner import Plan
|
|
19
22
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
20
23
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
21
24
|
|
|
@@ -25,13 +28,25 @@ class Backend(str, Enum):
|
|
|
25
28
|
BDD = ...
|
|
26
29
|
Z3 = ...
|
|
27
30
|
SHARPSAT = ...
|
|
31
|
+
SDD = ...
|
|
32
|
+
DNNF = ...
|
|
33
|
+
AUTO = ...
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
class FLAMAFeatureModel:
|
|
31
37
|
model_path: str
|
|
32
38
|
fm_model: FeatureModel
|
|
33
39
|
backend: Optional[Backend]
|
|
34
|
-
|
|
40
|
+
allow_approximate: Optional[bool]
|
|
41
|
+
raise_errors: Optional[bool]
|
|
42
|
+
last_result: Optional[OperationResult[Any]]
|
|
43
|
+
def __init__(self, model_path: str, backend: Optional[str] = ...,
|
|
44
|
+
*, allow_approximate: Optional[bool] = ...,
|
|
45
|
+
raise_errors: Optional[bool] = ...) -> None: ...
|
|
46
|
+
def plan(self, operation: str, *, allow_approximate: Optional[bool] = ...) -> Plan: ...
|
|
47
|
+
def _plan_operation(self, operation_name: str, allow_approximate: bool = ...) -> Plan: ...
|
|
48
|
+
def run_async(self, operation: str, **kwargs: Any) -> Any: ...
|
|
49
|
+
def iter_configurations(self, backend: Optional[str] = ...) -> Any: ...
|
|
35
50
|
'''
|
|
36
51
|
|
|
37
52
|
|
|
@@ -52,9 +67,16 @@ def _method_signature(descriptor: OperationDescriptor) -> str:
|
|
|
52
67
|
params.append(f'{input_spec.name}: {annotation} = ...')
|
|
53
68
|
if descriptor.selectable_backend:
|
|
54
69
|
params.append('backend: Optional[str] = ...')
|
|
70
|
+
params.append('*')
|
|
71
|
+
params.append('allow_approximate: Optional[bool] = ...')
|
|
55
72
|
return ', '.join(params)
|
|
56
73
|
|
|
57
74
|
|
|
75
|
+
def _return_annotation(descriptor: OperationDescriptor) -> str:
|
|
76
|
+
# Descriptors spell nullable returns 'Union[None, X]'; the stub uses 'Optional[X]'.
|
|
77
|
+
return re.sub(r'^Union\[None, (.*)\]$', r'Optional[\1]', descriptor.returns)
|
|
78
|
+
|
|
79
|
+
|
|
58
80
|
def render_stub(operations: 'dict[str, OperationDescriptor] | None' = None) -> str:
|
|
59
81
|
if operations is None:
|
|
60
82
|
operations = DiscoverMetamodels().available_operations()
|
|
@@ -64,7 +86,8 @@ def render_stub(operations: 'dict[str, OperationDescriptor] | None' = None) -> s
|
|
|
64
86
|
if descriptor.kind == 'producer':
|
|
65
87
|
lines.append(' @staticmethod')
|
|
66
88
|
lines.append(
|
|
67
|
-
f' def {name}({_method_signature(descriptor)})
|
|
89
|
+
f' def {name}({_method_signature(descriptor)}) '
|
|
90
|
+
f'-> {_return_annotation(descriptor)}: ...')
|
|
68
91
|
return '\n'.join(lines) + '\n'
|
|
69
92
|
|
|
70
93
|
|
|
@@ -10,15 +10,31 @@ per-operation or per-backend code here. Static typing is provided by a generated
|
|
|
10
10
|
"""
|
|
11
11
|
import inspect
|
|
12
12
|
import logging
|
|
13
|
+
import os
|
|
14
|
+
import threading
|
|
13
15
|
from enum import Enum
|
|
14
|
-
from typing import Any, Dict, Optional, Union
|
|
16
|
+
from typing import Any, Dict, Iterator, Optional, Union
|
|
15
17
|
|
|
18
|
+
import dataclasses
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
from flamapy.core import cache as artifact_cache
|
|
16
22
|
from flamapy.core.discover import DiscoverMetamodels
|
|
17
|
-
from flamapy.core.exceptions import FlamaException
|
|
23
|
+
from flamapy.core.exceptions import FlamaException, OperationTimeout, PluginNotFound
|
|
24
|
+
from flamapy.core.execution import Interruptible, Job, default_engine, run_and_wrap
|
|
18
25
|
from flamapy.core.models import VariabilityModel
|
|
19
|
-
from flamapy.core.
|
|
26
|
+
from flamapy.core.models.language_level import LanguageLevel
|
|
27
|
+
from flamapy.core.operations import (
|
|
28
|
+
OperationDescriptor,
|
|
29
|
+
OperationResult,
|
|
30
|
+
StreamingOperation,
|
|
31
|
+
)
|
|
32
|
+
from flamapy.core.planner import Plan, plan as plan_backend
|
|
20
33
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
21
34
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
35
|
+
from flamapy.metamodels.fm_metamodel.operations.fm_language_level import (
|
|
36
|
+
calculate_language_level,
|
|
37
|
+
)
|
|
22
38
|
|
|
23
39
|
logger = logging.getLogger(__name__)
|
|
24
40
|
|
|
@@ -33,12 +49,38 @@ class Backend(str, Enum):
|
|
|
33
49
|
SAT = "sat"
|
|
34
50
|
BDD = "bdd"
|
|
35
51
|
Z3 = "z3"
|
|
52
|
+
# Optional backends, installed via the flamapy extras of the same name
|
|
53
|
+
# (e.g. pip install "flamapy[sharpsat]", or "flamapy[all-solvers]" for all three).
|
|
36
54
|
SHARPSAT = "sharpsat"
|
|
55
|
+
SDD = "sdd"
|
|
56
|
+
DNNF = "dnnf"
|
|
57
|
+
# Opt-in: let the query planner pick the backend per operation, from the plugins'
|
|
58
|
+
# capability manifests (language level, scale, cost). Also enabled process-wide by
|
|
59
|
+
# setting FLAMAPY_BACKEND=auto. Becomes the default in flamapy 3.0.
|
|
60
|
+
AUTO = "auto"
|
|
37
61
|
|
|
38
62
|
|
|
39
63
|
# Facade backend name -> fm-to-backend transformation extension (the only non-identity mapping).
|
|
40
64
|
_BACKEND_ALIAS = {"sat": "pysat"}
|
|
41
65
|
|
|
66
|
+
# Operations that ran on a fixed backend in 2.6 and accept ``backend=`` since 2.9. In 2.x only a
|
|
67
|
+
# per-call ``backend=`` changes them: the workspace default and FLAMAPY_BACKEND do not, so code
|
|
68
|
+
# that set a default backend in 2.6 keeps the same results (the default would otherwise move
|
|
69
|
+
# atomic sets from the tree to SAT, or send commonality to a backend without it). 3.0 applies
|
|
70
|
+
# the defaults to them like to every other selectable operation.
|
|
71
|
+
_SELECTABLE_SINCE_2_9 = frozenset({'atomic_sets', 'commonality'})
|
|
72
|
+
|
|
73
|
+
_TRUE_VALUES = ('1', 'true', 'yes', 'on')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _env_flag(name: str) -> bool:
|
|
77
|
+
return os.environ.get(name, '').strip().lower() in _TRUE_VALUES
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _raise_errors(setting: Optional[bool]) -> bool:
|
|
81
|
+
"""The model's ``raise_errors`` when set, else the FLAMAPY_RAISE_ERRORS variable."""
|
|
82
|
+
return setting if setting is not None else _env_flag('FLAMAPY_RAISE_ERRORS')
|
|
83
|
+
|
|
42
84
|
|
|
43
85
|
class FLAMAFeatureModel:
|
|
44
86
|
"""A feature model together with the analysis operations discovered from the installed plugins.
|
|
@@ -47,18 +89,46 @@ class FLAMAFeatureModel:
|
|
|
47
89
|
them as normal methods, e.g. ``FLAMAFeatureModel(path).satisfiable()``.
|
|
48
90
|
"""
|
|
49
91
|
|
|
50
|
-
def __init__(self, model_path: str, backend: Optional[str] = None
|
|
92
|
+
def __init__(self, model_path: str, backend: Optional[str] = None, *,
|
|
93
|
+
allow_approximate: Optional[bool] = None,
|
|
94
|
+
raise_errors: Optional[bool] = None) -> None:
|
|
51
95
|
"""``model_path`` is the filesystem path to the model (UVL, FaMaXML or FeatureIDE).
|
|
52
96
|
|
|
53
|
-
``backend`` optionally sets a workspace-wide default backend ("sat", "bdd" or "z3"
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
``backend`` optionally sets a workspace-wide default backend ("sat", "bdd" or "z3",
|
|
98
|
+
plus "sharpsat"/"sdd"/"dnnf" when the corresponding optional flamapy extra is
|
|
99
|
+
installed) for every operation that supports more than one. When left as ``None``
|
|
100
|
+
each operation keeps its historical default, so existing code behaves exactly as
|
|
101
|
+
before. ``atomic_sets`` and ``commonality``, selectable only since 2.9, follow just a
|
|
102
|
+
per-call ``backend=`` in 2.x.
|
|
103
|
+
|
|
104
|
+
``allow_approximate`` lets the ``'auto'`` backend choose approximate implementations
|
|
105
|
+
(e.g. sharpsat's ApproxMC counting, or the structural core features that ignore
|
|
106
|
+
cross-tree constraints); by default the planner only picks exact ones. It has no
|
|
107
|
+
effect on other backends. ``None`` falls back to ``FLAMAPY_ALLOW_APPROXIMATE=1``; a
|
|
108
|
+
per-call ``allow_approximate=`` overrides both.
|
|
109
|
+
|
|
110
|
+
``raise_errors`` opts into the 3.0 error contract today: a failing operation raises
|
|
111
|
+
its ``FlamaException`` instead of logging it and returning ``None``. ``None`` falls
|
|
112
|
+
back to ``FLAMAPY_RAISE_ERRORS=1``, which also applies to the producers (e.g.
|
|
113
|
+
``generate_random_feature_model``).
|
|
114
|
+
|
|
115
|
+
Both flags are keyword-only.
|
|
56
116
|
"""
|
|
57
117
|
self.model_path = model_path
|
|
58
118
|
self.discover_metamodel = DiscoverMetamodels()
|
|
59
119
|
self.fm_model = self._read(model_path)
|
|
60
120
|
self.backend = Backend(backend) if backend is not None else None
|
|
121
|
+
self.allow_approximate = allow_approximate
|
|
122
|
+
self.raise_errors = raise_errors
|
|
61
123
|
self._backend_models: Dict[str, VariabilityModel] = {}
|
|
124
|
+
# Provenance envelope of the last executed operation (value, plugin, backend, exact,
|
|
125
|
+
# wall time, model hash). Purely informational in 2.9; methods still return bare
|
|
126
|
+
# values. In 3.0 the envelope becomes the return type.
|
|
127
|
+
self.last_result: Optional[OperationResult] = None
|
|
128
|
+
# How the last operation's backend was chosen: None (historical default),
|
|
129
|
+
# 'explicit', or 'auto: <planner rationale>'.
|
|
130
|
+
self._last_planner: Optional[str] = None
|
|
131
|
+
self._model_language_level: Optional[LanguageLevel] = None
|
|
62
132
|
|
|
63
133
|
def _read(self, model_path: str) -> FeatureModel:
|
|
64
134
|
return self.discover_metamodel.use_transformation_t2m(model_path, "fm")
|
|
@@ -78,61 +148,227 @@ class FLAMAFeatureModel:
|
|
|
78
148
|
return self.discover_metamodel.use_transformation_t2m(configuration, "configuration")
|
|
79
149
|
|
|
80
150
|
def _model_for(
|
|
81
|
-
self, descriptor: OperationDescriptor, backend_arg: Optional[str]
|
|
151
|
+
self, descriptor: OperationDescriptor, backend_arg: Optional[str],
|
|
152
|
+
allow_approximate: Optional[bool] = None,
|
|
82
153
|
) -> VariabilityModel:
|
|
83
154
|
"""Resolve the operation's target model, lazily transforming (and caching) fm->backend.
|
|
84
155
|
|
|
85
|
-
The backend is the per-call one > workspace default > the
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
156
|
+
The backend is the per-call one > workspace default > the ``FLAMAPY_BACKEND``
|
|
157
|
+
environment variable > the operation's default (for backend-selectable ops), or
|
|
158
|
+
simply the fixed default. The value ``'auto'`` (opt-in) hands the choice to the
|
|
159
|
+
query planner. When the backend resolves to ``None`` (an fm-level op, or a
|
|
160
|
+
selectable op with no backend requested) the operation runs on the feature model
|
|
161
|
+
directly. Otherwise the name is mapped to a transformation extension (only
|
|
89
162
|
``sat -> pysat`` differs) and cached per extension.
|
|
90
163
|
"""
|
|
164
|
+
self._last_planner = None
|
|
165
|
+
defaults_apply = (descriptor.selectable_backend
|
|
166
|
+
and descriptor.name not in _SELECTABLE_SINCE_2_9)
|
|
167
|
+
workspace_default = (self.backend or os.environ.get('FLAMAPY_BACKEND')
|
|
168
|
+
if defaults_apply else None)
|
|
91
169
|
if descriptor.selectable_backend:
|
|
92
|
-
resolved = backend_arg or
|
|
170
|
+
resolved = backend_arg or workspace_default or descriptor.default_backend
|
|
93
171
|
else:
|
|
94
172
|
resolved = descriptor.default_backend
|
|
95
173
|
if resolved is None:
|
|
96
174
|
return self.fm_model
|
|
97
175
|
name = resolved.value if isinstance(resolved, Backend) else str(resolved)
|
|
176
|
+
if name == Backend.AUTO.value and descriptor.selectable_backend:
|
|
177
|
+
return self._model_via_planner(descriptor, self._approximate_allowed(allow_approximate))
|
|
178
|
+
if backend_arg or workspace_default:
|
|
179
|
+
self._last_planner = 'explicit'
|
|
98
180
|
extension = _BACKEND_ALIAS.get(name, name)
|
|
181
|
+
return self._backend_model(extension)
|
|
182
|
+
|
|
183
|
+
def _backend_model(self, extension: str) -> VariabilityModel:
|
|
184
|
+
if extension == self.fm_model.get_extension():
|
|
185
|
+
return self.fm_model
|
|
99
186
|
if extension not in self._backend_models:
|
|
100
187
|
self._backend_models[extension] = self.discover_metamodel.use_transformation_m2m(
|
|
101
188
|
self.fm_model, extension)
|
|
102
189
|
return self._backend_models[extension]
|
|
103
190
|
|
|
191
|
+
def _language_level(self) -> LanguageLevel:
|
|
192
|
+
if self._model_language_level is None:
|
|
193
|
+
self._model_language_level = calculate_language_level(self.fm_model)
|
|
194
|
+
return self._model_language_level
|
|
195
|
+
|
|
196
|
+
def _approximate_allowed(self, per_call: Optional[bool]) -> bool:
|
|
197
|
+
"""Per-call flag > the model's setting > the FLAMAPY_ALLOW_APPROXIMATE variable."""
|
|
198
|
+
if per_call is not None:
|
|
199
|
+
return per_call
|
|
200
|
+
if self.allow_approximate is not None:
|
|
201
|
+
return self.allow_approximate
|
|
202
|
+
return _env_flag('FLAMAPY_ALLOW_APPROXIMATE')
|
|
203
|
+
|
|
204
|
+
def _plan_operation(self, operation_name: str, allow_approximate: bool = False) -> Plan:
|
|
205
|
+
"""Ask the query planner which backend should run ``operation_name`` on this model.
|
|
206
|
+
|
|
207
|
+
The backend models this facade has already built count as free for the planner.
|
|
208
|
+
"""
|
|
209
|
+
edges = [(t.get_source_extension(), t.get_destination_extension())
|
|
210
|
+
for t in self.discover_metamodel.get_transformations_m2m()]
|
|
211
|
+
model_hash = self.fm_model.canonical_hash()
|
|
212
|
+
return plan_backend(
|
|
213
|
+
operation_name,
|
|
214
|
+
source_extension=self.fm_model.get_extension(),
|
|
215
|
+
model_level=self._language_level(),
|
|
216
|
+
n_features=len(self.fm_model.get_features()),
|
|
217
|
+
plugins=self.discover_metamodel.plugins,
|
|
218
|
+
m2m_edges=edges,
|
|
219
|
+
artifact_available=lambda ext: artifact_cache.available_for(model_hash, ext),
|
|
220
|
+
available_models=set(self._backend_models),
|
|
221
|
+
allow_approximate=allow_approximate,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def plan(self, operation: str, *, allow_approximate: Optional[bool] = None) -> Plan:
|
|
225
|
+
"""Which backend ``backend='auto'`` would run ``operation`` on this model, and why.
|
|
226
|
+
|
|
227
|
+
Returns the planner's :class:`~flamapy.core.planner.Plan` (plugin, target extension,
|
|
228
|
+
transformation chain, rationale, estimated cost in ms, alternatives) without running
|
|
229
|
+
anything. Raises :class:`~flamapy.core.exceptions.NoCompatibleBackendError` when no
|
|
230
|
+
installed plugin can run the operation on this model, and ``FlamaException`` when
|
|
231
|
+
``operation`` is not a backend-selectable facade operation. ``allow_approximate``
|
|
232
|
+
falls back to the model's setting and the environment, like the operations do.
|
|
233
|
+
"""
|
|
234
|
+
method = getattr(type(self), operation, None)
|
|
235
|
+
descriptor = getattr(method, '_facade_descriptor', None)
|
|
236
|
+
if descriptor is None or not descriptor.selectable_backend:
|
|
237
|
+
raise FlamaException(f"'{operation}' is not a backend-selectable facade operation.")
|
|
238
|
+
return self._plan_operation(operation, self._approximate_allowed(allow_approximate))
|
|
239
|
+
|
|
240
|
+
def _model_via_planner(self, descriptor: OperationDescriptor,
|
|
241
|
+
allow_approximate: bool = False) -> VariabilityModel:
|
|
242
|
+
chosen = self._plan_operation(descriptor.name, allow_approximate)
|
|
243
|
+
self._last_planner = f'auto: {chosen.rationale}'
|
|
244
|
+
model = self.fm_model
|
|
245
|
+
for _, destination in chosen.transformation_chain:
|
|
246
|
+
if destination not in self._backend_models:
|
|
247
|
+
self._backend_models[destination] = (
|
|
248
|
+
self.discover_metamodel.use_transformation_m2m(model, destination))
|
|
249
|
+
model = self._backend_models[destination]
|
|
250
|
+
return model
|
|
251
|
+
|
|
252
|
+
def _prepare_operation(
|
|
253
|
+
self, descriptor: OperationDescriptor, backend_arg: Optional[str],
|
|
254
|
+
call_kwargs: Dict[str, Any], allow_approximate: Optional[bool] = None,
|
|
255
|
+
) -> tuple[Any, VariabilityModel]:
|
|
256
|
+
"""Resolve the target model and a fully input-wired operation instance."""
|
|
257
|
+
model = self._model_for(descriptor, backend_arg, allow_approximate)
|
|
258
|
+
operation = self.discover_metamodel.get_operation(model, descriptor.operation)
|
|
259
|
+
if descriptor.input_adapter is not None:
|
|
260
|
+
descriptor.input_adapter(operation, self, call_kwargs)
|
|
261
|
+
else:
|
|
262
|
+
for input_spec in descriptor.inputs:
|
|
263
|
+
value = call_kwargs.get(input_spec.name)
|
|
264
|
+
if value is None:
|
|
265
|
+
continue
|
|
266
|
+
if input_spec.kind == 'configuration':
|
|
267
|
+
value = self._as_configuration(value)
|
|
268
|
+
if input_spec.setter is not None:
|
|
269
|
+
getattr(operation, input_spec.setter)(value)
|
|
270
|
+
return operation, model
|
|
271
|
+
|
|
104
272
|
def _run_operation(
|
|
105
273
|
self, descriptor: OperationDescriptor, call_kwargs: Dict[str, Any]
|
|
106
274
|
) -> Any:
|
|
107
275
|
"""Resolve model + operation, wire inputs, execute, and marshal the result.
|
|
108
276
|
|
|
109
277
|
Preserves the facade's historical contract: any :class:`FlamaException` is logged and the
|
|
110
|
-
call returns ``None
|
|
278
|
+
call returns ``None``, unless ``raise_errors`` (or ``FLAMAPY_RAISE_ERRORS=1``) asks for
|
|
279
|
+
the 3.0 contract, which raises it.
|
|
111
280
|
"""
|
|
281
|
+
self.last_result = None
|
|
112
282
|
try:
|
|
113
283
|
backend_arg = call_kwargs.pop('backend', None)
|
|
114
|
-
|
|
115
|
-
operation = self.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if input_spec.kind == 'configuration':
|
|
124
|
-
value = self._as_configuration(value)
|
|
125
|
-
if input_spec.setter is not None:
|
|
126
|
-
getattr(operation, input_spec.setter)(value)
|
|
127
|
-
operation.execute(model)
|
|
128
|
-
result = operation.get_result()
|
|
284
|
+
allow_approximate = call_kwargs.pop('allow_approximate', None)
|
|
285
|
+
operation, model = self._prepare_operation(
|
|
286
|
+
descriptor, backend_arg, call_kwargs, allow_approximate)
|
|
287
|
+
# The hash is always the source feature model's (memoized there), whichever
|
|
288
|
+
# backend model the operation actually runs on.
|
|
289
|
+
envelope = run_and_wrap(
|
|
290
|
+
operation, model,
|
|
291
|
+
operation_name=descriptor.name, plugin=self._plugin_name_for(model),
|
|
292
|
+
model_hash=self.fm_model.canonical_hash())
|
|
129
293
|
if descriptor.result_adapter is not None:
|
|
130
|
-
|
|
131
|
-
|
|
294
|
+
envelope = dataclasses.replace(
|
|
295
|
+
envelope, value=descriptor.result_adapter(envelope.value))
|
|
296
|
+
if self._last_planner is not None:
|
|
297
|
+
envelope = dataclasses.replace(envelope, planner=self._last_planner)
|
|
298
|
+
if getattr(model, '_flamapy_cache_hit', False):
|
|
299
|
+
envelope = dataclasses.replace(envelope, cache_hit=True)
|
|
300
|
+
self.last_result = envelope
|
|
301
|
+
return envelope.value
|
|
132
302
|
except FlamaException as exception:
|
|
303
|
+
if _raise_errors(self.raise_errors):
|
|
304
|
+
raise
|
|
133
305
|
logger.error("Error: %s", exception)
|
|
306
|
+
warnings.warn(
|
|
307
|
+
"Returning None on operation errors is deprecated: flamapy 3.0 will raise "
|
|
308
|
+
"the FlamaException instead. Catch FlamaException rather than checking for "
|
|
309
|
+
"None.", DeprecationWarning, stacklevel=3)
|
|
134
310
|
return None
|
|
135
311
|
|
|
312
|
+
def _plugin_name_for(self, model: VariabilityModel) -> Optional[str]:
|
|
313
|
+
try:
|
|
314
|
+
return self.discover_metamodel.plugins.get_plugin_by_variability_model(model).name
|
|
315
|
+
except PluginNotFound:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
def run_async(self, operation: str, **kwargs: Any) -> Job:
|
|
319
|
+
"""Run a facade operation asynchronously; returns a :class:`Job` immediately.
|
|
320
|
+
|
|
321
|
+
``operation`` is the facade name (e.g. ``'configurations_number'``); ``kwargs``
|
|
322
|
+
are the same arguments the synchronous method takes (including ``backend=``).
|
|
323
|
+
``job.result(timeout=...)`` waits for the value, raising ``OperationTimeout`` on
|
|
324
|
+
expiry (after requesting cooperative cancellation) — unlike the 2.x synchronous
|
|
325
|
+
methods, errors are **raised**, not swallowed into ``None``.
|
|
326
|
+
|
|
327
|
+
Everything after resolving the operation name runs inside the job — including the
|
|
328
|
+
fm->backend transformation (e.g. BDD compilation, often the expensive step on large
|
|
329
|
+
models) — so the timeout covers it and preparation errors (unknown backend,
|
|
330
|
+
invalid inputs) are raised by ``job.result()``.
|
|
331
|
+
"""
|
|
332
|
+
method = getattr(type(self), operation, None)
|
|
333
|
+
descriptor = getattr(method, '_facade_descriptor', None)
|
|
334
|
+
if descriptor is None or getattr(method, '_facade_kind', None) != 'operation':
|
|
335
|
+
raise FlamaException(f"'{operation}' is not a facade analysis operation.")
|
|
336
|
+
call_kwargs = dict(kwargs)
|
|
337
|
+
backend_arg = call_kwargs.pop('backend', None)
|
|
338
|
+
allow_approximate = call_kwargs.pop('allow_approximate', None)
|
|
339
|
+
|
|
340
|
+
def task(interrupt_event: threading.Event) -> OperationResult[Any]:
|
|
341
|
+
prepared, model = self._prepare_operation(
|
|
342
|
+
descriptor, backend_arg, call_kwargs, allow_approximate)
|
|
343
|
+
if interrupt_event.is_set():
|
|
344
|
+
# The deadline expired (or cancel() was called) during preparation: the
|
|
345
|
+
# caller has already given up, so don't start the operation at all.
|
|
346
|
+
raise OperationTimeout(
|
|
347
|
+
f"'{descriptor.name}' was cancelled before execution started.")
|
|
348
|
+
if isinstance(prepared, Interruptible):
|
|
349
|
+
prepared.set_interrupt_event(interrupt_event)
|
|
350
|
+
return run_and_wrap(
|
|
351
|
+
prepared, model,
|
|
352
|
+
operation_name=descriptor.name, plugin=self._plugin_name_for(model),
|
|
353
|
+
model_hash=self.fm_model.canonical_hash())
|
|
354
|
+
|
|
355
|
+
return default_engine().submit_call(task, result_adapter=descriptor.result_adapter)
|
|
356
|
+
|
|
357
|
+
def iter_configurations(self, backend: Optional[str] = None) -> Iterator[Configuration]:
|
|
358
|
+
"""Iterate the valid configurations, yielding each as the solver produces it.
|
|
359
|
+
|
|
360
|
+
Unlike :meth:`configurations` (whose historical default is bdd), the backend
|
|
361
|
+
defaults to ``sat`` — the streaming-capable implementation. Non-streaming
|
|
362
|
+
backends fall back to full enumeration first. Same errors contract as
|
|
363
|
+
:meth:`run_async` (raises, never ``None``).
|
|
364
|
+
"""
|
|
365
|
+
descriptor = type(self).configurations._facade_descriptor # type: ignore[attr-defined]
|
|
366
|
+
operation, model = self._prepare_operation(descriptor, backend or 'sat', {})
|
|
367
|
+
if isinstance(operation, StreamingOperation):
|
|
368
|
+
return operation.iter_execute(model)
|
|
369
|
+
operation.execute(model)
|
|
370
|
+
return iter(operation.get_result())
|
|
371
|
+
|
|
136
372
|
|
|
137
373
|
# One discovery instance, reused to install the operations and to run producers (which have no
|
|
138
374
|
# per-facade instance to hold their own).
|
|
@@ -143,7 +379,9 @@ def _run_producer(descriptor: OperationDescriptor, call_kwargs: Dict[str, Any])
|
|
|
143
379
|
"""Run a 'producer' operation — one that *creates* a model instead of analysing one.
|
|
144
380
|
|
|
145
381
|
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
|
|
382
|
+
model. Same FlamaException -> None contract as :meth:`FLAMAFeatureModel._run_operation`;
|
|
383
|
+
``FLAMAPY_RAISE_ERRORS=1`` makes it raise instead (producers have no instance to carry
|
|
384
|
+
``raise_errors``).
|
|
147
385
|
"""
|
|
148
386
|
try:
|
|
149
387
|
operation_class = next(
|
|
@@ -161,10 +399,21 @@ def _run_producer(descriptor: OperationDescriptor, call_kwargs: Dict[str, Any])
|
|
|
161
399
|
result = descriptor.result_adapter(result)
|
|
162
400
|
return result
|
|
163
401
|
except FlamaException as exception:
|
|
402
|
+
if _raise_errors(None):
|
|
403
|
+
raise
|
|
164
404
|
logger.error("Error: %s", exception)
|
|
405
|
+
warnings.warn(
|
|
406
|
+
"Returning None on operation errors is deprecated: flamapy 3.0 will raise "
|
|
407
|
+
"the FlamaException instead. Catch FlamaException rather than checking for "
|
|
408
|
+
"None.", DeprecationWarning, stacklevel=3)
|
|
165
409
|
return None
|
|
166
410
|
|
|
167
411
|
|
|
412
|
+
_SELECTABLE_NOTE = (
|
|
413
|
+
'\n\n``backend`` chooses the plugin that runs the operation (``"auto"`` lets the query '
|
|
414
|
+
'planner choose); ``allow_approximate`` only has an effect with ``backend="auto"``.')
|
|
415
|
+
|
|
416
|
+
|
|
168
417
|
def _make_operation_method(descriptor: OperationDescriptor) -> Any:
|
|
169
418
|
"""Build a real callable from a descriptor.
|
|
170
419
|
|
|
@@ -184,6 +433,10 @@ def _make_operation_method(descriptor: OperationDescriptor) -> Any:
|
|
|
184
433
|
parameters.append(inspect.Parameter(
|
|
185
434
|
'backend', inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None,
|
|
186
435
|
annotation=Optional[str]))
|
|
436
|
+
# Only meaningful under backend='auto': let the planner pick approximate backends.
|
|
437
|
+
parameters.append(inspect.Parameter(
|
|
438
|
+
'allow_approximate', inspect.Parameter.KEYWORD_ONLY, default=None,
|
|
439
|
+
annotation=Optional[bool]))
|
|
187
440
|
signature = inspect.Signature(parameters)
|
|
188
441
|
|
|
189
442
|
def method(*args: Any, **kwargs: Any) -> Any:
|
|
@@ -197,7 +450,7 @@ def _make_operation_method(descriptor: OperationDescriptor) -> Any:
|
|
|
197
450
|
|
|
198
451
|
method.__name__ = descriptor.name
|
|
199
452
|
method.__qualname__ = f'{FLAMAFeatureModel.__name__}.{descriptor.name}'
|
|
200
|
-
method.__doc__ = descriptor.doc
|
|
453
|
+
method.__doc__ = descriptor.doc + (_SELECTABLE_NOTE if descriptor.selectable_backend else '')
|
|
201
454
|
method.__signature__ = signature # type: ignore[attr-defined]
|
|
202
455
|
# Let the CLI/REST/tests tell analysis ops from producers/transformers without re-discovering.
|
|
203
456
|
setattr(method, '_facade_kind', descriptor.kind)
|
|
@@ -211,4 +464,10 @@ def _install_operations(cls: type) -> None:
|
|
|
211
464
|
setattr(cls, name, staticmethod(method) if descriptor.kind == 'producer' else method)
|
|
212
465
|
|
|
213
466
|
|
|
467
|
+
# Async/streaming API methods, not descriptor-generated operations: the CLI generator and
|
|
468
|
+
# the descriptor lock test filter on _facade_kind != 'operation'.
|
|
469
|
+
FLAMAFeatureModel.plan._facade_kind = 'api' # type: ignore[attr-defined]
|
|
470
|
+
FLAMAFeatureModel.run_async._facade_kind = 'api' # type: ignore[attr-defined]
|
|
471
|
+
FLAMAFeatureModel.iter_configurations._facade_kind = 'api' # type: ignore[attr-defined]
|
|
472
|
+
|
|
214
473
|
_install_operations(FLAMAFeatureModel)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Auto-generated by flamapy.interfaces.python._stub — do not edit by hand.
|
|
2
2
|
# Regenerate with: python -m flamapy.interfaces.python._stub
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Union # noqa: F401
|
|
5
5
|
|
|
6
|
+
from flamapy.core.operations import OperationResult
|
|
7
|
+
from flamapy.core.planner import Plan
|
|
6
8
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
7
9
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
8
10
|
|
|
@@ -12,52 +14,71 @@ class Backend(str, Enum):
|
|
|
12
14
|
BDD = ...
|
|
13
15
|
Z3 = ...
|
|
14
16
|
SHARPSAT = ...
|
|
17
|
+
SDD = ...
|
|
18
|
+
DNNF = ...
|
|
19
|
+
AUTO = ...
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class FLAMAFeatureModel:
|
|
18
23
|
model_path: str
|
|
19
24
|
fm_model: FeatureModel
|
|
20
25
|
backend: Optional[Backend]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def
|
|
28
|
-
def
|
|
29
|
-
def
|
|
30
|
-
def
|
|
31
|
-
def
|
|
32
|
-
def
|
|
33
|
-
def
|
|
34
|
-
def
|
|
35
|
-
def
|
|
36
|
-
def
|
|
37
|
-
def
|
|
38
|
-
def
|
|
39
|
-
def
|
|
40
|
-
def
|
|
41
|
-
def
|
|
42
|
-
def
|
|
43
|
-
def
|
|
44
|
-
def
|
|
26
|
+
allow_approximate: Optional[bool]
|
|
27
|
+
raise_errors: Optional[bool]
|
|
28
|
+
last_result: Optional[OperationResult[Any]]
|
|
29
|
+
def __init__(self, model_path: str, backend: Optional[str] = ...,
|
|
30
|
+
*, allow_approximate: Optional[bool] = ...,
|
|
31
|
+
raise_errors: Optional[bool] = ...) -> None: ...
|
|
32
|
+
def plan(self, operation: str, *, allow_approximate: Optional[bool] = ...) -> Plan: ...
|
|
33
|
+
def _plan_operation(self, operation_name: str, allow_approximate: bool = ...) -> Plan: ...
|
|
34
|
+
def run_async(self, operation: str, **kwargs: Any) -> Any: ...
|
|
35
|
+
def iter_configurations(self, backend: Optional[str] = ...) -> Any: ...
|
|
36
|
+
def all_feature_bounds(self) -> Optional[Dict[str, Dict[str, Any]]]: ...
|
|
37
|
+
def atomic_sets(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[List[Any]]]: ...
|
|
38
|
+
def attribute_optimization(self, objectives: Any, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[Configuration]]: ...
|
|
39
|
+
def average_branching_factor(self) -> Optional[float]: ...
|
|
40
|
+
def backbone(self) -> Optional[Dict[str, List[Any]]]: ...
|
|
41
|
+
def commonality(self, configuration_path: str, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[float]: ...
|
|
42
|
+
def configuration_conflict(self, configuration_path: str) -> Optional[List[Any]]: ...
|
|
43
|
+
def configuration_repair(self, configuration_path: str) -> Optional[List[Any]]: ...
|
|
44
|
+
def configurations(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[Configuration]]: ...
|
|
45
|
+
def configurations_number(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[int]: ...
|
|
46
|
+
def configurations_with_n_features(self, n: int) -> Optional[List[Configuration]]: ...
|
|
47
|
+
def conflict(self, configuration_path: str, test_case_path: Optional[str] = ..., max_conflicts: Optional[int] = ...) -> Optional[List[str]]: ...
|
|
48
|
+
def core_features(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[str]]: ...
|
|
49
|
+
def count_leafs(self) -> Optional[int]: ...
|
|
50
|
+
def coverage(self, configurations: Any, t: int = ...) -> Optional[float]: ...
|
|
51
|
+
def covering_array(self, t: int = ..., seed: Optional[int] = ...) -> Optional[List[Configuration]]: ...
|
|
52
|
+
def dead_features(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[str]]: ...
|
|
53
|
+
def diagnosis(self, configuration_path: str, test_case_path: Optional[str] = ..., max_diagnoses: Optional[int] = ...) -> Optional[List[str]]: ...
|
|
54
|
+
def estimated_number_of_configurations(self) -> Optional[int]: ...
|
|
55
|
+
def explain_dead_feature(self, feature_name: str) -> Optional[List[str]]: ...
|
|
56
|
+
def explain_false_optional(self, feature_name: str) -> Optional[List[str]]: ...
|
|
57
|
+
def explain_void_model(self) -> Optional[List[str]]: ...
|
|
58
|
+
def false_optional_features(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[str]]: ...
|
|
59
|
+
def feature_ancestors(self, feature_name: str) -> Optional[List[str]]: ...
|
|
60
|
+
def feature_bounds(self, variable_name: str) -> Optional[Dict[str, Any]]: ...
|
|
61
|
+
def feature_explanation(self, configuration_path: str, feature_name: str) -> Optional[List[Any]]: ...
|
|
62
|
+
def feature_inclusion_probability(self) -> Optional[Dict[str, float]]: ...
|
|
63
|
+
def filter(self, configuration_path: str) -> Optional[List[Configuration]]: ...
|
|
45
64
|
def generate_random_attribute(self, name: str, min_value: int = ..., max_value: int = ..., only_leaf_features: bool = ...) -> FeatureModel: ...
|
|
46
65
|
@staticmethod
|
|
47
66
|
def generate_random_feature_model(num_features: int = ..., max_constraints: int = ..., seed: int = ..., void: bool = ..., language_level: str = ...) -> FeatureModel: ...
|
|
48
|
-
def homogeneity(self) ->
|
|
49
|
-
def language_level(self) ->
|
|
50
|
-
def leaf_features(self) ->
|
|
51
|
-
def max_depth(self) ->
|
|
52
|
-
def metrics(self) ->
|
|
53
|
-
def
|
|
54
|
-
def
|
|
55
|
-
def
|
|
56
|
-
def
|
|
57
|
-
def
|
|
58
|
-
def
|
|
59
|
-
def
|
|
60
|
-
def
|
|
61
|
-
def
|
|
62
|
-
def
|
|
63
|
-
def
|
|
67
|
+
def homogeneity(self) -> Optional[float]: ...
|
|
68
|
+
def language_level(self) -> Optional[Any]: ...
|
|
69
|
+
def leaf_features(self) -> Optional[List[str]]: ...
|
|
70
|
+
def max_depth(self) -> Optional[int]: ...
|
|
71
|
+
def metrics(self) -> Optional[List[Dict[str, Any]]]: ...
|
|
72
|
+
def minimal_corrections(self, max_corrections: Optional[int] = ...) -> Optional[List[List[str]]]: ...
|
|
73
|
+
def minimum_configuration(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[Configuration]: ...
|
|
74
|
+
def product_distribution(self) -> Optional[List[int]]: ...
|
|
75
|
+
def pure_optional_features(self) -> Optional[List[str]]: ...
|
|
76
|
+
def sample_reduction(self, configurations: Any, t: int = ...) -> Optional[List[Configuration]]: ...
|
|
77
|
+
def sampling(self, size: int, with_replacement: bool = ..., backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[Configuration]]: ...
|
|
78
|
+
def satisfiable(self, backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[bool]: ...
|
|
79
|
+
def satisfiable_configuration(self, configuration_path: str, full_configuration: bool = ..., backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[bool]: ...
|
|
80
|
+
def t_wise_sampling(self, t: int = ..., backend: Optional[str] = ..., *, allow_approximate: Optional[bool] = ...) -> Optional[List[Configuration]]: ...
|
|
81
|
+
def unique_features(self) -> Optional[List[str]]: ...
|
|
82
|
+
def variability(self) -> Optional[Tuple[float, float]]: ...
|
|
83
|
+
def variant_features(self) -> Optional[List[str]]: ...
|
|
84
|
+
def variation_points(self) -> Optional[Dict[str, List[str]]]: ...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0.dev0
|
|
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,21 @@ 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.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.
|
|
13
|
-
Requires-Dist: flamapy-sat~=2.
|
|
14
|
-
Requires-Dist: flamapy-bdd~=2.
|
|
15
|
-
Requires-Dist: flamapy-z3~=2.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.9.0.dev0
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.9.0.dev0
|
|
13
|
+
Requires-Dist: flamapy-sat~=2.9.0.dev0
|
|
14
|
+
Requires-Dist: flamapy-bdd~=2.9.0.dev0
|
|
15
|
+
Requires-Dist: flamapy-z3~=2.9.0.dev0
|
|
16
|
+
Provides-Extra: sharpsat
|
|
17
|
+
Requires-Dist: flamapy-sharpsat~=2.9.0.dev0; extra == "sharpsat"
|
|
18
|
+
Provides-Extra: dnnf
|
|
19
|
+
Requires-Dist: flamapy-dnnf~=2.9.0.dev0; extra == "dnnf"
|
|
20
|
+
Provides-Extra: sdd
|
|
21
|
+
Requires-Dist: flamapy-sdd~=2.9.0.dev0; extra == "sdd"
|
|
22
|
+
Provides-Extra: all-solvers
|
|
23
|
+
Requires-Dist: flamapy-sharpsat~=2.9.0.dev0; extra == "all-solvers"
|
|
24
|
+
Requires-Dist: flamapy-dnnf~=2.9.0.dev0; extra == "all-solvers"
|
|
25
|
+
Requires-Dist: flamapy-sdd~=2.9.0.dev0; extra == "all-solvers"
|
|
16
26
|
Provides-Extra: dev
|
|
17
27
|
Requires-Dist: pytest; extra == "dev"
|
|
18
28
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
flamapy/commands/__init__.py,sha256=DdRS2Xyt37H_k2YaaFWUcE6xfJALzllraerIup1l_OU,14646
|
|
2
|
+
flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
|
|
3
|
+
flamapy/interfaces/python/_stub.py,sha256=7JMmnmxHFbpC7XHVqozZUooLwPbHS4p1-B8QXH5wQf8,3898
|
|
4
|
+
flamapy/interfaces/python/flamapy_feature_model.py,sha256=Kfq-zizQViAx_KdfFZTo_SNkFakFo3FmNt32Z5Jaats,24180
|
|
5
|
+
flamapy/interfaces/python/flamapy_feature_model.pyi,sha256=u-GtNc1wRslPO1I2sIIsNuOxrdT-dR0GZpqzyskHT34,6189
|
|
6
|
+
flamapy-2.9.0.dev0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
7
|
+
flamapy-2.9.0.dev0.dist-info/METADATA,sha256=lRl3wpwDCFvW98uPFWwZBzAYLmzQ2TCrh07g9yR7hso,1731
|
|
8
|
+
flamapy-2.9.0.dev0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
flamapy-2.9.0.dev0.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
|
|
10
|
+
flamapy-2.9.0.dev0.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
|
|
11
|
+
flamapy-2.9.0.dev0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|