From 4dbf8b08bbdcf909644975a27decbf286d8e63bf Mon Sep 17 00:00:00 2001 From: Gabriel Melillo Date: Thu, 6 Dec 2018 17:14:44 +0100 Subject: [PATCH] Added docgen command, and markdown generation --- lib/commands/docgen.py | 65 ++++++++++++++++++++++++++++++++ lib/kube_help.py | 84 ++++++++++++++++++++++++++++++++++++++++++ lib/kube_types.py | 6 +++ lib/load_python.py | 23 ++++++++++++ lib/var_types.py | 7 ++++ 5 files changed, 185 insertions(+) create mode 100644 lib/commands/docgen.py diff --git a/lib/commands/docgen.py b/lib/commands/docgen.py new file mode 100644 index 0000000..e694881 --- /dev/null +++ b/lib/commands/docgen.py @@ -0,0 +1,65 @@ +# (c) Copyright 2017-2018 OLX + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from command import Command +from .bases import CommandRepositoryBase +import load_python +import os + +class Command_docgen(Command, CommandRepositoryBase): + """ + Generate the documentation of all classes supported by rubiks on your local rubiks folder. + """ + + _documentation = """ + This document is automatically generated using the command `docgen` and describe all the object and types that can be used inside Rubiks to configure your cluster + + It is possible to generate this documentation locally running inside your rubiks repo `rubiks docgen`. + """ + + def populate_args(self, parser): + pass + + def run(self, args): + self.get_repository(can_fail=True) + objs = load_python.PythonBaseFile.get_kube_objs() + types = load_python.PythonBaseFile.get_kube_types() + formats = load_python.PythonBaseFile.get_kube_vartypes() + + r = self.get_repository() + + doc = '\n'.join([line.strip() for line in self._documentation.split('\n')]) + + md = '' + header = '

