Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 if mock_cpu_count is not None else 1
Comment thread
sushant-suse marked this conversation as resolved.
Outdated
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 if mock_cpu_count is not None else 1
Comment thread
sushant-suse marked this conversation as resolved.
Outdated
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