|
1 | 1 | import csv |
| 2 | +import ast |
| 3 | +from typing import Any |
| 4 | +import pathlib |
2 | 5 |
|
3 | 6 | from flamapy.core.transformations.text_to_model import TextToModel |
| 7 | +from flamapy.core.exceptions import ConfigurationNotFound |
4 | 8 |
|
5 | 9 | from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration |
6 | | -from flamapy.core.utils import file_exists |
7 | | -from flamapy.core.exceptions import ConfigurationNotFound |
8 | 10 |
|
9 | 11 |
|
10 | 12 | class ConfigurationBasicReader(TextToModel): |
| 13 | + """Reads a configuration from a csvconf file and transforms it into a Configuration model. |
| 14 | + |
| 15 | + The csvconf file should have two columns (separated by commas): |
| 16 | + - The first column contains the element names (e.g., features). |
| 17 | + - The second column contains the values for those elements. |
| 18 | + Values can be of various types, including Boolean, numeric, strings, lists, or dictionaries. |
| 19 | +
|
| 20 | + If the path is a directory, it will read all csvconf files in that directory and concatenate |
| 21 | + their configurations into a single Configuration object. |
| 22 | + This allows for managing multiple configurations associated with variability models split in |
| 23 | + several files (e.g., imports in UVL models). |
| 24 | +
|
| 25 | + If the csvconf file or directory does not exist, it raises a ConfigurationNotFound exception. |
| 26 | + """ |
| 27 | + |
11 | 28 | @staticmethod |
12 | 29 | def get_source_extension() -> str: |
13 | 30 | return 'csvconf' |
14 | 31 |
|
15 | 32 | def __init__(self, path: str) -> None: |
16 | | - self._path = path |
| 33 | + self._path: str = path |
17 | 34 |
|
18 | 35 | def transform(self) -> Configuration: |
19 | | - csv_reader = self.get_configuration_from_csv(self._path) |
| 36 | + path = pathlib.Path(self._path) |
| 37 | + if path.is_file(): |
| 38 | + return self.get_configuration_from_csv(path) |
| 39 | + elif path.is_dir(): |
| 40 | + return self.get_configuration_from_directory(path) |
| 41 | + else: |
| 42 | + raise ConfigurationNotFound |
| 43 | + |
| 44 | + def get_configuration_from_csv(self, path: pathlib.Path) -> Configuration: |
20 | 45 | elements = {} |
21 | | - for row in csv_reader: |
22 | | - # Assuming that row[1] is supposed to represent a boolean value |
23 | | - # Convert 'true'/'false' strings to actual boolean values |
24 | | - elements[row[0]] = row[1].lower() == 'true' |
| 46 | + with open(path, 'r', encoding='utf-8') as csvfile: |
| 47 | + csv_reader = list(csv.reader(csvfile)) |
| 48 | + for row in csv_reader: |
| 49 | + element = row[0] |
| 50 | + value = convert(row[1]) |
| 51 | + elements[element] = value |
| 52 | + return Configuration(elements) |
| 53 | + |
| 54 | + def get_configuration_from_directory(self, directory: pathlib.Path) -> Configuration: |
| 55 | + """Reads all CSV files in a directory and returns a Configuration object as |
| 56 | + result of concatenating all configurations.""" |
| 57 | + elements = {} |
| 58 | + for filepath in directory.rglob('*.csvconf'): |
| 59 | + if filepath.is_file(): |
| 60 | + config = self.get_configuration_from_csv(filepath) |
| 61 | + elements.update(config.elements) |
25 | 62 | return Configuration(elements) |
26 | 63 |
|
27 | | - def get_configuration_from_csv(self, path: str) -> list[list[str]]: |
28 | | - # Returns a list of list |
29 | | - if not file_exists(path): |
30 | | - raise ConfigurationNotFound |
31 | 64 |
|
32 | | - with open(path, 'r', encoding='utf-8') as csvfile: |
33 | | - csvreader = list(csv.reader(csvfile)) |
| 65 | +def convert(value: str) -> Any: |
| 66 | + """ |
| 67 | + Converts a string to the most appropriate Python type. |
| 68 | + |
| 69 | + Handles: |
| 70 | + - Boolean values like 'true', 'FALSE', etc. |
| 71 | + - Numeric values (int, float) |
| 72 | + - Python literals (lists, dicts, tuples, None) |
| 73 | + - Returns the original string if no conversion is possible |
| 74 | + """ |
| 75 | + stripped = value.strip() |
| 76 | + # Empty or whitespace-only → None |
| 77 | + if stripped == '': |
| 78 | + return None |
| 79 | + |
| 80 | + # Normalize and check for boolean values |
| 81 | + val_lower = value.strip().lower() |
| 82 | + if val_lower == 'true': |
| 83 | + return True |
| 84 | + if val_lower == 'false': |
| 85 | + return False |
34 | 86 |
|
35 | | - return csvreader |
| 87 | + try: |
| 88 | + # Try to evaluate safe Python literals (e.g. numbers, lists, tuples) |
| 89 | + return ast.literal_eval(value) |
| 90 | + except (ValueError, SyntaxError): |
| 91 | + # If evaluation fails, return the string itself |
| 92 | + return value |
0 commit comments