Skip to content

Configuration normalization#25

Merged
chmouel merged 3 commits intomainfrom
fix-24
Oct 8, 2025
Merged

Configuration normalization#25
chmouel merged 3 commits intomainfrom
fix-24

Conversation

@chmouel
Copy link
Copy Markdown
Owner

@chmouel chmouel commented Oct 7, 2025

This pull request improves configuration file handling in the nextmeeting CLI by ensuring that both hyphens and underscores are accepted in configuration keys, making the tool more flexible and user-friendly. It also adds comprehensive tests to verify this normalization logic and updates the documentation to reflect the change.

Configuration normalization improvements:

  • Updated _load_config in src/nextmeeting/cli.py to normalize configuration keys by converting hyphens to underscores, ensuring compatibility with both formats and matching argparse behavior.

Testing enhancements:

  • Added new tests in tests/test_config_loading.py to verify that configuration keys with hyphens are correctly normalized to underscores, that both formats work interchangeably, and that parse_args properly handles config files with hyphenated keys.

Documentation update:

  • Modified README.md to clarify that both hyphens and underscores are accepted in configuration keys.

The project version was updated in the lock file, reflecting a new
release iteration.

Signed-off-by: Chmouel Boudjnah <chmouel@chmouel.com>
Updated configuration loading to normalize keys by converting hyphens to
underscores. This was done to ensure consistency between command-line
arguments (which use underscores) and configuration file entries,
allowing both hyphenated and underscored keys to be accepted in TOML
files. Added tests to verify this behavior.

Fixes #47

Signed-off-by: Chmouel Boudjnah <chmouel@chmouel.com>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @chmouel, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant quality-of-life improvement for configuration handling by enabling greater flexibility in how configuration keys are defined. It allows users to interchangeably use hyphens or underscores in their configuration files, which are then consistently normalized internally. This enhancement is thoroughly documented and backed by new test cases, alongside a routine update to the project's version.

Highlights

  • Configuration Key Flexibility: The configuration loading mechanism has been enhanced to allow both hyphenated (e.g., "caldav-url") and underscore-separated (e.g., "caldav_url") keys in the configuration files. All keys are internally normalized to underscores for consistent processing.
  • Documentation Update: The README.md file has been updated to reflect the new flexibility in configuration key naming, explicitly stating that both hyphens and underscores are now supported.
  • Enhanced Test Suite: New unit tests have been added to tests/test_config_loading.py to thoroughly verify the correct normalization of hyphenated configuration keys to underscores and ensure compatibility with parse_args.
  • Project Version Update: The project version in uv.lock has been updated from 2.0.0 to 3.0.0, reflecting a new release iteration.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a nice quality-of-life improvement by allowing hyphens in configuration file keys, which are then normalized to underscores for internal use. The implementation is straightforward and is accompanied by a good set of new tests. My review includes suggestions to make the code more idiomatic and robust:

  • In src/nextmeeting/cli.py, I've suggested using a dictionary comprehension to simplify the key normalization logic.
  • In tests/test_config_loading.py, I've recommended refactoring the tests to use pytest's tmp_path fixture for cleaner and safer temporary file management.
    Overall, this is a solid contribution that improves user experience.

Comment thread src/nextmeeting/cli.py Outdated
Comment on lines +1199 to +1202
normalized_config = {}
for key, value in config_data.items():
normalized_key = key.replace("-", "_")
normalized_config[normalized_key] = value
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 loop for normalizing keys can be expressed more concisely and idiomatically using a dictionary comprehension. This improves readability by reducing boilerplate code.

Suggested change
normalized_config = {}
for key, value in config_data.items():
normalized_key = key.replace("-", "_")
normalized_config[normalized_key] = value
normalized_config = {key.replace("-", "_"): value for key, value in config_data.items()}

Comment on lines +10 to +39
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)
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 +42 to +79
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)
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 +82 to +106
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)
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

@chmouel chmouel changed the title chore: Update project version to 3.0.0 in lock file Configuration normalization Oct 7, 2025
@chmouel
Copy link
Copy Markdown
Owner Author

chmouel commented Oct 7, 2025

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a great improvement, making the configuration more user-friendly by accepting both hyphens and underscores. The addition of comprehensive tests is also excellent. My feedback focuses on a couple of areas:

  1. In src/nextmeeting/cli.py, I've suggested a more concise way to implement the key normalization using a dictionary comprehension.
  2. In the new test file tests/test_config_loading.py, I've provided suggestions to refactor the tests to use standard pytest features like the tmp_path fixture and test parametrization. This will make the tests cleaner, more robust, and easier to maintain.

Comment thread src/nextmeeting/cli.py Outdated
Comment on lines +1199 to +1204
normalized_config = {}
for key, value in config_data.items():
normalized_key = key.replace("-", "_")
normalized_config[normalized_key] = value

return normalized_config
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

The key normalization logic can be expressed more concisely and idiomatically using a dictionary comprehension. This is a common Python pattern for creating a new dictionary based on an existing iterable.

Suggested change
normalized_config = {}
for key, value in config_data.items():
normalized_key = key.replace("-", "_")
normalized_config[normalized_key] = value
return normalized_config
return {key.replace("-", "_"): value for key, value in config_data.items()}

Comment on lines +10 to +39
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)
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

Comment on lines +42 to +79
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)
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

Comment on lines +82 to +106
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)
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

Used a dictionary comprehension for normalizing configuration keys
during loading. This simplified the creation of the normalized
configuration dictionary.
@chmouel chmouel merged commit d386d94 into main Oct 8, 2025
2 checks passed
@chmouel chmouel deleted the fix-24 branch October 8, 2025 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant