Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 27, 2026

  • Create versions command in src/rhiza/commands/versions.py
  • Add CLI entry point in src/rhiza/cli.py
  • Create comprehensive tests in tests/test_commands/test_versions.py
  • Update CLI documentation in CLI.md
  • Fix linting issues
  • Handle TOML decode errors gracefully
  • Fix regex for version specifier validation
  • Add test coverage for malformed TOML
  • Add test coverage for all operators (<=, >, ==, !=)
  • Export versions command in init.py
  • Update module docstring with versions command
  • Add clarifying comments for exceptions and CANDIDATES
  • Clarify docstring example in parse_version
  • Add security note for CI/CD example with version pinning
Original prompt

This section details on the original issue you should resolve

<issue_title>I want a versions command</issue_title>
<issue_description>The version command should extract the feasible version from a script like the one below.
The pyproject file should either be in the local path right now, if called from this path or specified as an argument

#!/usr/bin/env python3
"""Emit the list of supported Python versions from pyproject.toml.

This helper is used in GitHub Actions to compute the test matrix.
"""

import json
import re
import tomllib
from pathlib import Path

PYPROJECT = Path(file).resolve().parents[2] / "pyproject.toml"
CANDIDATES = ["3.11", "3.12", "3.13", "3.14"] # extend as needed

class RhizaError(Exception):
"""Base exception for Rhiza-related errors."""

class VersionSpecifierError(RhizaError):
"""Raised when a version string or specifier is invalid."""

class PyProjectError(RhizaError):
"""Raised when there are issues with pyproject.toml configuration."""

def parse_version(v: str) -> tuple[int, ...]:
"""Parse a version string into a tuple of integers.

This is intentionally simple and only supports numeric components.
If a component contains non-numeric suffixes (e.g. '3.11.0rc1'),
the leading numeric portion will be used (e.g. '0rc1' -> 0). If a
component has no leading digits at all, a VersionSpecifierError is raised.

Args:
    v: Version string to parse (e.g., "3.11", "3.11.0rc1").

Returns:
    Tuple of integers representing the version.

Raises:
    VersionSpecifierError: If a version component has no numeric prefix.
"""
parts: list[int] = []
for part in v.split("."):
    match = re.match(r"\d+", part)
    if not match:
        msg = f"Invalid version component {part!r} in version {v!r}; expected a numeric prefix."
        raise VersionSpecifierError(msg)
    parts.append(int(match.group(0)))
return tuple(parts)

def _check_operator(version_tuple: tuple[int, ...], op: str, spec_v_tuple: tuple[int, ...]) -> bool:
"""Check if a version tuple satisfies an operator constraint."""
operators = {
">=": lambda v, s: v >= s,
"<=": lambda v, s: v <= s,
">": lambda v, s: v > s,
"<": lambda v, s: v < s,
"==": lambda v, s: v == s,
"!=": lambda v, s: v != s,
}
return operators[op](version_tuple, spec_v_tuple)

def satisfies(version: str, specifier: str) -> bool:
"""Check if a version satisfies a comma-separated list of specifiers.

This is a simplified version of packaging.specifiers.SpecifierSet.
Supported operators: >=, <=, >, <, ==, !=

Args:
    version: Version string to check (e.g., "3.11").
    specifier: Comma-separated specifier string (e.g., ">=3.11,<3.14").

Returns:
    True if the version satisfies all specifiers, False otherwise.

Raises:
    VersionSpecifierError: If the specifier format is invalid.
"""
version_tuple = parse_version(version)

# Split by comma for multiple constraints
for spec in specifier.split(","):
    spec = spec.strip()
    # Match operator and version part
    match = re.match(r"(>=|<=|>|<|==|!=)\s*([\d.]+)", spec)
    if not match:
        # If no operator, assume ==
        if re.match(r"[\d.]+", spec):
            if version_tuple != parse_version(spec):
                return False
            continue
        msg = f"Invalid specifier {spec!r}; expected format like '>=3.11' or '3.11'"
        raise VersionSpecifierError(msg)

    op, spec_v = match.groups()
    spec_v_tuple = parse_version(spec_v)

    if not _check_operator(version_tuple, op, spec_v_tuple):
        return False

return True

def supported_versions() -> list[str]:
"""Return all supported Python versions declared in pyproject.toml.

Reads project.requires-python, evaluates candidate versions against the
specifier, and returns the subset that satisfy the constraint, in ascending order.

Returns:
    list[str]: The supported versions (e.g., ["3.11", "3.12"]).

Raises:
    PyProjectError: If requires-python is missing or no candidates match.
"""
# Load pyproject.toml using the tomllib standard library (Python 3.11+)
with PYPROJECT.open("rb") as f:
    data = tomllib.load(f)

