Coverage for /usr/lib/python3/dist-packages/fontTools/misc/classifyTools.py: 17%

59 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" fontTools.misc.classifyTools.py -- tools for classifying things. 

2""" 

3 

4 

5class Classifier(object): 

6 

7 """ 

8 Main Classifier object, used to classify things into similar sets. 

9 """ 

10 

11 def __init__(self, sort=True): 

12 self._things = set() # set of all things known so far 

13 self._sets = [] # list of class sets produced so far 

14 self._mapping = {} # map from things to their class set 

15 self._dirty = False 

16 self._sort = sort 

17 

18 def add(self, set_of_things): 

19 """ 

20 Add a set to the classifier. Any iterable is accepted. 

21 """ 

22 if not set_of_things: 

23 return 

24 

25 self._dirty = True 

26 

27 things, sets, mapping = self._things, self._sets, self._mapping 

28 

29 s = set(set_of_things) 

30 intersection = s.intersection(things) # existing things 

31 s.difference_update(intersection) # new things 

32 difference = s 

33 del s 

34 

35 # Add new class for new things 

36 if difference: 

37 things.update(difference) 

38 sets.append(difference) 

39 for thing in difference: 

40 mapping[thing] = difference 

41 del difference 

42 

43 while intersection: 

44 # Take one item and process the old class it belongs to 

45 old_class = mapping[next(iter(intersection))] 

46 old_class_intersection = old_class.intersection(intersection) 

47 

48 # Update old class to remove items from new set 

49 old_class.difference_update(old_class_intersection) 

50 

51 # Remove processed items from todo list 

52 intersection.difference_update(old_class_intersection) 

53 

54 # Add new class for the intersection with old class 

55 sets.append(old_class_intersection) 

56 for thing in old_class_intersection: 

57 mapping[thing] = old_class_intersection 

58 del old_class_intersection 

59 

60 def update(self, list_of_sets): 

61 """ 

62 Add a a list of sets to the classifier. Any iterable of iterables is accepted. 

63 """ 

64 for s in list_of_sets: 

65 self.add(s) 

66 

67 def _process(self): 

68 if not self._dirty: 

69 return 

70 

71 # Do any deferred processing 

72 sets = self._sets 

73 self._sets = [s for s in sets if s] 

74 

75 if self._sort: 

76 self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s))) 

77 

78 self._dirty = False 

79 

80 # Output methods 

81 

82 def getThings(self): 

83 """Returns the set of all things known so far. 

84 

85 The return value belongs to the Classifier object and should NOT 

86 be modified while the classifier is still in use. 

87 """ 

88 self._process() 

89 return self._things 

90 

91 def getMapping(self): 

92 """Returns the mapping from things to their class set. 

93 

94 The return value belongs to the Classifier object and should NOT 

95 be modified while the classifier is still in use. 

96 """ 

97 self._process() 

98 return self._mapping 

99 

100 def getClasses(self): 

101 """Returns the list of class sets. 

102 

103 The return value belongs to the Classifier object and should NOT 

104 be modified while the classifier is still in use. 

105 """ 

106 self._process() 

107 return self._sets 

108 

109 

110def classify(list_of_sets, sort=True): 

111 """ 

112 Takes a iterable of iterables (list of sets from here on; but any 

113 iterable works.), and returns the smallest list of sets such that 

114 each set, is either a subset, or is disjoint from, each of the input 

115 sets. 

116 

117 In other words, this function classifies all the things present in 

118 any of the input sets, into similar classes, based on which sets 

119 things are a member of. 

120 

121 If sort=True, return class sets are sorted by decreasing size and 

122 their natural sort order within each class size. Otherwise, class 

123 sets are returned in the order that they were identified, which is 

124 generally not significant. 

125 

126 >>> classify([]) == ([], {}) 

127 True 

128 >>> classify([[]]) == ([], {}) 

129 True 

130 >>> classify([[], []]) == ([], {}) 

131 True 

132 >>> classify([[1]]) == ([{1}], {1: {1}}) 

133 True 

134 >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}}) 

135 True 

136 >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) 

137 True 

138 >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) 

139 True 

140 >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}}) 

141 True 

142 >>> classify([[1,2],[2,4,5]]) == ( 

143 ... [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) 

144 True 

145 >>> classify([[1,2],[2,4,5]], sort=False) == ( 

146 ... [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) 

147 True 

148 >>> classify([[1,2,9],[2,4,5]], sort=False) == ( 

149 ... [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5}, 

150 ... 9: {1, 9}}) 

151 True 

152 >>> classify([[1,2,9,15],[2,4,5]], sort=False) == ( 

153 ... [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5}, 

154 ... 5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}}) 

155 True 

156 >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False) 

157 >>> set([frozenset(c) for c in classes]) == set( 

158 ... [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})]) 

159 True 

160 >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}} 

161 True 

162 """ 

163 classifier = Classifier(sort=sort) 

164 classifier.update(list_of_sets) 

165 return classifier.getClasses(), classifier.getMapping() 

166 

167 

168if __name__ == "__main__": 

169 import sys, doctest 

170 

171 sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)