Coverage for structured_tutorials / models / base.py: 100%

47 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-26 12:41 +0100

1# Copyright (c) 2025 Mathias Ertl 

2# Licensed under the MIT License. See LICENSE file for details. 

3 

4"""Base model classes.""" 

5 

6from pathlib import Path 

7from typing import Annotated, Any 

8 

9from pydantic import ( 

10 BaseModel, 

11 ConfigDict, 

12 Field, 

13 NonNegativeFloat, 

14 NonNegativeInt, 

15 field_validator, 

16 model_validator, 

17) 

18from pydantic.fields import FieldInfo 

19 

20from structured_tutorials.typing import Self 

21 

22# Type for commands to execute 

23CommandType = str | tuple[str, ...] 

24 

25TEMPLATE_DESCRIPTION = "This value is rendered as a template with the current context." 

26 

27 

28def default_tutorial_root_factory(data: dict[str, Any]) -> Path: 

29 """Default factory for the tutorial_root variable.""" 

30 tutorial_root = data["path"].parent 

31 assert isinstance(tutorial_root, Path) 

32 return tutorial_root 

33 

34 

35def template_field_title_generator(field_name: str, field_info: FieldInfo) -> str: 

36 """Field title generator for template fields.""" 

37 return f"{field_name.title()} (template)" 

38 

39 

40class CommandBaseModel(BaseModel): 

41 """Base model for commands.""" 

42 

43 model_config = ConfigDict(extra="forbid") 

44 

45 status_code: Annotated[int, Field(ge=0, le=255)] = 0 

46 clear_environment: bool = Field(default=False, description="Clear the environment.") 

47 update_environment: dict[str, str] = Field( 

48 default_factory=dict, description="Update the environment for all subsequent commands." 

49 ) 

50 environment: dict[str, str] = Field( 

51 default_factory=dict, description="Additional environment variables for the process." 

52 ) 

53 show_output: bool = Field( 

54 default=True, description="Set to `False` to always hide the output of this command." 

55 ) 

56 

57 

58class TestSpecificationMixin: 

59 """Mixin for specifying tests.""" 

60 

61 delay: Annotated[float, Field(ge=0)] = 0 

62 retry: NonNegativeInt = 0 

63 backoff_factor: NonNegativeFloat = 0 # {backoff factor} * (2 ** ({number of previous retries})) 

64 

65 

66class ConfigurationMixin: 

67 """Mixin for configuration models.""" 

68 

69 skip: bool = Field(default=False, description="Skip this part.") 

70 update_context: dict[str, Any] = Field(default_factory=dict) 

71 

72 

73class DocumentationConfigurationMixin: 

74 """Mixin for documentation configuration models.""" 

75 

76 text_before: str = Field(default="", description="Text before documenting this part.") 

77 text_after: str = Field(default="", description="Text after documenting this part.") 

78 

79 

80class FileMixin: 

81 """Mixin for specifying a file (used in file part and for stdin of commands).""" 

82 

83 contents: str | None = Field( 

84 default=None, 

85 field_title_generator=template_field_title_generator, 

86 description=f"Contents of the file. {TEMPLATE_DESCRIPTION}", 

87 ) 

88 source: Path | None = Field( 

89 default=None, 

90 field_title_generator=template_field_title_generator, 

91 description="The source path of the file to create. Unless `template` is `False`, the file is loaded " 

92 "into memory and rendered as template.", 

93 ) 

94 template: bool = Field( 

95 default=True, description="Whether the file contents should be rendered in a template." 

96 ) 

97 

98 @field_validator("source", mode="after") 

99 @classmethod 

100 def validate_source(cls, value: Path) -> Path: 

101 if value.is_absolute(): 

102 raise ValueError(f"{value}: Must be a relative path (relative to the current cwd).") 

103 return value 

104 

105 @model_validator(mode="after") 

106 def validate_contents_or_source(self) -> Self: 

107 if self.contents is None and self.source is None: 

108 raise ValueError("Either contents or source is required.") 

109 if self.contents is not None and self.source is not None: 

110 raise ValueError("Only one of contents or source is allowed.") 

111 return self