Welcome, guest | Sign In | My Account | Store | Cart

This module provides a powerful 'switch'-like dispatcher system, using decorators to give a syntax somewhat like that of the switch statement in C. Values for switch cases can be anything comparable via '==', a string for use on the left-hand side of the 'in' operator, or a regular expression. Iterables of these types can also be used.

Python, 279 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
##
# This module provides a powerful 'switch'-like dispatcher system.
# Values for switch cases can be anything comparable via '==', a string
# for use on the left-hand side of the 'in' operator, or a regular expression.
# Iterables of these types can also be used.

__author__ = 'Mike Kent'

import re

class SwitchError(Exception): pass

CPAT_TYPE = type(re.compile('.'))
STR_TYPE = type('')
LIST_TYPE = type([])
TUPLE_TYPE = type(())

class Switch(object):
    
    def __init__(self):
        self.exactCases = {}
        self.inCases = []
        self.patternCases = []
        self.defaultHandler = None
        
    ##
    # Try each 'in' case, in the order they were
    # specified, stopping if we get a match.
    # Return a tuple of the string we are searching for in the target string,
    # and the case handler found, or (None, None) if no match found.
    def _findInCase(self, switchValue):
        for inStr, aHandler in self.inCases:
            if inStr in switchValue:
                return (inStr, aHandler)
        return (None, None)
    
    ##
    # Try each regex pattern (using re.search), in the order they were
    # specified, stopping if we get a match.
    # Return a tuple of the re match object and the case handler found, or
    # (None, None) if no match found.
    def _findRegExCase(self, switchValue):
        for cpat, aHandler in self.patternCases:
            matchObj = cpat.search(switchValue)
            if matchObj is not None:
                return (matchObj, aHandler)
        return (None, None)
        
    ##
    # Switch on a switch value.  A match against the exact
    # (non-regular-expression) case matches is tried first.  If that doesn't
    # find a match, then if the switch value is a string, the 'in' case
    # matches are tried next, in the order they were registered.  If that
    # doesn't find a match, then if the switch value is a string,
    # the regular-expression case matches are tried next, in
    # the order they were registered.  If that doesn't find a match, and
    # a default case handler was registered, the default case handler is used.
    # If no match was found, and no default case handler was registered,
    # SwitchError is raised.
    # If a switch match is found, the corresponding case handler is called.
    # The switch value is passed as the first positional parameter, along with
    # any other positional and keyword parameters that were passed to the
    # switch method.  The switch method returns the return value of the
    # called case handler.
    
    def switch(self, switchValue, *args, **kwargs):

        caseHandler = None
        switchType = type(switchValue)
        
        try:
            # Can we find an exact match for this switch value?
            # For an exact match, we will pass the case value to the case
            # handler.
            caseHandler = self.exactCases.get(switchValue)
            caseValue = switchValue
        except TypeError:
            pass

        # If no exact match, and we have 'in' cases to try,
        # see if we have a matching 'in' case for this switch value.
        # For an 'in' operation, we will be passing the left-hand side of
        # 'in' operator to the case handler.
        if not caseHandler and switchType in (STR_TYPE, LIST_TYPE, TUPLE_TYPE) \
           and self.inCases:
            caseValue, caseHandler = self._findInCase(switchValue)
        
        # If no 'in' match, and we have regex patterns to try,
        # see if we have a matching regex pattern for this switch value.
        # For a RegEx match, we will be passing the re.matchObject to the
        # case handler.
        if not caseHandler and switchType == STR_TYPE and self.patternCases:
            caseValue, caseHandler = self._findRegExCase(switchValue)
                    
        # If still no match, see if we have a default case handler to use.
        if not caseHandler:
            caseHandler = self.defaultHandler
            caseValue = switchValue
            
        # If still no case handler was found for the switch value, 
        # raise a SwitchError.
        if not caseHandler:        
            raise SwitchError("Unknown case value %r" % switchValue)
        
        # Call the case handler corresponding to the switch value,
        # passing it the case value, and any other parameters passed
        # to the switch, and return that case handler's return value.
        return caseHandler(caseValue, *args, **kwargs)
        
    ##
    # Register a case handler, and the case value is should handle.
    # This is a function decorator for a case handler.  It doesn't
    # actually modify the decorated case handler, it just registers it.
    # It takes a case value (any object that is valid as a dict key),
    # or any iterable of such case values.
    
    def case(self, caseValue):
        def wrap(caseHandler):
            
            # If caseValue is not an iterable, turn it into one so
            # we can handle everything the same.
            caseValues = ([ caseValue ] if not hasattr(caseValue, '__iter__') \
                          else caseValue)
                
            for aCaseValue in caseValues:
                # Raise SwitchError on a dup case value.
                if aCaseValue in self.exactCases:
                    raise SwitchError("Duplicate exact case value '%s'" % \
                                      aCaseValue)
                # Add it to the dict for finding exact case matches.
                self.exactCases[aCaseValue] = caseHandler
            
            return caseHandler
        return wrap
    
    ##
    # Register a case handler for handling a regular expression.
    def caseRegEx(self, caseValue):
        def wrap(caseHandler):
            
            # If caseValue is not an iterable, turn it into one so
            # we can handle everything the same.
            caseValues = ([ caseValue ] if not hasattr(caseValue, '__iter__') \
                          else caseValue)
                
            for aCaseValue in caseValues:
                # If this item is not a compiled regular expression, compile it.
                if type(aCaseValue) != CPAT_TYPE:
                    aCaseValue = re.compile(aCaseValue)
                    
                # Raise SwitchError on a dup case value.
                for thisCaseValue, _ in self.patternCases:
                    if aCaseValue.pattern == thisCaseValue.pattern:
                        raise SwitchError("Duplicate regex case value '%s'" % \
                                          aCaseValue.pattern)
                self.patternCases.append((aCaseValue, caseHandler))
                
            return caseHandler
        return wrap
        
    ##
    # Register a case handler for handling an 'in' operation.
    def caseIn(self, caseValue):
        def wrap(caseHandler):
            
            # If caseValue is not an iterable, turn it into one so
            # we can handle everything the same.
            caseValues = ([ caseValue ] if not hasattr(caseValue, '__iter__') \
                          else caseValue)
                
            for aCaseValue in caseValues:
                # Raise SwitchError on a dup case value.
                for thisCaseValue, _ in self.inCases:
                    if aCaseValue == thisCaseValue:
                        raise SwitchError("Duplicate 'in' case value '%s'" % \
                                          aCaseValue)
                # Add it to the the list of 'in' values.
                self.inCases.append((aCaseValue, caseHandler))
            
            return caseHandler
        return wrap
    
    ##
    # This is a function decorator for registering the default case handler.
    
    def default(self, caseHandler):
        self.defaultHandler = caseHandler
        return caseHandler

    
