Coverage for src/robotide/publish/publisher.py: 80%

84 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-06 10:40 +0100

1# Copyright 2008-2015 Nokia Networks 

2# Copyright 2016- Robot Framework Foundation 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); 

5# you may not use this file except in compliance with the License. 

6# You may obtain a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15 

16import sys 1ag

17import inspect 1ag

18import types 1ag

19from pubsub import pub 1ag

20from typing import Type, Callable 1ag

21from ..publish.messages import RideMessage 1ag

22 

23 

24class _Publisher: 1ag

25 

26 def __init__(self): 1ag

27 self.publisher = pub.getDefaultPublisher() 1ag

28 self.publisher.setListenerExcHandler(ListenerExceptionHandler()) 1ag

29 

30 @staticmethod 1ag

31 def _get_topic(topic_cls: Type[RideMessage]) -> str: 1ag

32 if inspect.isclass(topic_cls) and issubclass(topic_cls, RideMessage): 2a rbsbtbs ubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZb0b1b2bt 3b4b5b6b7b8b9b!b#b$b%b'b(b)b*b+b,b-b.b/b:b;b=b?b@b[b]b^b_b`b{b|b}b~bacbcccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZc0c1c2c3c4c5c6c7c8c9c!c#c$c%c'c(c)c*c+c,c-c.c/c:c;c=c?c@c[c]c^c_c`c{c|c}c~cadbdcdddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdydzdAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZd0d1d2d3d4d5d6d7d8d9d!d#d$d%d'd(d)d*d+d,d-du v w x y z .d/d:d;d=d?d@d[d]d^d_d`d{d|d}d~daebecedeeefegeheiejekelemeneoepeqereseteuevewexeyezeAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZe0e1e2e3e4e5e6eA B C D 7e8e9eE !e#e$e%e'e(e)e*e+e,eo -e.e/e:e;e=e?e@e[e]e^e_e`e{e|e}e~eafbfcfdfefffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfyfzfAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPf, - . / : ; = ? @ [ ] ^ _ ` { | h } ~ F abG bbcbdbebfbgbhbH I J K L QfRfSfM TfUfVfWfXfYfZf0f1f2f3fN O 4f5f6f7f8f9f!f#f$fibi %f'f(f)f*fjbe mb+fnb,fobpbqbP -f.f/f:f;f=fkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r ?f@f[f]f^f_f`f{f|f}f~fagbgcgdgegfggghgigjgkglgmgngogpgqgrgsgtgugvgwgxgygzgAg1 2 3 4 5 6 7 8 9 ! # $ % ' ( ) Bg* CgDgEgFg+ GgHgIgJgKgLg

33 return topic_cls.topic() 2a rbsbtbs ubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZb0b1b2bt 3b4b5b6b7b8b9b!b#b$b%b'b(b)b*b+b,b-b.b/b:b;b=b?b@b[b]b^b_b`b{b|b}b~bacbcccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZc0c1c2c3c4c5c6c7c8c9c!c#c$c%c'c(c)c*c+c,c-c.c/c:c;c=c?c@c[c]c^c_c`c{c|c}c~cadbdcdddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdydzdAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZd0d1d2d3d4d5d6d7d8d9d!d#d$d%d'd(d)d*d+d,d-du v w x y z .d/d:d;d=d?d@d[d]d^d_d`d{d|d}d~daebecedeeefegeheiejekelemeneoepeqereseteuevewexeyezeAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZe0e1e2e3e4e5e6eA B C D 7e8e9eE !e#e$e%e'e(e)e*e+e,eo -e.e/e:e;e=e?e@e[e]e^e_e`e{e|e}e~eafbfcfdfefffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfyfzfAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPf, - . / : ; = ? @ [ ] ^ _ ` { | h } ~ F abG bbcbdbebfbgbhbH I J K L QfRfSfM TfUfVfWfXfYfZf0f1f2f3fN O 4f5f6f7f8f9f!f#f$fibi %f'f(f)f*fe mbnb,fobpbqbP -f.f/f:f;f=fQ p R S T j k l U V W X Y Z 0 b c m f d n q r ?f@f[f]f^f_f`f{f|f}f~fagbgcgdgegfggghgigjgkglgmgngogpgqgrgsgtgugvgwgxgygzgAg1 2 3 4 5 6 7 8 9 ! # $ % ' ( ) Bg* CgDgEgFg+ GgHgIgJgKgLg

