Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ is loaded if present, or you can point to a custom file with `--config`.

Command-line flags map directly to keys in the `[nextmeeting]` table: remove the
leading `--` and keep hyphen separators (`--max-title-length` → `max-title-length`).
Both hyphens and underscores work in configuration keys (`caldav-url` and `caldav_url` are equivalent).

Example `~/.config/nextmeeting/config.toml`:

Expand Down
13 changes: 11 additions & 2 deletions src/nextmeeting/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,8 +1190,17 @@ def _load_config(path: Path) -> dict:
data = tomllib.load(f)
# Allow a top-level [nextmeeting] table or flat keys
if "nextmeeting" in data and isinstance(data["nextmeeting"], dict):
return data["nextmeeting"]
return data
config_data = data["nextmeeting"]
else:
config_data = data

# Normalize keys: convert hyphens to underscores to match argparse behavior
# This allows both caldav-url and caldav_url in config files
normalized_config = {
key.replace("-", "_"): value for key, value in config_data.items()
}

return normalized_config
except Exception: # noqa: BLE001
return {}

Expand Down
106 changes: 106 additions & 0 deletions tests/test_config_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import tempfile
import os
import sys
from pathlib import Path
from unittest.mock import patch

from nextmeeting.cli import _load_config, parse_args


def test_load_config_normalizes_hyphens_to_underscores():
"""Test that configuration keys with hyphens are normalized to underscores."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""[nextmeeting]
caldav-url = "https://example.com/calendar"
caldav-username = "user"
caldav-password = "pass"
max-title-length = 30
today-only = true
""")
config_path = Path(f.name)

try:
config = _load_config(config_path)

# All keys should be normalized to underscores
assert config["caldav_url"] == "https://example.com/calendar"
assert config["caldav_username"] == "user"
assert config["caldav_password"] == "pass"
assert config["max_title_length"] == 30
assert config["today_only"] is True

# Hyphens should not exist in the keys
assert "caldav-url" not in config
assert "caldav-username" not in config
assert "caldav-password" not in config
assert "max-title-length" not in config
assert "today-only" not in config
finally:
os.unlink(config_path)
Comment on lines +10 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test can be simplified by using pytest's built-in tmp_path fixture. It automatically creates and cleans up a temporary directory for the test, removing the need for tempfile.NamedTemporaryFile with delete=False and manual os.unlink in a try...finally block. This makes the test cleaner, more readable, and less prone to errors if cleanup fails.

def test_load_config_normalizes_hyphens_to_underscores(tmp_path: Path):
    """Test that configuration keys with hyphens are normalized to underscores."""
    config_content = """[nextmeeting]
caldav-url = "https://example.com/calendar"
caldav-username = "user"
caldav-password = "pass"
max-title-length = 30
today-only = true
"""
    config_path = tmp_path / "config.toml"
    config_path.write_text(config_content)

    config = _load_config(config_path)

    # All keys should be normalized to underscores
    assert config["caldav_url"] == "https://example.com/calendar"
    assert config["caldav_username"] == "user"
    assert config["caldav_password"] == "pass"
    assert config["max_title_length"] == 30
    assert config["today_only"] is True

    # Hyphens should not exist in the keys
    assert "caldav-url" not in config
    assert "caldav-username" not in config
    assert "caldav-password" not in config
    assert "max-title-length" not in config
    assert "today-only" not in config

Comment on lines +10 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For cleaner and more idiomatic pytest tests, consider using the tmp_path fixture for creating temporary files. It automatically handles cleanup, removing the need for try...finally blocks and manual os.unlink calls. You will need to add import pytest at the top of the file and change the function signature to accept the tmp_path fixture. This pattern can be applied to the other tests in this file as well.

def test_load_config_normalizes_hyphens_to_underscores(tmp_path: Path):
    """Test that configuration keys with hyphens are normalized to underscores."""
    config_path = tmp_path / "config.toml"
    config_path.write_text("""[nextmeeting]
caldav-url = "https://example.com/calendar"
caldav-username = "user"
caldav-password = "pass"
max-title-length = 30
today-only = true
""")

    config = _load_config(config_path)

    # All keys should be normalized to underscores
    assert config["caldav_url"] == "https://example.com/calendar"
    assert config["caldav_username"] == "user"
    assert config["caldav_password"] == "pass"
    assert config["max_title_length"] == 30
    assert config["today_only"] is True

    # Hyphens should not exist in the keys
    assert "caldav-url" not in config
    assert "caldav-username" not in config
    assert "caldav-password" not in config
    assert "max-title-length" not in config
    assert "today-only" not in config



def test_load_config_works_with_both_formats():
"""Test that both hyphen and underscore formats work in config files."""
# Test with hyphens (README format)
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""[nextmeeting]
caldav-url = "https://example.com/hyphens"
caldav-username = "user-hyphens"
""")
config_path_hyphens = Path(f.name)

# Test with underscores (internal format)
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""[nextmeeting]
caldav_url = "https://example.com/underscores"
caldav_username = "user_underscores"
""")
config_path_underscores = Path(f.name)

try:
# Both should work and produce the same key format
config_hyphens = _load_config(config_path_hyphens)
config_underscores = _load_config(config_path_underscores)

# Both should have normalized underscore keys
assert "caldav_url" in config_hyphens
assert "caldav_username" in config_hyphens
assert "caldav_url" in config_underscores
assert "caldav_username" in config_underscores

# Values should be preserved
assert config_hyphens["caldav_url"] == "https://example.com/hyphens"
assert config_hyphens["caldav_username"] == "user-hyphens"
assert config_underscores["caldav_url"] == "https://example.com/underscores"
assert config_underscores["caldav_username"] == "user_underscores"

