Coverage for src/epublib/soup.py: 96%

23 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-09-30 17:43 -0300

1# type: ignore 

2 

3from typing import Annotated, Any, Protocol, runtime_checkable 

4 

5import bs4 

6 

7from epublib.exceptions import EPUBError 

8 

9# Reproducing the behavior of typing.Required 

10type Required[T] = Annotated[T, "required"] 

11 

12 

13class EnforcingSoup(bs4.BeautifulSoup): 

14 """A BeautifulSoup subclass that enforces the presence of certain tags.""" 

15 

16 def __init__( 

17 self, 

18 markup: str | bytes, 

19 features: str | None = None, 

20 *args: Any, 

21 **kwargs: Any, 

22 ): 

23 super().__init__( 

24 markup, 

25 features, 

26 *args, 

27 **kwargs, 

28 ) 

29 

30 required = ( 

31 field 

32 for field, annotation in self.__annotations__.items() 

33 if annotation.__origin__ is Required 

34 ) 

35 

36 for tag in required: 

37 if not getattr(self, tag): 

38 raise EPUBError(f"Package document missing {tag}") 

39 

40 

41class PackageDocumentSoup(EnforcingSoup): 

42 """A BeautifulSoup subclass for the package document.""" 

43 

44 manifest: Required[bs4.Tag] 

45 metadata: Required[bs4.Tag] 

46 spine: Required[bs4.Tag] 

47 

48 

49class NCXSoup(EnforcingSoup): 

50 """A BeautifulSoup subclass for the NCX file.""" 

51 

52 ncx: Required[bs4.Tag] 

53 docTitle: Required[bs4.Tag] 

54 head: Required[bs4.Tag] 

55 navMap: Required[bs4.Tag] 

56 

57 

58@runtime_checkable 

59class WithSoupProtocol(Protocol): 

60 """A protocol for classes that have a BeautifulSoup attribute.""" 

61 

62 soup: bs4.BeautifulSoup