34 raise TypeError('Expected topic type {}, actual {}.'.format(RideMessage, topic_cls)) 2jb+fkbp n

35 

36 @staticmethod 1ag

37 def _validate_listener(listener: Callable): 1ag

38 sig = inspect.signature(listener) 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

39 params = sig.parameters 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

40 error_msg = 'only 1 required param (message) is expected.' 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

41 assert len(params) == 1, 'Too many listener params, ' + error_msg 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

42 assert str(list(params.values())[0]) in ['message', 'data'], 'Invalid listener param, ' + error_msg 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

43 

44 def subscribe(self, listener: Callable, topic: Type[RideMessage]): 1ag

45 """ The listener's param signature must be (message) """ 

46 self._validate_listener(listener) 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP lbkbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

47 self.publisher.subscribe(listener, self._get_topic(topic)) 2a s t u v w x y z A B C D E o , - . / : ; = ? @ [ ] ^ _ ` { | h } ~ abG bbcbdbebfbgbhbH I J K L M N O ibi jbP kbQ p R S T j k l U V W X Y Z 0 b c m f d n q r * +

48 

49 def publish(self, topic: Type[RideMessage], message): 1ag

50 """ All subscribed listeners' param signatures have been guaranteed """ 

51 self.publisher.sendMessage(self._get_topic(topic), message=message) 2a rbsbtbs ubvbwbxbybzbAbBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZb0b1b2bt 3b4b5b6b7b8b9b!b#b$b%b'b(b)b*b+b,b-b.b/b:b;b=b?b@b[b]b^b_b`b{b|b}b~bacbcccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZc0c1c2c3c4c5c6c7c8c9c!c#c$c%c'c(c)c*c+c,c-c.c/c:c;c=c?c@c[c]c^c_c`c{c|c}c~cadbdcdddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdydzdAdBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZd0d1d2d3d4d5d6d7d8d9d!d#d$d%d'd(d)d*d+d,d-du v w x y z .d/d:d;d=d?d@d[d]d^d_d`d{d|d}d~daebecedeeefegeheiejekelemeneoepeqereseteuevewexeyezeAeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZe0e1e2e3e4e5e6eA B C D 7e8e9eE !e#e$e%e'e(e)e*e+e,eo -e.e/e:e;e=e?e@e[e]e^e_e`e{e|e}e~eafbfcfdfefffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfyfzfAfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPfh F G H I J K L QfRfSfM TfUfVfWfXfYfZf0f1f2f3fN O 4f5f6f7f8f9f!f#f$fi %f'f(f)f*fe mbnbobpbqbP -f.f/f:f;f=fQ p R S T j k l U V W X Y Z 0 b c m f d n q r ?f@f[f]f^f_f`f{f|f}f~fagbgcgdgegfggghgigjgkglgmgngogpgqgrgsgtgugvgwgxgygzgAg1 2 3 4 5 6 7 8 9 ! # $ % ' ( ) Bg* CgDgEgFg+ GgHgIgJgKgLg

52 

53 def unsubscribe(self, listener: Callable, topic: Type[RideMessage]): 1ag

54 self.publisher.unsubscribe(listener, self._get_topic(topic)) 2a o mb+fnb,fobpbqbn q r

55 

56 def unsubscribe_all(self, obj=None): 1ag

57 """ If the given object's: 

58 

59 1. object method 

60 2. class static function 

61 3. class function 

62 

63 is subscribed into PUBLISHER, call this method to unsubscribe all its topics. 

64 

65 Unsubscribe all topics when input is None. 

66 """ 

67 

68 def _listener_filter(listener): 1aebcmfd

69 _callable = listener.getCallable() 1aebcfd

70 functions = [func for _, func in _get_members_safely(obj, inspect.isfunction)] 1aebcfd

