flamapy 2.6.0.dev12__tar.gz → 2.9.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/PKG-INFO +16 -6
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/commands/__init__.py +114 -10
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/interfaces/python/_stub.py +26 -3
- flamapy-2.9.0.dev0/flamapy/interfaces/python/flamapy_feature_model.py +473 -0
- flamapy-2.9.0.dev0/flamapy/interfaces/python/flamapy_feature_model.pyi +84 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/PKG-INFO +16 -6
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/SOURCES.txt +5 -1
- flamapy-2.9.0.dev0/flamapy.egg-info/requires.txt +26 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/pyproject.toml +16 -6
- flamapy-2.9.0.dev0/tests/test_2_9_compatibility.py +88 -0
- flamapy-2.9.0.dev0/tests/test_async_streaming.py +98 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_basic_units.py +2 -2
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_cli_basic_units.py +2 -1
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_descriptors_cover_facade.py +15 -3
- flamapy-2.9.0.dev0/tests/test_metrics_fanout.py +19 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_optimization_and_sampling.py +25 -0
- flamapy-2.9.0.dev0/tests/test_planner_routing.py +145 -0
- flamapy-2.6.0.dev12/flamapy/interfaces/python/flamapy_feature_model.py +0 -214
- flamapy-2.6.0.dev12/flamapy/interfaces/python/flamapy_feature_model.pyi +0 -63
- flamapy-2.6.0.dev12/flamapy.egg-info/requires.txt +0 -12
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/LICENSE +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/README.md +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/interfaces/python/__init__.py +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/dependency_links.txt +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/entry_points.txt +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/top_level.txt +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/setup.cfg +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/setup.py +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_documentation_examples.py +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_fama_suite_bdd.py +0 -0
- {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_fama_suite_sat.py +0 -0
|
@@ -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"
|
|
@@ -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
|
|