Coverage for src/mkdocstrings/inventory.py: 95.12%

56 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-14 19:41 +0100

1"""Module responsible for the objects inventory.""" 

2 

3# Credits to Brian Skinn and the sphobjinv project: 

4# https://github.com/bskinn/sphobjinv 

5 

6from __future__ import annotations 

7 

8import re 

9import zlib 

10from textwrap import dedent 

11from typing import BinaryIO, Collection 

12 

13 

14class InventoryItem: 

15 """Inventory item.""" 

16 

17 def __init__( 

18 self, 

19 name: str, 

20 domain: str, 

21 role: str, 

22 uri: str, 

23 priority: int = 1, 

24 dispname: str | None = None, 

25 ): 

26 """Initialize the object. 

27 

28 Arguments: 

29 name: The item name. 

30 domain: The item domain, like 'python' or 'crystal'. 

31 role: The item role, like 'class' or 'method'. 

32 uri: The item URI. 

33 priority: The item priority. Only used internally by mkdocstrings and Sphinx. 

34 dispname: The item display name. 

35 """ 

36 self.name: str = name 

37 self.domain: str = domain 

38 self.role: str = role 

39 self.uri: str = uri 

40 self.priority: int = priority 

41 self.dispname: str = dispname or name 

42 

43 def format_sphinx(self) -> str: 

44 """Format this item as a Sphinx inventory line. 

45 

46 Returns: 

47 A line formatted for an `objects.inv` file. 

48 """ 

49 dispname = self.dispname 

50 if dispname == self.name: 50 ↛ 52line 50 didn't jump to line 52, because the condition on line 50 was never false

51 dispname = "-" 

52 uri = self.uri 

53 if uri.endswith(self.name): 

54 uri = uri[: -len(self.name)] + "$" 

55 return f"{self.name} {self.domain}:{self.role} {self.priority} {uri} {dispname}" 

56 

57 sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$") 

58 

59 @classmethod 

60 def parse_sphinx(cls, line: str) -> InventoryItem: 

61 """Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it.""" 

62 match = cls.sphinx_item_regex.search(line) 

63 if not match: 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true

64 raise ValueError(line) 

65 name, domain, role, priority, uri, dispname = match.groups() 

66 if uri.endswith("$"): 

67 uri = uri[:-1] + name 

68 if dispname == "-": 

69 dispname = name 

70 return cls(name, domain, role, uri, int(priority), dispname) 

71 

72 

73class Inventory(dict): 

74 """Inventory of collected and rendered objects.""" 

75 

76 def __init__(self, items: list[InventoryItem] | None = None, project: str = "project", version: str = "0.0.0"): 

77 """Initialize the object. 

78 

79 Arguments: 

80 items: A list of items. 

81 project: The project name. 

82 version: The project version. 

83 """ 

84 super().__init__() 

85 items = items or [] 

86 for item in items: 

87 self[item.name] = item 

88 self.project = project 

89 self.version = version 

90 

91 def register( 

92 self, 

93 name: str, 

94 domain: str, 

95 role: str, 

96 uri: str, 

97 priority: int = 1, 

98 dispname: str | None = None, 

99 ) -> None: 

100 """Create and register an item. 

101 

102 Arguments: 

103 name: The item name. 

104 domain: The item domain, like 'python' or 'crystal'. 

105 role: The item role, like 'class' or 'method'. 

106 uri: The item URI. 

107 priority: The item priority. Only used internally by mkdocstrings and Sphinx. 

108 dispname: The item display name. 

109 """ 

110 self[name] = InventoryItem( 

111 name=name, 

112 domain=domain, 

113 role=role, 

114 uri=uri, 

115 priority=priority, 

116 dispname=dispname, 

117 ) 

118 

119 def format_sphinx(self) -> bytes: 

120 """Format this inventory as a Sphinx `objects.inv` file. 

121 

122 Returns: 

123 The inventory as bytes. 

124 """ 

125 header = ( 

126 dedent( 

127 f""" 

128 # Sphinx inventory version 2 

129 # Project: {self.project} 

130 # Version: {self.version} 

131 # The remainder of this file is compressed using zlib. 

132 """, 

133 ) 

134 .lstrip() 

135 .encode("utf8") 

136 ) 

137 

138 lines = [ 

139 item.format_sphinx().encode("utf8") 

140 for item in sorted(self.values(), key=lambda item: (item.domain, item.name)) 

141 ] 

142 return header + zlib.compress(b"\n".join(lines) + b"\n", 9) 

143 

144 @classmethod 

145 def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) -> Inventory: 

146 """Parse a Sphinx v2 inventory file and return an `Inventory` from it. 

147 

148 Arguments: 

149 in_file: The binary file-like object to read from. 

150 domain_filter: A collection of domain values to allow (and filter out all other ones). 

151 

152 Returns: 

153 An inventory containing the collected items. 

154 """ 

155 for _ in range(4): 

156 in_file.readline() 

157 lines = zlib.decompress(in_file.read()).splitlines() 

158 items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines] 

159 if domain_filter: 159 ↛ 161line 159 didn't jump to line 161, because the condition on line 159 was never false

160 items = [item for item in items if item.domain in domain_filter] 

161 return cls(items)