Skip to content

Commit

Permalink
feat: support user specific configuration files
Browse files Browse the repository at this point in the history
  • Loading branch information
cuinixam committed Sep 29, 2024
1 parent 92137d1 commit 96ef57a
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 14 deletions.
30 changes: 29 additions & 1 deletion docs/features/configuration.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# {octicon}`tools;1.5em;sd-mr-1` Configuration

The project configuration is defined as `.yaml` files. YANGA parses all `yanga.yaml` files in the project. Some directories are excluded while searching for the configuration files (e.g., `.venv`, `.git`, etc.) to avoid unnecessary parsing of files.
The project configuration is defined as `.yaml` files.
YANGA parses all `yanga.yaml` files in the project.
Some directories are excluded while searching for the configuration files (e.g., `.venv`, `.git`, etc.) to avoid unnecessary parsing of files.

One can override the default configuration by providing a custom configuration file. Either by providing a `yanga.ini` file or adding the configuration in the `pyproject.toml` file.

Ini file example:

```{code-block} ini
:linenos:
:name: yanga.ini
[default]
configuration_file_name = my_yanga.txt
exclude_dirs = .git, build, .venv
```

TOML file example:

```{code-block} toml
:linenos:
:name: pyproject.toml
[tool.yanga]
configuration_file_name = "my_yanga.txt"
exclude_dirs = [".git", "build", ".venv"]
```

