def ensure_no_documentation_if_intentionally_undocumented( self, url_pattern: str, method: str) -> None: try: get_openapi_parameters(url_pattern, method) raise AssertionError("We found some OpenAPI \ documentation for %s %s, so maybe we shouldn't mark it as intentionally \ undocumented in the urls." % (method, url_pattern)) # nocoverage except KeyError: return
def ensure_no_documentation_if_intentionally_undocumented(self, url_pattern: str, method: str, msg: Optional[str]=None) -> None: try: get_openapi_parameters(url_pattern, method) if not msg: # nocoverage msg = """ We found some OpenAPI documentation for {method} {url_pattern}, so maybe we shouldn't mark it as intentionally undocumented in the urls. """.format(method=method, url_pattern=url_pattern) raise AssertionError(msg) # nocoverage except KeyError: return
def run(self, lines: List[str]) -> List[str]: done = False while not done: for line in lines: loc = lines.index(line) match = REGEXP.search(line) if not match: continue filename = match.group(1) doc_name = match.group(2) filename = os.path.expanduser(filename) is_openapi_format = filename.endswith('.yaml') if not os.path.isabs(filename): parent_dir = self.base_path filename = os.path.normpath( os.path.join(parent_dir, filename)) if is_openapi_format: endpoint, method = doc_name.rsplit(':', 1) arguments = [] # type: List[Dict[str, Any]] try: arguments = get_openapi_parameters(endpoint, method) except KeyError as e: # Don't raise an exception if the "parameters" # field is missing; we assume that's because the # endpoint doesn't accept any parameters if e.args != ('parameters', ): raise e else: with open(filename, 'r') as fp: json_obj = ujson.load(fp) arguments = json_obj[doc_name] if arguments: text = self.render_table(arguments) else: text = ['This endpoint does not consume any arguments.'] # The line that contains the directive to include the macro # may be preceded or followed by text or tags, in that case # we need to make sure that any preceding or following text # stays the same. line_split = REGEXP.split(line, maxsplit=0) preceding = line_split[0] following = line_split[-1] text = [preceding] + text + [following] lines = lines[:loc] + text + lines[loc + 1:] break else: done = True return lines
def run(self, lines: List[str]) -> List[str]: done = False while not done: for line in lines: loc = lines.index(line) match = REGEXP.search(line) if not match: continue filename = match.group(1) doc_name = match.group(2) filename = os.path.expanduser(filename) is_openapi_format = filename.endswith('.yaml') if not os.path.isabs(filename): parent_dir = self.base_path filename = os.path.normpath(os.path.join(parent_dir, filename)) if is_openapi_format: endpoint, method = doc_name.rsplit(':', 1) arguments = [] # type: List[Dict[str, Any]] try: arguments = get_openapi_parameters(endpoint, method) except KeyError as e: # Don't raise an exception if the "parameters" # field is missing; we assume that's because the # endpoint doesn't accept any parameters if e.args != ('parameters',): raise e else: with open(filename, 'r') as fp: json_obj = ujson.load(fp) arguments = json_obj[doc_name] if arguments: text = self.render_table(arguments) else: text = ['This endpoint does not consume any arguments.'] # The line that contains the directive to include the macro # may be preceded or followed by text or tags, in that case # we need to make sure that any preceding or following text # stays the same. line_split = REGEXP.split(line, maxsplit=0) preceding = line_split[0] following = line_split[-1] text = [preceding] + text + [following] lines = lines[:loc] + text + lines[loc+1:] break else: done = True return lines
def test_get_openapi_parameters(self) -> None: actual = get_openapi_parameters(TEST_ENDPOINT, TEST_METHOD) expected_item = { 'name': 'message_id', 'in': 'path', 'description': 'The ID of the message that you wish to edit/update.', 'example': 42, 'required': True, 'schema': {'type': 'integer'} } assert(expected_item in actual)
def run(self, lines: List[str]) -> List[str]: done = False while not done: for line in lines: loc = lines.index(line) match = REGEXP.search(line) if match: filename = match.group(1) doc_name = match.group(2) filename = os.path.expanduser(filename) is_openapi_format = filename.endswith('.yaml') if not os.path.isabs(filename): parent_dir = self.base_path filename = os.path.normpath( os.path.join(parent_dir, filename)) try: if is_openapi_format: endpoint, method = doc_name.rsplit(':', 1) arguments = get_openapi_parameters( endpoint, method) else: with open(filename, 'r') as fp: json_obj = ujson.load(fp) arguments = json_obj[doc_name] text = self.render_table(arguments) except Exception as e: print('Warning: could not find file {}. Ignoring ' 'statement. Error: {}'.format(filename, e)) # If the file cannot be opened, just substitute an empty line # in place of the macro include line lines[loc] = REGEXP.sub('', line) break # The line that contains the directive to include the macro # may be preceded or followed by text or tags, in that case # we need to make sure that any preceding or following text # stays the same. line_split = REGEXP.split(line, maxsplit=0) preceding = line_split[0] following = line_split[-1] text = [preceding] + text + [following] lines = lines[:loc] + text + lines[loc + 1:] break else: done = True return lines
def run(self, lines: List[str]) -> List[str]: done = False while not done: for line in lines: loc = lines.index(line) match = REGEXP.search(line) if match: filename = match.group(1) doc_name = match.group(2) filename = os.path.expanduser(filename) is_openapi_format = filename.endswith('.yaml') if not os.path.isabs(filename): parent_dir = self.base_path filename = os.path.normpath(os.path.join(parent_dir, filename)) try: if is_openapi_format: endpoint, method = doc_name.rsplit(':', 1) arguments = get_openapi_parameters(endpoint, method) else: with open(filename, 'r') as fp: json_obj = ujson.load(fp) arguments = json_obj[doc_name] text = self.render_table(arguments) except Exception as e: print('Warning: could not find file {}. Ignoring ' 'statement. Error: {}'.format(filename, e)) # If the file cannot be opened, just substitute an empty line # in place of the macro include line lines[loc] = REGEXP.sub('', line) break # The line that contains the directive to include the macro # may be preceded or followed by text or tags, in that case # we need to make sure that any preceding or following text # stays the same. line_split = REGEXP.split(line, maxsplit=0) preceding = line_split[0] following = line_split[-1] text = [preceding] + text + [following] lines = lines[:loc] + text + lines[loc+1:] break else: done = True return lines
def generate_curl_example(endpoint: str, method: str, auth_email: str = DEFAULT_AUTH_EMAIL, auth_api_key: str = DEFAULT_AUTH_API_KEY, api_url: str = DEFAULT_API_URL, exclude: List[str] = []) -> List[str]: lines = ["```curl"] openapi_entry = openapi_spec.spec()['paths'][endpoint][method.lower()] curl_first_line_parts = ["curl"] + curl_method_arguments( endpoint, method, api_url) lines.append(" ".join(curl_first_line_parts)) authentication_required = openapi_entry.get("security", False) if authentication_required: lines.append(" -u %s:%s" % (auth_email, auth_api_key)) openapi_example_params = get_openapi_parameters(endpoint, method) for packet in openapi_example_params: param_name = packet["name"] if param_name in exclude: continue param_type = packet["schema"]["type"] if param_type in ["object", "array"]: example_value = packet.get("example", None) if not example_value: msg = """All array and object type request parameters must have concrete examples. The openAPI documentation for {}/{} is missing an example value for the {} parameter. Without this we cannot automatically generate a cURL example.""".format(endpoint, method, param_name) raise ValueError(msg) ordered_ex_val_str = json.dumps(example_value, sort_keys=True) line = " --data-urlencode {}='{}'".format( param_name, ordered_ex_val_str) else: example_value = packet.get("example", DEFAULT_EXAMPLE[param_type]) if type(example_value) == bool: example_value = str(example_value).lower() line = " -d '{}={}'".format(param_name, example_value) lines.append(line) for i in range(1, len(lines) - 1): lines[i] = lines[i] + " \\" lines.append("```") return lines
def test_openapi_arguments(self) -> None: """This end-to-end API documentation test compares the arguments defined in the actual code using @has_request_variables and REQ(), with the arguments declared in our API documentation for every API endpoint in Zulip. First, we import the fancy-Django version of zproject/urls.py by doing this, each has_request_variables wrapper around each imported view function gets called to generate the wrapped view function and thus filling the global arguments_map variable. Basically, we're exploiting code execution during import. Then we need to import some view modules not already imported in urls.py. We use this different syntax because of the linters complaining of an unused import (which is correct, but we do this for triggering the has_request_variables decorator). At the end, we perform a reverse mapping test that verifies that every url pattern defined in the openapi documentation actually exists in code. """ import zproject.urls as urlconf # We loop through all the API patterns, looking in particular # for those using the rest_dispatch decorator; we then parse # its mapping of (HTTP_METHOD -> FUNCTION). for p in urlconf.v1_api_and_json_patterns + urlconf.v1_api_mobile_patterns: if p.lookup_str != 'zerver.lib.rest.rest_dispatch': # Endpoints not using rest_dispatch don't have extra data. methods_endpoints = dict( GET=p.lookup_str, ) else: methods_endpoints = p.default_args # since the module was already imported and is now residing in # memory, we won't actually face any performance penalties here. for method, value in methods_endpoints.items(): if isinstance(value, str): function_name = value tags = set() # type: Set[str] else: function_name, tags = value if function_name == 'zerver.tornado.views.get_events': # Work around the fact that the registered # get_events view function isn't where we do # @has_request_variables. # # TODO: Make this configurable via an optional argument # to has_request_variables, e.g. # @has_request_variables(view_func_name="zerver.tornado.views.get_events") function_name = 'zerver.tornado.views.get_events_backend' lookup_parts = function_name.split('.') module = __import__('.'.join(lookup_parts[:-1]), {}, {}, ['']) function = getattr(module, lookup_parts[-1]) # Our accounting logic in the `has_request_variables()` # code means we have the list of all arguments # accepted by every view function in arguments_map. accepted_arguments = set(arguments_map[function_name]) regex_pattern = p.regex.pattern url_pattern = self.convert_regex_to_url_pattern(regex_pattern) if "intentionally_undocumented" in tags: self.ensure_no_documentation_if_intentionally_undocumented(url_pattern, method) continue if url_pattern in self.pending_endpoints: # HACK: After all pending_endpoints have been resolved, we should remove # this segment and the "msg" part of the `ensure_no_...` method. msg = """ We found some OpenAPI documentation for {method} {url_pattern}, so maybe we shouldn't include it in pending_endpoints. """.format(method=method, url_pattern=url_pattern) self.ensure_no_documentation_if_intentionally_undocumented(url_pattern, method, msg) continue try: # Don't include OpenAPI parameters that live in # the path; these are not extracted by REQ. openapi_parameters = get_openapi_parameters(url_pattern, method, include_url_parameters=False) except Exception: # nocoverage raise AssertionError("Could not find OpenAPI docs for %s %s" % (method, url_pattern)) # We now have everything we need to understand the # function as defined in our urls.py: # # * method is the HTTP method, e.g. GET, POST, or PATCH # # * p.regex.pattern is the URL pattern; might require # some processing to match with OpenAPI rules # # * accepted_arguments is the full set of arguments # this method accepts (from the REQ declarations in # code). # # * The documented parameters for the endpoint as recorded in our # OpenAPI data in zerver/openapi/zulip.yaml. # # We now compare these to confirm that the documented # argument list matches what actually appears in the # codebase. openapi_parameter_names = set( [parameter['name'] for parameter in openapi_parameters] ) if len(accepted_arguments - openapi_parameter_names) > 0: # nocoverage print("Undocumented parameters for", url_pattern, method, function_name) print(" +", openapi_parameter_names) print(" -", accepted_arguments) assert(url_pattern in self.buggy_documentation_endpoints) elif len(openapi_parameter_names - accepted_arguments) > 0: # nocoverage print("Documented invalid parameters for", url_pattern, method, function_name) print(" -", openapi_parameter_names) print(" +", accepted_arguments) assert(url_pattern in self.buggy_documentation_endpoints) else: self.assertEqual(openapi_parameter_names, accepted_arguments) self.check_argument_types(function, openapi_parameters) self.checked_endpoints.add(url_pattern) self.check_for_non_existant_openapi_endpoints()
def test_openapi_arguments(self) -> None: # Verifies that every REQ-defined argument appears in our API # documentation for the target endpoint where possible. # These should have docs added PENDING_ENDPOINTS = set([ '/users/me/avatar', '/user_uploads', '/settings/display', '/settings/notifications', '/users/me/profile_data', '/user_groups', '/user_groups/create', '/users/me/pointer', '/users/me/presence', '/users/me', '/bot_storage', '/users/me/api_key/regenerate', '/default_streams', '/default_stream_groups/create', '/users/me/alert_words', '/users/me/status', '/users/me/subscriptions', '/messages/matches_narrow', '/settings', '/submessage', '/attachments', '/calls/create', '/export/realm', '/mark_all_as_read', '/zcommand', '/realm', '/realm/deactivate', '/realm/domains', '/realm/emoji', '/realm/filters', '/realm/icon', '/realm/logo', '/realm/presence', '/realm/profile_fields', '/queue_id', '/invites', '/invites/multiuse', '/bots', # Mobile-app only endpoints '/users/me/android_gcm_reg_id', '/users/me/apns_device_token', ]) # These endpoints have a mismatch between the documentation # and the actual API. There are situations where we may want # to have undocumented parameters for e.g. backwards # compatibility, which could be the situation for some of # these, in which case we may want a more clever exclude # system. This list can serve as a TODO list for such an # investigation. BUGGY_DOCUMENTATION_ENDPOINTS = set([ '/events', '/register', '/messages', '/typing', '/users/me/subscriptions/muted_topics', ]) # First, we import the fancy-Django version of zproject/urls.py urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, ['']) # We loop through all the API patterns, looking in particular # those using the rest_dispatch decorator; we then parse its # mapping of (HTTP_METHOD -> FUNCTION). for p in urlconf.v1_api_and_json_patterns: if p.lookup_str != 'zerver.lib.rest.rest_dispatch': continue for method, value in p.default_args.items(): if isinstance(value, str): function = value tags = set() # type: Set[str] else: function, tags = value # Our accounting logic in the `has_request_variables()` # code means we have the list of all arguments # accepted by every view function in arguments_map. # # TODO: Probably with a bit more work, we could get # the types, too; `check_int` -> `int`, etc., and # verify those too! accepted_arguments = set(arguments_map[function]) # TODO: The purpose of this block is to match our URL # pattern regular expressions to the corresponding # configuration in OpenAPI. The means matching # # /messages/{message_id} <-> r'^messages/(?P<message_id>[0-9]+)$' # /events <-> r'^events$' # # The below is a giant hack that only handles the simple case. regex_pattern = p.regex.pattern self.assertTrue(regex_pattern.startswith("^")) self.assertTrue(regex_pattern.endswith("$")) # Obviously this is a huge hack and won't work for # some URLs. url_pattern = '/' + regex_pattern[1:][:-1] if url_pattern in PENDING_ENDPOINTS: # TODO: Once these endpoints have been aptly documented, # we should remove this block and the associated List. continue if "intentionally_undocumented" in tags: # Don't do any validation on endpoints with no API # documentation by design. try: get_openapi_parameters(url_pattern, method) raise AssertionError("We found some OpenAPI \ documentation for %s %s, so maybe we shouldn't mark it as intentionally \ undocumented in the urls." % (method, url_pattern)) except KeyError: continue if "(?P<" in url_pattern: # See above TODO about our matching algorithm not # handling captures in the regular expressions. continue try: openapi_parameters = get_openapi_parameters( url_pattern, method) # TODO: Record which entries in the OpenAPI file # found a match, and throw an error for any that # unexpectedly didn't. except Exception: # nocoverage raise AssertionError( "Could not find OpenAPI docs for %s %s" % (method, url_pattern)) # We now have everything we need to understand the # function as defined in our urls.py # # * method is the HTTP method, e.g. GET, POST, or PATCH # # * p.regex.pattern is the URL pattern; might require # some processing to match with OpenAPI rules # # * accepted_arguments_list is the full set of arguments # this method accepts. # # * The documented parameters for the endpoint as recorded in our # OpenAPI data in zerver/openapi/zulip.yaml. # # We now compare these to confirm that the documented # argument list matches what actually appears in the # codebase. openapi_parameter_names = set( [parameter['name'] for parameter in openapi_parameters]) if len(openapi_parameter_names - accepted_arguments) > 0: print("Undocumented parameters for", url_pattern, method, function) print(" +", openapi_parameter_names) print(" -", accepted_arguments) assert (url_pattern in BUGGY_DOCUMENTATION_ENDPOINTS) elif len(accepted_arguments - openapi_parameter_names) > 0: print("Documented invalid parameters for", url_pattern, method, function) print(" -", openapi_parameter_names) print(" +", accepted_arguments) assert (url_pattern in BUGGY_DOCUMENTATION_ENDPOINTS) else: self.assertEqual(openapi_parameter_names, accepted_arguments)
def test_openapi_arguments(self) -> None: """This end-to-end API documentation test compares the arguments defined in the actual code using @has_request_variables and REQ(), with the arguments declared in our API documentation for every API endpoint in Zulip. First, we import the fancy-Django version of zproject/urls.py by doing this, each has_request_variables wrapper around each imported view function gets called to generate the wrapped view function and thus filling the global arguments_map variable. Basically, we're exploiting code execution during import. Then we need to import some view modules not already imported in urls.py. We use this different syntax because of the linters complaining of an unused import (which is correct, but we do this for triggering the has_request_variables decorator). At the end, we perform a reverse mapping test that verifies that every url pattern defined in the openapi documentation actually exists in code. """ urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, ['']) __import__('zerver.views.typing') __import__('zerver.views.events_register') __import__('zerver.views.realm_emoji') # We loop through all the API patterns, looking in particular # for those using the rest_dispatch decorator; we then parse # its mapping of (HTTP_METHOD -> FUNCTION). for p in urlconf.v1_api_and_json_patterns: if p.lookup_str != 'zerver.lib.rest.rest_dispatch': continue for method, value in p.default_args.items(): if isinstance(value, str): function_name = value tags = set() # type: Set[str] else: function_name, tags = value # Our accounting logic in the `has_request_variables()` # code means we have the list of all arguments # accepted by every view function in arguments_map. # # TODO: Probably with a bit more work, we could get # the types, too; `check_int` -> `int`, etc., and # verify those too! accepted_arguments = set(arguments_map[function_name]) regex_pattern = p.regex.pattern url_pattern = self.convert_regex_to_url_pattern(regex_pattern) if "intentionally_undocumented" in tags: self.ensure_no_documentation_if_intentionally_undocumented( url_pattern, method) continue if url_pattern in self.pending_endpoints: # HACK: After all pending_endpoints have been resolved, we should remove # this segment and the "msg" part of the `ensure_no_...` method. msg = """ We found some OpenAPI documentation for {method} {url_pattern}, so maybe we shouldn't include it in pending_endpoints. """.format(method=method, url_pattern=url_pattern) self.ensure_no_documentation_if_intentionally_undocumented( url_pattern, method, msg) continue try: openapi_parameters = get_openapi_parameters( url_pattern, method) except Exception: # nocoverage raise AssertionError( "Could not find OpenAPI docs for %s %s" % (method, url_pattern)) # We now have everything we need to understand the # function as defined in our urls.py: # # * method is the HTTP method, e.g. GET, POST, or PATCH # # * p.regex.pattern is the URL pattern; might require # some processing to match with OpenAPI rules # # * accepted_arguments is the full set of arguments # this method accepts (from the REQ declarations in # code). # # * The documented parameters for the endpoint as recorded in our # OpenAPI data in zerver/openapi/zulip.yaml. # # We now compare these to confirm that the documented # argument list matches what actually appears in the # codebase. openapi_parameter_names = set( [parameter['name'] for parameter in openapi_parameters]) if len(openapi_parameter_names - accepted_arguments) > 0: print("Undocumented parameters for", url_pattern, method, function_name) print(" +", openapi_parameter_names) print(" -", accepted_arguments) assert (url_pattern in self.buggy_documentation_endpoints) elif len(accepted_arguments - openapi_parameter_names) > 0: print("Documented invalid parameters for", url_pattern, method, function_name) print(" -", openapi_parameter_names) print(" +", accepted_arguments) assert (url_pattern in self.buggy_documentation_endpoints) else: self.assertEqual(openapi_parameter_names, accepted_arguments) self.checked_endpoints.add(url_pattern) self.check_for_non_existant_openapi_endpoints()
def test_openapi_arguments(self) -> None: """This end-to-end API documentation test compares the arguments defined in the actual code using @has_request_variables and REQ(), with the arguments declared in our API documentation for every API endpoint in Zulip. First, we import the fancy-Django version of zproject/urls.py by doing this, each has_request_variables wrapper around each imported view function gets called to generate the wrapped view function and thus filling the global arguments_map variable. Basically, we're exploiting code execution during import. Then we need to import some view modules not already imported in urls.py. We use this different syntax because of the linters complaining of an unused import (which is correct, but we do this for triggering the has_request_variables decorator). At the end, we perform a reverse mapping test that verifies that every url pattern defined in the openapi documentation actually exists in code. """ urlconf = __import__(getattr(settings, "ROOT_URLCONF"), {}, {}, ['']) __import__('zerver.views.typing') __import__('zerver.views.events_register') __import__('zerver.views.realm_emoji') # We loop through all the API patterns, looking in particular # for those using the rest_dispatch decorator; we then parse # its mapping of (HTTP_METHOD -> FUNCTION). for p in urlconf.v1_api_and_json_patterns: if p.lookup_str != 'zerver.lib.rest.rest_dispatch': continue for method, value in p.default_args.items(): if isinstance(value, str): function = value tags = set() # type: Set[str] else: function, tags = value # Our accounting logic in the `has_request_variables()` # code means we have the list of all arguments # accepted by every view function in arguments_map. # # TODO: Probably with a bit more work, we could get # the types, too; `check_int` -> `int`, etc., and # verify those too! accepted_arguments = set(arguments_map[function]) # Convert regular expressions style URL patterns to their # corresponding OpenAPI style formats. # E.G. # /messages/{message_id} <-> r'^messages/(?P<message_id>[0-9]+)$' # /events <-> r'^events$' regex_pattern = p.regex.pattern self.assertTrue(regex_pattern.startswith("^")) self.assertTrue(regex_pattern.endswith("$")) url_pattern = '/' + regex_pattern[1:][:-1] # Deal with the conversion of named capturing groups: # Two possible ways to denote variables in urls exist. # {var_name} and <var_name>. So we need to consider both. url_patterns = [ re.sub(r"\(\?P<(\w+)>[^/]+\)", r"<\1>", url_pattern), re.sub(r"\(\?P<(\w+)>[^/]+\)", r"{\1}", url_pattern) ] if any([ url_patterns[0] in self.pending_endpoints, url_patterns[1] in self.pending_endpoints ]): continue if "intentionally_undocumented" in tags: error = AssertionError( "We found some OpenAPI \ documentation for %s %s, so maybe we shouldn't mark it as intentionally \ undocumented in the urls." % (method, url_patterns[0] + " or " + url_patterns[1])) try: get_openapi_parameters(url_patterns[0], method) raise error # nocoverage except KeyError: pass try: get_openapi_parameters(url_patterns[1], method) raise error # nocoverage except KeyError: pass continue # nocoverage # although, this *is* covered. try: openapi_parameters = get_openapi_parameters( url_patterns[0], method) except Exception: # nocoverage try: openapi_parameters = get_openapi_parameters( url_patterns[1], method) except Exception: # nocoverage raise AssertionError( "Could not find OpenAPI docs for %s %s" % (method, url_patterns[0] + " or " + url_patterns[1])) # We now have everything we need to understand the # function as defined in our urls.py: # # * method is the HTTP method, e.g. GET, POST, or PATCH # # * p.regex.pattern is the URL pattern; might require # some processing to match with OpenAPI rules # # * accepted_arguments is the full set of arguments # this method accepts (from the REQ declarations in # code). # # * The documented parameters for the endpoint as recorded in our # OpenAPI data in zerver/openapi/zulip.yaml. # # We now compare these to confirm that the documented # argument list matches what actually appears in the # codebase. openapi_parameter_names = set( [parameter['name'] for parameter in openapi_parameters]) if len(openapi_parameter_names - accepted_arguments) > 0: print("Undocumented parameters for", url_patterns[0] + " or " + url_patterns[1], method, function) print(" +", openapi_parameter_names) print(" -", accepted_arguments) assert (any([ url_patterns[0] in self.buggy_documentation_endpoints, url_patterns[1] in self.buggy_documentation_endpoints ])) elif len(accepted_arguments - openapi_parameter_names) > 0: print("Documented invalid parameters for", url_patterns[0] + " or " + url_patterns[1], method, function) print(" -", openapi_parameter_names) print(" +", accepted_arguments) assert (any([ url_patterns[0] in self.buggy_documentation_endpoints, url_patterns[1] in self.buggy_documentation_endpoints ])) else: self.assertEqual(openapi_parameter_names, accepted_arguments) self.checked_endpoints.add(url_patterns[0]) self.checked_endpoints.add(url_patterns[1]) openapi_paths = set(get_openapi_paths()) undocumented_paths = openapi_paths - self.checked_endpoints undocumented_paths -= self.buggy_documentation_endpoints undocumented_paths -= self.pending_endpoints try: self.assertEqual(len(undocumented_paths), 0) except AssertionError: # nocoverage msg = "The following endpoints have been documented but can't be found in urls.py:" for undocumented_path in undocumented_paths: msg += "\n + {}".format(undocumented_path) raise AssertionError(msg)