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.
Files changed (31) hide show
  1. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/PKG-INFO +16 -6
  2. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/commands/__init__.py +114 -10
  3. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/interfaces/python/_stub.py +26 -3
  4. flamapy-2.9.0.dev0/flamapy/interfaces/python/flamapy_feature_model.py +473 -0
  5. flamapy-2.9.0.dev0/flamapy/interfaces/python/flamapy_feature_model.pyi +84 -0
  6. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/PKG-INFO +16 -6
  7. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/SOURCES.txt +5 -1
  8. flamapy-2.9.0.dev0/flamapy.egg-info/requires.txt +26 -0
  9. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/pyproject.toml +16 -6
  10. flamapy-2.9.0.dev0/tests/test_2_9_compatibility.py +88 -0
  11. flamapy-2.9.0.dev0/tests/test_async_streaming.py +98 -0
  12. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_basic_units.py +2 -2
  13. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_cli_basic_units.py +2 -1
  14. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_descriptors_cover_facade.py +15 -3
  15. flamapy-2.9.0.dev0/tests/test_metrics_fanout.py +19 -0
  16. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_optimization_and_sampling.py +25 -0
  17. flamapy-2.9.0.dev0/tests/test_planner_routing.py +145 -0
  18. flamapy-2.6.0.dev12/flamapy/interfaces/python/flamapy_feature_model.py +0 -214
  19. flamapy-2.6.0.dev12/flamapy/interfaces/python/flamapy_feature_model.pyi +0 -63
  20. flamapy-2.6.0.dev12/flamapy.egg-info/requires.txt +0 -12
  21. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/LICENSE +0 -0
  22. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/README.md +0 -0
  23. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy/interfaces/python/__init__.py +0 -0
  24. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/dependency_links.txt +0 -0
  25. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/entry_points.txt +0 -0
  26. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/flamapy.egg-info/top_level.txt +0 -0
  27. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/setup.cfg +0 -0
  28. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/setup.py +0 -0
  29. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_documentation_examples.py +0 -0
  30. {flamapy-2.6.0.dev12 → flamapy-2.9.0.dev0}/tests/test_fama_suite_bdd.py +0 -0
  31. {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.6.0.dev12
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.6.0.dev12
12
- Requires-Dist: flamapy-fm~=2.6.0.dev12
13
- Requires-Dist: flamapy-sat~=2.6.0.dev12
14
- Requires-Dist: flamapy-bdd~=2.6.0.dev12
15
- Requires-Dist: flamapy-z3~=2.6.0.dev12
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
- continue # producers/transformers return models; Python-API only for now
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("__NAME__", name.capitalize()).replace("__EXT__", ext)
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
- cls_instance = FLAMAFeatureModel(args.model_path)
175
- method_parameters = [param.name for param in args.parameters]
176
- command_args = {k: v for k, v in vars(args).items() if k in method_parameters}
177
- method = getattr(cls_instance, args.method_name)
178
- result = method(**command_args)
179
- if result is not None:
180
- print(result)
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
- def __init__(self, model_path: str, backend: Optional[str] = ...) -> None: ...
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)}) -> {descriptor.returns}: ...')
89
+ f' def {name}({_method_signature(descriptor)}) '
90
+ f'-> {_return_annotation(descriptor)}: ...')
68
91
  return '\n'.join(lines) + '\n'
69
92
 
70
93