stdlib: Small cleanup of docs generation library

Change-Id: I882eeff37262bea59f282b9dcc47d7038736104d
diff --git a/infra/perfetto.dev/src/gen_stdlib_docs_md.py b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
index 6fc5283..e1e60bd 100644
--- a/infra/perfetto.dev/src/gen_stdlib_docs_md.py
+++ b/infra/perfetto.dev/src/gen_stdlib_docs_md.py
@@ -22,216 +22,7 @@
 import json
 from typing import Any, List, Dict
 
-
-# Escapes special characters in a markdown table.
-def escape_in_table(desc: str):
-  return desc.replace('|', '\\|')
-
-
-# Responsible for module level markdown generation.
-class ModuleMd:
-
-  def __init__(self, module_name: str, module_files: List[Dict[str,
-                                                               Any]]) -> None:
-    self.module_name = module_name
-    self.files_md = sorted([
-        FileMd(module_name, file_dict) for file_dict in module_files
-    ], key=lambda x: x.import_key)
-    self.summary_objs = '\n'.join(
-        file.summary_objs for file in self.files_md if file.summary_objs)
-    self.summary_funs = '\n'.join(
-        file.summary_funs for file in self.files_md if file.summary_funs)
-    self.summary_view_funs = '\n'.join(file.summary_view_funs
-                                       for file in self.files_md
-                                       if file.summary_view_funs)
-    self.summary_macros = '\n'.join(
-        file.summary_macros for file in self.files_md if file.summary_macros)
-
-  def print_description(self):
-    if not self.files_md:
-      return ''
-
-    long_s = []
-    long_s.append(f'## Module: {self.module_name}')
-
-    if self.module_name == 'prelude':
-      # Prelude is a special module which is automatically imported and doesn't
-      # have any include keys.
-      objs = '\n'.join(obj for file in self.files_md for obj in file.objs)
-      if objs:
-        long_s.append('#### Views/Tables')
-        long_s.append(objs)
-      funs = '\n'.join(fun for file in self.files_md for fun in file.funs)
-      if funs:
-        long_s.append('#### Functions')
-        long_s.append(funs)
-      table_funs = '\n'.join(
-          view_fun for file in self.files_md for view_fun in file.view_funs)
-      if table_funs:
-        long_s.append('#### Table Functions')
-        long_s.append(table_funs)
-      macros = '\n'.join(
-          macro for file in self.files_md for macro in file.macros)
-      if macros:
-        long_s.append('#### Macros')
-        long_s.append(macros)
-      return '\n'.join(long_s)
-
-    for file in self.files_md:
-      if not any((file.objs, file.funs, file.view_funs, file.macros)):
-        continue
-
-      long_s.append(f'### {file.import_key}')
-      if file.objs:
-        long_s.append('#### Views/Tables')
-        long_s.append('\n'.join(file.objs))
-      if file.funs:
-        long_s.append('#### Functions')
-        long_s.append('\n'.join(file.funs))
-      if file.view_funs:
-        long_s.append('#### Table Functions')
-        long_s.append('\n'.join(file.view_funs))
-      if file.macros:
-        long_s.append('#### Macros')
-        long_s.append('\n'.join(file.macros))
-
-    return '\n'.join(long_s)
-
-
-# Responsible for file level markdown generation.
-class FileMd:
-
-  def __init__(self, module_name, file_dict):
-    self.import_key = file_dict['import_key']
-    import_key_name = self.import_key if module_name != 'prelude' else 'N/A'
-    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
-    summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], []
-
-    # Add imports if in file.
-    for data in file_dict['imports']:
-      # Anchor
-      anchor = f'''obj/{module_name}/{data['name']}'''
-
-      # Add summary of imported view/table
-      summary_objs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.objs.append(f'''\n\n<a name="{anchor}"></a>'''
-                       f'''**{data['name']}**, {data['type']}\n\n'''
-                       f'''{escape_in_table(data['desc'])}\n''')
-
-      self.objs.append(
-          'Column | Type | Description\n------ | --- | -----------')
-      for name, info in data['cols'].items():
-        self.objs.append(
-            f'{name} | {info["type"]} | {escape_in_table(info["desc"])}')
-
-      self.objs.append('\n\n')
-
-    # Add functions if in file
-    for data in file_dict['functions']:
-      # Anchor
-      anchor = f'''fun/{module_name}/{data['name']}'''
-
-      # Add summary of imported function
-      summary_funs_list.append(f'''[{data['name']}](#{anchor})|'''
-                               f'''{import_key_name}|'''
-                               f'''{data['return_type']}|'''
-                               f'''{escape_in_table(data['summary_desc'])}''')
-      self.funs.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.funs.append('Argument | Type | Description\n'
-                         '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.funs.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-
-        self.funs.append('\n\n')
-
-    # Add table functions if in file
-    for data in file_dict['table_functions']:
-      # Anchor
-      anchor = rf'''view_fun/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_view_funs_list.append(
-          f'''[{data['name']}](#{anchor})|'''
-          f'''{import_key_name}|'''
-          f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.view_funs.append(f'''\n\n<a name="{anchor}"></a>'''
-                            f'''**{data['name']}**\n'''
-                            f'''{data['desc']}\n\n''')
-      if data['args']:
-        self.view_funs.append('Argument | Type | Description\n'
-                              '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.view_funs.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-        self.view_funs.append('\n')
-      self.view_funs.append('Column | Type | Description\n'
-                            '------ | -- | -----------')
-      for name, column in data['cols'].items():
-        self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}')
-
-      self.view_funs.append('\n\n')
-
-    # Add macros if in file
-    for data in file_dict['macros']:
-      # Anchor
-      anchor = rf'''macro/{module_name}/{data['name']}'''
-      # Add summary of imported view function
-      summary_macros_list.append(f'''[{data['name']}](#{anchor})|'''
-                                 f'''{import_key_name}|'''
-                                 f'''{escape_in_table(data['summary_desc'])}''')
-
-      self.macros.append(
-          f'''\n\n<a name="{anchor}"></a>'''
-          f'''**{data['name']}**\n'''
-          f'''{data['desc']}\n\n'''
-          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
-      if data['args']:
-        self.macros.append('Argument | Type | Description\n'
-                           '-------- | ---- | -----------')
-        for name, arg_dict in data['args'].items():
-          self.macros.append(
-              f'''{name} | {arg_dict['type']} | {escape_in_table(arg_dict['desc'])}'''
-          )
-        self.macros.append('\n')
-      self.macros.append('\n\n')
-
-    self.summary_objs = '\n'.join(summary_objs_list)
-    self.summary_funs = '\n'.join(summary_funs_list)
-    self.summary_view_funs = '\n'.join(summary_view_funs_list)
-    self.summary_macros = '\n'.join(summary_macros_list)
-
-
-def main():
-  parser = argparse.ArgumentParser()
-  parser.add_argument('--input', required=True)
-  parser.add_argument('--output', required=True)
-  args = parser.parse_args()
-
-  with open(args.input) as f:
-    modules_json_dict = json.load(f)
-
-  modules_dict: Dict[str, ModuleMd] = {}
-
-  for module_name, module_files in modules_json_dict.items():
-    # Remove 'common' when it has been removed from the code.
-    if module_name not in ['deprecated', 'common']:
-      modules_dict[module_name] = ModuleMd(module_name, module_files)
-
-  prelude_module = modules_dict.pop('prelude')
-
-  with open(args.output, 'w') as f:
-    f.write('''
+INTRODUCTION = '''
 # PerfettoSQL standard library
 *This page documents the PerfettoSQL standard library.*
 
@@ -271,71 +62,294 @@
 <!-- TODO(b/290185551): talk about experimental module and contributions. -->
 
 ## Summary
-''')
+'''
+
+
+def _escape_in_table(desc: str):
+  """Escapes special characters in a markdown table."""
+  return desc.replace('|', '\\|')
+
+
+def _md_table(cols: List[str]):
+  col_str = ' | '.join(cols) + '\n'
+  lines = ['-' * len(col) for col in cols]
+  underlines = ' | '.join(lines)
+  return col_str + underlines
+
+
+def _write_summary(sql_type: str, table_cols: List[str],
+                   summary_objs: List[str]) -> str:
+  table_data = '\n'.join(s.strip() for s in summary_objs if s)
+  return f"""
+### {sql_type}
+
+{_md_table(table_cols)}
+{table_data}
+
+"""
+
+
+class FileMd:
+  """Responsible for file level markdown generation."""
+
+  def __init__(self, module_name, file_dict):
+    self.import_key = file_dict['import_key']
+    import_key_name = self.import_key if module_name != 'prelude' else 'N/A'
+    self.objs, self.funs, self.view_funs, self.macros = [], [], [], []
+    summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], []
+
+    # Add imports if in file.
+    for data in file_dict['imports']:
+      # Anchor
+      anchor = f'''obj/{module_name}/{data['name']}'''
+
+      # Add summary of imported view/table
+      summary_objs_list.append(f'''[{data['name']}](#{anchor})|'''
+                               f'''{import_key_name}|'''
+                               f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.objs.append(f'''\n\n<a name="{anchor}"></a>'''
+                       f'''**{data['name']}**, {data['type']}\n\n'''
+                       f'''{_escape_in_table(data['desc'])}\n''')
+
+      self.objs.append(_md_table(['Column', 'Type', 'Description']))
+      for name, info in data['cols'].items():
+        self.objs.append(
+            f'{name} | {info["type"]} | {_escape_in_table(info["desc"])}')
+
+      self.objs.append('\n\n')
+
+    # Add functions if in file
+    for data in file_dict['functions']:
+      # Anchor
+      anchor = f'''fun/{module_name}/{data['name']}'''
+
+      # Add summary of imported function
+      summary_funs_list.append(f'''[{data['name']}](#{anchor})|'''
+                               f'''{import_key_name}|'''
+                               f'''{data['return_type']}|'''
+                               f'''{_escape_in_table(data['summary_desc'])}''')
+      self.funs.append(
+          f'''\n\n<a name="{anchor}"></a>'''
+          f'''**{data['name']}**\n\n'''
+          f'''{data['desc']}\n\n'''
+          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
+      if data['args']:
+        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.funs.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+
+        self.funs.append('\n\n')
+
+    # Add table functions if in file
+    for data in file_dict['table_functions']:
+      # Anchor
+      anchor = rf'''view_fun/{module_name}/{data['name']}'''
+      # Add summary of imported view function
+      summary_view_funs_list.append(
+          f'''[{data['name']}](#{anchor})|'''
+          f'''{import_key_name}|'''
+          f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.view_funs.append(f'''\n\n<a name="{anchor}"></a>'''
+                            f'''**{data['name']}**\n'''
+                            f'''{data['desc']}\n\n''')
+      if data['args']:
+        self.funs.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.view_funs.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+        self.view_funs.append('\n')
+        self.view_funs.append(_md_table(['Column', 'Type', 'Description']))
+      for name, column in data['cols'].items():
+        self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}')
+
+      self.view_funs.append('\n\n')
+
+    # Add macros if in file
+    for data in file_dict['macros']:
+      # Anchor
+      anchor = rf'''macro/{module_name}/{data['name']}'''
+      # Add summary of imported view function
+      summary_macros_list.append(
+          f'''[{data['name']}](#{anchor})|'''
+          f'''{import_key_name}|'''
+          f'''{_escape_in_table(data['summary_desc'])}''')
+
+      self.macros.append(
+          f'''\n\n<a name="{anchor}"></a>'''
+          f'''**{data['name']}**\n'''
+          f'''{data['desc']}\n\n'''
+          f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''')
+      if data['args']:
+        self.macros.append(_md_table(['Argument', 'Type', 'Description']))
+        for name, arg_dict in data['args'].items():
+          self.macros.append(
+              f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}'''
+          )
+        self.macros.append('\n')
+      self.macros.append('\n\n')
+
+    self.summary_objs = '\n'.join(summary_objs_list)
+    self.summary_funs = '\n'.join(summary_funs_list)
+    self.summary_view_funs = '\n'.join(summary_view_funs_list)
+    self.summary_macros = '\n'.join(summary_macros_list)
+
+
+class ModuleMd:
+  """Responsible for module level markdown generation."""
+
+  def __init__(self, module_name: str, module_files: List[Dict[str,
+                                                               Any]]) -> None:
+    self.module_name = module_name
+    self.files_md = sorted(
+        [FileMd(module_name, file_dict) for file_dict in module_files],
+        key=lambda x: x.import_key)
+    self.summary_objs = '\n'.join(
+        file.summary_objs for file in self.files_md if file.summary_objs)
+    self.summary_funs = '\n'.join(
+        file.summary_funs for file in self.files_md if file.summary_funs)
+    self.summary_view_funs = '\n'.join(file.summary_view_funs
+                                       for file in self.files_md
+                                       if file.summary_view_funs)
+    self.summary_macros = '\n'.join(
+        file.summary_macros for file in self.files_md if file.summary_macros)
+
+  def get_prelude_description(self) -> str:
+    if not self.module_name == 'prelude':
+      raise ValueError("Only callable on prelude module")
+
+    lines = []
+    lines.append(f'## Module: {self.module_name}')
+
+    # Prelude is a special module which is automatically imported and doesn't
+    # have any include keys.
+    objs = '\n'.join(obj for file in self.files_md for obj in file.objs)
+    if objs:
+      lines.append('#### Views/Tables')
+      lines.append(objs)
+
+    funs = '\n'.join(fun for file in self.files_md for fun in file.funs)
+    if funs:
+      lines.append('#### Functions')
+      lines.append(funs)
+
+    table_funs = '\n'.join(
+        view_fun for file in self.files_md for view_fun in file.view_funs)
+    if table_funs:
+      lines.append('#### Table Functions')
+      lines.append(table_funs)
+
+    macros = '\n'.join(macro for file in self.files_md for macro in file.macros)
+    if macros:
+      lines.append('#### Macros')
+      lines.append(macros)
+
+    return '\n'.join(lines)
+
+  def get_description(self) -> str:
+    if not self.files_md:
+      return ''
+
+    if self.module_name == 'prelude':
+      raise ValueError("Can't be called with prelude module")
+
+    lines = []
+    lines.append(f'## Module: {self.module_name}')
+
+    for file in self.files_md:
+      if not any((file.objs, file.funs, file.view_funs, file.macros)):
+        continue
+
+      lines.append(f'### {file.import_key}')
+      if file.objs:
+        lines.append('#### Views/Tables')
+        lines.append('\n'.join(file.objs))
+      if file.funs:
+        lines.append('#### Functions')
+        lines.append('\n'.join(file.funs))
+      if file.view_funs:
+        lines.append('#### Table Functions')
+        lines.append('\n'.join(file.view_funs))
+      if file.macros:
+        lines.append('#### Macros')
+        lines.append('\n'.join(file.macros))
+
+    return '\n'.join(lines)
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--input', required=True)
+  parser.add_argument('--output', required=True)
+  args = parser.parse_args()
+
+  with open(args.input) as f:
+    modules_json_dict = json.load(f)
+
+  # Fetch the modules from json documentation.
+  modules_dict: Dict[str, ModuleMd] = {}
+  for module_name, module_files in modules_json_dict.items():
+    # Remove 'common' when it has been removed from the code.
+    if module_name not in ['deprecated', 'common']:
+      modules_dict[module_name] = ModuleMd(module_name, module_files)
+
+  prelude_module = modules_dict.pop('prelude')
+
+  with open(args.output, 'w') as f:
+    f.write(INTRODUCTION)
 
     summary_objs = [prelude_module.summary_objs
                    ] if prelude_module.summary_objs else []
     summary_objs += [
         module.summary_objs
-        for name, module in modules_dict.items()
-        if (module.summary_objs and name != 'experimental')
+        for module in modules_dict.values()
+        if (module.summary_objs)
     ]
 
     summary_funs = [prelude_module.summary_funs
                    ] if prelude_module.summary_funs else []
-    summary_funs += [
-        module.summary_funs
-        for name, module in modules_dict.items()
-        if (module.summary_funs and name != 'experimental')
-    ]
+    summary_funs += [module.summary_funs for module in modules_dict.values()]
     summary_view_funs = [prelude_module.summary_view_funs
                         ] if prelude_module.summary_view_funs else []
     summary_view_funs += [
-        module.summary_view_funs
-        for name, module in modules_dict.items()
-        if (module.summary_view_funs and name != 'experimental')
+        module.summary_view_funs for module in modules_dict.values()
     ]
     summary_macros = [prelude_module.summary_macros
                      ] if prelude_module.summary_macros else []
     summary_macros += [
-        module.summary_macros
-        for name, module in modules_dict.items()
-        if (module.summary_macros and name != 'experimental')
+        module.summary_macros for module in modules_dict.values()
     ]
 
     if summary_objs:
-      f.write('### Views/tables\n\n'
-              'Name | Import | Description\n'
-              '---- | ------ | -----------\n')
-      f.write('\n'.join(summary_objs))
-      f.write('\n')
+      f.write(
+          _write_summary('Views/tables', ['Name', 'Import', 'Description'],
+                         summary_objs))
 
     if summary_funs:
-      f.write('### Functions\n\n'
-              'Name | Import | Return type | Description\n'
-              '---- | ------ | ----------- | -----------\n')
-      f.write('\n'.join(summary_funs))
-      f.write('\n')
+      f.write(
+          _write_summary('Functions',
+                         ['Name', 'Import', 'Return type', 'Description'],
+                         summary_funs))
 
     if summary_view_funs:
-      f.write('### Table Functions\n\n'
-              'Name | Import |  Description\n'
-              '---- | ------ |  -----------\n')
-      f.write('\n'.join(summary_view_funs))
-      f.write('\n')
+      f.write(
+          _write_summary('Table functions', ['Name', 'Import', 'Description'],
+                         summary_view_funs))
 
     if summary_macros:
-      f.write('### Macros\n\n'
-              'Name | Import |  Description\n'
-              '---- | ------ |  -----------\n')
-      f.write('\n'.join(summary_macros))
-      f.write('\n')
+      f.write(
+          _write_summary('Macros', ['Name', 'Import', 'Description'],
+                         summary_macros))
 
     f.write('\n\n')
-    f.write(prelude_module.print_description())
+    f.write(prelude_module.get_prelude_description())
     f.write('\n')
     f.write('\n'.join(
-        module.print_description() for module in modules_dict.values()))
+        module.get_description() for module in modules_dict.values()))
 
   return 0
 
diff --git a/python/generators/sql_processing/docs_parse.py b/python/generators/sql_processing/docs_parse.py
index 6e8cd60..9062973 100644
--- a/python/generators/sql_processing/docs_parse.py
+++ b/python/generators/sql_processing/docs_parse.py
@@ -17,7 +17,7 @@
 from dataclasses import dataclass
 import re
 import sys
-from typing import Any, Dict, List, Optional, Set, Tuple, NamedTuple
+from typing import Dict, List, Optional, Set, NamedTuple
 
 from python.generators.sql_processing.docs_extractor import DocsExtractor
 from python.generators.sql_processing.utils import ObjKind
@@ -31,30 +31,21 @@
 from python.generators.sql_processing.utils import ARG_ANNOTATION_PATTERN
 
 
-def is_internal(name: str) -> bool:
+def _is_internal(name: str) -> bool:
   return re.match(r'^_.*', name, re.IGNORECASE) is not None
 
 
-def is_snake_case(s: str) -> bool:
-  """Returns true if the string is snake_case."""
+def _is_snake_case(s: str) -> bool:
   return re.fullmatch(r'^[a-z_0-9]*$', s) is not None
 
 
-# Parse a SQL comment (i.e. -- Foo\n -- bar.) into a string (i.e. "Foo bar.").
 def parse_comment(comment: str) -> str:
+  """Parse a SQL comment (i.e. -- Foo\n -- bar.) into a string (i.e. "Foo bar.")."""
   return ' '.join(line.strip().lstrip('--').lstrip()
                   for line in comment.strip().split('\n'))
 
-
-class Arg(NamedTuple):
-  # TODO(b/307926059): the type is missing on old-style documentation for
-  # tables. Make it "str" after stdlib is migrated.
-  type: Optional[str]
-  description: str
-
-
-# Returns: error message if the name is not correct, None otherwise.
 def get_module_prefix_error(name: str, path: str, module: str) -> Optional[str]:
+  """Returns error message if the name is not correct, None otherwise."""
   prefix = name.lower().split('_')[0]
   if module in ["common", "prelude", "deprecated"]:
     if prefix == module:
@@ -77,6 +68,13 @@
       f'with one of following names: {", ".join(allowed_prefixes)}')
 
 
+class Arg(NamedTuple):
+  # TODO(b/307926059): the type is missing on old-style documentation for
+  # tables. Make it "str" after stdlib is migrated.
+  type: Optional[str]
+  description: str
+
+
 class AbstractDocParser(ABC):
 
   @dataclass
@@ -244,7 +242,7 @@
           f'{type} "{self.name}": CREATE OR REPLACE is not allowed in stdlib '
           f'as standard library modules can only included once. Please just '
           f'use CREATE instead.')
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     is_perfetto_table_or_view = (
@@ -294,12 +292,12 @@
           f'use CREATE instead.')
 
     # Ignore internal functions.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Function name "{name}" is not snake_case'
                   f' (should be {name.casefold()})')
 
