├── .gitignore ├── LICENSE ├── README.md ├── regex_decorator ├── __init__.py ├── parser.py ├── with_parse.py └── with_re.py ├── setup.py └── tests ├── test_parsing.py └── test_strings.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__ 2 | *.egg-info 3 | .coverage 4 | coverage.xml 5 | .cache 6 | build/ 7 | dist/ 8 | MANIFEST 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Eddy Hintze 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regex Decorator 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/regex_decorator.svg)](https://pypi.python.org/pypi/regex_decorator) 4 | [![PyPI](https://img.shields.io/pypi/l/regex_decorator.svg)](https://pypi.python.org/pypi/regex_decorator) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/ce0745991c4f49a0b9805d4cbeb10d2a)](https://www.codacy.com/app/eddy-hintze/regex-decorator?utm_source=github.com&utm_medium=referral&utm_content=xtream1101/regex-decorator&utm_campaign=Badge_Coverage) 6 | 7 | This library allows you to place a decorator on a function which has a regex as an argument. Then when parsing text, if that regex is found it will run the function and optionally return data 8 | 9 | 10 | ## Install 11 | ``` 12 | $ pip3 install regex_decorator 13 | ``` 14 | 15 | ## Example 16 | 17 | Can use `re` or `parse` in the decorator 18 | 19 | Default for `parse_using` is `parse` 20 | 21 | `@p.listener('my name is (\w+).', re.IGNORECASE, parse_using='re')` 22 | 23 | `@p.listener('my name is (?P\w+).', re.IGNORECASE, parse_using='re')` 24 | 25 | `@p.listener('my name is {}.')` 26 | 27 | `@p.listener('my name is {name}.')` 28 | 29 | Both of the above will match `Eddy` with the input of `my name is Eddy.` 30 | 31 | ```python 32 | # Content of test_strings.txt 33 | # Foo 1 34 | # Don't match this line 35 | # maybe this line because the answer is 99 36 | # Hi, my name is Eddy! 37 | # the answer is 44, foo 4 38 | 39 | import re 40 | import logging 41 | from regex_decorator import Parser 42 | 43 | logging.basicConfig(level=logging.INFO, 44 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | p = Parser() 49 | 50 | 51 | @p.listener('my name is (\w+)', re.IGNORECASE, parse_using='re') 52 | def name(matched_str, name): 53 | return name 54 | 55 | 56 | @p.listener('The answer is (\d+)', re.IGNORECASE, parse_using='re') 57 | def answer(matched_str, answer): 58 | return answer 59 | 60 | 61 | @p.listener('foo (\d)', re.IGNORECASE, parse_using='re') 62 | def foo(matched_str, value): 63 | return value 64 | 65 | 66 | @p.listener('What is (?P\d+) \+ (?P\d+)', re.IGNORECASE, parse_using='re') 67 | def add(matched_str, val2, val1): 68 | """ 69 | When using named args in the regex `(?P)`, the order of the args does not matter 70 | """ 71 | ans = int(val1) + int(val2) 72 | print(val1, "+", val2, "=", ans) 73 | 74 | 75 | example1 = p.parse("My name is Eddy, and the answer is 42.") 76 | print(example1) # Returns: Eddy 77 | 78 | example2 = p.parse_all("My name is Eddy, and the answer is 42.") 79 | print(example2) # Returns: ['Eddy', '42'] 80 | 81 | example3 = p.parse_file('test_strings.txt') 82 | print(example3) # Returns: ['1', '99', 'Eddy', '44'] 83 | 84 | example4 = p.parse_file_all('test_strings.txt') 85 | print(example4) # Returns: ['1', '99', 'Eddy', '44', '4'] 86 | 87 | # It does not always have to return something and all action can be completed in the function like so: 88 | p.parse("what is 2 + 3") # Prints: 2 + 3 = 5 89 | 90 | # Reference here for more examples: https://github.com/xtream1101/regex-decorator/blob/master/test_parsing.py 91 | 92 | ``` 93 | 94 | ## Example Use case 95 | Use it with a speach to text library and create custom home automation commands. 96 | This example requires `speech_recognition` 97 | 98 | `$ pip3 install speech_recognition` 99 | 100 | ```python 101 | import re 102 | import speech_recognition as sr 103 | from regex_decorator import Parser 104 | 105 | p = Parser() 106 | 107 | 108 | @p.listener('Turn (?P\w+) (?:my|the)?\s?(?P.+)', re.IGNORECASE) 109 | def on(matched_str, action, item): 110 | # Some home automation action here 111 | print("Turning", action, item) 112 | 113 | 114 | # Try and say: 115 | # "Turn on living room lights" 116 | # "Turn off the living room lights" 117 | 118 | # obtain audio from the microphone 119 | r = sr.Recognizer() 120 | with sr.Microphone() as source: 121 | print("Say something!") 122 | audio = r.listen(source) 123 | 124 | # recognize speech using Google Speech Recognition 125 | try: 126 | result = r.recognize_google(audio) 127 | print("Google Speech Recognition thinks you said " + result) 128 | p.parse(result) 129 | except sr.UnknownValueError: 130 | print("Google Speech Recognition could not understand audio") 131 | except sr.RequestError as e: 132 | print("Could not request results from Google Speech Recognition service; {0}".format(e)) 133 | ``` 134 | 135 | ## Run tests 136 | Just run the command `pytest` in the root directory (may need to `pip3 install pytest`) 137 | -------------------------------------------------------------------------------- /regex_decorator/__init__.py: -------------------------------------------------------------------------------- 1 | from regex_decorator.parser import Parser # noqa: F401 2 | -------------------------------------------------------------------------------- /regex_decorator/parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from regex_decorator.with_re import WithRe 4 | from regex_decorator.with_parse import WithParse 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def _read_file(file_path): 10 | content = '' 11 | # Check if file exists 12 | if os.path.isfile(file_path) is False: 13 | logger.error("File {file} does not exist".format(file=file_path)) 14 | return content 15 | 16 | with open(file_path) as f: 17 | content = f.readlines() 18 | # Strip out just the newline char at the end of line 19 | content = [l.rstrip('\n') for l in content] 20 | 21 | return content 22 | 23 | 24 | class Parser(): 25 | 26 | def __init__(self): 27 | self.listeners = [] 28 | 29 | def listener(self, match_str, flags=0, parse_using='parse'): 30 | 31 | def wrapper(func): 32 | if parse_using == 're': 33 | parse_with = WithRe(match_str, func, flags=flags) 34 | 35 | else: 36 | # Default parser 37 | parse_with = WithParse(match_str, func) 38 | 39 | self.listeners.append(parse_with) 40 | logger.info("Registered listener '{func}' to regex '{str}'" 41 | .format(func=func.__name__, str=match_str)) 42 | return func 43 | 44 | return wrapper 45 | 46 | def _base_parse(self, input_data, find_all=False, **kwargs): 47 | """ 48 | All of the parsing is done ehre 49 | """ 50 | orig_input = input_data 51 | if isinstance(input_data, str): 52 | input_data = [input_data] 53 | 54 | rdata = [] 55 | # Check each string that was passed 56 | for test_string in input_data: 57 | # Check each listener for a match, if *_all, then always check all, else break after first match 58 | for listener in self.listeners: 59 | logger.debug("Test string '{str}' with regex '{regex}'" 60 | .format(str=test_string, regex=listener.regex)) 61 | 62 | matched_data = listener.parse(test_string, find_all=find_all, **kwargs) 63 | 64 | if len(matched_data) != 0: 65 | rdata.extend(matched_data) 66 | if find_all is False: 67 | break 68 | 69 | if len(rdata) == 0: 70 | rdata = None 71 | 72 | elif isinstance(orig_input, str) and find_all is False: 73 | # If a single item was passed in, return a single item to be consistent 74 | rdata = rdata[0] 75 | 76 | return rdata 77 | 78 | def parse(self, input_data, **kwargs): 79 | """ 80 | Find the first occurrence in a string 81 | If a list is passed in then the first occurrence in each item in that list 82 | """ 83 | return self._base_parse(input_data, find_all=False, **kwargs) 84 | 85 | def parse_all(self, input_data): 86 | """ 87 | Find all occurrences in a single string or in a list of strings 88 | """ 89 | return self._base_parse(input_data, find_all=True) 90 | 91 | def parse_file(self, file_path): 92 | """ 93 | Read in a file and parse each line for the first occurrence of a match 94 | """ 95 | content = _read_file(file_path) 96 | rdata = self.parse(content) 97 | 98 | return rdata 99 | 100 | def parse_file_all(self, file_path): 101 | """ 102 | Read in a file and parse each line for all occurrences of a match 103 | """ 104 | content = _read_file(file_path) 105 | rdata = self.parse_all(content) 106 | 107 | return rdata 108 | -------------------------------------------------------------------------------- /regex_decorator/with_parse.py: -------------------------------------------------------------------------------- 1 | import parse 2 | 3 | 4 | class WithParse: 5 | 6 | def __init__(self, regex_str, callback): 7 | self.regex = parse.compile(regex_str) 8 | self.callback = callback 9 | self.text = None 10 | self.find_all = None 11 | 12 | def _parse_matches(self, results, **kwargs): 13 | rdata = [] 14 | 15 | def append_rdata(data): 16 | output = self._parse_result(data, **kwargs) 17 | if output is not None: 18 | rdata.append(output) 19 | 20 | if self.find_all is False: 21 | # Only care about the first match 22 | append_rdata(results) 23 | else: 24 | if results is not None: 25 | for result in results: 26 | append_rdata(result) 27 | 28 | return rdata 29 | 30 | def _parse_result(self, result, **kwargs): 31 | rdata = None 32 | 33 | if result is not None: 34 | if len(result.named.keys()) != 0: 35 | rdata = self.callback(self.text, **result.named, **kwargs) 36 | else: 37 | rdata = self.callback(self.text, *result.fixed, **kwargs) 38 | 39 | return rdata 40 | 41 | def parse(self, text, find_all=False, **kwargs): 42 | """ 43 | Return a list, even if empty 44 | """ 45 | self.text = text 46 | self.find_all = find_all 47 | 48 | if self.find_all is True: 49 | parse_func = self.regex.findall 50 | else: 51 | parse_func = self.regex.search 52 | 53 | matches = parse_func(self.text) 54 | 55 | return self._parse_matches(matches, **kwargs) 56 | -------------------------------------------------------------------------------- /regex_decorator/with_re.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class WithRe: 5 | 6 | def __init__(self, regex_str, callback, flags=0): 7 | self.regex = re.compile(regex_str, flags) 8 | self.callback = callback 9 | self.text = None 10 | self.find_all = None 11 | 12 | def _parse_matches(self, results, **kwargs): 13 | rdata = [] 14 | 15 | def append_rdata(data): 16 | output = self._parse_result(data, **kwargs) 17 | if output is not None: 18 | rdata.append(output) 19 | 20 | if self.find_all is False: 21 | # Only care about the first match 22 | append_rdata(results) 23 | else: 24 | if results is not None: 25 | for result in results: 26 | append_rdata(result) 27 | 28 | return rdata 29 | 30 | def _parse_result(self, result, **kwargs): 31 | rdata = None 32 | 33 | if result is not None: 34 | if len(result.groupdict().keys()) != 0: 35 | rdata = self.callback(self.text, **result.groupdict(), **kwargs) 36 | else: 37 | rdata = self.callback(self.text, *result.groups(), **kwargs) 38 | 39 | return rdata 40 | 41 | def parse(self, text, find_all=False, **kwargs): 42 | self.text = text 43 | self.find_all = find_all 44 | 45 | if self.find_all is True: 46 | parse_func = re.finditer 47 | else: 48 | parse_func = re.search 49 | 50 | matched = parse_func(self.regex, self.text) 51 | 52 | return self._parse_matches(matched, **kwargs) 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | setup( 5 | name='regex_decorator', 6 | packages=['regex_decorator'], 7 | version='0.3.1', 8 | description='Apply a decorator to a function that is triggered by a regex', 9 | author='Eddy Hintze', 10 | author_email="eddy.hintze@gmail.com", 11 | url="https://github.com/xtream1101/regex-decorator", 12 | license='MIT', 13 | classifiers=[ 14 | "Programming Language :: Python :: 3", 15 | "Development Status :: 4 - Beta", 16 | "Environment :: Other Environment", 17 | "Operating System :: OS Independent", 18 | "Topic :: Software Development :: Libraries :: Python Modules", 19 | "Topic :: Utilities", 20 | ], 21 | install_requires=['parse', 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /tests/test_parsing.py: -------------------------------------------------------------------------------- 1 | import re 2 | from regex_decorator import Parser 3 | 4 | p = Parser() 5 | 6 | 7 | @p.listener('my name is (\w+)', re.IGNORECASE, parse_using='re') 8 | def name(matched_str, value): 9 | return value 10 | 11 | 12 | @p.listener('The answer is (\d+)', re.IGNORECASE, parse_using='re') 13 | def answer(matched_str, value): 14 | return value 15 | 16 | 17 | @p.listener('foo (\d)', re.IGNORECASE, parse_using='re') 18 | def foo(matched_str, value): 19 | return value 20 | 21 | 22 | @p.listener('foo (?P\w+) bar (?P\w+)', re.IGNORECASE, parse_using='re') 23 | def this_that(matched_str, bar, foo): 24 | """ 25 | Checks that even though the args are reversed, the data is collected correctly 26 | """ 27 | return foo + '-' + bar 28 | 29 | 30 | # 31 | # Test p.parse() 32 | # 33 | def test_parse_1(): 34 | """ 35 | Simple word parse 36 | """ 37 | assert p.parse('my name is Eddy') == 'Eddy' 38 | 39 | 40 | def test_parse_2(): 41 | """ 42 | Simple number parse 43 | """ 44 | assert p.parse('The answer is 42') == '42' 45 | 46 | 47 | def test_parse_3(): 48 | """ 49 | Only parse the first thing found 50 | """ 51 | assert p.parse('foo 1, foo 2, Foo 3') == '1' 52 | 53 | 54 | def test_parse_4(): 55 | """ 56 | Dont match anything 57 | """ 58 | assert p.parse('Dont match anything') is None 59 | 60 | 61 | def test_parse_5(): 62 | """ 63 | Dont match anything 64 | """ 65 | assert p.parse(['Dont match anything', 'stil, nothing']) is None 66 | 67 | 68 | def test_parse_6(): 69 | """ 70 | Match only one thing 71 | Since we passed in an array, the single match will be returned in an array 72 | """ 73 | assert p.parse(['Dont match anything', 'foo 3', 'jk, just match that last one :)']) == ['3'] 74 | 75 | 76 | def test_parse_7(): 77 | """ 78 | Parse the first thing found in each of the items in the array 79 | """ 80 | assert p.parse(['Foo 1', 'foo 2', 'Foo 3 foo 4']) == ['1', '2', '3'] 81 | 82 | 83 | def test_parse_8(): 84 | """ 85 | This works as it should, only getting the first result, but test_parse_9() broke it 86 | """ 87 | assert p.parse('The answer is 42 and foo 5') == '42' 88 | 89 | 90 | def test_parse_9(): 91 | """ 92 | Bug found, if a string contains something that will match two different regex's it will return both 93 | That should not happen 94 | Fixed in 0.1.1 95 | """ 96 | assert p.parse(['The answer is 42 and foo 5']) == ['42'] 97 | 98 | 99 | def test_parse_10(): 100 | """ 101 | When using named args, order should not matter 102 | """ 103 | assert p.parse("foo this bar that") == 'this-that' 104 | 105 | 106 | def test_parse_11(): 107 | """ 108 | When using named args, order should not matter 109 | """ 110 | assert p.parse(["foo this bar that", "foo one bar two"]) == ['this-that', 'one-two'] 111 | 112 | 113 | # 114 | # Test p.parse_all() 115 | # 116 | def test_parse_all_1(): 117 | """ 118 | return all instances found 119 | """ 120 | assert p.parse_all('foo 1 and foo 2 and another foo 3') == ['1', '2', '3'] 121 | 122 | 123 | def test_parse_all_2(): 124 | """ 125 | Parse all instances in each item in the list 126 | """ 127 | assert p.parse_all(['Foo 1', 'foo 2', 'Foo 3 foo 4']) == ['1', '2', '3', '4'] 128 | 129 | 130 | def test_parse_all_3(): 131 | """ 132 | return all instances found, from 2 different functions 133 | """ 134 | assert p.parse_all('The answer is 42 as well as foo 3') == ['42', '3'] 135 | 136 | 137 | def test_parse_all_4(): 138 | """ 139 | return all instances found, from 2 different functions 140 | """ 141 | assert p.parse_all(['The answer is 42 as well as foo 3']) == ['42', '3'] 142 | 143 | 144 | def test_parse_all_5(): 145 | """ 146 | When using named args, order should not matter 147 | """ 148 | assert p.parse_all("foo this bar that as well as foo one bar two") == ['this-that', 'one-two'] 149 | 150 | 151 | # 152 | # Test p.parse_file() 153 | # 154 | def test_parse_file_1(): 155 | """ 156 | return all instances found 157 | """ 158 | assert p.parse_file('tests/test_strings.txt') == ['1', '99', 'Eddy', '44'] 159 | 160 | 161 | def test_parse_file_2(): 162 | """ 163 | Test when a file does not exist 164 | """ 165 | assert p.parse_file('not_a_file.txt') is None 166 | 167 | 168 | # 169 | # Test p.parse_file_all() 170 | # 171 | def test_parse_file_all_1(): 172 | """ 173 | return all instances found 174 | """ 175 | assert p.parse_file_all('tests/test_strings.txt') == ['1', '99', 'Eddy', '44', '4'] 176 | -------------------------------------------------------------------------------- /tests/test_strings.txt: -------------------------------------------------------------------------------- 1 | Foo 1 2 | Don't match this line 3 | maybe this line because the answer is 99 4 | Hi, my name is Eddy! 5 | the answer is 44, foo 4 6 | --------------------------------------------------------------------------------