With the example above, YANGA will look for the `my_yanga.txt` file in the project root directory and exclude the `.git`, `build`, and `.venv` directories while searching for the configuration files.
2 changes: 1 addition & 1 deletion docs/internals/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ yanga init --help
```

```{eval-rst}
.. autoclass:: yanga.commands.init::InitCommand
.. autoclass:: yanga.commands.init::YangaInit
:members:
:undoc-members:
```
8 changes: 7 additions & 1 deletion src/yanga/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from yanga.domain.config import PlatformConfig, VariantConfig
from yanga.domain.execution_context import ExecutionContext, UserRequest, UserRequestScope
from yanga.domain.project_slurper import YangaProjectSlurper
from yanga.ini import YangaIni

from .base import CommandConfigBase, CommandConfigFactory, prompt_user_to_select_option

Expand Down Expand Up @@ -66,7 +67,7 @@ def run(self, args: Namespace) -> int:
return 0

def do_run(self, config: RunCommandConfig) -> int:
project_slurper = YangaProjectSlurper(config.project_dir)
project_slurper = self.create_project_slurper(config.project_dir)
if config.print:
project_slurper.print_project_info()
return 0
Expand All @@ -90,6 +91,11 @@ def do_run(self, config: RunCommandConfig) -> int:
)
return 0

@staticmethod
def create_project_slurper(project_dir: Path) -> YangaProjectSlurper:
ini_config = YangaIni.from_toml_or_ini(project_dir / "yanga.ini", project_dir / "pyproject.toml")
return YangaProjectSlurper(project_dir, ini_config.configuration_file_name)

@staticmethod
def execute_pipeline_steps(
project_dir: Path,
Expand Down
13 changes: 5 additions & 8 deletions src/yanga/domain/config_slurper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import List, Set
from typing import List, Optional, Set

from .config import YangaUserConfig

Expand Down Expand Up @@ -45,19 +45,16 @@ def _is_excluded(dir_path: Path, exclude_paths: Set[Path]) -> bool:
class YangaConfigSlurper:
"""Read all 'yanga.yaml' configuration files from the project."""

CONFIG_FILE = "yanga.yaml"

def __init__(self, project_dir: Path, exclude_dirs: List[str] | None = None) -> None:
def __init__(self, project_dir: Path, exclude_dirs: List[str] | None = None, configuration_file_name: Optional[str] = None) -> None:
self.project_dir = project_dir
self.exclude_dirs = exclude_dirs if exclude_dirs else []
self.configuration_file_name = configuration_file_name or "yanga.yaml"

def slurp(self) -> List[YangaUserConfig]:
user_configs = []
config_files = find_files(self.project_dir, self.CONFIG_FILE, self.exclude_dirs)
config_files = find_files(self.project_dir, self.configuration_file_name, self.exclude_dirs)
with ThreadPoolExecutor() as executor:
future_to_file = {
executor.submit(self.parse_config_file, config_file): config_file for config_file in config_files
}
future_to_file = {executor.submit(self.parse_config_file, config_file): config_file for config_file in config_files}
for future in as_completed(future_to_file):
user_configs.append(future.result())
return user_configs
Expand Down
7 changes: 5 additions & 2 deletions src/yanga/domain/project_slurper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@


class YangaProjectSlurper:
def __init__(self, project_dir: Path) -> None:
def __init__(self, project_dir: Path, configuration_file_name: Optional[str] = None, exclude_dirs: Optional[List[str]] = None) -> None:
self.logger = logger.bind()
self.project_dir = project_dir
exclude = exclude_dirs if exclude_dirs else []
# Merge the exclude directories with the hardcoded ones
exclude = list({*exclude, ".git", ".github", ".vscode", "build", ".venv"})
# TODO: Get rid of the exclude directories hardcoded list. Maybe use an ini file?
self.user_configs: List[YangaUserConfig] = YangaConfigSlurper(project_dir=self.project_dir, exclude_dirs=[".git", ".github", ".vscode", "build", ".venv"]).slurp()
self.user_configs: List[YangaUserConfig] = YangaConfigSlurper(project_dir=self.project_dir, exclude_dirs=exclude, configuration_file_name=configuration_file_name).slurp()
self.components_configs_pool: ComponentsConfigsPool = self._collect_components_configs(self.user_configs)
self.pipeline: Optional[PipelineConfig] = self._find_pipeline_config(self.user_configs)
self.variants: List[VariantConfig] = self._collect_variants(self.user_configs)
Expand Down
2 changes: 1 addition & 1 deletion src/yanga/gui/ygui.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ def run_command(self, user_request: UserRequest) -> None:

def _create_project_slurper(self) -> Optional[YangaProjectSlurper]:
try:
project_slurper = YangaProjectSlurper(self.project_dir)
project_slurper = RunCommand.create_project_slurper(self.project_dir)
project_slurper.print_project_info()
return project_slurper
except UserNotificationException as e:
Expand Down
63 changes: 63 additions & 0 deletions src/yanga/ini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import configparser
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

from mashumaro import DataClassDictMixin

try:
# For Python 3.11 and later
import tomllib
except ModuleNotFoundError:
import tomli as tomllib


@dataclass
class YangaIni(DataClassDictMixin):
#: Custom name for the YANGA configuration files. Default is 'yanga.yaml'
configuration_file_name: Optional[str] = None
#: Exclude directories from parsing
exclude_dirs: List[str] = field(default_factory=list)

@classmethod
def from_toml_or_ini(cls, ini_file: Optional[Path], pyproject_toml: Optional[Path]) -> "YangaIni":
# Initialize an empty dictionary to hold configurations
config_data: Dict[str, Any] = {}

# Load configurations from the INI file if provided
if ini_file and ini_file.is_file():
ini_config = cls.load_ini_config(ini_file)
config_data.update(ini_config)

# Load configurations from the TOML file if provided
if pyproject_toml and pyproject_toml.is_file():
toml_config = cls.load_toml_config(pyproject_toml)
# TOML configurations take precedence over INI configurations
config_data.update(toml_config)
return cls.from_dict(config_data)

@staticmethod
def load_ini_config(ini_file: Path) -> Dict[str, Any]:
"""Read the ini file and return the configuration as a dictionary."""
config: Dict[str, Any] = {}
parser = configparser.ConfigParser()
parser.read(ini_file)
for section in parser.sections():
for key, value in parser.items(section):
if key == "exclude_dirs":
config[key] = [x.strip() for x in value.split(",")]
else:
config[key] = value
return config

@staticmethod
def load_toml_config(pyproject_toml: Path) -> Dict[str, Any]:
"""Read the pyproject.toml file and return the configuration as a dictionary."""
config = {}
with pyproject_toml.open("rb") as f:
data = tomllib.load(f)

# Access the [tool.yanga] section
yanga_config = data.get("tool", {}).get("yanga", {})
config.update(yanga_config)
return config
64 changes: 64 additions & 0 deletions tests/test_ini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# test_yanga_ini.py

from pathlib import Path

import pytest

from yanga.ini import YangaIni


@pytest.fixture
def ini_file(tmp_path: Path) -> Path:
"""Create a temporary INI file for testing."""
ini_content = """
[default]
configuration_file_name = yanga_from_ini.yaml
exclude_dirs = .git, .github, .vscode, build, .venv
"""
ini_file_path = tmp_path / "yanga.ini"
ini_file_path.write_text(ini_content)
return ini_file_path


@pytest.fixture
def toml_file(tmp_path: Path) -> Path:
"""Create a temporary pyproject.toml file for testing."""
toml_content = """
[tool.yanga]
configuration_file_name = "yanga_from_toml.yaml"
exclude_dirs = [".git", ".github", ".vscode", "build", ".venv"]
"""
toml_file_path = tmp_path / "pyproject.toml"
toml_file_path.write_text(toml_content)
return toml_file_path


def test_load_ini_config(ini_file: Path) -> None:
"""Test loading configuration from an INI file."""
config = YangaIni.load_ini_config(ini_file)
expected_config = {"configuration_file_name": "yanga_from_ini.yaml", "exclude_dirs": [".git", ".github", ".vscode", "build", ".venv"]}
assert config == expected_config


def test_load_toml_config(toml_file: Path) -> None:
"""Test loading configuration from a TOML file."""
config = YangaIni.load_toml_config(toml_file)
expected_config = {"configuration_file_name": "yanga_from_toml.yaml", "exclude_dirs": [".git", ".github", ".vscode", "build", ".venv"]}
assert config == expected_config


def test_from_toml_or_ini_only_ini(ini_file: Path, toml_file: Path) -> None:
"""Test instantiating YangaIni from only an INI file."""
config = YangaIni.from_toml_or_ini(ini_file=ini_file, pyproject_toml=toml_file)
assert config.configuration_file_name == "yanga_from_toml.yaml"


def test_from_toml_without_yanga_info(tmp_path: Path) -> None:
toml_content = """
[tool.pytest]
configuration_file_name = "yanga_from_toml.yaml"
"""
toml_file_path = tmp_path / "pyproject.toml"
toml_file_path.write_text(toml_content)
config = YangaIni.from_toml_or_ini(ini_file=None, pyproject_toml=toml_file_path)
assert config.configuration_file_name is None

0 comments on commit 96ef57a

Please sign in to comment.