\n\n' + header += '# Rubiks Object Index\n{}\n\n'.format(doc) + header += '# Table of contents\n\n' + + header += '- [Formats](#formats)\n' + md += '\n# Formats\n\n' + for fname in sorted(formats.keys()): + header += ' - [{}](#{})\n'.format(fname, fname.lower()) + md += '## {}\n\n'.format(fname) + md += '{}\n\n'.format(formats[fname].get_description()) + + header += '- [Types](#types)\n' + md += '\n# Types\n\n' + for tname in sorted(types.keys()): + header += ' - [{}](#{})\n'.format(tname, tname.lower()) + md += '## {}\n\n'.format(tname) + md += '{}\n\n'.format(types[tname].get_description()) + + + header += '- [Objects](#objects)\n' + md += '\n# Objects\n\n' + for oname in sorted(objs.keys()): + header += ' - [{}](#{})\n'.format(oname, oname.lower()) + md += objs[oname].get_help().render_markdown() + + with open(os.path.join(r.basepath, 'docs/rubiks.class.md'), 'w') as f: + f.write(header.encode('utf-8')) + f.write(md.encode('utf-8')) \ No newline at end of file diff --git a/lib/kube_help.py b/lib/kube_help.py index 3a78d0f..728c076 100644 --- a/lib/kube_help.py +++ b/lib/kube_help.py @@ -109,5 +109,89 @@ def render_terminal(self): return txt + def _get_markdown_link(self, objects): + if isinstance(objects, list): + return ['[{}](#{})'.format(classname, classname.lower()) for classname in objects] + else: + return '[{}](#{})'.format(objects, objects.lower()) + def _docstring_formatter(self): return '\n'.join([line.strip() for line in self.class_doc.split('\n')]) + + def _decorate_obj_links(self, display, links): + for link in links: + display = display.replace(link, self._get_markdown_link(link)) + + return display + + def render_markdown(self): + md_render = lambda x: '[{}](#{})'.format(x, x.lower()) + # Title generation based on link + if self.class_doc_link is not None: + txt = '## [{}]({})\n'.format(self.class_name, self.class_doc_link) + else: + txt = '## {}\n'.format(self.class_name) + + # Add class doc string if exists + if self.class_doc is not None: + txt += '\n{}\n'.format(self._docstring_formatter()) + + # Parents + if len(self.class_superclasses) != 0: + txt += '### Parents: \n' + for obj in self._get_markdown_link(self.class_superclasses): + txt += '- {}\n'.format(obj) + + # Children + if len(self.class_subclasses) != 0: + txt += '### Children: \n' + for obj in self._get_markdown_link(self.class_subclasses): + txt += '- {}\n'.format(obj) + + # Parent types + if len(self.class_parent_types) != 0: + txt += '### Parent types: \n' + for obj in self._get_markdown_link(sorted(self.class_parent_types.keys())): + txt += '- {}\n'.format(obj) + + # Metadata + if self.class_has_metadata: + txt += '### Metadata\n' + txt += 'Name | Format\n' + txt += '---- | ------\n' + txt += 'annotations | {}\n'.format(Map(String, String).name(render=md_render)) + txt += 'labels | {}\n'.format(Map(String, String).name(render=md_render)) + + # Properties table + if len(self.class_types.keys()) == 0: + return txt + + txt += '### Properties:\n\n' + txt += 'Name | Type | Identifier | Type Transformation | Aliases\n' + txt += '---- | ---- | ---------- | ------------------- | -------\n' + if self.class_identifier is not None: + txt += '{} | {} | True | - | - \n'.format(self.class_identifier, self._get_markdown_link(self.class_types[self.class_identifier].name())) + + for p in sorted(self.class_types.keys()): + if p == self.class_identifier: + continue + + # Prepare Type transformation and remove special character that could ruin visualization in markdown + xf_data = self.class_xf_detail[p] if p in self.class_xf_detail else '-' + xf_data = xf_data.replace('<', '<').replace('>', '>') + + is_mapped = ', '.join(self._get_markdown_link(self.class_mapping[p])) if p in self.class_mapping else '-' + + original_type = self.class_types[p].original_type() + display_type = self.class_types[p].name(render=md_render).replace('<', '<').replace('>', '>') + + if original_type is not None: + if isinstance(original_type, list): + original_type = original_type[0] + elif isinstance(original_type, dict): + original_type = original_type['value'] + + + txt += '{} | {} | False | {} | {} \n'.format(p, display_type, xf_data, is_mapped) + + return txt diff --git a/lib/kube_types.py b/lib/kube_types.py index feacc12..80270a8 100644 --- a/lib/kube_types.py +++ b/lib/kube_types.py @@ -61,6 +61,12 @@ def construct_arg(cls, arg): ret = Object(ret.__class__) return ret + + @classmethod + def get_description(cls): + if cls.__doc__ is not None and cls.__doc__ is not '': + return '\n'.join([line.strip() for line in cls.__doc__.split('\n')]) + return 'TODO: Description is still missing from the class docstring.\nStay tuned to have more hint about this variable.' def original_type(self): if self.wrapper: diff --git a/lib/load_python.py b/lib/load_python.py index 1713f72..155bd0d 100644 --- a/lib/load_python.py +++ b/lib/load_python.py @@ -23,6 +23,7 @@ from util import mkdir_p import kube_objs +import kube_types import kube_vartypes @@ -209,6 +210,7 @@ def gen_output(self): class PythonBaseFile(object): _kube_objs = None _kube_vartypes = None + _kube_type = None compile_in_init = True default_export_objects = False can_cluster_context = True @@ -242,6 +244,26 @@ def get_kube_vartypes(cls): pass return cls._kube_vartypes + @classmethod + def get_kube_types(cls): + if cls._kube_type is None: + cls._kube_type = {} + + for k in kube_types.__dict__: + if isinstance(kube_types.__dict__[k], type) and k not in ('KubeType', 'SurgeCheck'): + try: + if hasattr(kube_types.__dict__[k], 'wrapper'): + if kube_types.__dict__[k].wrapper: + if isinstance(kube_types.__dict__[k](kube_types.String), kube_types.KubeType): + cls._kube_type[k] = kube_types.__dict__[k] + else: + if isinstance(kube_types.__dict__[k](), kube_types.KubeType): + cls._kube_type[k] = kube_types.__dict__[k] + except: + pass + + return cls._kube_type + def __init__(self, collection, path): if path.basename == '' or path.basename.lower().strip('0123456789abcdefghijklmnopqrstuvwxyz_') != '': raise UserError(ValueError( @@ -515,6 +537,7 @@ def cluster_info(c): ret.update(self.__class__.get_kube_objs()) ret.update(self.__class__.get_kube_vartypes()) + ret.update(self.__class__.get_kube_types()) self.reserved_names = tuple(ret.keys()) diff --git a/lib/var_types.py b/lib/var_types.py index 13420a5..f01d09b 100644 --- a/lib/var_types.py +++ b/lib/var_types.py @@ -133,3 +133,10 @@ def __str__(self): if self.renderer is None: return self._internal_render() return self.renderer(self._internal_render()) + + @classmethod + def get_description(cls): + if cls.__doc__ is not None and cls.__doc__ is not '': + return '\n'.join([line.strip() for line in cls.__doc__.split('\n')]) + return 'TODO: Description is still missing from the class docstring.\nStay tuned to have more hint about this variable.' +