71 methods = [method for _, method in _get_members_safely(obj, inspect.ismethod)] 1aebcfd

72 if _callable in functions or _callable in methods: 1aebcfd

73 return True 1aebcd

74 

75 _listener_filter = _listener_filter if obj is not None else None 1aebcmfd

76 self.publisher.unsubAll(listenerFilter=_listener_filter) 1aebcmfd

77 

78 

79class ListenerExceptionHandler(pub.IListenerExcHandler): 1ag

80 

81 def __call__(self, listener_id: str, topic_obj: pub.Topic): 1ag

82 from .messages import RideLogException 1ahFijkl123456789!#$%'()

83 topic_name = topic_obj.getName() 1ahFijkl123456789!#$%'()

84 if topic_name != RideLogException.topic(): 1ahFijkl123456789!#$%'()

85 error_msg = 'Error in listener: {}, topic: {}'.format(listener_id, topic_name) 1ahFijkl123456789!#$%'()

86 log_message = RideLogException(message=error_msg, 1ahFijkl123456789!#$%'()

87 exception=None, level='ERROR') 

88 sys.stderr.write(log_message.__getattribute__('message')) 1ahFijkl123456789!#$%'()

89 log_message.publish() 1ahFijkl123456789!#$%'()

90 

91 

92def _get_members_safely(obj, predicate=None): 1ag

93 """Return all members of an object as (name, value) pairs sorted by name. 

94 Optionally, only return members that satisfy a given predicate. 

95 

96 Copied from inspect.getmembers(). 

97 

98 Added protection logic to bypass unexpected exceptions in object attribute iterations. 

99 """ 

100 if inspect.isclass(obj): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true1aebcfd

101 mro = (obj,) + inspect.getmro(obj) 

102 else: 

103 mro = () 1aebcfd

104 results = [] 1aebcfd

105 processed = set() 1aebcfd

106 names = dir(obj) 1aebcfd

107 # :dd any DynamicClassAttributes to the list of names if object is a class; 

108 # this may result in duplicate entries if, for example, a virtual 

109 # attribute with the same name as a DynamicClassAttribute exists 

110 try: 1aebcfd

111 for base in obj.__bases__: 111 ↛ anywhereline 111 didn't jump anywhere: it always raised an exception.1aebcfd

112 for k, v in base.__dict__.items(): 

113 if isinstance(v, types.DynamicClassAttribute): 

114 names.append(k) 

115 except AttributeError: 1aebcfd

116 pass 1aebcfd

117 for key in names: 1aebcfd

118 # First try to get the value via getattr. Some descriptors don't 

119 # like calling their __get__ (see bug #1785), so fall back to 

120 # looking in the __dict__. 

121 try: 1aebcfd

122 value = getattr(obj, key) 1aebcfd

123 # handle the duplicate key 

124 if key in processed: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true1aebcfd

125 raise AttributeError 

126 except Exception as e: 1ae

127 """ UPDATED HERE: Catch all types of exceptions. """ 1ae

128 if isinstance(e, AttributeError): 128 ↛ 139line 128 didn't jump to line 139 because the condition on line 128 was always true1ae

129 """ UPDATED HERE: Use old logic if exception is AttributeError. """ 1ae

130 for base in mro: 130 ↛ 131line 130 didn't jump to line 131 because the loop on line 130 never started1ae

131 if key in base.__dict__: 

132 value = base.__dict__[key] 

133 break 

134 else: 

135 # could be a (currently) missing slot member, or a buggy 

136 # __dir__; discard and move on 

137 continue 1ae

138 else: 

139 """ UPDATED HERE: Ignore this attribute when other types of exception raised. """ 

140 continue 

141 if not predicate or predicate(value): 1aebcfd

142 results.append((key, value)) 1aebcd

143 processed.add(key) 1aebcfd

144 results.sort(key=lambda pair: pair[0]) 1aebcfd

145 return results 1aebcfd

146 

147 

148"""Global `Publisher` instance for subscribing to and unsubscribing from RideMessages.""" 1ag

149PUBLISHER = _Publisher() 1ag