Coverage for src/lexigram/admin/schema/base.py: 100%

41 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1from __future__ import annotations 

2 

3from abc import ABC, abstractmethod 

4from dataclasses import dataclass, field 

5from typing import Any, Generic, TypeVar 

6 

7from lexigram.admin.schema.exceptions import FieldError 

8from lexigram.admin.schema.validators import FieldValidator 

9from lexigram.result import Ok, Result 

10from lexigram.ui import Element 

11 

12T = TypeVar("T") 

13 

14 

15@dataclass(frozen=True, kw_only=True) 

16class SchemaField(ABC, Generic[T]): 

17 """.. stability:: stable""" 

18 

19 name: str 

20 label: str | None = None 

21 help_text: str | None = None 

22 placeholder: str | None = None 

23 

24 nullable: bool = True 

25 readonly: bool = False 

26 required: bool = False 

27 sortable: bool = True 

28 searchable: bool = False 

29 filterable: bool = True 

30 visible_in_form: bool = True 

31 visible_in_list: bool = True 

32 visible_in_view: bool = True 

33 

34 validators: list[FieldValidator] = field(default_factory=list) 

35 default: T | None = None 

36 

37 @abstractmethod 

38 def render_form( 

39 self, value: T | None, *, errors: list[str] | None = None 

40 ) -> Element: 

41 """Render this field as a form input.""" 

42 

43 @abstractmethod 

44 def render_column(self, record: Any, value: T | None) -> Element: 

45 """Render this field as a table-cell value.""" 

46 

47 def render_infolist_entry(self, value: T | None) -> Any: 

48 """Render this field as a read-only detail entry.""" 

49 from lexigram.ui import InfolistEntry, InfolistEntryType 

50 

51 return InfolistEntry( 

52 name=self.name, 

53 label=self.label or self.name.replace("_", " ").title(), 

54 value=value, 

55 type=InfolistEntryType.TEXT, 

56 ) 

57 

58 def render_filter(self, current_value: Any | None = None) -> Element | None: 

59 """Render this field as a filter widget. Return None to opt out.""" 

60 return None 

61 

62 def get_default(self) -> T | None: 

63 """Return the default value for this field.""" 

64 return self.default 

65 

66 def from_form(self, raw: str | None) -> Result[T | None, FieldError]: 

67 """Coerce a raw form string to the field's Python type.""" 

68 return Ok(raw) # type: ignore[arg-type] 

69 

70 def to_form(self, value: T | None) -> str: 

71 """Coerce the field's Python value to a form-display string.""" 

72 return "" if value is None else str(value)