# Extract the requires-python field from project metadata
# This specifies the Python version constraint (e.g., ">=3.11")
spec_str = data.get("project", {}).get("requires-python")
if not spec_str:
    msg = "pyproject.toml: missing 'project.requires-python'"
    raise PyProjectError(msg)

# Filter candidate versions to find which ones satisfy the constraint
versions: list[str] = []
for v in CANDIDATES:
    if satisfies(v, spec_str):
        versions.append(v)

if not versions:
    msg = f"pyproject.toml: no supported Python versions match ...

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits January 27, 2026 04:26
…ect.toml

Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
Copilot AI changed the title [WIP] Add versions command to extract feasible version Add versions command to extract supported Python versions from pyproject.toml Jan 27, 2026
@tschm tschm marked this pull request as ready for review January 27, 2026 04:31
Copilot AI requested a review from tschm January 27, 2026 04:31
@tschm tschm requested a review from HarryCampion January 27, 2026 04:35
@tschm
Copy link
Member

tschm commented Jan 27, 2026

thomasschmelzer@Thomass-MacBook-Pro rhiza-cli % git status
On branch copilot/add-versions-command
Your branch is up to date with 'origin/copilot/add-versions-command'.

nothing to commit, working tree clean
thomasschmelzer@Thomass-MacBook-Pro rhiza-cli % git pull
Already up to date.
thomasschmelzer@Thomass-MacBook-Pro rhiza-cli % uv run rhiza versions
2026-01-27 08:34:22.287 | INFO     | rhiza.commands.versions:versions:172 - Reading Python version requirements from: /Users/thomasschmelzer/projects/jqr/repos/rhiza-cli/pyproject.toml
2026-01-27 08:34:22.287 | SUCCESS  | rhiza.commands.versions:versions:176 - Supported Python versions: ['3.11', '3.12', '3.13', '3.14']
["3.11", "3.12", "3.13", "3.14"]
thomasschmelzer@Thomass-MacBook-Pro rhiza-cli %

@tschm
Copy link
Member

tschm commented Jan 27, 2026

@HarryCampion We can remove the .rhiza/utils folder by just calling this command

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a rhiza versions command to extract supported Python versions from pyproject.toml and output them as JSON, primarily for CI/CD test matrix generation. The command parses the requires-python field, evaluates candidate versions (3.11-3.14) against version specifiers, and outputs matching versions.

Changes:

  • Adds versions command implementation with version parsing and constraint checking logic
  • Integrates command into CLI with support for directory or file path arguments
  • Provides comprehensive test coverage for parsing, validation, and CLI integration
  • Documents command usage with GitHub Actions integration example

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
src/rhiza/commands/versions.py Core implementation with version parsing, constraint checking, and JSON output generation
src/rhiza/cli.py CLI integration adding versions subcommand with path argument support
tests/test_commands/test_versions.py Comprehensive test suite covering parsing, validation, command behavior, and CLI integration
CLI.md Documentation with usage examples and GitHub Actions integration pattern

tschm and others added 2 commits January 27, 2026 09:04
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…arify comments

- Add tests for missing operators (<=, >, ==, !=)
- Add test for malformed TOML handling
- Export versions command in __init__.py
- Update module docstring to document versions command
- Add clarifying comments for custom exceptions and CANDIDATES list

Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Clarify parse_version docstring example for better understanding
- Add security note and version pinning to GitHub Actions example in CLI.md

Co-authored-by: tschm <2046079+tschm@users.noreply.github.com>
tschm and others added 5 commits January 27, 2026 10:27
Use ValueError and RuntimeError instead of custom VersionSpecifierError
and PyProjectError classes to match exception handling patterns used in
other commands (init.py, materialize.py). Also export versions command
in __init__.py.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add test cases for <=, >, ==, and != operators which were documented
but not tested. Also add test for malformed TOML syntax to verify
tomllib.TOMLDecodeError is raised appropriately.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add type annotation to operators dict to fix mypy error
- Add match parameter to pytest.raises for more specific assertions
- Remove unnecessary noqa comments (auto-cleaned by ruff)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cover implicit equality specifiers and exception handlers in the
versions function that were previously untested.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use match parameter in pytest.raises() instead of separate assertion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@tschm
Copy link
Member

tschm commented Jan 27, 2026

@HarryCampion I guess this should better be in rhiza-tools?

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.

I want a versions command

2 participants