Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 332 lines (226 sloc) 8.51 KB
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import os
import textwrap
from abc import abstractmethod, ABC
__version__ = "0.2.1"
class Parser(ABC):
""" Parser of components. """
def __init__(self, entry):
self._entry = entry
@staticmethod
def get_parser(entry_path):
if os.path.isdir(entry_path):
parser = ModuleParser(entry_path)
else:
parser = FileParser(entry_path)
return parser
def parse_components(self):
pass
class ModuleParser(Parser):
""" Parse components in a module """
def __init__(self, entry):
super().__init__(entry)
def parse_components(self):
components = []
for entry in os.listdir(self._entry):
entry_path = os.path.join(self._entry, entry)
entry_parser = Parser.get_parser(entry_path)
components.extend(entry_parser.parse_components())
return components
class FileParser(Parser):
""" Parse components (classes) in a file"""
def __init__(self, entry):
super().__init__(entry)
def parse_components(self):
# Only managing python file for now
if not self._entry.endswith(".py"):
return []
with open(file=self._entry, mode="r") as fd:
text = fd.read()
class_blocks = text.split("\nclass ")
if "import " in class_blocks[0]:
class_blocks = class_blocks[1:]
# TODO : resolve this
if len(class_blocks) > 1 and "if __name__ == __main__" in class_blocks[-1]:
class_blocks = class_blocks[:-2]
classes = list(map(self._parse_class_block, class_blocks))
return classes
def _parse_class_block(self, block):
"""
Return a Class object from a block of code.
:param block: block of code (without the "class" keyword)
:return:
"""
lines = block.split("\n")
name_class = lines[0].replace(":", "").strip()
upper_class = None
if "(" in name_class:
tokens = name_class.split("(")
name_class = tokens[0]
upper_class = tokens[1].split(")")[0]
methods = block.split("def ")
del methods[0]
public_methods = []
private_methods = []
public_attributes, private_attributes = [], []
for method in methods:
method_signature = method.split(")")[0].strip() + ")"
if "__init__" == method_signature:
public_attributes, private_attributes = self._parse_init_block(method)
if method_signature[0] == "_" and method_signature[1] != "_":
private_methods.append(method_signature[1:])
else:
public_methods.append(method_signature)
clazz = Class(name_class, upper_class, public_attributes, public_methods, private_attributes, private_methods)
return clazz
def _parse_init_block(self, block):
"""
Gather class attributes of a class using a block of code.
:param block: the block of code of the __init__ method (without the "def" keyword)
:return: list of public and private attributes names
"""
lines = block.split("\n")
private_attributes = []
public_attributes = []
for line in lines:
if "self." in line:
attribute_name = line.replace("self.", "").split("=")[0].strip()
if attribute_name[0] == "_" and attribute_name[1] != "_":
private_attributes.append(attribute_name[1:])
else:
public_attributes.append(attribute_name)
return public_attributes, private_attributes
class Composite(ABC):
""" A wrapper of Components.
A Composite can also be a Component.
Used to recursively construct the plant-uml script information.
"""
def __init__(self, name):
self._name = name
self._components = {}
self._indentation = ""
def register_components(self, components):
assert len(set(components).intersection(self._components.keys())) == 0
for compo in components:
self._components[compo._name] = compo
def __repr__(self):
str_repr = self._header()
for component_name, component in self._components.items():
str_repr += textwrap.indent(str(component), self._indentation)
str_repr += self._footer()
return str_repr
@abstractmethod
def _header(self):
pass
@abstractmethod
def _footer(self):
pass
class Class(object):
""" Wrapper of a python class.
A component only.
"""
def __init__(self, name, upper_class, public_attributes, public_methods, private_attributes, private_methods):
self._name = name
self._upper_class = upper_class
self._public_attributes = public_attributes
self._public_methods = public_methods
self._private_attributes = private_attributes
self._private_methods = private_methods
def __repr__(self):
if self._name == "":
return ""
str_repr = "\n"
str_repr += "class %s { \n" % self._name
for pa in self._public_attributes:
str_repr += f" + {pa}\n"
for pa in self._private_attributes:
str_repr += f" - {pa}\n"
for pm in self._public_methods:
str_repr += f" + {pm}\n"
for pm in self._private_methods:
str_repr += f" - {pm}\n"
str_repr += "}\n"
if self._upper_class is not None:
str_repr += f"\n{self._name} --|> {self._upper_class}\n"
str_repr += "\n"
return str_repr
class Scope(Composite):
""" The given scope of the project.
Produces a plant-uml script when getting the string representation of it.
Is the base Composite.
"""
def __init__(self, name):
super().__init__(name)
self._custom_headers = []
def register_custom_headers(self, headers):
self._custom_headers.extend(headers)
def _header(self):
"""
Print the header of the plant-uml script
:return:
"""
str_repr = "@startuml\n\n"
for custom_header in self._custom_headers:
str_repr += custom_header
str_repr += f"\ntitle {self._name}\n"
return str_repr
def _footer(self):
"""
Print the footer of the plant-uml script
:return:
"""
str_repr = "\n@enduml\n"
return str_repr
class Module(Composite):
""" Composite for a module (ie a set of Classes or Modules) """
def __init__(self, name):
super().__init__(name)
self._indentation = " "
def _header(self):
"""
Print the header of the plantuml script
:return:
"""
str_repr = "\npackage %s {\n\n" % self._name
return str_repr
def _footer(self):
"""
Print the footer of the plantuml script
:return:
"""
str_repr = "\n}\n"
return str_repr
def main():
default_title = "Class Diagram for {path}"
argparser = argparse.ArgumentParser(description="Evaluate a model using a serialized version of it.")
argparser.add_argument('-v', '--version', action='version',
version='%(prog)s {version}'.format(version=__version__))
argparser.add_argument("path", metavar="path",
type=str,
help="the file or module to generate UML for.")
argparser.add_argument("--font", metavar="font",
type=str, required=False,
help="the font to use for the script.")
argparser.add_argument("--title", metavar="title",
type=str, required=False,
default=default_title,
help="the title to use for the project.")
args = argparser.parse_args()
to_parse_path = args.path
title = args.title
font = args.font
custom_headers = list()
if font:
custom_headers.append(f"skinparam classFontName {args.fonts}\n")
if title == default_title:
basename_path = to_parse_path.split(os.sep)[-1]
title = title.format(path=basename_path)
parser = Parser.get_parser(to_parse_path)
components = parser.parse_components()
# Zipping it
landscape = Scope(title)
landscape.register_components(components)
landscape.register_custom_headers(custom_headers)
print(landscape)
You can’t perform that action at this time.