flamapy 2.6.0.dev12__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.
@@ -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
 
@@ -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.operations import OperationDescriptor
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) -> 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") 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.
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 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
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 self.backend or descriptor.default_backend
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
- 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)
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
- result = descriptor.result_adapter(result)
131
- return result
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
- 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]]: ...
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) -> 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]]]: ...
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.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"
@@ -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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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.dev12.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
7
- flamapy-2.6.0.dev12.dist-info/METADATA,sha256=-MRGwPu0iqotGx_mCjPPnuIOe5pw2CkOppIpX2lcPjU,1271
8
- flamapy-2.6.0.dev12.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
- flamapy-2.6.0.dev12.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
10
- flamapy-2.6.0.dev12.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
11
- flamapy-2.6.0.dev12.dist-info/RECORD,,