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 src/docbuild/cli/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DEFAULT_APP_CONFIG = {
"debug": False,
"role": "production",
"max_workers": "half",
"paths": {
"config_dir": "/etc/docbuild",
"repo_dir": "/data/docserv/repos/permanent-full/",
Expand Down
41 changes: 40 additions & 1 deletion src/docbuild/models/config/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from collections.abc import Sequence # Added Sequence for internal use
from copy import deepcopy
import os
from typing import Any, Literal, Self

from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from docbuild.config.app import (
CircularReferenceError,
Expand Down Expand Up @@ -188,8 +189,46 @@ class AppConfig(BaseModel):
description="Configuration for the application's logging system.",
)

# Added max_workers with support for ints and descriptive strings
max_workers: int | str = Field(
default="half",
description="Max concurrent workers. Supports integers or 'all', 'all2'/'half'.",
)

model_config = ConfigDict(extra="allow")

@field_validator("max_workers")
@classmethod
def _resolve_worker_count(cls, v: int | str) -> int:
"""Resolve keywords 'all', 'half', 'all2' into concrete integers."""
cpu_count = os.cpu_count() or 1

keyword_map = {
"all": lambda cpu: cpu,
"half": lambda cpu: max(1, cpu // 2),
"all2": lambda cpu: max(1, cpu // 2),
}

if isinstance(v, str):
val = v.lower()
if val in keyword_map:
# Return immediately once resolved
return keyword_map[val](cpu_count)

if val.isdigit():
v = int(val)
else:
raise ValueError(
f"Invalid max_workers value: '{v}'. "
"Use an integer, 'all', or 'half'/'all2'."
)
Comment thread
sushant-suse marked this conversation as resolved.

# At this point, v is guaranteed to be an int
if v < 1:
raise ValueError("max_workers must be at least 1")

return v

@model_validator(mode="before")
@classmethod
def _resolve_placeholders(cls, data: dict[str, Any]) -> dict[str, Any] | None:
Expand Down
47 changes: 47 additions & 0 deletions tests/models/test_config_workers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from unittest.mock import patch

import pytest

from docbuild.models.config import app
from docbuild.models.config.app import AppConfig


# os.cpu_count() returns None if the count is indeterminate.
@pytest.fixture(params=[1, 2, 8, None])
def mock_cpu_count(request):
"""Mock os.cpu_count in app module to ensure deterministic tests."""
cpu_count = request.param
with patch.object(app, "os") as mock_os:
mock_os.cpu_count.return_value = cpu_count
yield cpu_count

# Separate test cases for better clarity
def test_max_workers_resolution_all(mock_cpu_count):
"""Verify 'all' keyword resolves to the full CPU count (min 1)."""
expected = mock_cpu_count or 1
conf = AppConfig(max_workers="all")
assert conf.max_workers == expected

def test_max_workers_resolution_half(mock_cpu_count):
"""Verify 'half' and 'all2' resolve to 50% CPU count (min 1)."""
cpu = mock_cpu_count or 1
expected_half = max(1, cpu // 2)

assert AppConfig(max_workers="half").max_workers == expected_half
assert AppConfig(max_workers="all2").max_workers == expected_half

def test_max_workers_resolution_explicit_values():
"""Verify that specific integers/strings override CPU-based logic.
We don't need the fixture here because these values are
independent of the host hardware.
"""
assert AppConfig(max_workers=4).max_workers == 4
assert AppConfig(max_workers="8").max_workers == 8

def test_max_workers_validation_errors():
"""Verify error handling for invalid inputs."""
with pytest.raises(ValueError, match="at least 1"):
AppConfig(max_workers=0)

with pytest.raises(ValueError, match="Invalid max_workers"):
AppConfig(max_workers="infinite")
Loading