if __name__ == '__main__': # pragma: no cover
    
    # Example uses
    
    # Instantiate a switch object.
    mySwitch = Switch()
    
    # Register some cases and case handlers, using the handy-dandy
    # decorators.
    
    # A default handler
    @mySwitch.default
    def gotDefault(value, *args, **kwargs):
        print "Default handler: I got unregistered value %r, "\
              "with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value
        
    # A single numeric case value.
    @mySwitch.case(0)
    def gotZero(value, *args, **kwargs):
        print "gotZero: I got a %d, with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value

    # A range of numeric case values.
    @mySwitch.case(range(5, 10))
    def gotFiveThruNine(value, *args, **kwargs):    
        print "gotFiveThruNine: I got a %d, with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value
        
    # A string case value, for an exact match.
    @mySwitch.case('Guido')
    def gotGuido(value, *args, **kwargs):
        print "gotGuido: I got '%s', with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value
        
    # A string value for use with the 'in' operator.
    @mySwitch.caseIn('lo')
    def gotLo(value, *args, **kwargs):
        print "gotLo: I got '%s', with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value
        
    # A regular expression pattern match in a string.
    # You can also pass in a pre-compiled regular expression.
    @mySwitch.caseRegEx(r'\b([Pp]y\w*)\b')
    def gotPyword(matchObj, *args, **kwargs):
        print "gotPyword: I got a matchObject where group(1) is '%s', "\
              "with args: %r and kwargs: %r" % \
              (matchObj.group(1), args, kwargs)
        return matchObj
        
    # And lastly, you can pass a iterable to case, caseIn, and
    # caseRegEx.
    @mySwitch.case([ 99, 'yo', 200 ])
    def gotStuffInSeq(value, *args, **kwargs):
        print "gotStuffInSeq: I got %r, with args: %r and kwargs: %r" % \
              (value, args, kwargs)
        return value
    
    
    # Now show what we can do.
    got = mySwitch.switch(0)
    # Returns 0, prints "gotZero: I got a 0, with args: () and kwargs: {}"
    got = mySwitch.switch(6, flag='boring')    
    # Returns 6, prints "gotFiveThruNine: I got a 6, with args: () and
    # kwargs: {'flag': 'boring'}"
    got = mySwitch.switch(10, 42)
    # Returns 10, prints "Default handler: I got unregistered value 10,
    # with args: (42,) and kwargs: {}"
    got = mySwitch.switch('Guido', BDFL=True)
    # Returns 'Guido', prints "gotGuido: I got 'Guido', with args: () and 
    # kwargs: {'BDFL': True}"
    got = mySwitch.switch('Anyone seen Guido around?')
    # Returns 'Anyone Seen Guido around?', prints "Default handler: I got 
    # unregistered value 'Anyone seen Guido around?', with args: () and 
    # kwargs: {}", 'cause we used 'case' and not 'caseIn'.
    got = mySwitch.switch('Yep, and he said "hello".', 99, yes='no')
    # Returns 'lo', prints "gotLo: I got 'lo', with args: (99,) and 
    # kwargs: {'yes': 'no'}", 'cause we found the 'lo' in 'hello'.
    got = mySwitch.switch('Bird is the Python word of the day.')    
    # Returns a matchObject, prints "gotPyword: I got a matchObject where 
    # group(1) is 'Python', with args: () and kwargs: {}"
    got = mySwitch.switch('yo')
    # Returns 'yo', prints "gotStuffInSeq: I got 'yo', with args: () and
    # kwargs: {}"

While it doesn't happen too often, I ocasionally find myself in Python missing the 'switch' statement from C. In the past, I've used one-off quick-and-simple dictionary-based dispatchers, but I decided to scratch that itch and come up with something that had the simplicity of a C 'switch', read like one, but with more of the power available in Python.

Created by Michael Kent on Tue, 21 Dec 2010 (MIT)
Python recipes (4591)
Michael Kent's recipes (2)

Required Modules

Other Information and Tasks