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
46 changes: 42 additions & 4 deletions github_linter/tests/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,21 @@ def validate_readme_configured(
repo.error(CATEGORY, "No 'readme' field in [project] section of config")
return False

expected_readme = repo.config[CATEGORY]["readme"]

project_readme = project_object["readme"]
if project_readme != expected_readme:
if not isinstance(project_readme, str):
repo.error(
CATEGORY,
f"Readme invalid - expected a string path, found {type(project_readme).__name__}",
)
return False

if repo.cached_get_file(project_readme) is None:
repo.error(
CATEGORY,
f"Readme invalid - should be {expected_readme}, is {project_readme}",
f"Readme invalid - file not found: {project_readme}",
)
return False

return True


Expand Down Expand Up @@ -167,13 +173,45 @@ def check_pyproject_toml(
validate_pyproject_authors(repo, project)
# TODO: make this its own check
validate_project_name(repo, project)
validate_readme_configured(repo, project)

if "urls" in project:
for url in project["urls"]:
logger.debug("URL: {} - {}", url, project["urls"][url])
return None


def fix_pyproject_readme(repo: RepoLinter) -> None:
"""ensures the project readme points to a file that exists"""
pyproject = repo.load_pyproject()
pyproject_file = repo.cached_get_file("pyproject.toml")

if pyproject is None or pyproject_file is None:
logger.info("No pyproject file found in repo {}", repo.repository.full_name)
return

project = pyproject.get("project")
if not isinstance(project, dict):
logger.info("No project section found in pyproject.toml for {}", repo.repository.full_name)
return

project_readme = project.get("readme")
if isinstance(project_readme, str) and repo.cached_get_file(project_readme) is not None:
logger.debug("Readme path already valid in pyproject.toml: {}", project_readme)
return

expected_readme = repo.config[CATEGORY]["readme"]
project["readme"] = expected_readme
result = repo.create_or_update_file(
"pyproject.toml",
tomli_w.dumps(pyproject),
pyproject_file,
"Updated project.readme in pyproject.toml",
)
if result:
repo.fix(CATEGORY, f"Updated pyproject.toml readme setting - commit url {result}")


# TODO: moving away from flit, don't need this
# def check_sdist_exclude_list(
# repo: RepoLinter,
Expand Down
208 changes: 127 additions & 81 deletions tests/test_pyproject.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,127 @@
"""testing pyproject"""

# from io import BytesIO

# from github.ContentFile import ContentFile
# from github_linter.tests.pyproject import validate_project_name, validate_readme_configured


# class TestRepoFoo:
# """ just for testing """
# name = "foobar"

# def get_contents(filename: str):
# """ kinda like a file, but not really """
# if filename == "README.md":
# readme = ContentFile("","","","",)
# readme._content.value = open("README.md", encoding="utf8").read()
# return readme
# return BytesIO()

# class TestGithub:
# """ test instance """
# config = {
# "pyproject.toml" : {
# "readme" : "README.md"
# }
# }

# def test_validate_project_name_fails_when_bad():
# """ if the name doesn't match, then we should yell """

# testproject = {
# "project" : {
# "name" : "zotbar"
# }
# }
# assert not validate_project_name(None, TestRepoFoo, testproject, {}, {})


# def test_validate_project_name_good():
# """ if the name matches we're good """
# testproject = {
# "name" : "foobar"
# }
# assert validate_project_name(None, TestRepoFoo, testproject, {}, {})

# def test_validate_project_name_fails_when_missing():
# """ if the name is missing we yell """

# testproject = {
# # "name" : "foobar"
# }
# assert not validate_project_name(None, TestRepoFoo, testproject, {}, {})

# def test_validate_readme_configured_invalid():
# """ checks the readme is set and is invalid """
# testproject = {
# "name" : "zotbar",
# "readme" : "foobar"
# }

# errors_object = {}
# warnings_object = {}
# result = validate_readme_configured(TestGithub, TestRepoFoo, testproject, errors_object, warnings_object)
# assert errors_object
# assert not warnings_object
# assert not result

# def test_validate_readme_configured():
# """ checks the readme is set and is invalid """

# testproject = {
# "name" : "zotbar",
# "readme" : "README.md"
# }
# errors_object = {}
# warnings_object = {}
# result = validate_readme_configured(TestGithub, testproject, errors_object, warnings_object)
# assert not errors_object
# assert not warnings_object
# assert result
"""Tests for pyproject module."""

from unittest.mock import Mock

from github_linter.repolinter import RepoLinter
from github_linter.tests.pyproject import check_pyproject_toml, fix_pyproject_readme


def create_mock_pyproject_file() -> Mock:
"""Create a mock pyproject.toml file object."""
mock_file = Mock()
mock_file.decoded_content = b"[project]\nname = 'test1'\n"
mock_file.sha = "deadbeef"
return mock_file


def create_mock_repo(pyproject: dict, existing_files: set[str] | None = None) -> Mock:
"""Create a mocked RepoLinter for pyproject tests."""
mock_repo = Mock(spec=RepoLinter)
mock_repo.config = {"pyproject": {"readme": "README.md"}}
mock_repo.repository = Mock()
mock_repo.repository.name = "test1"
mock_repo.repository.full_name = "testuser/test1"
pyproject_file = create_mock_pyproject_file()
files = existing_files or set()

def cached_get_file(path: str) -> Mock | None:
if path == "pyproject.toml":
return pyproject_file
if path in files:
mock_file = Mock()
mock_file.decoded_content = b"file"
return mock_file
return None

mock_repo.load_pyproject.return_value = pyproject
mock_repo.cached_get_file.side_effect = cached_get_file
return mock_repo


def test_check_pyproject_toml_accepts_default_readme_path() -> None:
"""No error when the configured readme file exists."""
mock_repo = create_mock_repo({"project": {"name": "test1", "authors": [], "readme": "README.md"}}, {"README.md"})

check_pyproject_toml(mock_repo)

mock_repo.error.assert_not_called()


def test_check_pyproject_toml_accepts_alternate_existing_readme_path() -> None:
"""No error when readme points at another file that exists."""
mock_repo = create_mock_repo({"project": {"name": "test1", "authors": [], "readme": "docs/README.md"}}, {"docs/README.md"})

check_pyproject_toml(mock_repo)

mock_repo.error.assert_not_called()


def test_check_pyproject_toml_errors_when_readme_missing() -> None:
"""Missing readme should be reported."""
mock_repo = create_mock_repo({"project": {"name": "test1", "authors": []}})

check_pyproject_toml(mock_repo)

mock_repo.error.assert_any_call("pyproject", "No 'readme' field in [project] section of config")


def test_check_pyproject_toml_errors_when_readme_file_missing() -> None:
"""Missing readme file target should be reported."""
mock_repo = create_mock_repo({"project": {"name": "test1", "authors": [], "readme": "docs/README.md"}})

check_pyproject_toml(mock_repo)

mock_repo.error.assert_any_call("pyproject", "Readme invalid - file not found: docs/README.md")


def test_check_pyproject_toml_errors_when_readme_not_string() -> None:
"""Non-string readme metadata should be rejected."""
mock_repo = create_mock_repo({"project": {"name": "test1", "authors": [], "readme": {"file": "README.md"}}})

check_pyproject_toml(mock_repo)

mock_repo.error.assert_any_call("pyproject", "Readme invalid - expected a string path, found dict")


def test_fix_pyproject_readme_sets_default_when_missing() -> None:
"""Fix should add the configured readme when absent."""
mock_repo = create_mock_repo({"project": {"name": "test1"}})
mock_repo.create_or_update_file.return_value = "https://example.com/commit/1"

fix_pyproject_readme(mock_repo)

updated_contents = mock_repo.create_or_update_file.call_args.args[1]
assert 'readme = "README.md"' in updated_contents
mock_repo.fix.assert_called_once_with("pyproject", "Updated pyproject.toml readme setting - commit url https://example.com/commit/1")


def test_fix_pyproject_readme_sets_default_when_invalid() -> None:
"""Fix should replace an invalid readme path."""
mock_repo = create_mock_repo({"project": {"name": "test1", "readme": "missing.md"}})
mock_repo.create_or_update_file.return_value = "https://example.com/commit/2"

fix_pyproject_readme(mock_repo)

updated_contents = mock_repo.create_or_update_file.call_args.args[1]
assert 'readme = "README.md"' in updated_contents
mock_repo.fix.assert_called_once_with("pyproject", "Updated pyproject.toml readme setting - commit url https://example.com/commit/2")


def test_fix_pyproject_readme_does_not_write_when_path_is_valid() -> None:
"""Valid existing readme paths should be left alone."""
mock_repo = create_mock_repo({"project": {"name": "test1", "readme": "docs/README.md"}}, {"docs/README.md"})

fix_pyproject_readme(mock_repo)

mock_repo.create_or_update_file.assert_not_called()
mock_repo.fix.assert_not_called()


def test_fix_pyproject_readme_does_not_create_project_section() -> None:
"""Fix should not invent a project section."""
mock_repo = create_mock_repo({"tool": {"hatch": {}}})

fix_pyproject_readme(mock_repo)

mock_repo.create_or_update_file.assert_not_called()
mock_repo.fix.assert_not_called()