1# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13import logging
14import os
15import re
16
17from botocore.compat import OrderedDict
18from botocore.docs.bcdoc.docstringparser import DocStringParser
19from botocore.docs.bcdoc.style import ReSTStyle
20
21DEFAULT_AWS_DOCS_LINK = 'https://docs.aws.amazon.com/index.html'
22DOCUMENTATION_LINK_REGEX = re.compile(
23 r'`AWS API Documentation '
24 r'<https://docs.aws.amazon.com/goto/WebAPI/[a-z0-9-.]*/[a-zA-Z]*>`_'
25)
26LARGE_SECTION_MESSAGE = """
27
28 **{}**
29 ::
30
31 # This section is too large to render.
32 # Please see the AWS API Documentation linked below.
33
34 {}
35 """
36LOG = logging.getLogger('bcdocs')
37SECTION_LINE_LIMIT_CONFIG = {
38 'response-example': {'name': 'Response Syntax', 'line_limit': 1500},
39 'description': {'name': 'Response Structure', 'line_limit': 5000},
40 'request-example': {'name': 'Request Syntax', 'line_limit': 1500},
41 'request-params': {'name': 'Parameters', 'line_limit': 5000},
42}
43SECTION_METHOD_PATH_DEPTH = {
44 'client-api': 4,
45 'paginator-api': 3,
46 'waiter-api': 3,
47}
48
49
50class ReSTDocument:
51 def __init__(self, target='man'):
52 self.style = ReSTStyle(self)
53 self.target = target
54 self.parser = DocStringParser(self)
55 self.keep_data = True
56 self.do_translation = False
57 self.translation_map = {}
58 self.hrefs = {}
59 self._writes = []
60 self._last_doc_string = None
61
62 def _write(self, s):
63 if self.keep_data and s is not None:
64 self._writes.append(s)
65
66 def write(self, content):
67 """
68 Write content into the document.
69 """
70 self._write(content)
71
72 def writeln(self, content):
73 """
74 Write content on a newline.
75 """
76 self._write(f'{self.style.spaces()}{content}\n')
77
78 def peek_write(self):
79 """
80 Returns the last content written to the document without
81 removing it from the stack.
82 """
83 return self._writes[-1]
84
85 def pop_write(self):
86 """
87 Removes and returns the last content written to the stack.
88 """
89 return self._writes.pop() if len(self._writes) > 0 else None
90
91 def push_write(self, s):
92 """
93 Places new content on the stack.
94 """
95 self._writes.append(s)
96
97 def getvalue(self):
98 """
99 Returns the current content of the document as a string.
100 """
101 if self.hrefs:
102 self.style.new_paragraph()
103 for refname, link in self.hrefs.items():
104 self.style.link_target_definition(refname, link)
105 return ''.join(self._writes).encode('utf-8')
106
107 def translate_words(self, words):
108 return [self.translation_map.get(w, w) for w in words]
109
110 def handle_data(self, data):
111 if data and self.keep_data:
112 self._write(data)
113
114 def include_doc_string(self, doc_string):
115 if doc_string:
116 try:
117 start = len(self._writes)
118 self.parser.feed(doc_string)
119 self.parser.close()
120 end = len(self._writes)
121 self._last_doc_string = (start, end)
122 except Exception:
123 LOG.debug('Error parsing doc string', exc_info=True)
124 LOG.debug(doc_string)
125
126 def remove_last_doc_string(self):
127 # Removes all writes inserted by last doc string
128 if self._last_doc_string is not None:
129 start, end = self._last_doc_string
130 del self._writes[start:end]
131
132
133class DocumentStructure(ReSTDocument):
134 def __init__(self, name, section_names=None, target='man', context=None):
135 """Provides a Hierarichial structure to a ReSTDocument
136
137 You can write to it similiar to as you can to a ReSTDocument but
138 has an innate structure for more orginaztion and abstraction.
139
140 :param name: The name of the document
141 :param section_names: A list of sections to be included
142 in the document.
143 :param target: The target documentation of the Document structure
144 :param context: A dictionary of data to store with the strucuture. These
145 are only stored per section not the entire structure.
146 """
147 super().__init__(target=target)
148 self._name = name
149 self._structure = OrderedDict()
150 self._path = [self._name]
151 self._context = {}
152 if context is not None:
153 self._context = context
154 if section_names is not None:
155 self._generate_structure(section_names)
156
157 @property
158 def name(self):
159 """The name of the document structure"""
160 return self._name
161
162 @property
163 def path(self):
164 """
165 A list of where to find a particular document structure in the
166 overlying document structure.
167 """
168 return self._path
169
170 @path.setter
171 def path(self, value):
172 self._path = value
173
174 @property
175 def available_sections(self):
176 return list(self._structure)
177
178 @property
179 def context(self):
180 return self._context
181
182 def _generate_structure(self, section_names):
183 for section_name in section_names:
184 self.add_new_section(section_name)
185
186 def add_new_section(self, name, context=None):
187 """Adds a new section to the current document structure
188
189 This document structure will be considered a section to the
190 current document structure but will in itself be an entirely
191 new document structure that can be written to and have sections
192 as well
193
194 :param name: The name of the section.
195 :param context: A dictionary of data to store with the strucuture. These
196 are only stored per section not the entire structure.
197 :rtype: DocumentStructure
198 :returns: A new document structure to add to but lives as a section
199 to the document structure it was instantiated from.
200 """
201 # Add a new section
202 section = self.__class__(
203 name=name, target=self.target, context=context
204 )
205 section.path = self.path + [name]
206 # Indent the section apporpriately as well
207 section.style.indentation = self.style.indentation
208 section.translation_map = self.translation_map
209 section.hrefs = self.hrefs
210 self._structure[name] = section
211 return section
212
213 def get_section(self, name):
214 """Retrieve a section"""
215 return self._structure[name]
216
217 def has_section(self, name):
218 return name in self._structure
219
220 def delete_section(self, name):
221 """Delete a section"""
222 del self._structure[name]
223
224 def flush_structure(self, docs_link=None):
225 """Flushes a doc structure to a ReSTructed string
226
227 The document is flushed out in a DFS style where sections and their
228 subsections' values are added to the string as they are visited.
229 """
230 # We are at the root flush the links at the beginning of the
231 # document
232 path_length = len(self.path)
233 if path_length == 1:
234 if self.hrefs:
235 self.style.new_paragraph()
236 for refname, link in self.hrefs.items():
237 self.style.link_target_definition(refname, link)
238 # Clear docs_link at the correct depth to prevent passing a non-related link.
239 elif path_length == SECTION_METHOD_PATH_DEPTH.get(self.path[1]):
240 docs_link = None
241 value = self.getvalue()
242 for name, section in self._structure.items():
243 # Checks is the AWS API Documentation link has been generated.
244 # If it has been generated, it gets passed as a the doc_link parameter.
245 match = DOCUMENTATION_LINK_REGEX.search(value.decode())
246 docs_link = (
247 f'{match.group(0)}\n\n'.encode() if match else docs_link
248 )
249 value += section.flush_structure(docs_link)
250
251 # Replace response/request sections if the line number exceeds our limit.
252 # The section is replaced with a message linking to AWS API Documentation.
253 line_count = len(value.splitlines())
254 section_config = SECTION_LINE_LIMIT_CONFIG.get(self.name)
255 aws_docs_link = (
256 docs_link.decode()
257 if docs_link is not None
258 else DEFAULT_AWS_DOCS_LINK
259 )
260 if section_config and line_count > section_config['line_limit']:
261 value = LARGE_SECTION_MESSAGE.format(
262 section_config['name'], aws_docs_link
263 ).encode()
264 return value
265
266 def getvalue(self):
267 return ''.join(self._writes).encode('utf-8')
268
269 def remove_all_sections(self):
270 self._structure = OrderedDict()
271
272 def clear_text(self):
273 self._writes = []
274
275 def add_title_section(self, title):
276 title_section = self.add_new_section('title')
277 title_section.style.h1(title)
278 return title_section
279
280 def write_to_file(self, full_path, file_name):
281 if not os.path.exists(full_path):
282 os.makedirs(full_path)
283 sub_resource_file_path = os.path.join(full_path, f'{file_name}.rst')
284 with open(sub_resource_file_path, 'wb') as f:
285 f.write(self.flush_structure())