Coverage for pycommons / ds / cache.py: 100%

18 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-02 06:36 +0000

1"""A factory for functions checking whether argument values are new.""" 

2from typing import Callable, Final, TypeVar 

3 

4#: a type variable for the representation-base cache 

5#: :func:`pycommons.ds.cache.repr_cache`. 

6T = TypeVar("T") 

7 

8 

9def is_new() -> Callable[[str], bool]: 

10 """ 

11 Create a function returning `True` when seeing new values. 

12 

13 Creates a function which returns `True` only the first time it receives a 

14 given argument and `False` all subsequent times. 

15 This is based on https://stackoverflow.com/questions/27427067 

16 

17 :returns: a function `is_new(xx)` that will return `True` the first 

18 time it encounters any value `xx` and `False` for all values it has 

19 already seen 

20 

21 >>> check1: Callable[[str], bool] = is_new() 

22 >>> print(check1("a")) 

23 True 

24 >>> print(check1("a")) 

25 False 

26 >>> print(check1("b")) 

27 True 

28 >>> print(check1("b")) 

29 False 

30 

31 >>> check2: Callable[[int], bool] = is_new() 

32 >>> print(check2(1)) 

33 True 

34 >>> print(check2(1)) 

35 False 

36 >>> print(check2(3)) 

37 True 

38 >>> print(check2(4)) 

39 True 

40 >>> print(check2(3)) 

41 False 

42 """ 

43 setdefault: Final[Callable] = {}.setdefault 

44 n = 0 # noqa 

45 

46 def __add(x) -> bool: 

47 nonlocal n 

48 n += 1 

49 return setdefault(x, n) == n 

50 

51 return __add 

52 

53 

54def repr_cache() -> Callable[[T], T]: 

55 """ 

56 Create a cache based on the string representation of an object. 

57 

58 In this type of cache is that the `repr`-representations of objects 

59 are used as keys. The first time an object with a given representation is 

60 encountered, it is stored in the cache and returned. The next time an 

61 object with the same representation is put into this method, the original 

62 object with that representation is returned instead. 

63 

64 This can be used to still cache and canonically retrieve objects which by 

65 themselves are not hashable, like numpy arrays. While the cache itself is 

66 not memory-friendly, it can be used to build data structures that re-use 

67 the same objects again and again. If these data structures are heavily 

68 used, then this can improve the hardware-cache-friendliness of the 

69 corresponding code. 

70 

71 :return: the cache function 

72 :raises TypeError: if the type of a cached object is incompatible with the 

73 type of a requested object 

74 

75 >>> cache1: Callable[[str], str] = repr_cache() 

76 >>> a = f"{1 * 5}" 

77 >>> b = "5" 

78 >>> a is b 

79 False 

80 

81 >>> cache1(a) 

82 '5' 

83 >>> cache1(b) is a 

84 True 

85 

86 >>> cache2: Callable[[float], float] = repr_cache() 

87 >>> x = 5.78 

88 >>> y = 578 / 100 

89 >>> y is x 

90 False 

91 >>> cache2(y) 

92 5.78 

93 >>> cache2(x) is y 

94 True 

95 

96 >>> cache3: Callable[[dict[int, str]], dict[int, str]] = repr_cache() 

97 >>> a = {1: '1', 3: '3', 7: '7'} 

98 >>> b = {1: '1', 3: '3', 7: '7'} 

99 >>> print(cache3(a)) 

100 {1: '1', 3: '3', 7: '7'} 

101 >>> print(cache3(a) is a) 

102 True 

103 >>> print(cache3(b) is a) 

104 True 

105 >>> print(cache3(b) is b) 

106 False 

107 

108 >>> class Dummy: 

109 ... def __repr__(self): 

110 ... return "22222" 

111 >>> cache4: Callable[[Dummy], Dummy] = repr_cache() 

112 >>> _ = cache4(Dummy()) 

113 >>> try: 

114 ... cache4(22222) 

115 ... except TypeError as te: 

116 ... s = str(te) 

117 >>> print(s[:34]) 

118 Cache yields element of wrong type 

119 >>> "Dummy" in s 

120 True 

121 >>> "int" in s 

122 True 

123 """ 

124 setdefault: Final[Callable] = {}.setdefault 

125 

126 def __add(x: T) -> T: 

127 z: Final[T] = setdefault(repr(x), x) 

128 tpe = type(x) 

129 if not isinstance(z, tpe): 

130 raise TypeError("Cache yields element of wrong type " 

131 f"{type(z)}, should be {tpe}.") 

132 return z 

133 

134 return __add