flamapy-fm 2.5.0.dev0__tar.gz → 2.6.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 (63) hide show
  1. {flamapy_fm-2.5.0.dev0/flamapy_fm.egg-info → flamapy_fm-2.6.0.dev0}/PKG-INFO +11 -8
  2. flamapy_fm-2.6.0.dev0/README.md +13 -0
  3. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/models/feature_model.py +16 -7
  4. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +2 -0
  5. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +26 -16
  6. flamapy_fm-2.6.0.dev0/flamapy/metamodels/fm_metamodel/transformations/xml_writer.py +140 -0
  7. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0/flamapy_fm.egg-info}/PKG-INFO +11 -8
  8. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy_fm.egg-info/SOURCES.txt +1 -0
  9. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy_fm.egg-info/requires.txt +2 -2
  10. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/pyproject.toml +3 -3
  11. flamapy_fm-2.5.0.dev0/README.md +0 -10
  12. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/LICENSE +0 -0
  13. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
  14. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/models/__init__.py +0 -0
  15. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -0
  16. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +0 -0
  17. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +0 -0
  18. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
  19. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +0 -0
  20. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +0 -0
  21. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +0 -0
  22. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +0 -0
  23. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +0 -0
  24. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -0
  25. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +0 -0
  26. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +0 -0
  27. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +0 -0
  28. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
  29. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
  30. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
  31. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
  32. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
  33. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
  34. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
  35. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
  36. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
  37. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
  38. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
  39. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
  40. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
  41. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
  42. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
  43. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
  44. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
  45. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
  46. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
  47. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
  48. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
  49. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
  50. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
  51. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
  52. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
  53. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
  54. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
  55. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
  56. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
  57. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
  58. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
  59. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
  60. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy_fm.egg-info/dependency_links.txt +0 -0
  61. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/flamapy_fm.egg-info/top_level.txt +0 -0
  62. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/setup.cfg +0 -0
  63. {flamapy_fm-2.5.0.dev0 → flamapy_fm-2.6.0.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-fm
3
- Version: 2.5.0.dev0
3
+ Version: 2.6.0.dev0
4
4
  Summary: flamapy-fm is a plugin to Flamapy module
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,8 +8,8 @@ Project-URL: Homepage, https://github.com/flamapy/fm_metamodel
8
8
  Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.5.0.dev0
12
- Requires-Dist: uvlparser~=2.5.0.dev63
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev0
12
+ Requires-Dist: uvlparser~=2.5.0
13
13
  Requires-Dist: afmparser~=1.0.3
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
@@ -20,13 +20,16 @@ Requires-Dist: coverage; extra == "dev"
20
20
  Requires-Dist: antlr4-tools; extra == "dev"
21
21
  Dynamic: license-file
22
22
 
23
- # fm_metamodel
23
+ # flamapy-fm
24
24
 
25
- This repo host the feature model concrete classes
25
+ The feature model metamodel plugin for [flamapy](https://flamapy.org): the
26
+ feature model classes plus readers and writers for many formats (UVL,
27
+ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
26
28
 
29
+ **Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
27
30
 
28
- ## Install for development
31
+ ## Installation
29
32
 
30
- ```
31
- pip install -e .
33
+ ```bash
34
+ pip install flamapy-fm
32
35
  ```
@@ -0,0 +1,13 @@
1
+ # flamapy-fm
2
+
3
+ The feature model metamodel plugin for [flamapy](https://flamapy.org): the
4
+ feature model classes plus readers and writers for many formats (UVL,
5
+ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
6
+
7
+ **Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install flamapy-fm
13
+ ```
@@ -3,7 +3,7 @@ from functools import total_ordering
3
3
  from enum import Enum
4
4
 
5
5
  from flamapy.core.exceptions import FlamaException
6
- from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
6
+ from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation, NodeType
7
7
  from flamapy.core.models.ast import LOGICAL_OPERATORS, ARITHMETIC_OPERATORS, AGGREGATION_OPERATORS
8
8
  from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
9
9
 
@@ -247,7 +247,11 @@ class Constraint:
247
247
  self._ast = ast
248
248
 
249
249
  def get_features(self) -> list[str]:
250
- """List of features' names involved in the constraint."""
250
+ """List of features' names involved in the constraint.
251
+
252
+ Uses node_type information when available (set by readers that support it).
253
+ Falls back to heuristics for nodes without explicit type info.
254
+ """
251
255
  features = set()
252
256
  stack = [self.ast.root]
253
257
  while stack:
@@ -255,9 +259,15 @@ class Constraint:
255
259
  if node is None:
256
260
  continue
257
261
  if node.is_unique_term():
258
- if (isinstance(node.data, (int, float)) or node.data.startswith("'")):
259
- continue
260
- features.add(node.data)
262
+ if node.node_type is not None:
263
+ if node.node_type == NodeType.FEATURE:
264
+ features.add(node.data)
265
+ # NodeType.LITERAL: skip
266
+ # Fallback heuristics for nodes without explicit type (backward compatibility)
267
+ elif not isinstance(node.data, (int, float)) and not (
268
+ isinstance(node.data, str) and node.data.startswith("'")
269
+ ):
270
+ features.add(node.data)
261
271
  elif node.is_unary_op():
262
272
  stack.append(node.left)
263
273
  elif node.is_binary_op():
@@ -281,8 +291,7 @@ class Constraint:
281
291
  """Return true if the constraint is a single feature or its negation."""
282
292
  root_op = self._ast.root
283
293
  return (root_op.is_term() or
284
- (root_op.data == ASTOperation.NOT and
285
- (root_op.left.is_term() or root_op.right.is_term())))
294
+ (root_op.data == ASTOperation.NOT and root_op.left.is_term()))
286
295
 
287
296
  def is_simple_constraint(self) -> bool:
288
297
  """Return true if the constraint is a simple constraint (requires or excludes)."""
@@ -8,6 +8,7 @@ from .splot_writer import SPLOTWriter
8
8
  from .uvl_reader import UVLReader
9
9
  from .uvl_writer import UVLWriter
10
10
  from .xml_reader import XMLReader
11
+ from .xml_writer import XMLWriter
11
12
  from .glencoe_reader import GlencoeReader
12
13
  from .glencoe_writer import GlencoeWriter
13
14
  from .clafer_writer import ClaferWriter
@@ -33,4 +34,5 @@ __all__ = [
33
34
  'UVLReader',
34
35
  'UVLWriter',
35
36
  'XMLReader',
37
+ 'XMLWriter',
36
38
  ]
@@ -9,7 +9,7 @@ from uvl.UVLPythonParser import UVLPythonParser
9
9
 
10
10
  from flamapy.core.exceptions import FlamaException
11
11
  from flamapy.core.transformations import TextToModel
12
- from flamapy.core.models.ast import AST, ASTOperation, Node
12
+ from flamapy.core.models.ast import AST, ASTOperation, Node, NodeType
13
13
  from flamapy.metamodels.fm_metamodel.models import (
14
14
  Constraint,
15
15
  Feature,
@@ -169,7 +169,7 @@ class UVLReader(TextToModel):
169
169
  feature_type = FeatureType.REAL
170
170
  else:
171
171
  raise FlamaException('Error: unknow feature type for '
172
- f'{typed_text} of feature {feature.name}.')
172
+ f'{typed_text} of feature "{feature.name}".')
173
173
  feature.feature_type = feature_type
174
174
 
175
175
  def _check_attributes(
@@ -233,7 +233,7 @@ class UVLReader(TextToModel):
233
233
  raise FlamaException(f'Feature {feature_reference_name} not found in '
234
234
  f'imported model {namespace}.')
235
235
 
236
- def process_relationship_type(self,
236
+ def process_relationship_type(self, # noqa: C901 - flat dispatch over UVL group types
237
237
  feature: Feature,
238
238
  feature_node: UVLPythonParser.FeatureContext) -> None:
239
239
  for relationship in feature_node.group():
@@ -255,7 +255,13 @@ class UVLReader(TextToModel):
255
255
  feature.add_relation(Relation(feature, childs, min_value, max_value))
256
256
  if max_value > len(childs):
257
257
  logging.warning(
258
- "Cardinality error: max value is greater than the number of childs"
258
+ f'Cardinality error in feature "{feature.name}": '
259
+ 'max value is greater than the number of childs'
260
+ )
261
+ if min_value < 0:
262
+ logging.warning(
263
+ f'Cardinality error in feature "{feature.name}": '
264
+ 'min value cannot be negative'
259
265
  )
260
266
 
261
267
  def process_feature(
@@ -328,7 +334,7 @@ class UVLReader(TextToModel):
328
334
  if isinstance(ctx, UVLPythonParser.EquationConstraintContext):
329
335
  return self.process_equation(ctx.equation())
330
336
  if isinstance(ctx, UVLPythonParser.LiteralConstraintContext):
331
- return Node(ctx.reference().getText().replace('"', '')) # procesar literal
337
+ return Node(ctx.reference().getText().replace('"', ''), node_type=NodeType.FEATURE)
332
338
 
333
339
  raise NotImplementedError(f"Unknown type of constraint: {type(ctx)}")
334
340
 
@@ -388,22 +394,23 @@ class UVLReader(TextToModel):
388
394
  self.process_expression(ctx.multiplicativeExpression()),
389
395
  self.process_expression(ctx.primaryExpression()))
390
396
 
391
- def _process_expression_leaves(self, ctx: Any) -> Node:
397
+ def _process_expression_leaves(self, ctx: Any) -> Node: # noqa: PLR0911 - one return per UVL leaf type
392
398
  """Helper to process literal leaves and primary expressions."""
393
399
  if isinstance(ctx, UVLPythonParser.FloatLiteralExpressionContext):
394
- return Node(float(ctx.getText()))
400
+ return Node(float(ctx.getText()), node_type=NodeType.LITERAL)
395
401
  if isinstance(ctx, UVLPythonParser.IntegerLiteralExpressionContext):
396
- return Node(int(ctx.getText()))
397
- if isinstance(ctx, (UVLPythonParser.StringLiteralExpressionContext,
398
- UVLPythonParser.LiteralExpressionContext)):
399
- return Node(ctx.getText().replace('"', '').replace("'", ''))
402
+ return Node(int(ctx.getText()), node_type=NodeType.LITERAL)
403
+ if isinstance(ctx, UVLPythonParser.StringLiteralExpressionContext):
404
+ return Node(ctx.getText().replace('"', '').replace("'", ''), node_type=NodeType.LITERAL)
405
+ if isinstance(ctx, UVLPythonParser.LiteralExpressionContext):
406
+ return Node(ctx.getText().replace('"', '').replace("'", ''), node_type=NodeType.FEATURE)
400
407
  if isinstance(ctx, UVLPythonParser.BracketExpressionContext):
401
408
  return self.process_expression(ctx.expression())
402
409
  if isinstance(ctx, UVLPythonParser.AggregateFunctionExpressionContext):
403
410
  return self.process_aggregate(ctx.aggregateFunction())
404
411
 
405
- # Default fallback
406
- return Node(ctx.getText().replace('"', '').replace("'", ''))
412
+ # Default fallback: treat as feature reference
413
+ return Node(ctx.getText().replace('"', '').replace("'", ''), node_type=NodeType.FEATURE)
407
414
 
408
415
  def process_aggregate(self, ctx: UVLPythonParser.AggregateFunctionContext) -> Node:
409
416
  # 1. SUM: aggregateFunction -> sumAggregateFunction
@@ -419,14 +426,17 @@ class UVLReader(TextToModel):
419
426
  # 3. STRING: aggregateFunction -> stringAggregateFunction
420
427
  if isinstance(ctx, UVLPythonParser.StringAggregateFunctionExpressionContext):
421
428
  string_func = ctx.stringAggregateFunction()
429
+ ref_node = Node(string_func.reference().getText().replace('"', ''),
430
+ node_type=NodeType.FEATURE)
422
431
  # Here we handle the tag # LengthAggregateFunction
423
432
  if isinstance(string_func, UVLPythonParser.LengthAggregateFunctionContext):
424
- return Node(ASTOperation.LEN, Node(string_func.reference().getText()))
433
+ return Node(ASTOperation.LEN, ref_node)
425
434
 
426
435
  # 4. NUMERIC: aggregateFunction -> numericAggregateFunction
427
436
  if isinstance(ctx, UVLPythonParser.NumericAggregateFunctionExpressionContext):
428
437
  num_func = ctx.numericAggregateFunction()
429
- ref_node = Node(num_func.reference().getText())
438
+ ref_node = Node(num_func.reference().getText().replace('"', ''),
439
+ node_type=NodeType.FEATURE)
430
440
  # Handle tags # FloorAggregateFunction and # CeilAggregateFunction
431
441
  if isinstance(num_func, UVLPythonParser.FloorAggregateFunctionContext):
432
442
  return Node(ASTOperation.FLOOR, ref_node)
@@ -437,7 +447,7 @@ class UVLReader(TextToModel):
437
447
 
438
448
  def _build_aggregate_node(self, operation: ASTOperation, references: list[Any]) -> Node:
439
449
  """Helper for Sum y Avg that can take 1 or 2 references"""
440
- nodes = [Node(r.getText().replace('"', '')) for r in references]
450
+ nodes = [Node(r.getText().replace('"', ''), node_type=NodeType.FEATURE) for r in references]
441
451
  return Node(operation, *nodes)
442
452
 
443
453
  def process_includes(
@@ -0,0 +1,140 @@
1
+ import logging
2
+ from xml.dom import minidom
3
+ import xml.etree.ElementTree as ET
4
+
5
+ from flamapy.core.models.ast import ASTOperation
6
+ from flamapy.core.transformations import ModelToText
7
+
8
+ from flamapy.metamodels.fm_metamodel.models import Feature, FeatureModel, Relation
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class XMLWriter(ModelToText):
14
+ """Serialize a FeatureModel to FAMA XML format.
15
+
16
+ This is the counterpart of XMLReader. The output is readable by XMLReader
17
+ and compatible with the FaMA analysis tool.
18
+
19
+ Only requires/excludes cross-tree constraints are supported by this format.
20
+ Complex constraints are skipped; inspect ``skipped_constraints`` after
21
+ calling ``transform()`` to retrieve the ones that were dropped.
22
+ """
23
+
24
+ @staticmethod
25
+ def get_destination_extension() -> str:
26
+ return 'xml'
27
+
28
+ def __init__(self, path: str, source_model: FeatureModel) -> None:
29
+ self._path = path
30
+ self._source_model = source_model
31
+ self.skipped_constraints: list[str] = []
32
+
33
+ def transform(self) -> str:
34
+ self.skipped_constraints = []
35
+
36
+ fm_el = ET.Element('feature-model')
37
+ fm_el.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
38
+ fm_el.set('xsi:noNamespaceSchemaLocation', 'feature-model-schema.xsd')
39
+
40
+ root_feature = self._source_model.root
41
+ feat_el = ET.SubElement(fm_el, 'feature')
42
+ feat_el.set('name', root_feature.name)
43
+
44
+ counters = [0, 0] # [br_count, sr_count]
45
+ _write_feature(root_feature, feat_el, counters)
46
+
47
+ requires_list, excludes_list = self._extract_binary_constraints()
48
+
49
+ re_count = 0
50
+ for feat_a, feat_b in requires_list:
51
+ re_count += 1
52
+ req_el = ET.SubElement(fm_el, 'requires')
53
+ req_el.set('name', f'Re-{re_count}')
54
+ req_el.set('feature', feat_a)
55
+ req_el.set('requires', feat_b)
56
+
57
+ ex_count = 0
58
+ for feat_a, feat_b in excludes_list:
59
+ ex_count += 1
60
+ exc_el = ET.SubElement(fm_el, 'excludes')
61
+ exc_el.set('name', f'Ex-{ex_count}')
62
+ exc_el.set('feature', feat_a)
63
+ exc_el.set('excludes', feat_b)
64
+
65
+ rough_str = ET.tostring(fm_el, encoding='unicode')
66
+ dom = minidom.parseString(rough_str)
67
+ lines = dom.toprettyxml(indent='\t').splitlines()
68
+ if lines and lines[0].startswith('<?xml'):
69
+ lines[0] = '<?xml version="1.0" encoding="UTF-8"?>'
70
+ xml_str = '\n'.join(lines)
71
+
72
+ if self._path is not None:
73
+ with open(self._path, 'w', encoding='utf-8') as fh:
74
+ fh.write(xml_str)
75
+
76
+ return xml_str
77
+
78
+ def _extract_binary_constraints(
79
+ self,
80
+ ) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
81
+ requires_list: list[tuple[str, str]] = []
82
+ excludes_list: list[tuple[str, str]] = []
83
+ for constraint in self._source_model.get_constraints():
84
+ raw = str(constraint)
85
+ ast_root = constraint.ast.root
86
+ op = getattr(ast_root.data, 'value', str(ast_root.data)).upper()
87
+ left = ast_root.left
88
+ right = ast_root.right
89
+ if (
90
+ left is not None
91
+ and right is not None
92
+ and left.is_term()
93
+ and right.is_term()
94
+ ):
95
+ if op in ('REQUIRES', 'IMPLIES', '=>') or ast_root.data is ASTOperation.REQUIRES:
96
+ requires_list.append((str(left.data), str(right.data)))
97
+ continue
98
+ if op == 'EXCLUDES' or ast_root.data is ASTOperation.EXCLUDES:
99
+ excludes_list.append((str(left.data), str(right.data)))
100
+ continue
101
+ logger.warning('Constraint skipped (unsupported in XML format): %s', raw)
102
+ self.skipped_constraints.append(raw)
103
+ return requires_list, excludes_list
104
+
105
+
106
+ def _write_feature(feature: Feature, parent_el: ET.Element, counters: list[int]) -> None:
107
+ """Recursively append FAMA XML child elements to *parent_el*."""
108
+ for relation in feature.get_relations():
109
+ _write_relation(relation, parent_el, counters)
110
+
111
+
112
+ def _write_relation(relation: Relation, parent_el: ET.Element, counters: list[int]) -> None:
113
+ children = list(relation.children)
114
+ if len(children) == 1:
115
+ counters[0] += 1
116
+ br_el = ET.SubElement(parent_el, 'binaryRelation')
117
+ br_el.set('name', f'BR-{counters[0]}')
118
+
119
+ card_el = ET.SubElement(br_el, 'cardinality')
120
+ card_el.set('min', str(relation.card_min))
121
+ card_el.set('max', str(relation.card_max))
122
+
123
+ child = children[0]
124
+ sol_el = ET.SubElement(br_el, 'solitaryFeature')
125
+ sol_el.set('name', child.name)
126
+ _write_feature(child, sol_el, counters)
127
+
128
+ elif len(children) > 1:
129
+ counters[1] += 1
130
+ sr_el = ET.SubElement(parent_el, 'setRelation')
131
+ sr_el.set('name', f'SR-{counters[1]}')
132
+
133
+ card_el = ET.SubElement(sr_el, 'cardinality')
134
+ card_el.set('min', str(relation.card_min))
135
+ card_el.set('max', str(relation.card_max))
136
+
137
+ for child in children:
138
+ gf_el = ET.SubElement(sr_el, 'groupedFeature')
139
+ gf_el.set('name', child.name)
140
+ _write_feature(child, gf_el, counters)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-fm
3
- Version: 2.5.0.dev0
3
+ Version: 2.6.0.dev0
4
4
  Summary: flamapy-fm is a plugin to Flamapy module
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,8 +8,8 @@ Project-URL: Homepage, https://github.com/flamapy/fm_metamodel
8
8
  Requires-Python: >=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.5.0.dev0
12
- Requires-Dist: uvlparser~=2.5.0.dev63
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev0
12
+ Requires-Dist: uvlparser~=2.5.0
13
13
  Requires-Dist: afmparser~=1.0.3
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
@@ -20,13 +20,16 @@ Requires-Dist: coverage; extra == "dev"
20
20
  Requires-Dist: antlr4-tools; extra == "dev"
21
21
  Dynamic: license-file
22
22
 
23
- # fm_metamodel
23
+ # flamapy-fm
24
24
 
25
- This repo host the feature model concrete classes
25
+ The feature model metamodel plugin for [flamapy](https://flamapy.org): the
26
+ feature model classes plus readers and writers for many formats (UVL,
27
+ FeatureIDE, Glencoe, AFM, SPLOT, JSON, …).
26
28
 
29
+ **Documentation:** https://docs.flamapy.org/framework/plugins/feature_model_plugin
27
30
 
28
- ## Install for development
31
+ ## Installation
29
32
 
30
- ```
31
- pip install -e .
33
+ ```bash
34
+ pip install flamapy-fm
32
35
  ```
@@ -38,6 +38,7 @@ flamapy/metamodels/fm_metamodel/transformations/splot_writer.py
38
38
  flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py
39
39
  flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py
40
40
  flamapy/metamodels/fm_metamodel/transformations/xml_reader.py
41
+ flamapy/metamodels/fm_metamodel/transformations/xml_writer.py
41
42
  flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py
42
43
  flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py
43
44
  flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py
@@ -1,5 +1,5 @@
1
- flamapy-fw~=2.5.0.dev0
2
- uvlparser~=2.5.0.dev63
1
+ flamapy-fw~=2.6.0.dev0
2
+ uvlparser~=2.5.0
3
3
  afmparser~=1.0.3
4
4
 
5
5
  [dev]
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flamapy-fm"
7
- version = "2.5.0.dev0"
7
+ version = "2.6.0.dev0"
8
8
  description = "flamapy-fm is a plugin to Flamapy module"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
11
11
  authors = [{ name = "Flamapy", email = "flamapy@us.es" }]
12
12
  requires-python = ">=3.9"
13
13
  dependencies = [
14
- "flamapy-fw~=2.5.0.dev0",
15
- "uvlparser~=2.5.0.dev63",
14
+ "flamapy-fw~=2.6.0.dev0",
15
+ "uvlparser~=2.5.0",
16
16
  "afmparser~=1.0.3",
17
17
  ]
18
18
 
@@ -1,10 +0,0 @@
1
- # fm_metamodel
2
-
3
- This repo host the feature model concrete classes
4
-
5
-
6
- ## Install for development
7
-
8
- ```
9
- pip install -e .
10
- ```
File without changes