@@ -345,14 +343,14 @@
           f'use CREATE instead.')
 
     # Ignore internal functions.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     self._validate_only_contains_annotations(doc.annotations,
                                              {'@arg', '@column'})
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Function name "{name}" is not snake_case'
                   f' (should be "{name.casefold()}")')
 
@@ -396,13 +394,13 @@
           f'use CREATE instead.')
 
     # Ignore internal macros.
-    if is_internal(self.name):
+    if _is_internal(self.name):
       return None
 
     self._validate_only_contains_annotations(doc.annotations, set())
     name = self._parse_name()
 
-    if not is_snake_case(name):
+    if not _is_snake_case(name):
       self._error(f'Macro name "{name}" is not snake_case'
                   f' (should be "{name.casefold()}")')
 
@@ -416,6 +414,7 @@
 
 
 class ParsedFile:
+  """Data class containing all of the docmentation of single SQL file"""
   errors: List[str] = []
   table_views: List[TableOrView] = []
   functions: List[Function] = []
@@ -432,9 +431,9 @@
     self.macros = macros
 
 
-# Reads the provided SQL and, if possible, generates a dictionary with data
-# from documentation together with errors from validation of the schema.
 def parse_file(path: str, sql: str) -> Optional[ParsedFile]:
+  """Reads the provided SQL and, if possible, generates a dictionary with data
+    from documentation together with errors from validation of the schema."""
   if sys.platform.startswith('win'):
     path = path.replace('\\', '/')