finally:
os.unlink(config_path_hyphens)
os.unlink(config_path_underscores)
Comment on lines +42 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous test, this can be simplified using the tmp_path fixture to manage temporary files. This avoids manual file creation and cleanup, making the test more robust and easier to read.

def test_load_config_works_with_both_formats(tmp_path: Path):
    """Test that both hyphen and underscore formats work in config files."""
    # Test with hyphens (README format)
    config_content_hyphens = """[nextmeeting]
caldav-url = "https://example.com/hyphens"
caldav-username = "user-hyphens"
"""
    config_path_hyphens = tmp_path / "config_hyphens.toml"
    config_path_hyphens.write_text(config_content_hyphens)

    # Test with underscores (internal format)
    config_content_underscores = """[nextmeeting]
caldav_url = "https://example.com/underscores"
caldav_username = "user_underscores"
"""
    config_path_underscores = tmp_path / "config_underscores.toml"
    config_path_underscores.write_text(config_content_underscores)

    # Both should work and produce the same key format
    config_hyphens = _load_config(config_path_hyphens)
    config_underscores = _load_config(config_path_underscores)

    # Both should have normalized underscore keys
    assert "caldav_url" in config_hyphens
    assert "caldav_username" in config_hyphens
    assert "caldav_url" in config_underscores
    assert "caldav_username" in config_underscores

    # Values should be preserved
    assert config_hyphens["caldav_url"] == "https://example.com/hyphens"
    assert config_hyphens["caldav_username"] == "user-hyphens"
    assert config_underscores["caldav_url"] == "https://example.com/underscores"
    assert config_underscores["caldav_username"] == "user_underscores"

Comment on lines +42 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test can be made more concise and maintainable by using pytest.mark.parametrize. This avoids duplicating the test logic for different inputs (hyphens vs. underscores) and makes it easier to add more test cases in the future. This also uses the tmp_path fixture for cleaner file handling. You will need to add import pytest at the top of the file.

@pytest.mark.parametrize(
    ("config_content", "expected_values"),
    [
        (
            """[nextmeeting]
caldav-url = "https://example.com/hyphens"
caldav-username = "user-hyphens"
""",
            {
                "caldav_url": "https://example.com/hyphens",
                "caldav_username": "user-hyphens",
            },
        ),
        (
            """[nextmeeting]
caldav_url = "https://example.com/underscores"
caldav_username = "user_underscores"
""",
            {
                "caldav_url": "https://example.com/underscores",
                "caldav_username": "user_underscores",
            },
        ),
    ],
)
def test_load_config_works_with_both_formats(tmp_path: Path, config_content: str, expected_values: dict):
    """Test that both hyphen and underscore formats work in config files."""
    config_path = tmp_path / "config.toml"
    config_path.write_text(config_content)

    config = _load_config(config_path)

    assert config == expected_values



def test_parse_args_accepts_config_with_hyphens():
"""Test that parse_args properly handles config files with hyphenated keys."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""[nextmeeting]
caldav-url = "https://example.com/config-test"
caldav-username = "config-user"
max-title-length = 25
""")
config_path = f.name

# Clear environment variables that might interfere
with patch.dict(os.environ, {}, clear=True):
original_argv = sys.argv
try:
sys.argv = ["nextmeeting", "--config", config_path]
args = parse_args()

# The arguments should be accessible with underscores
assert getattr(args, "caldav_url") == "https://example.com/config-test"
assert getattr(args, "caldav_username") == "config-user"
assert getattr(args, "max_title_length") == 25

finally:
sys.argv = original_argv
os.unlink(config_path)
Comment on lines +82 to +106
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test can also be refactored to use the tmp_path fixture. This simplifies the test setup by handling temporary file management automatically, which is the standard practice in pytest.

def test_parse_args_accepts_config_with_hyphens(tmp_path: Path):
    """Test that parse_args properly handles config files with hyphenated keys."""
    config_content = """[nextmeeting]
caldav-url = "https://example.com/config-test"
caldav-username = "config-user"
max-title-length = 25
"""
    config_path = tmp_path / "config.toml"
    config_path.write_text(config_content)

    # Clear environment variables that might interfere
    with patch.dict(os.environ, {}, clear=True):
        original_argv = sys.argv
        try:
            sys.argv = ["nextmeeting", "--config", str(config_path)]
            args = parse_args()

            # The arguments should be accessible with underscores
            assert getattr(args, "caldav_url") == "https://example.com/config-test"
            assert getattr(args, "caldav_username") == "config-user"
            assert getattr(args, "max_title_length") == 25

        finally:
            sys.argv = original_argv

Comment on lines +82 to +106
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly to the other tests in this file, using the tmp_path fixture here will simplify the code by removing manual file creation and cleanup. Also, using direct attribute access (args.caldav_url) can be slightly more readable than getattr(args, 'caldav_url') when the attribute name is known.

def test_parse_args_accepts_config_with_hyphens(tmp_path: Path):
    """Test that parse_args properly handles config files with hyphenated keys."""
    config_path = tmp_path / "config.toml"
    config_path.write_text("""[nextmeeting]
caldav-url = "https://example.com/config-test"
caldav-username = "config-user"
max-title-length = 25
""")

    # Clear environment variables that might interfere
    with patch.dict(os.environ, {}, clear=True):
        original_argv = sys.argv
        try:
            sys.argv = ["nextmeeting", "--config", str(config_path)]
            args = parse_args()

            # The arguments should be accessible with underscores
            assert args.caldav_url == "https://example.com/config-test"
            assert args.caldav_username == "config-user"
            assert args.max_title_length == 25

        finally:
            sys.argv = original_argv

2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.