├── requirements-dev.txt ├── switchlang ├── __init__.py └── __switchlang_impl.py ├── ruff.toml ├── LICENSE ├── setup.py ├── .gitignore ├── README.md └── tests └── test_core.py /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | # twine 3 | -------------------------------------------------------------------------------- /switchlang/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | switchlang - Adds switch blocks to Python 3 | 4 | See https://github.com/mikeckennedy/python-switch for full details. 5 | Copyright Michael Kennedy (https://twitter.com/mkennedy) 6 | License: MIT 7 | """ 8 | 9 | __version__ = '0.1.1' 10 | __author__ = 'Michael Kennedy ' 11 | __all__ = ['switch', 'closed_range'] 12 | 13 | from .__switchlang_impl import switch 14 | from .__switchlang_impl import closed_range 15 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # [ruff] 2 | line-length = 120 3 | format.quote-style = "single" 4 | 5 | # Enable Pyflakes `E` and `F` codes by default. 6 | lint.select = ["E", "F", "I"] 7 | lint.ignore = [] 8 | 9 | # Exclude a variety of commonly ignored directories. 10 | exclude = [ 11 | ".bzr", 12 | ".direnv", 13 | ".eggs", 14 | ".git", 15 | ".hg", 16 | ".mypy_cache", 17 | ".nox", 18 | ".pants.d", 19 | ".ruff_cache", 20 | ".svn", 21 | ".tox", 22 | "__pypackages__", 23 | "_build", 24 | "buck-out", 25 | "build", 26 | "dist", 27 | "node_modules", 28 | ".env", 29 | ".venv", 30 | "venv", 31 | "typings/**/*.pyi", 32 | ] 33 | lint.per-file-ignores = { } 34 | 35 | # Allow unused variables when underscore-prefixed. 36 | # dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 37 | 38 | # Assume Python 3.13. 39 | target-version = "py313" 40 | 41 | #[tool.ruff.mccabe] 42 | ## Unlike Flake8, default to a complexity level of 10. 43 | lint.mccabe.max-complexity = 10 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Kennedy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | 5 | from setuptools import find_packages 6 | from setuptools import setup 7 | 8 | 9 | def read(filename): 10 | filename = os.path.join(os.path.dirname(__file__), filename) 11 | text_type = type('') 12 | with io.open(filename, mode='r', encoding='utf-8') as fd: 13 | return re.sub(text_type(r':[a-z]+:`~?(.*?)`'), text_type(r'``\1``'), fd.read()) 14 | 15 | 16 | def read_version(): 17 | filename = os.path.join(os.path.dirname(__file__), 'switchlang', '__init__.py') 18 | with open(filename, mode='r', encoding='utf-8') as fin: 19 | for line in fin: 20 | if line and line.strip() and line.startswith('__version__'): 21 | return line.split('=')[1].strip().strip("'") 22 | 23 | return '0.0.0.0' 24 | 25 | 26 | requires = [] 27 | 28 | setup( 29 | name='switchlang', 30 | version=read_version(), 31 | url='https://github.com/mikeckennedy/python-switch', 32 | license='MIT', 33 | author='Michael Kennedy', 34 | author_email='michael@talkpython.fm', 35 | description='Adds switch blocks to the Python language.', 36 | long_description=read('README.md'), 37 | long_description_content_type='text/markdown', 38 | packages=find_packages(exclude=('tests',)), 39 | install_requires=requires, 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'License :: OSI Approved :: MIT License', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.6', 46 | 'Programming Language :: Python :: 3.7', 47 | 'Programming Language :: Python :: 3.8', 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .idea 103 | -------------------------------------------------------------------------------- /switchlang/__switchlang_impl.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import uuid 3 | 4 | 5 | class switch: 6 | """ 7 | switch is a module-level implementation of the switch statement for Python. 8 | See https://github.com/mikeckennedy/python-switch for full details. 9 | Copyright Michael Kennedy (https://mkennedy.codes) 10 | License: MIT 11 | """ 12 | 13 | __no_result: typing.Any = uuid.uuid4() 14 | __default: typing.Any = uuid.uuid4() 15 | 16 | def __init__(self, value: typing.Any): 17 | self.value = value 18 | self.cases: typing.Set[typing.Any] = set() 19 | self._found = False 20 | self.__result = switch.__no_result 21 | self._falling_through = False 22 | self._func_stack: typing.List[typing.Callable[[], typing.Any]] = [] 23 | 24 | def default(self, func: typing.Callable[[], typing.Any]): 25 | """ 26 | Use as option final statement in switch block. 27 | 28 | ``` 29 | with switch(val) as s: 30 | s.case(...) 31 | s.case(...) 32 | s.default(function) 33 | ``` 34 | 35 | :param func: Any callable taking no parameters to be executed if this (default) case matches. 36 | :return: None 37 | """ 38 | self.case(switch.__default, func) 39 | 40 | def case( 41 | self, 42 | key: typing.Any, 43 | func: typing.Callable[[], typing.Any], 44 | fallthrough: typing.Optional[bool] = False, 45 | ): 46 | """ 47 | Specify a case for the switch block: 48 | 49 | ``` 50 | with switch(val) as s: 51 | s.case('a', function) 52 | s.case('b', function, fallthrough=True) 53 | s.default(function) 54 | ``` 55 | 56 | :param key: Key for the case test (if this is a list or range, the items will each be added as a case) 57 | :param func: Any callable taking no parameters to be executed if this case matches. 58 | :param fallthrough: Optionally fall through to the subsequent case (defaults to False) 59 | :return: 60 | """ 61 | if fallthrough is not None: 62 | if self._falling_through: 63 | self._func_stack.append(func) 64 | if not fallthrough: 65 | self._falling_through = False 66 | 67 | if isinstance(key, range): 68 | key = list(key) 69 | 70 | if isinstance(key, list): 71 | if not key: 72 | raise ValueError('You cannot pass an empty collection as the case. It will never match.') 73 | 74 | found = False 75 | for i in key: 76 | if self.case(i, func, fallthrough=None): 77 | found = True 78 | if fallthrough is not None: 79 | self._falling_through = fallthrough 80 | 81 | return found 82 | 83 | if key in self.cases: 84 | raise ValueError(f'Duplicate case: {key}') 85 | if not func: 86 | raise ValueError('Action for case cannot be None.') 87 | if not callable(func): 88 | raise ValueError('Func must be callable.') 89 | 90 | self.cases.add(key) 91 | if key == self.value or not self._found and key == self.__default: 92 | self._func_stack.append(func) 93 | self._found = True 94 | if fallthrough is not None: 95 | self._falling_through = fallthrough 96 | return True 97 | 98 | def __enter__(self): 99 | return self 100 | 101 | def __exit__(self, exc_type, exc_val, exc_tb): 102 | if exc_val: 103 | raise exc_val 104 | 105 | if not self._func_stack: 106 | raise Exception('Value does not match any case and there is no default case: value {}'.format(self.value)) 107 | 108 | for func in self._func_stack: 109 | # noinspection PyCallingNonCallable 110 | self.__result = func() 111 | 112 | @property 113 | def result(self): 114 | """ 115 | The value captured from the method called for a given case. 116 | 117 | ``` 118 | value = 4 119 | with switch(value) as s: 120 | s.case(closed_range(1, 5), lambda: "1-to-5") 121 | # ... 122 | 123 | res = s.result # res == '1-to-5' 124 | ``` 125 | 126 | :return: The value captured from the method called for a given case. 127 | """ 128 | if self.__result == switch.__no_result: 129 | raise Exception('No result has been computed (did you access switch.result inside the with block?)') 130 | 131 | return self.__result 132 | 133 | 134 | def closed_range(start: int, stop: int, step=1) -> range: 135 | """ 136 | Creates a closed range that allows you to specify a case 137 | from [start, stop] inclusively. 138 | 139 | ``` 140 | with switch(value) as s: 141 | s.case(closed_range(1, 5), lambda: "1-to-5") 142 | s.case(closed_range(6, 7), lambda: "6") 143 | s.default(lambda: 'default') 144 | ``` 145 | 146 | :param start: The inclusive lower bound of the range [start, stop]. 147 | :param stop: The inclusive upper bound of the range [start, stop]. 148 | :param step: The step size between elements (defaults to 1). 149 | :return: A range() generator that has a closed upper bound. 150 | """ 151 | if start >= stop: 152 | raise ValueError('Start must be less than stop.') 153 | 154 | return range(start, stop + step, step) 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # switchlang 2 | [![](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/) 3 | [![](https://img.shields.io/pypi/l/markdown-subtemplate.svg)](https://github.com/mikeckennedy/python-switch/blob/master/LICENSE) 4 | [![](https://img.shields.io/pypi/dm/switchlang.svg)](https://pypi.org/project/switchlang/) 5 | 6 | 7 | Adds switch blocks to the Python language. 8 | 9 | This module adds explicit switch functionality to Python 10 | without changing the language. It builds upon a standard 11 | way to define execution blocks: the `with` statement. 12 | 13 | ## Example 14 | 15 | ```python 16 | from switchlang import switch 17 | 18 | def main(): 19 | num = 7 20 | val = input("Enter a character, a, b, c or any other: ") 21 | 22 | with switch(val) as s: 23 | s.case('a', process_a) 24 | s.case('b', lambda: process_with_data(val, num, 'other values still')) 25 | s.default(process_any) 26 | 27 | def process_a(): 28 | print("Found A!") 29 | 30 | def process_any(): 31 | print("Found Default!") 32 | 33 | def process_with_data(*value): 34 | print("Found with data: {}".format(value)) 35 | 36 | main() 37 | ``` 38 | 39 | ## Installation 40 | 41 | Simply install via pip: 42 | 43 | ```bash 44 | pip install switchlang 45 | ``` 46 | 47 | ## Features 48 | 49 | * More explicit than using dictionaries with functions as values. 50 | * Verifies the signatures of the methods 51 | * Supports default case 52 | * Checks for duplicate keys / cases 53 | * Keys can be anything hashable (numbers, strings, objects, etc.) 54 | * Could be extended for "fall-through" cases (doesn't yet) 55 | * Use range and list for multiple cases mapped to a single action 56 | 57 | ## Multiple cases, one action 58 | 59 | You can map ranges and lists of cases to a single action as follows: 60 | 61 | ```python 62 | # with lists: 63 | value = 4 # matches even number case 64 | 65 | with switch(value) as s: 66 | s.case([1, 3, 5, 7], lambda: ...) 67 | s.case([0, 2, 4, 6, 8], lambda: ...) 68 | s.default(lambda: ...) 69 | ``` 70 | 71 | ```python 72 | # with ranges: 73 | value = 4 # matches first case 74 | 75 | with switch(value) as s: 76 | s.case(range(1, 6), lambda: ...) 77 | s.case(range(6, 10), lambda: ...) 78 | s.default(lambda: ...) 79 | ``` 80 | 81 | ## Closed vs. Open ranges 82 | 83 | Looking at the above code it's a bit weird that 6 appears 84 | at the end of one case, beginning of the next. But `range()` is 85 | half open/closed. 86 | 87 | To handle the inclusive case, I've added `closed_range(start, stop)`. 88 | For example, `closed_range(1,5)` -> `[1,2,3,4,5]` 89 | 90 | ## Why not just raw `dict`? 91 | 92 | The biggest push back on this idea is that we already have this problem solved. 93 | You write the following code. 94 | 95 | ```python 96 | switch = { 97 | 1: method_on_one, 98 | 2: method_on_two, 99 | 3: method_three 100 | } 101 | 102 | result = switch.get(value, default_method_to_run)() 103 | ``` 104 | 105 | This works but is very low on the functionality level. We have a better solution here 106 | I believe. Let's take this example and see how it looks in python-switch vs raw dicts: 107 | 108 | ```python 109 | # with python-switch: 110 | 111 | while True: 112 | action = get_action(action) 113 | 114 | with switch(action) as s: 115 | s.case(['c', 'a'], create_account) 116 | s.case('l', log_into_account) 117 | s.case('r', register_cage) 118 | s.case('u', update_availability) 119 | s.case(['v', 'b'], view_bookings) 120 | s.case('x', exit_app) 121 | s.case('', lambda: None) 122 | s.case(range(1,6), lambda: set_level(action)) 123 | s.default(unknown_command) 124 | 125 | print('Result is {}'.format(s.result)) 126 | ``` 127 | 128 | Now compare that to the espoused *pythonic* way: 129 | 130 | ```python 131 | # with raw dicts 132 | 133 | while True: 134 | action = get_action(action) 135 | 136 | switch = { 137 | 'c': create_account, 138 | 'a': create_account, 139 | 'l': log_into_account, 140 | 'r': register_cage, 141 | 'u': update_availability, 142 | 'v': view_bookings, 143 | 'b': view_bookings, 144 | 'x': exit_app, 145 | 1: lambda: set_level(action), 146 | 2: lambda: set_level(action), 147 | 3: lambda: set_level(action), 148 | 4: lambda: set_level(action), 149 | 5: lambda: set_level(action), 150 | '': lambda: None, 151 | } 152 | result = switch.get(action, unknown_command)() 153 | print('Result is {}'.format(result)) 154 | ``` 155 | 156 | Personally, I much prefer to read and write the one above. That's why I wrote this module. 157 | It seems to convey the intent of switch way more than the dict. But either are options. 158 | 159 | ## Why not just `if / elif / else`? 160 | 161 | The another push back on this idea is that we already have this problem solved. 162 | Switch statements are really if / elif / else blocks. So you write the following code. 163 | 164 | ```python 165 | # with if / elif / else 166 | 167 | while True: 168 | action = get_action(action) 169 | 170 | if action == 'c' or action == 'a': 171 | result = create_account() 172 | elif action == 'l': 173 | result = log_into_account() 174 | elif action == 'r': 175 | result = register_cage() 176 | elif action == 'a': 177 | result = update_availability() 178 | elif action == 'v' or action == 'b': 179 | result = view_bookings() 180 | elif action == 'x': 181 | result = exit_app() 182 | elif action in {1, 2, 3, 4, 5}: 183 | result = set_level(action) 184 | else: 185 | unknown_command() 186 | 187 | print('Result is {}'.format(result)) 188 | ``` 189 | 190 | I actually believe this is a little better than the 191 | [raw dict option](https://github.com/mikeckennedy/python-switch#why-not-just-raw-dict). 192 | But there are still things that are harder. 193 | 194 | * How would you deal with fall-through cleanly? 195 | * Did you notice the bug? We forgot to set result in default case (`else`) and will result in a runtime error (but only if that case hits). 196 | * There is another bug. Update `update_availability` will never run because it's command (`a`) is bound to two cases. 197 | This is guarded against in switch and you would receive a duplicate case error the first time it runs at all. 198 | * While it's pretty clear, it's much more verbose and less declarative than the switch version. 199 | 200 | Again, compare the if / elif / else to what you have with switch. This code is identical except 201 | doesn't have the default case bug. 202 | 203 | ```python 204 | while True: 205 | action = get_action(action) 206 | 207 | with switch(action) as s: 208 | s.case(['c', 'a'], create_account) 209 | s.case('l', log_into_account) 210 | s.case('r', register_cage) 211 | s.case('u', update_availability) 212 | s.case(['v', 'b'], view_bookings) 213 | s.case('x', exit_app) 214 | s.case('', lambda: None) 215 | s.case(range(1,6), lambda: set_level(action)) 216 | s.default(unknown_command) 217 | 218 | print('Result is {}'.format(s.result)) 219 | ``` 220 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from switchlang import switch, closed_range 3 | 4 | 5 | # here is a custom type we can use as a key for our tests 6 | class TestKeyObject: 7 | pass 8 | 9 | 10 | class CoreTests(unittest.TestCase): 11 | def test_has_matched_case_int(self): 12 | value = 7 13 | with switch(value) as s: 14 | s.case(1, lambda: 'one') 15 | s.case(5, lambda: 'five') 16 | s.case(7, lambda: 'seven') 17 | s.default(lambda: 'default') 18 | 19 | self.assertEqual(s.result, 'seven') 20 | 21 | def test_has_matched_case_object(self): 22 | t1 = TestKeyObject() 23 | t2 = TestKeyObject() 24 | t3 = TestKeyObject() 25 | 26 | with switch(t2) as s: 27 | s.case(t1, lambda: t1) 28 | s.case(t2, lambda: t2) 29 | s.case(t3, lambda: t3) 30 | s.default(lambda: None) 31 | 32 | self.assertEqual(s.result, t2) 33 | 34 | def test_default_passthrough(self): 35 | value = 11 36 | with switch(value) as s: 37 | s.case(1, lambda: '1') 38 | s.case(2, lambda: '2') 39 | s.default(lambda: 'default') 40 | 41 | self.assertEqual(s.result, 'default') 42 | 43 | def test_none_as_valid_case(self): 44 | with switch(None) as s: 45 | s.case(1, lambda: 'one') 46 | s.case(None, lambda: 'none') 47 | s.default(lambda: 'default') 48 | 49 | self.assertEqual(s.result, 'none') 50 | 51 | def test_error_no_match_no_default(self): 52 | with self.assertRaises(Exception): 53 | with switch('val') as s: 54 | s.case(1, lambda: None) 55 | s.case(2, lambda: None) 56 | 57 | def test_error_duplicate_case(self): 58 | with self.assertRaises(ValueError): 59 | with switch('val') as s: 60 | s.case(1, lambda: None) 61 | s.case(1, lambda: None) 62 | 63 | def test_multiple_values_one_case_range(self): 64 | for value in range(1, 5): 65 | with switch(value) as s: 66 | s.case(range(1, 6), lambda: '1-to-5') 67 | s.case(range(6, 7), lambda: '6') 68 | s.default(lambda: 'default') 69 | 70 | self.assertEqual(s.result, '1-to-5') 71 | 72 | for value in range(6, 7): 73 | with switch(value) as s: 74 | s.case(range(1, 6), lambda: '1-to-5') 75 | s.case(range(6, 7), lambda: '6') 76 | s.default(lambda: 'default') 77 | 78 | self.assertEqual(s.result, '6') 79 | 80 | with switch(7) as s: 81 | s.case(range(1, 6), lambda: '1-to-5') 82 | s.case(range(6, 7), lambda: '6') 83 | s.default(lambda: 'default') 84 | 85 | self.assertEqual(s.result, 'default') 86 | 87 | def test_multiple_values_one_case_list(self): 88 | with switch(6) as s: 89 | s.case([1, 3, 5, 7], lambda: 'odd') 90 | s.case([0, 2, 4, 6, 8], lambda: 'even') 91 | s.default(lambda: 'default') 92 | 93 | self.assertEqual(s.result, 'even') 94 | 95 | def test_return_value_from_case(self): 96 | value = 4 97 | with switch(value) as s: 98 | s.case([1, 3, 5, 7], lambda: value + 1) 99 | s.case([0, 2, 4, 6, 8], lambda: value * value) 100 | s.default(lambda: 0) 101 | 102 | self.assertEqual(s.result, 16) 103 | 104 | # noinspection PyStatementEffect 105 | def test_result_inaccessible_if_hasnt_run(self): 106 | with self.assertRaises(Exception): 107 | s = switch(7) 108 | s.result 109 | 110 | def test_closed_range(self): 111 | for value in [1, 2, 3, 4, 5]: 112 | with switch(value) as s: 113 | s.case(closed_range(1, 5), lambda: '1-to-5') 114 | s.case(closed_range(6, 7), lambda: '6') 115 | s.default(lambda: 'default') 116 | 117 | self.assertEqual(s.result, '1-to-5') 118 | 119 | with switch(0) as s: 120 | s.case(closed_range(1, 5), lambda: '1-to-5') 121 | s.case(closed_range(6, 7), lambda: '6') 122 | s.default(lambda: 'default') 123 | 124 | self.assertEqual(s.result, 'default') 125 | 126 | with switch(6) as s: 127 | s.case(closed_range(1, 5), lambda: '1-to-5') 128 | s.case(closed_range(6, 7), lambda: '6') 129 | s.default(lambda: 'default') 130 | 131 | self.assertEqual(s.result, '6') 132 | 133 | def test_fallthrough_simple(self): 134 | visited = [] 135 | value = 2 136 | with switch(value) as s: 137 | s.case(1, lambda: visited.append(1) or 1) 138 | s.case(2, lambda: visited.append(2) or 2, fallthrough=True) 139 | s.default(lambda: visited.append('default') or 'default') 140 | 141 | self.assertEqual(s.result, 'default') 142 | self.assertEqual(visited, [2, 'default']) 143 | 144 | def test_fallthrough_list(self): 145 | visited = [] 146 | value = 5 147 | with switch(value) as s: 148 | s.case([1, 2, 3], lambda: visited.append(1) or 1) 149 | s.case([4, 5, 6], lambda: visited.append(4) or 4, fallthrough=True) 150 | s.case([7, 8, 9], lambda: visited.append(7) or 7, fallthrough=True) 151 | s.default(lambda: visited.append('default') or 'default') 152 | 153 | self.assertEqual(s.result, 'default') 154 | self.assertEqual(visited, [4, 7, 'default']) 155 | 156 | def test_fallthrough_some_list(self): 157 | visited = [] 158 | value = 5 159 | with switch(value) as s: 160 | s.case([1, 2, 3], lambda: visited.append(1) or 1) 161 | s.case([4, 5, 6], lambda: visited.append(4) or 4, fallthrough=True) 162 | s.case([7, 8, 9], lambda: visited.append(7) or 7) 163 | s.default(lambda: visited.append('default') or 'default') 164 | 165 | self.assertEqual(s.result, 7) 166 | self.assertEqual(visited, [4, 7]) 167 | 168 | def test_fallthrough_then_stop(self): 169 | visited = [] 170 | value = 2 171 | with switch(value) as s: 172 | s.case(1, lambda: visited.append(1) or 1) 173 | s.case(2, lambda: visited.append(2) or 2, fallthrough=True) 174 | s.case(3, lambda: visited.append(3) or 3, fallthrough=True) 175 | s.case(4, lambda: visited.append(4) or 4) 176 | s.case(5, lambda: visited.append(5) or 5) 177 | s.default(lambda: visited.append('default') or 'default') 178 | 179 | self.assertEqual(s.result, 4) 180 | self.assertEqual(visited, [2, 3, 4]) 181 | 182 | def test_fallthrough_middle_then_stop(self): 183 | visited = [] 184 | value = 3 185 | with switch(value) as s: 186 | s.case(1, lambda: visited.append(1) or 1) 187 | s.case(2, lambda: visited.append(2) or 2, fallthrough=True) 188 | s.case(3, lambda: visited.append(3) or 3, fallthrough=True) 189 | s.case(4, lambda: visited.append(4) or 4) 190 | s.case(5, lambda: visited.append(5) or 5) 191 | s.default(lambda: visited.append('default') or 'default') 192 | 193 | self.assertEqual(s.result, 4) 194 | self.assertEqual(visited, [3, 4]) 195 | 196 | def test_fallthrough_available_but_not_hit(self): 197 | visited = [] 198 | value = 5 199 | with switch(value) as s: 200 | s.case(1, lambda: visited.append(1) or 1) 201 | s.case(2, lambda: visited.append(2) or 2, fallthrough=True) 202 | s.case(3, lambda: visited.append(3) or 3, fallthrough=True) 203 | s.case(4, lambda: visited.append(4) or 4) 204 | s.case(5, lambda: visited.append(5) or 5) 205 | s.default(lambda: visited.append('default') or 'default') 206 | 207 | self.assertEqual(s.result, 5) 208 | self.assertEqual(visited, [5]) 209 | 210 | def test_fallthrough__no_match_but_not_hit(self): 211 | visited = [] 212 | value = 'gone' 213 | with switch(value) as s: 214 | s.case(1, lambda: visited.append(1) or 1) 215 | s.case(2, lambda: visited.append(2) or 2, fallthrough=True) 216 | s.case(3, lambda: visited.append(3) or 3, fallthrough=True) 217 | s.case(4, lambda: visited.append(4) or 4) 218 | s.case(5, lambda: visited.append(5) or 5) 219 | s.default(lambda: visited.append('default') or 'default') 220 | 221 | self.assertEqual(s.result, 'default') 222 | self.assertEqual(visited, ['default']) 223 | 224 | def test_empty_collection_clause_is_error(self): 225 | with self.assertRaises(ValueError): 226 | with switch('val') as s: 227 | s.case([], lambda: None) 228 | s.default(lambda: 'default') 229 | 230 | 231 | if __name__ == '__main__': 232 | unittest.main() 233 | --------------------------------------------------------------------------------