Passed
Push — develop ( 2ca50f...3fcc25 )
by Plexxi
07:00 queued 03:34
created

ActionAliasFormatParser.get_extracted_param_value()   F

Complexity

Conditions 12

Size

Total Lines 88

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
dl 0
loc 88
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ActionAliasFormatParser.get_extracted_param_value() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  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
16
import re
17
18
from st2common.exceptions.content import ParseException
19
20
__all__ = [
21
    'ActionAliasFormatParser',
22
23
    'extract_parameters_for_action_alias_db',
24
    'extract_parameters',
25
]
26
27
28
class ActionAliasFormatParser(object):
29
30
    def __init__(self, alias_format=None, param_stream=None):
31
        self._format = alias_format or ''
32
        self._param_stream = param_stream or ''
33
34
    def get_extracted_param_value(self):
35
        """
36
        Match command against the format string and extract paramters from the command string.
37
38
        :rtype: ``dict``
39
        """
40
        result = {}
41
42
        param_stream = self._param_stream
43
44
        # As there's a lot of questions about using regular expressions,
45
        # I'll try to be thorough when documenting this code.
46
47
        # I'll split the whole convoluted regex into snippets to make it
48
        # a bit more readable (hopefully).
49
        snippets = dict()
50
51
        # Formats for keys and values: key is a non-spaced string,
52
        # value is anything in quotes or curly braces, or a single word.
53
        snippets['key'] = r'\s*(\S+?)\s*'
54
        snippets['value'] = r'""|\'\'|"(.+?)"|\'(.+?)\'|({.+?})|(\S+)'
55
56
        # Extended value: also matches unquoted text (caution).
57
        snippets['ext_value'] = r'""|\'\'|"(.+?)"|\'(.+?)\'|({.+?})|(.+?)'
58
59
        # Key-value pair:
60
        snippets['pairs'] = r'(?:^|\s+){key}=({value})'.format(**snippets)
61
62
        # End of string: multiple space-separated key-value pairs:
63
        snippets['ending'] = r'.*?(({pairs}\s*)*)$'.format(**snippets)
64
65
        # Default value in optional parameters:
66
        snippets['default'] = r'\s*=\s*(?:{ext_value})\s*'.format(**snippets)
67
68
        # Optional parameter (has a default value):
69
        snippets['optional'] = '{{' + snippets['key'] + snippets['default'] + '}}'
70
71
        # Required parameter (no default value):
72
        snippets['required'] = '{{' + snippets['key'] + '}}'
73
74
        # 1. Matching the arbitrary key-value pairs at the end of the command
75
        # to support extra parameters (not specified in the format string),
76
        # and cutting them from the command string afterwards.
77
        ending_pairs = re.match(snippets['ending'], param_stream, re.DOTALL)
78
        has_ending_pairs = ending_pairs and ending_pairs.group(1)
79
        if has_ending_pairs:
80
            kv_pairs = re.findall(snippets['pairs'], ending_pairs.group(1), re.DOTALL)
81
            param_stream = param_stream.replace(ending_pairs.group(1), '')
82
        param_stream = " %s " % (param_stream)
83
84
        # 2. Matching optional parameters (with default values).
85
        optional = re.findall(snippets['optional'], self._format, re.DOTALL)
86
87
        # Transforming our format string into a regular expression,
88
        # substituting {{ ... }} with regex named groups, so that param_stream
89
        # matched against this expression yields a dict of params with values.
90
        param_match = r'\1["\']?(?P<\2>(?:(?<=\').+?(?=\')|(?<=").+?(?=")|{.+?}|.+?))["\']?'
91
        reg = re.sub(r'(\s*)' + snippets['optional'], r'(?:' + param_match + r')?', self._format)
92
        reg = re.sub(r'(\s*)' + snippets['required'], param_match, reg)
93
        reg = '^\s*' + reg + r'\s*$'
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
94
95
        # 3. Matching the command against our regex to get the param values
96
        matched_stream = re.match(reg, param_stream, re.DOTALL)
97
98
        if not matched_stream:
99
            # If no match is found we throw since this indicates provided user string (command)
100
            # didn't match the provided format string
101
            raise ParseException('Command "%s" doesn\'t match format string "%s"' %
102
                                 (self._param_stream, self._format))
103
104
        # Compiling results from the steps 1-3.
105
        if matched_stream:
106
            result = matched_stream.groupdict()
107
108
        for param in optional:
109
            matched_value = result[param[0]] if matched_stream else None
110
            matched_result = matched_value or ''.join(param[1:])
111
            if matched_result is not None:
112
                result[param[0]] = matched_result
113
114
        if has_ending_pairs:
115
            for pair in kv_pairs:
116
                result[pair[0]] = ''.join(pair[2:])
117
118
        if self._format and not (self._param_stream.strip() or any(result.values())):
119
            raise ParseException('No value supplied and no default value found.')
120
121
        return result
122
123
124
def extract_parameters_for_action_alias_db(action_alias_db, format_str, param_stream):
125
    """
126
    Extract parameters from the user input based on the provided format string.
127
128
    Note: This function makes sure that the provided format string is indeed available in the
129
    action_alias_db.formats.
130
    """
131
    formats = []
132
    formats = action_alias_db.get_format_strings()
133
134
    if format_str not in formats:
135
        raise ValueError('Format string "%s" is not available on the alias "%s"' %
136
                         (format_str, action_alias_db.name))
137
138
    result = extract_parameters(format_str=format_str, param_stream=param_stream)
139
    return result
140
141
142
def extract_parameters(format_str, param_stream):
143
    parser = ActionAliasFormatParser(alias_format=format_str, param_stream=param_stream)
144
    return parser.get_extracted_param_value()
145