diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml index 3928ad7e..b1a627f5 100644 --- a/docs/source/_toctree.yml +++ b/docs/source/_toctree.yml @@ -67,4 +67,6 @@ title: kernels skills - local: cli-create-and-upload-card title: kernels create-and-upload-card + - local: cli-collate-readme + title: kernels collate-readme title: CLI Reference diff --git a/docs/source/cli-collate-readme.md b/docs/source/cli-collate-readme.md new file mode 100644 index 00000000..11d540b6 --- /dev/null +++ b/docs/source/cli-collate-readme.md @@ -0,0 +1,17 @@ +### kernels collate-readme + +Use `kernels collate-readme ` to generate a README for the `main` +branch of a kernel repository. This README lists all available version +branches (e.g., `v1`, `v2`) with links, so visitors landing on +the default branch can discover the kernel's versions. + +By default, the README is printed to stdout. Use `--push-to-hub` to upload +it directly to the `main` branch on the Hub. + +```bash +# Print +kernels collate-readme kernels-community/activation + +# Push to the Hub +kernels collate-readme kernels-community/activation --push-to-hub +``` diff --git a/kernels/src/kernels/cli/__init__.py b/kernels/src/kernels/cli/__init__.py index f85d5351..47b16259 100644 --- a/kernels/src/kernels/cli/__init__.py +++ b/kernels/src/kernels/cli/__init__.py @@ -4,6 +4,7 @@ import sys from pathlib import Path +from kernels.cli.collate_readme import collate_readme_from_versions from kernels.cli.doc import generate_readme_for_kernel from kernels.cli.init import parse_kernel_name, run_init from kernels.cli.skills import add_skill @@ -278,6 +279,22 @@ def main(): ) repocard_parser.set_defaults(func=create_and_upload_card) + collate_readme_parser = subparsers.add_parser( + "collate-readme", + help="Generate a main-branch README listing available kernel versions.", + ) + collate_readme_parser.add_argument( + "repo_id", + type=str, + help="The kernel repo ID (e.g., kernels-community/activation)", + ) + collate_readme_parser.add_argument( + "--push-to-hub", + action="store_true", + help="Push the generated README to the main branch on the Hub.", + ) + collate_readme_parser.set_defaults(func=run_collate_readme) + args = parser.parse_args() args.func(args) @@ -348,6 +365,13 @@ def upload_kernels(args): ) +def run_collate_readme(args): + collate_readme_from_versions( + repo_id=args.repo_id, + push_to_hub=args.push_to_hub, + ) + + def create_and_upload_card(args): if not args.repo_id and args.create_pr: raise ValueError("`create_pr` cannot be True when `repo_id` is None.") diff --git a/kernels/src/kernels/cli/collate_readme.py b/kernels/src/kernels/cli/collate_readme.py new file mode 100644 index 00000000..46168d29 --- /dev/null +++ b/kernels/src/kernels/cli/collate_readme.py @@ -0,0 +1,70 @@ +from kernels._versions import _get_available_versions +from kernels.utils import _get_hf_api + + +def generate_main_readme(repo_id: str) -> str: + versions = _get_available_versions(repo_id) + + if not versions: + raise ValueError( + f"No versions found for `{repo_id}`. " + "Upload at least one versioned kernel before generating a main README." + ) + + kernel_name = repo_id.split("/")[-1] + hub_url = f"https://huggingface.co/{repo_id}" + + lines = [ + "---", + "tags:", + "- kernels", + "library_name: kernels", + "---", + "", + f"# {kernel_name}", + "", + "This kernel is available in the following versions. " + "Please refer to the version branches for details.", + "", + "## Available versions", + "", + "| Version | Branch |", + "| ------- | ------ |", + ] + + for version_num in sorted(versions.keys()): + branch_name = f"v{version_num}" + branch_url = f"{hub_url}/tree/{branch_name}" + lines.append(f"| {version_num} | [{branch_name}]({branch_url}) |") + + lines.append("") + lines.append("## Quick start") + lines.append("") + lines.append("```python") + lines.append("from kernels import get_kernel") + lines.append("") + lines.append(f'kernel = get_kernel("{repo_id}", version=)') + lines.append("```") + lines.append("") + + return "\n".join(lines) + + +def collate_readme_from_versions( + repo_id: str, + push_to_hub: bool = False, +): + readme_content = generate_main_readme(repo_id) + + if push_to_hub: + api = _get_hf_api() + api.upload_file( + path_or_fileobj=readme_content.encode("utf-8"), + path_in_repo="README.md", + repo_id=repo_id, + revision="main", + commit_message="Update main README with available versions.", + ) + print(f"README pushed to https://huggingface.co/{repo_id}") + else: + print(readme_content) diff --git a/kernels/tests/test_collate_readme.py b/kernels/tests/test_collate_readme.py new file mode 100644 index 00000000..ac2b24f9 --- /dev/null +++ b/kernels/tests/test_collate_readme.py @@ -0,0 +1,110 @@ +from dataclasses import dataclass +from unittest.mock import MagicMock, patch + +import pytest + +from kernels.cli.collate_readme import ( + collate_readme_from_versions, + generate_main_readme, +) + + +def _make_versions(version_nums: list[int]) -> dict: + versions = {} + for v in version_nums: + ref = MagicMock() + ref.name = f"v{v}" + ref.ref = f"refs/heads/v{v}" + versions[v] = ref + return versions + + +@dataclass +class CollateArgs: + repo_id: str + push_to_hub: bool = False + + +PATCH_VERSIONS = "kernels.cli.collate_readme._get_available_versions" +PATCH_API = "kernels.cli.collate_readme._get_hf_api" + + +class TestGenerateMainReadme: + @patch(PATCH_VERSIONS) + def test_basic_output(self, mock_versions): + mock_versions.return_value = _make_versions([1, 2]) + readme = generate_main_readme("kernels-community/my-kernel") + + assert "# my-kernel" in readme + assert "| 1 | [v1]" in readme + assert "| 2 | [v2]" in readme + assert "kernels-community/my-kernel" in readme + + @patch(PATCH_VERSIONS) + def test_has_frontmatter(self, mock_versions): + mock_versions.return_value = _make_versions([1]) + readme = generate_main_readme("org/kernel") + + assert readme.startswith("---\n") + assert "tags:" in readme + assert "- kernels" in readme + assert "library_name: kernels" in readme + + @patch(PATCH_VERSIONS) + def test_versions_sorted(self, mock_versions): + mock_versions.return_value = _make_versions([3, 1, 2]) + readme = generate_main_readme("org/kernel") + + v1_pos = readme.index("| 1 |") + v2_pos = readme.index("| 2 |") + v3_pos = readme.index("| 3 |") + assert v1_pos < v2_pos < v3_pos + + @patch(PATCH_VERSIONS) + def test_hub_urls(self, mock_versions): + mock_versions.return_value = _make_versions([1]) + readme = generate_main_readme("kernels-community/activation") + + assert "https://huggingface.co/kernels-community/activation/tree/v1" in readme + + @patch(PATCH_VERSIONS) + def test_quick_start_section(self, mock_versions): + mock_versions.return_value = _make_versions([1]) + readme = generate_main_readme("org/kernel") + + assert "## Quick start" in readme + assert 'get_kernel("org/kernel"' in readme + + @patch(PATCH_VERSIONS) + def test_no_versions_raises(self, mock_versions): + mock_versions.return_value = {} + + with pytest.raises(ValueError, match="No versions found"): + generate_main_readme("org/kernel") + + +class TestCollateReadmeCLI: + @patch(PATCH_VERSIONS) + def test_prints_to_stdout(self, mock_versions, capsys): + mock_versions.return_value = _make_versions([1, 2]) + + collate_readme_from_versions(repo_id="org/kernel") + + captured = capsys.readouterr() + assert "# kernel" in captured.out + assert "| 1 |" in captured.out + + @patch(PATCH_API) + @patch(PATCH_VERSIONS) + def test_push_to_hub(self, mock_versions, mock_api): + mock_versions.return_value = _make_versions([1]) + api_instance = MagicMock() + mock_api.return_value = api_instance + + collate_readme_from_versions(repo_id="org/kernel", push_to_hub=True) + + api_instance.upload_file.assert_called_once() + call_kwargs = api_instance.upload_file.call_args[1] + assert call_kwargs["path_in_repo"] == "README.md" + assert call_kwargs["repo_id"] == "org/kernel" + assert call_kwargs["revision"] == "main"