-
Notifications
You must be signed in to change notification settings - Fork 1
Add config for API #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import nox | ||
|
|
||
|
|
||
| @nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"]) | ||
| def tests(session): | ||
| session.run("uv", "run", "--python", session.python, "pytest", external=True) |
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # TODO: Remove tomli fallback once Python 3.10 is no longer supported | ||||||||||||||||||||||||||||
| if sys.version_info >= (3, 11): | ||||||||||||||||||||||||||||
| import tomllib | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| import tomli as tomllib | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||||||||||||
| from os import getenv | ||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from rich import print as rprint | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| CONFIG_FILE_NAME = Path("pyproject.toml") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @dataclass | ||||||||||||||||||||||||||||
| class ApiConfig: | ||||||||||||||||||||||||||||
| """Handles loading and storing the config from disk""" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Initialize the arguments and default values | ||||||||||||||||||||||||||||
| _module: str | None = None | ||||||||||||||||||||||||||||
| _site: str | None = None | ||||||||||||||||||||||||||||
| _collection: str | None = None | ||||||||||||||||||||||||||||
| _config_loaded: bool = False | ||||||||||||||||||||||||||||
| config_file: str | Path | None = CONFIG_FILE_NAME | ||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+27
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def __post_init__(self): | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| def __post_init__(self): | |
| def __post_init__(self): | |
| if isinstance(self.config_file, str): | |
| self.config_file = Path(self.config_file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brass75 what do you think about this suggestion?
We aren't doing anything other than reading the path so I'm not too concerned.
kjaymiller marked this conversation as resolved.
Show resolved
Hide resolved
kjaymiller marked this conversation as resolved.
Show resolved
Hide resolved
kjaymiller marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output messages from load_config (lines 82 and 85) will be printed every time a config is loaded during tests, which could create noisy test output. Consider using a logging framework or adding a quiet parameter to suppress output during testing, or capturing the output in tests to verify the behavior.
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The success message "Config loaded from {self.config_file}" is printed even when the TOML file doesn't contain the expected [tool.render-engine.cli] section, resulting in an empty stored_config dict. This could mislead users into thinking their config was loaded successfully when it wasn't. Consider only printing the success message when the config section is found and has values.
| else: | |
| rprint(f"Config loaded from {self.config_file}") | |
| except FileNotFoundError: | |
| # TODO: Raise a custom except that can be caught in try/except in tooling | |
| rprint(f"No config file found at {self.config_file}") | |
| return | |
| except FileNotFoundError: | |
| # TODO: Raise a custom except that can be caught in try/except in tooling | |
| rprint(f"No config file found at {self.config_file}") | |
| return | |
| if stored_config: | |
| rprint(f"Config loaded from {self.config_file}") |
kjaymiller marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
||
| from hypothesis import given, settings | ||
| from hypothesis import strategies as st | ||
|
|
||
| from render_engine_api.config import ApiConfig | ||
|
|
||
|
|
||
| def _write_config(tmp_path, content): | ||
| """Helper to write a pyproject.toml in tmp_path.""" | ||
| config_file = tmp_path / "pyproject.toml" | ||
| config_file.write_text(content) | ||
| return config_file | ||
|
|
||
|
|
||
| # Strategy for valid TOML-safe identifier strings (no quotes, newlines, etc.) | ||
| toml_safe_text = st.text( | ||
| alphabet=st.characters(categories=("L", "N"), include_characters="_-"), | ||
| min_size=1, | ||
| max_size=50, | ||
| ) | ||
|
|
||
| # Strategy that produces an optional config key (present or absent) | ||
| optional_toml_value = st.one_of(st.none(), toml_safe_text) | ||
|
|
||
|
|
||
| class TestAPIConfigLoadConfig: | ||
| """Tests for APIConfig.load_config parsing pyproject.toml.""" | ||
|
|
||
| @given( | ||
| module=optional_toml_value, | ||
| site=optional_toml_value, | ||
| collection=optional_toml_value, | ||
| ) | ||
| @settings(max_examples=50, deadline=None) | ||
| def test_loads_module_site_from_valid_config(self, module, site, collection): | ||
| """Config correctly populates module, site, and collection from any combination of keys.""" | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| tmp_path = Path(tmpdir) | ||
| lines = ["[tool.render-engine.cli]"] | ||
| if module is not None: | ||
| lines.append(f'module = "{module}"') | ||
| if site is not None: | ||
| lines.append(f'site = "{site}"') | ||
| if collection is not None: | ||
| lines.append(f'collection = "{collection}"') | ||
|
|
||
| temp_config_file = _write_config(tmp_path, "\n".join(lines) + "\n") | ||
| config = ApiConfig(config_file=temp_config_file) | ||
| config.load_config() | ||
|
|
||
| assert config._module == module | ||
| assert config._site == site | ||
| assert config._collection == collection | ||
|
|
||
| def test_config_file_not_passed(self): | ||
| """Returns None for all properties when no config_file is passed.""" | ||
| config = ApiConfig(config_file=None) # intentional path that doesn't exist | ||
| assert config.module is None | ||
| assert config.site is None | ||
| assert config.collection is None | ||
| # Editor pulls from editor by default | ||
|
|
||
| def test_config_file_not_found(self, tmp_path): | ||
| """Returns None for all properties when the supplied config file does not exist.""" | ||
| config = ApiConfig(config_file=tmp_path / "no-pyproject.toml") # intentional path that doesn't exist | ||
| assert config.module is None | ||
| assert config.site is None | ||
| assert config.collection is None | ||
| # Editor pulls from editor by default | ||
|
|
||
| def test_invalid_toml_prints_error_and_returns_none(self, tmp_path, capsys): | ||
| """Returns None for all properties when the config file contains invalid TOML.""" | ||
| config_file = _write_config(tmp_path, "not valid toml [[[") | ||
| config = ApiConfig(config_file=config_file) | ||
| # Accessing properties should trigger config loading and TOML error handling. | ||
| assert config.module is None | ||
| assert config.site is None | ||
| assert config.collection is None | ||
| captured = capsys.readouterr() | ||
| # Ensure that an error message was written to stderr. | ||
| assert captured.err | ||
|
|
||
| def test_config_file_not_ran_if_self_config_loaded_equals_true(self, tmp_path): | ||
| config_file = _write_config( | ||
| tmp_path, | ||
| content=""" | ||
| module="app" | ||
| site="app" | ||
| editor="nvim" | ||
| """, | ||
| ) | ||
kjaymiller marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| config = ApiConfig(config_file=config_file, _config_loaded=True) | ||
| assert config.module is None | ||
| assert config.site is None | ||
| assert config.collection is None | ||
|
|
||
| def test_editor_from_config(self, tmp_path, monkeypatch): | ||
| """Reads the editor value from the config file.""" | ||
| config_file = _write_config(tmp_path, content='[tool.render-engine.cli]\neditor="nvim"\n') | ||
| config = ApiConfig(config_file=config_file) | ||
| assert config.editor == "nvim" | ||
|
|
||
| def test_editor_falls_back_to_env(self, tmp_path, monkeypatch): | ||
| """Falls back to the EDITOR environment variable when not in config.""" | ||
| monkeypatch.setenv("EDITOR", "fake-editor") | ||
| config = ApiConfig() | ||
| assert config.editor == "fake-editor" | ||
|
|
||
| def test_editor_none_when_not_set(self, monkeypatch): | ||
| """Returns None when editor is not in config or environment.""" | ||
| monkeypatch.delenv("EDITOR", raising=False) | ||
| config = ApiConfig() | ||
| assert config.editor is None | ||
|
|
||
|
|
||
| class TestApiConfigLazyLoading: | ||
| """Tests that ApiConfig lazily loads the config on first property access.""" | ||
|
|
||
| def test_config_not_loaded_until_property_accessed(self, tmp_path): | ||
| """Config file is not read until a property is first accessed.""" | ||
| config_file = _write_config( | ||
| tmp_path, | ||
| """ | ||
| [tool.render-engine.cli] | ||
| module = "myapp" | ||
| site = "MySite" | ||
| """, | ||
| ) | ||
| config = ApiConfig(config_file=config_file) | ||
| assert config._config_loaded is False | ||
| _ = config.module | ||
| assert config._config_loaded is True | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should at least pin >= 6.0.0