• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Helper functions.
15
16Facilitates the implementation of a new profile proxy or a PTS MMI.
17"""
18
19import functools
20import textwrap
21import unittest
22import re
23
24DOCSTRING_WIDTH = 80 - 8  # 80 cols - 8 indentation spaces
25
26
27def assert_description(f):
28    """Decorator which verifies the description of a PTS MMI implementation.
29
30    Asserts that the docstring of a function implementing a PTS MMI is the same
31    as the corresponding official MMI description.
32
33    Args:
34        f: function implementing a PTS MMI.
35
36    Raises:
37        AssertionError: the docstring of the function does not match the MMI
38            description.
39    """
40
41    @functools.wraps(f)
42    def wrapper(*args, **kwargs):
43        description = textwrap.fill(kwargs['description'], DOCSTRING_WIDTH, replace_whitespace=False)
44        docstring = textwrap.dedent(f.__doc__ or '')
45
46        if docstring.strip() != description.strip():
47            print(f'Expected description of {f.__name__}:')
48            print(description)
49
50            # Generate AssertionError.
51            test = unittest.TestCase()
52            test.maxDiff = None
53            test.assertMultiLineEqual(docstring.strip(), description.strip(),
54                                      f'description does not match with function docstring of'
55                                      f'{f.__name__}')
56
57        return f(*args, **kwargs)
58
59    return wrapper
60
61
62def match_description(f):
63    """Extracts parameters from PTS MMI descriptions.
64
65    Similar to assert_description, but treats the description as an (indented)
66    regex that can be used to extract named capture groups from the PTS command.
67
68    Args:
69        f: function implementing a PTS MMI.
70
71    Raises:
72        AssertionError: the docstring of the function does not match the MMI
73            description.
74    """
75
76    def normalize(desc):
77        return desc.replace("\n", " ").replace("\t", "    ").strip()
78
79    docstring = normalize(textwrap.dedent(f.__doc__))
80    regex = re.compile(docstring)
81
82    @functools.wraps(f)
83    def wrapper(*args, **kwargs):
84        description = normalize(kwargs['description'])
85        match = regex.fullmatch(description)
86
87        assert match is not None, f'description does not match with function docstring of {f.__name__}:\n{repr(description)}\n!=\n{repr(docstring)}'
88
89        return f(*args, **kwargs, **match.groupdict())
90
91    return wrapper
92
93
94def format_function(mmi_name, mmi_description):
95    """Returns the base format of a function implementing a PTS MMI."""
96    wrapped_description = textwrap.fill(mmi_description, DOCSTRING_WIDTH, replace_whitespace=False)
97    return (f'@assert_description\n'
98            f'def {mmi_name}(self, **kwargs):\n'
99            f'    """\n'
100            f'{textwrap.indent(wrapped_description, "    ")}\n'
101            f'    """\n'
102            f'\n'
103            f'    return "OK"\n')
104
105
106def format_proxy(profile, mmi_name, mmi_description):
107    """Returns the base format of a profile proxy including a given MMI."""
108    wrapped_function = textwrap.indent(format_function(mmi_name, mmi_description), '    ')
109    return (f'from mmi2grpc._helpers import assert_description\n'
110            f'from mmi2grpc._proxy import ProfileProxy\n'
111            f'\n'
112            f'from pandora_experimental.{profile.lower()}_grpc import {profile}\n'
113            f'\n'
114            f'\n'
115            f'class {profile}Proxy(ProfileProxy):\n'
116            f'\n'
117            f'    def __init__(self, channel):\n'
118            f'        super().__init__(channel)\n'
119            f'        self.{profile.lower()} = {profile}(channel)\n'
120            f'\n'
121            f'{wrapped_function}')
122