From 901fca2d0dd58a8bc8d6d339ca42dedfaf819976 Mon Sep 17 00:00:00 2001 From: thibault Date: Sun, 28 Feb 2021 05:00:49 -0500 Subject: [PATCH] Fix bugs with enums, add nesting support, improve namespaces --- .gitignore | 2 + doc/source/conf.py | 6 +- doc/source/org-doc.rst | 618 ++++++++++++++++-------- hpp2plantuml.org | 604 ++++++++++++++++------- src/hpp2plantuml/__init__.py | 2 +- src/hpp2plantuml/hpp2plantuml.py | 334 +++++++++---- src/hpp2plantuml/templates/default.puml | 7 + tests/simple_classes.puml | 97 ++-- tests/simple_classes_3.hpp | 10 + tests/simple_classes_nodep.puml | 97 ++-- tests/test_hpp2plantuml.py | 26 +- 11 files changed, 1248 insertions(+), 555 deletions(-) diff --git a/.gitignore b/.gitignore index b146cdf..5f6f2f1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ doc/build/ build/ dist/ *~ +*.egg* +__pycache__/ diff --git a/doc/source/conf.py b/doc/source/conf.py index c55a042..63fa9dd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -79,9 +79,9 @@ # built documents. # # The short X.Y version. -version = u'v' + u'0.7.1' +version = u'v' + u'0.8.0' # The full version, including alpha/beta/rc tags. -release = u'v' + u'0.7.1' +release = u'v' + u'0.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -155,7 +155,7 @@ # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = u'hpp2plantuml ' + u'v' + u'0.7.1' +# html_title = u'hpp2plantuml ' + u'v' + u'0.8.0' # A shorter title for the navigation bar. Default is the same as html_title. # diff --git a/doc/source/org-doc.rst b/doc/source/org-doc.rst index 2190db7..93aff1f 100644 --- a/doc/source/org-doc.rst +++ b/doc/source/org-doc.rst @@ -13,7 +13,7 @@ The current version of the code is: :: :name: hpp2plantuml-version - 0.7.1 + 0.8.0 The source code can be found on GitHub: @@ -181,17 +181,18 @@ of elementary properties and links. 'inherit': '<|--', 'aggregation': 'o--', 'composition': '*--', - 'dependency': '<..' + 'dependency': '<..', + 'nesting': '+--' } # Association between object names and objects # - The first element is the object type name in the CppHeader object # - The second element is the iterator used to loop over objects # - The third element is a function returning the corresponding internal object - CONTAINER_TYPE_MAP = [ - ['classes', lambda objs: objs.items(), lambda obj: Class(obj)], - ['enums', lambda objs: objs, lambda obj: Enum(obj)] - ] + CONTAINER_TYPE_MAP = { + 'classes': [lambda objs: objs.items(), lambda obj: Class(obj)], + 'enums': [lambda objs: objs, lambda obj: Enum(obj)] + } Objects ~~~~~~~ @@ -252,11 +253,13 @@ The ``Container`` class is abstract and contains: Object name (with ``<``, ``>`` characters removed) """ self._container_type = container_type - self._name = re.sub('[<>]', '', name) + self._name = re.sub('[<>]', '', re.sub('-', '_', name)) self._member_list = [] - self._namespace = None + self._namespace = '' + self._parent = None - def get_name(self): + @property + def name(self): """Name property accessor Returns @@ -280,10 +283,19 @@ The ``Container`` class is abstract and contains: header_container : CppClass or CppEnum Parsed header for container """ - namespace = header_container.get('namespace', None) + namespace = header_container.get('namespace', '') if namespace: - if not header_container.get('parent', None): + parent = header_container.get('parent', None) + # Presence of namespace and parent fields indicates a nested class + if not parent: self._namespace = _cleanup_namespace(namespace) + else: + #self._parent = re.sub('[<>]', '', parent['name']) + self._parent = '::'.join(self._name.split('::')[:-1]) + p = parent + while p.get('parent') is not None: + p = p.get('parent', None) + self._namespace = p['namespace'] self._do_parse_members(header_container) def _do_parse_members(self, header_container): @@ -312,8 +324,6 @@ The ``Container`` class is abstract and contains: for member in self._member_list: container_str += '\t' + member.render() + '\n' container_str += '}\n' - if self._namespace is not None: - return wrap_namespace(container_str, self._namespace) return container_str def comparison_keys(self): @@ -438,7 +448,7 @@ which is used to determine aggregation relationships between classes. Parameters ---------- - header_class : list (str, CppClass) + header_class : tuple(str, CppClass) Parsed header for class object (two-element list where the first element is the class name and the second element is a CppClass object) @@ -759,7 +769,7 @@ the actual values. This class defines a simple object inherited from the base `Container` class. It simply lists enumerated values. """ - def __init__(self, header_enum): + def __init__(self, header_enum, parent=None): """Constructor Parameters @@ -769,6 +779,8 @@ the actual values. """ super().__init__('enum', header_enum.get('name', 'empty')) self.parse_members(header_enum) + if parent: + self._parent = parent def _do_parse_members(self, header_enum): """Extract enum values from header @@ -810,6 +822,48 @@ the actual values. """ return self._name +Namespace +^^^^^^^^^ + +C++ namespaces are represented by the ``Namespace`` class. It simply contains a +list of objects and wraps the objects in a ``namespace`` block on rendering. + +.. code:: python + :name: py-render-classes + + # %% Class object + + + class Namespace(list): + """Representation of C++ namespace + + This class lists other containers or namespaces and wraps the rendered + output in a ``namespace`` block. + """ + def __init__(self, name, *args): + """Constructor + + Parameters + ---------- + name : str + Namespace name + """ + self._name = name + super().__init__(*args) + + def render(self): + """Render namespace content + + Render the elements and wrap the result in a ``namespace`` block + + Returns + ------- + str + String representation of namespace in PlantUML syntax + """ + return wrap_namespace('\n'.join([c.render() + for c in self]), self._name) + .. _sec-module-relationship: Class relationships @@ -839,24 +893,23 @@ strings and the text representation of a connection link is obtained from the This includes a parent/child pair and a relationship type (e.g. inheritance or aggregation). """ - def __init__(self, link_type, c_parent, c_child, flag_use_namespace=False): + def __init__(self, link_type, c_parent, c_child): """Constructor Parameters ---------- link_type : str Relationship type: ``inherit`` or ``aggregation`` - c_parent : str - Name of parent class - c_child : str - Name of child class + c_parent : Container + Parent container + c_child : Container + Child container """ - self._parent = c_parent.get_name() - self._child = c_child.get_name() + self._parent = c_parent.name + self._child = c_child.name self._link_type = link_type - self._parent_namespace = c_parent._namespace or None - self._child_namespace = c_child._namespace or None - self._flag_use_namespace = flag_use_namespace + self._parent_namespace = c_parent._namespace or '' + self._child_namespace = c_child._namespace or '' def comparison_keys(self): """Order comparison key between `ClassRelationship` objects @@ -871,7 +924,7 @@ strings and the text representation of a connection link is obtained from the """ return self._parent, self._child, self._link_type - def _render_name(self, class_name, class_namespace, flag_use_namespace): + def _render_name(self, class_name, class_namespace): """Render class name with namespace prefix if necessary Parameters @@ -880,22 +933,13 @@ strings and the text representation of a connection link is obtained from the Name of the class class_namespace : str Namespace or None if the class is defined in the default namespace - flag_use_namespace : bool - When False, do not use the namespace Returns ------- str Class name with appropriate prefix for use with link rendering """ - if not flag_use_namespace: - return class_name - - if class_namespace is None: - prefix = '.' - else: - prefix = class_namespace + '.' - return prefix + class_name + return get_namespace_link_name(class_namespace) + '.' + class_name def render(self): """Render class relationship to string @@ -912,26 +956,14 @@ strings and the text representation of a connection link is obtained from the """ link_str = '' - # Wrap the link in namespace block (if both parent and child are in the - # same namespace) - namespace_wrap = None - if self._parent_namespace == self._child_namespace and \ - self._parent_namespace is not None: - namespace_wrap = self._parent_namespace - # Prepend the namespace to the class name - flag_render_namespace = self._flag_use_namespace and not namespace_wrap - parent_str = self._render_name(self._parent, self._parent_namespace, - flag_render_namespace) - child_str = self._render_name(self._child, self._child_namespace, - flag_render_namespace) + parent_str = self._render_name(self._parent, self._parent_namespace) + child_str = self._render_name(self._child, self._child_namespace) # Link string - link_str += parent_str + ' ' + self._render_link_type() + \ - ' ' + child_str + '\n' + link_str += (parent_str + ' ' + self._render_link_type() + ' ' + + child_str + '\n') - if namespace_wrap is not None: - return wrap_namespace(link_str, namespace_wrap) return link_str def _render_link_type(self): @@ -1074,6 +1106,39 @@ the ``<..`` link type (`http://plantuml.com/class-diagram `_). + +.. code:: python + :name: py-class_nesting + + # %% Nested class + + + class ClassNestingRelationship(ClassRelationship): + """Nesting relationship + + Dependencies occur when member methods depend on an object of another class + in the diagram. + """ + def __init__(self, c_parent, c_child, **kwargs): + """Constructor + + Parameters + ---------- + c_parent : str + Class corresponding to the type of the member variable in the + nesting relationship + c_child : str + Child (or client) class of the dependency relationship + kwargs : dict + Additional parameters passed to parent class + """ + super().__init__('nesting', c_parent, c_child, **kwargs) + .. _sec-module-diagram: Diagram object @@ -1194,6 +1259,7 @@ be used to avoid this sorting step. self._inheritance_list = [] self._aggregation_list = [] self._dependency_list = [] + self._nesting_list = [] def _sort_list(input_list): """Sort list using `ClassRelationship` comparison @@ -1219,24 +1285,25 @@ be used to avoid this sorting step. Diagram._sort_list(self._inheritance_list) Diagram._sort_list(self._aggregation_list) Diagram._sort_list(self._dependency_list) + Diagram._sort_list(self._nesting_list) - def _build_helper(self, input, build_from='string', flag_build_lists=True, + def _build_helper(self, data_in, build_from='string', flag_build_lists=True, flag_reset=False): """Helper function to initialize a `Diagram` object from parsed headers Parameters ---------- - input : CppHeader or str or list(CppHeader) or list(str) + data_in : CppHeader or str or list(CppHeader) or list(str) Input of arbitrary type. The processing depends on the ``build_from`` parameter build_from : str - Determines the type of the ``input`` variable: + Determines the type of the ``data_in`` variable: - * ``string``: ``input`` is a string containing C++ header code - * ``file``: ``input`` is a filename to parse - * ``string_list``: ``input`` is a list of strings containing C++ + * ``string``: ``data_in`` is a string containing C++ header code + * ``file``: ``data_in`` is a filename to parse + * ``string_list``: ``data_in`` is a list of strings containing C++ header code - * ``file_list``: ``input`` is a list of filenames to parse + * ``file_list``: ``data_in`` is a list of filenames to parse flag_build_lists : bool When True, relationships lists are built and the objects in the @@ -1249,10 +1316,10 @@ be used to avoid this sorting step. if flag_reset: self.clear() if build_from in ('string', 'file'): - self.parse_objects(input, build_from) + self.parse_objects(data_in, build_from) elif build_from in ('string_list', 'file_list'): build_from_single = re.sub('_list$', '', build_from) - for single_input in input: + for single_input in data_in: self.parse_objects(single_input, build_from_single) if flag_build_lists: self.build_relationship_lists() @@ -1343,6 +1410,7 @@ be used to avoid this sorting step. """ self.build_inheritance_list() self.build_aggregation_list() + self.build_nesting_list() if self._flag_dep: self.build_dependency_list() @@ -1358,17 +1426,32 @@ be used to avoid this sorting step. A string containing C++ header code or a filename with C++ header code arg_type : str - It set to ``string``, ``header_file`` is considered to be a string, + If set to ``string``, ``header_file`` is considered to be a string, otherwise, it is assumed to be a filename """ # Parse header file parsed_header = CppHeaderParser.CppHeader(header_file, argType=arg_type) - for container_type, container_iterator, \ - container_handler in CONTAINER_TYPE_MAP: + for container_type, (container_iterator, + container_handler) in CONTAINER_TYPE_MAP.items(): objects = parsed_header.__getattribute__(container_type) for obj in container_iterator(objects): - self._objects.append(container_handler(obj)) + # Parse container + obj_c = container_handler(obj) + self._objects.append(obj_c) + # Look for nested enums + # Find value from iterator (may be a tuple) + if isinstance(obj, tuple) and len(obj) == 2: + obj_n = obj[-1] + else: + obj_n = obj + if 'enums' in obj_n: + for m in MEMBER_PROP_MAP.keys(): + for enum in obj_n['enums'][m]: + enum_c = Enum(enum, parent=obj_c.name) + # Adjust name to reflect nesting + enum_c._name = obj_c.name + '::' + enum_c._name + self._objects.append(enum_c) def _make_class_list(self): """Build list of classes @@ -1379,8 +1462,24 @@ be used to avoid this sorting step. Each entry is a dictionary with keys ``name`` (class name) and ``obj`` the instance of the `Class` class """ - return [{'name': obj.get_name(), 'obj': obj} - for obj in self._objects if isinstance(obj, Class)] + return [{'name': obj.name, 'obj': obj} + for obj in self._objects if isinstance(obj, (Class, Enum))] + + def _get_class_list(self): + """Build list of classes in diagram + + Returns + ------- + list + Class object list (returned by :func:`_make_class_list`) + list + Class names + bool + True when at least one container is a namespace + """ + class_list_obj = self._make_class_list() + class_list = [c['name'] for c in class_list_obj] + return class_list_obj, class_list def build_inheritance_list(self): """Build list of inheritance between objects @@ -1395,15 +1494,13 @@ be used to avoid this sorting step. """ self._inheritance_list = [] # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Inheritance for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): for parent in obj.build_inheritance_list(): if parent in class_list: @@ -1411,8 +1508,7 @@ be used to avoid this sorting step. class_list.index(parent)]['obj'] self._inheritance_list.append( ClassInheritanceRelationship( - parent_obj, obj, - flag_use_namespace=flag_use_namespace)) + parent_obj, obj)) def build_aggregation_list(self): """Build list of aggregation relationships @@ -1427,16 +1523,13 @@ be used to avoid this sorting step. for each relationships, using the calculated count. """ self._aggregation_list = [] - # Build list of classes in diagram - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() # Build member type list variable_type_list = {} for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): variable_type_list[obj_name] = obj.build_variable_type_list() # Create aggregation links @@ -1462,8 +1555,7 @@ be used to avoid this sorting step. self._aggregation_list.append( ClassAggregationRelationship( obj_class_obj, comp_parent_obj, comp_count, - rel_type=rel_type, - flag_use_namespace=flag_use_namespace)) + rel_type=rel_type)) def build_dependency_list(self): """Build list of dependency between objects @@ -1477,17 +1569,14 @@ be used to avoid this sorting step. """ self._dependency_list = [] - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Add all objects name to list objects_name = [] for obj in self._objects: - objects_name.append(obj.get_name()) + objects_name.append(obj.name) # Dependency for obj in self._objects: @@ -1504,14 +1593,31 @@ be used to avoid this sorting step. for o in objects_name].index(True) except ValueError: pass - if index != ValueError and \ - method[0] != obj.get_name(): + if index != ValueError and method[0] != obj.name: depend_obj = self._objects[index] self._dependency_list.append( ClassDependencyRelationship( - depend_obj, obj, - flag_use_namespace=flag_use_namespace)) + depend_obj, obj)) + + def build_nesting_list(self): + """Build list of nested objects + + """ + self._nesting_list = [] + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() + + for obj in self._objects: + obj_name = obj.name + if isinstance(obj, (Class, Enum)): + parent = obj._parent + if parent and parent in class_list: + parent_obj = class_list_obj[ + class_list.index(parent)]['obj'] + self._nesting_list.append( + ClassNestingRelationship( + parent_obj, obj)) def _augment_comp(self, c_dict, c_parent, c_child, rel_type='aggregation'): """Increment the aggregation reference count @@ -1557,10 +1663,39 @@ be used to avoid this sorting step. object, including objects and object relationships """ template = self._env.get_template(self._template_file) - return template.render(objects=self._objects, + # List namespaces + ns_list = [] + for obj in self._objects: + if obj._namespace and obj._namespace not in ns_list: + ns_list.append(obj._namespace) + # Ensure nested namespaces are processed first + ns_list = sorted(ns_list, key=lambda ns: len(ns.split('::')), + reverse=True) + # Create namespace objects (flat map) + ns_obj_map = {ns: Namespace(ns) for ns in ns_list} + # Build list of objects + objects_out = [] + # 1. Place objects in namespace container or in output list + for obj in self._objects: + if obj._namespace: + ns_obj_map[obj._namespace].append(obj) + else: + objects_out.append(obj) + # 2. Add namespaces: collapse nested namespaces and add top level + # namespaces to output list + for ns in ns_list: + ns_name_parts = ns.split('::') + if len(ns_name_parts) > 1: + ns_parent = '::'.join(ns_name_parts[:-1]) + ns_obj_map[ns_parent].append(ns_obj_map[ns]) + else: + objects_out.append(ns_obj_map[ns]) + # Render + return template.render(objects=objects_out, inheritance_list=self._inheritance_list, aggregation_list=self._aggregation_list, dependency_list=self._dependency_list, + nesting_list=self._nesting_list, flag_dep=self._flag_dep) Helper functions @@ -1603,8 +1738,8 @@ variable types by eliminating spaces around ``\*`` characters. def _cleanup_namespace(ns_str): """Cleanup string representing a C++ namespace - Cleanup simply consists in removing leading and trailing colon characters - (``:``), and ``<>`` blocks. + Cleanup simply consists in removing ``<>`` blocks and trailing ``:`` + characters. Parameters ---------- @@ -1616,10 +1751,9 @@ variable types by eliminating spaces around ``\*`` characters. str The namespace string after cleanup """ - return re.sub('<([^>]+)>', r'\1', - re.sub('(.+)<[^>]+>', r'\1', - re.sub('^:+', '', - re.sub(':+$', '', ns_str)))) + return re.sub(':+$', '', + re.sub('<([^>]+)>', r'\1', + re.sub('(.+)<[^>]+>', r'\1', ns_str))) The ``_cleanup_single_line`` function transforms a multiline input string into a single string version. @@ -1709,10 +1843,32 @@ block. ``input_str`` wrapped in ``namespace`` block """ return 'namespace {} {{\n'.format(namespace) + \ - '\n'.join([re.sub('^', '\t', line) + '\n'.join([re.sub('^', '\t', line) if line else line for line in input_str.splitlines()]) + \ '\n}\n' + def get_namespace_link_name(namespace): + """Generate namespace string for link + + Parameters + ---------- + namespace : str + Namespace name (in the form ``nested::ns``) + + Returns + ------- + str + The namespace name formatted for use in links + (e.g. ``nested.nested::ns``) + """ + if not namespace: + return '' + ns_list = namespace.split('::') + ns_list_out = [ns_list[0], ] + for ni, ns in enumerate(ns_list[1:]): + ns_list_out.append('{}::{}'.format(ns_list_out[ni - 1], ns)) + return '.'.join(ns_list_out) + .. _sec-module-create-uml: Main function: create PlantUML from C++ @@ -1808,6 +1964,13 @@ default template is as follows: {% endblock %} {% endif %} + {% block nested %} + /' Nested objects '/ + {% for link in nesting_list %} + {{ link.render() }} + {% endfor %} + {% endblock %} + @enduml The template successively prints the following blocks @@ -1864,7 +2027,7 @@ to parse input arguments. The function passes the command line arguments to the required=False, default=None, metavar='JINJA-FILE', help='path to jinja2 template file') parser.add_argument('--version', action='version', - version='%(prog)s ' + '0.7.1') + version='%(prog)s ' + '0.8.0') args = parser.parse_args() if len(args.input_files) > 0: CreatePlantUMLFile(args.input_files, args.output_file, @@ -2068,7 +2231,7 @@ sorting keys. c_type = "container_type" c_name = "container_name" c_obj = hpp2plantuml.hpp2plantuml.Container(c_type, c_name) - nt.assert_equal(c_obj.get_name(), c_name) + nt.assert_equal(c_obj.name, c_name) nt.assert_equal(c_obj.render(), 'container_type container_name {\n}\n') def test_comparison_keys(self): @@ -2085,7 +2248,7 @@ sorting keys. c_obj_list.sort(key=lambda obj: obj.comparison_keys()) for i in range(len(c_list)): - nt.assert_equal(c_obj_list[i].get_name(), + nt.assert_equal(c_obj_list[i].name, c_list[ref_sort_idx[i]][1]) Class @@ -2205,7 +2368,7 @@ TableĀ `tbl-unittest-class`_. It includes templates and abstract classes. +-----------------------------------------------------------------------+---------------------------------------------------------------------------------------+ | "template class Test{\nvirtual T\* func(T& arg)=0; };" | "abstract class Test > {\n\t-{abstract} func(T& arg) : T\*\n}\n" | +-----------------------------------------------------------------------+---------------------------------------------------------------------------------------+ - | "namespace Interface {\nclass Test {\nprotected:\nint & member; };};" | "namespace Interface {\n\tclass Test {\n\t\t#member : int&\n\t}\n}\n" | + | "namespace Interface {\nclass Test {\nprotected:\nint & member; };};" | "class Test {\n\t#member : int&\n}\n" | +-----------------------------------------------------------------------+---------------------------------------------------------------------------------------+ .. code:: python @@ -2277,25 +2440,25 @@ relationships (with and without count). .. table:: List of test segments and corresponding PlantUML strings. :name: tbl-unittest-link - +----------------------------------------------------------+----------------------------------+ - | C++ | plantuml | - +==========================================================+==================================+ - | "class A{};\nclass B : A{};" | "A <@-- B\n" | - +----------------------------------------------------------+----------------------------------+ - | "class A{};\nclass B : public A{};" | "A <@-- B\n" | - +----------------------------------------------------------+----------------------------------+ - | "class B{};\nclass A{B obj;};" | "A \*-- B\n" | - +----------------------------------------------------------+----------------------------------+ - | "class B{};\nclass A{B\* obj;};" | "A o-- B\n" | - +----------------------------------------------------------+----------------------------------+ - | "class B{};\nclass A{B \* obj\_ptr; B\* ptr;};" | "A \\"2\\" o-- B\n" | - +----------------------------------------------------------+----------------------------------+ - | "class A{};\nclass B{void Method(A\* obj);};" | "A <.. B\n" | - +----------------------------------------------------------+----------------------------------+ - | "namespace T {class A{}; class B: A{};};" | "namespace T {\n\tA <@-- B\n}\n" | - +----------------------------------------------------------+----------------------------------+ - | "namespace T {\nclass A{};};\nclass B{T\:\:A\* \_obj;};" | ".B o-- T.A\n" | - +----------------------------------------------------------+----------------------------------+ + +----------------------------------------------------------+-----------------------+ + | C++ | plantuml | + +==========================================================+=======================+ + | "class A{};\nclass B : A{};" | ".A <@-- .B\n" | + +----------------------------------------------------------+-----------------------+ + | "class A{};\nclass B : public A{};" | ".A <@-- .B\n" | + +----------------------------------------------------------+-----------------------+ + | "class B{};\nclass A{B obj;};" | ".A \*-- .B\n" | + +----------------------------------------------------------+-----------------------+ + | "class B{};\nclass A{B\* obj;};" | ".A o-- .B\n" | + +----------------------------------------------------------+-----------------------+ + | "class B{};\nclass A{B \* obj\_ptr; B\* ptr;};" | ".A \\"2\\" o-- .B\n" | + +----------------------------------------------------------+-----------------------+ + | "class A{};\nclass B{void Method(A\* obj);};" | ".A <.. .B\n" | + +----------------------------------------------------------+-----------------------+ + | "namespace T {class A{}; class B: A{};};" | "T.A <@-- T.B\n" | + +----------------------------------------------------------+-----------------------+ + | "namespace T {\nclass A{};};\nclass B{T\:\:A\* \_obj;};" | ".B o-- T.A\n" | + +----------------------------------------------------------+-----------------------+ .. code:: python @@ -2396,6 +2559,7 @@ The following can be extended to improve testing, as long as the corresponding bool _flag; Class01* _obj; T _var; + Enum01 _val; }; class Class04_derived : public Class04 { @@ -2409,6 +2573,15 @@ The following can be extended to improve testing, as long as the corresponding struct Struct { int a; }; + enum Enum { A, B }; + + namespace NestedNamespace { + class Class04_ns : private Class04_derived { + protected: + Struct _s; + Enum _e; + }; + }; }; // Anonymous union (issue #9) @@ -2434,7 +2607,6 @@ Reference output Following is the reference output for the input header files defined `sec-test-system-hpp`_. The comparison takes into account the white space, indentation, etc. - :: :name: puml-simple-classes @@ -2484,26 +2656,6 @@ The comparison takes into account the white space, indentation, etc. } - namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } - } - - - namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } - } - - enum Enum01 { VALUE_0 VALUE_1 @@ -2511,32 +2663,59 @@ The comparison takes into account the white space, indentation, etc. } - namespace Interface { - class Struct { - +a : int - } - } - - - class anon-union-1::anon-struct-1 { + class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } - class anon-union-1::anon-struct-2 { + class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } - class anon-union-1 { + class anon_union_1 { +vec : float } + namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } + } + + @@ -2545,9 +2724,10 @@ The comparison takes into account the white space, indentation, etc. .Class01 <|-- .Class02 - namespace Interface { - Class04 <|-- Class04_derived - } + Interface.Class04 <|-- Interface.Class04_derived + + + Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -2564,6 +2744,15 @@ The comparison takes into account the white space, indentation, etc. Interface.Class04 o-- .Class01 + Interface.Class04 *-- .Enum01 + + + Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + + Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + @@ -2576,6 +2765,20 @@ The comparison takes into account the white space, indentation, etc. + + /' Nested objects '/ + + .Class02 +-- .Class02::ClassNested + + + .anon_union_1 +-- .anon_union_1::anon_struct_1 + + + .anon_union_1 +-- .anon_union_1::anon_struct_2 + + + + @enduml :: @@ -2627,26 +2830,6 @@ The comparison takes into account the white space, indentation, etc. } - namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } - } - - - namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } - } - - enum Enum01 { VALUE_0 VALUE_1 @@ -2654,32 +2837,59 @@ The comparison takes into account the white space, indentation, etc. } - namespace Interface { - class Struct { - +a : int - } - } - - - class anon-union-1::anon-struct-1 { + class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } - class anon-union-1::anon-struct-2 { + class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } - class anon-union-1 { + class anon_union_1 { +vec : float } + namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } + } + + @@ -2688,9 +2898,10 @@ The comparison takes into account the white space, indentation, etc. .Class01 <|-- .Class02 - namespace Interface { - Class04 <|-- Class04_derived - } + Interface.Class04 <|-- Interface.Class04_derived + + + Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -2707,6 +2918,29 @@ The comparison takes into account the white space, indentation, etc. Interface.Class04 o-- .Class01 + Interface.Class04 *-- .Enum01 + + + Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + + Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + + + + + + /' Nested objects '/ + + .Class02 +-- .Class02::ClassNested + + + .anon_union_1 +-- .anon_union_1::anon_struct_1 + + + .anon_union_1 +-- .anon_union_1::anon_struct_2 + @@ -3038,7 +3272,7 @@ obtained using the source block described `sec-org-el-version`_. __title__ = "hpp2plantuml" __description__ = "Convert C++ header files to PlantUML" - __version__ = '0.7.1' + __version__ = '0.8.0' __uri__ = "https://github.com/thibaultmarin/hpp2plantuml" __doc__ = __description__ + " <" + __uri__ + ">" __author__ = "Thibault Marin" @@ -3553,9 +3787,9 @@ content of the file is mostly following the defaults, with a few exceptions: # built documents. # # The short X.Y version. - version = u'v' + u'0.7.1' + version = u'v' + u'0.8.0' # The full version, including alpha/beta/rc tags. - release = u'v' + u'0.7.1' + release = u'v' + u'0.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -3629,7 +3863,7 @@ content of the file is mostly following the defaults, with a few exceptions: # The name for this set of Sphinx documents. # " v documentation" by default. # - # html_title = u'hpp2plantuml ' + u'v' + u'0.7.1' + # html_title = u'hpp2plantuml ' + u'v' + u'0.8.0' # A shorter title for the navigation bar. Default is the same as html_title. # diff --git a/hpp2plantuml.org b/hpp2plantuml.org index 5f17d1a..3c99821 100644 --- a/hpp2plantuml.org +++ b/hpp2plantuml.org @@ -15,7 +15,7 @@ this single org-file. The current version of the code is: #+NAME: hpp2plantuml-version -: 0.7.1 +: 0.8.0 The source code can be found on GitHub: https://github.com/thibaultmarin/hpp2plantuml. @@ -169,17 +169,18 @@ LINK_TYPE_MAP = { 'inherit': '<|--', 'aggregation': 'o--', 'composition': '*--', - 'dependency': '<..' + 'dependency': '<..', + 'nesting': '+--' } # Association between object names and objects # - The first element is the object type name in the CppHeader object # - The second element is the iterator used to loop over objects # - The third element is a function returning the corresponding internal object -CONTAINER_TYPE_MAP = [ - ['classes', lambda objs: objs.items(), lambda obj: Class(obj)], - ['enums', lambda objs: objs, lambda obj: Enum(obj)] -] +CONTAINER_TYPE_MAP = { + 'classes': [lambda objs: objs.items(), lambda obj: Class(obj)], + 'enums': [lambda objs: objs, lambda obj: Enum(obj)] +} #+END_SRC @@ -234,11 +235,13 @@ class Container(object): Object name (with ``<``, ``>`` characters removed) """ self._container_type = container_type - self._name = re.sub('[<>]', '', name) + self._name = re.sub('[<>]', '', re.sub('-', '_', name)) self._member_list = [] - self._namespace = None + self._namespace = '' + self._parent = None - def get_name(self): + @property + def name(self): """Name property accessor Returns @@ -262,10 +265,19 @@ class Container(object): header_container : CppClass or CppEnum Parsed header for container """ - namespace = header_container.get('namespace', None) + namespace = header_container.get('namespace', '') if namespace: - if not header_container.get('parent', None): + parent = header_container.get('parent', None) + # Presence of namespace and parent fields indicates a nested class + if not parent: self._namespace = _cleanup_namespace(namespace) + else: + #self._parent = re.sub('[<>]', '', parent['name']) + self._parent = '::'.join(self._name.split('::')[:-1]) + p = parent + while p.get('parent') is not None: + p = p.get('parent', None) + self._namespace = p['namespace'] self._do_parse_members(header_container) def _do_parse_members(self, header_container): @@ -294,8 +306,6 @@ class Container(object): for member in self._member_list: container_str += '\t' + member.render() + '\n' container_str += '}\n' - if self._namespace is not None: - return wrap_namespace(container_str, self._namespace) return container_str def comparison_keys(self): @@ -420,7 +430,7 @@ class Class(Container): Parameters ---------- - header_class : list (str, CppClass) + header_class : tuple(str, CppClass) Parsed header for class object (two-element list where the first element is the class name and the second element is a CppClass object) @@ -744,7 +754,7 @@ class Enum(Container): This class defines a simple object inherited from the base `Container` class. It simply lists enumerated values. """ - def __init__(self, header_enum): + def __init__(self, header_enum, parent=None): """Constructor Parameters @@ -754,6 +764,8 @@ class Enum(Container): """ super().__init__('enum', header_enum.get('name', 'empty')) self.parse_members(header_enum) + if parent: + self._parent = parent def _do_parse_members(self, header_enum): """Extract enum values from header @@ -797,6 +809,48 @@ class EnumValue(ContainerMember): #+END_SRC +*** DONE Namespace + +C++ namespaces are represented by the ~Namespace~ class. It simply contains a +list of objects and wraps the objects in a =namespace= block on rendering. + +#+NAME: py-render-classes +#+BEGIN_SRC python +# %% Class object + + +class Namespace(list): + """Representation of C++ namespace + + This class lists other containers or namespaces and wraps the rendered + output in a ``namespace`` block. + """ + def __init__(self, name, *args): + """Constructor + + Parameters + ---------- + name : str + Namespace name + """ + self._name = name + super().__init__(*args) + + def render(self): + """Render namespace content + + Render the elements and wrap the result in a ``namespace`` block + + Returns + ------- + str + String representation of namespace in PlantUML syntax + """ + return wrap_namespace('\n'.join([c.render() + for c in self]), self._name) +#+END_SRC + + *** DONE Class relationships :PROPERTIES: :CUSTOM_ID: sec-module-relationship @@ -825,24 +879,23 @@ class ClassRelationship(object): This includes a parent/child pair and a relationship type (e.g. inheritance or aggregation). """ - def __init__(self, link_type, c_parent, c_child, flag_use_namespace=False): + def __init__(self, link_type, c_parent, c_child): """Constructor Parameters ---------- link_type : str Relationship type: ``inherit`` or ``aggregation`` - c_parent : str - Name of parent class - c_child : str - Name of child class + c_parent : Container + Parent container + c_child : Container + Child container """ - self._parent = c_parent.get_name() - self._child = c_child.get_name() + self._parent = c_parent.name + self._child = c_child.name self._link_type = link_type - self._parent_namespace = c_parent._namespace or None - self._child_namespace = c_child._namespace or None - self._flag_use_namespace = flag_use_namespace + self._parent_namespace = c_parent._namespace or '' + self._child_namespace = c_child._namespace or '' def comparison_keys(self): """Order comparison key between `ClassRelationship` objects @@ -857,7 +910,7 @@ class ClassRelationship(object): """ return self._parent, self._child, self._link_type - def _render_name(self, class_name, class_namespace, flag_use_namespace): + def _render_name(self, class_name, class_namespace): """Render class name with namespace prefix if necessary Parameters @@ -866,22 +919,13 @@ class ClassRelationship(object): Name of the class class_namespace : str Namespace or None if the class is defined in the default namespace - flag_use_namespace : bool - When False, do not use the namespace Returns ------- str Class name with appropriate prefix for use with link rendering """ - if not flag_use_namespace: - return class_name - - if class_namespace is None: - prefix = '.' - else: - prefix = class_namespace + '.' - return prefix + class_name + return get_namespace_link_name(class_namespace) + '.' + class_name def render(self): """Render class relationship to string @@ -898,26 +942,14 @@ class ClassRelationship(object): """ link_str = '' - # Wrap the link in namespace block (if both parent and child are in the - # same namespace) - namespace_wrap = None - if self._parent_namespace == self._child_namespace and \ - self._parent_namespace is not None: - namespace_wrap = self._parent_namespace - # Prepend the namespace to the class name - flag_render_namespace = self._flag_use_namespace and not namespace_wrap - parent_str = self._render_name(self._parent, self._parent_namespace, - flag_render_namespace) - child_str = self._render_name(self._child, self._child_namespace, - flag_render_namespace) + parent_str = self._render_name(self._parent, self._parent_namespace) + child_str = self._render_name(self._child, self._child_namespace) # Link string - link_str += parent_str + ' ' + self._render_link_type() + \ - ' ' + child_str + '\n' + link_str += (parent_str + ' ' + self._render_link_type() + ' ' + + child_str + '\n') - if namespace_wrap is not None: - return wrap_namespace(link_str, namespace_wrap) return link_str def _render_link_type(self): @@ -1062,6 +1094,39 @@ class ClassDependencyRelationship(ClassRelationship): #+END_SRC +**** DONE Nesting + +The nesting relationship handles nested objects (classes, enums). In PlantUML, +it corresponds to the =+..= link type (http://plantuml.com/class-diagram). + +#+NAME: py-class_nesting +#+BEGIN_SRC python +# %% Nested class + + +class ClassNestingRelationship(ClassRelationship): + """Nesting relationship + + Dependencies occur when member methods depend on an object of another class + in the diagram. + """ + def __init__(self, c_parent, c_child, **kwargs): + """Constructor + + Parameters + ---------- + c_parent : str + Class corresponding to the type of the member variable in the + nesting relationship + c_child : str + Child (or client) class of the dependency relationship + kwargs : dict + Additional parameters passed to parent class + """ + super().__init__('nesting', c_parent, c_child, **kwargs) +#+END_SRC + + *** DONE Diagram object :PROPERTIES: :CUSTOM_ID: sec-module-diagram @@ -1170,6 +1235,7 @@ class Diagram(object): self._inheritance_list = [] self._aggregation_list = [] self._dependency_list = [] + self._nesting_list = [] def _sort_list(input_list): """Sort list using `ClassRelationship` comparison @@ -1195,24 +1261,25 @@ class Diagram(object): Diagram._sort_list(self._inheritance_list) Diagram._sort_list(self._aggregation_list) Diagram._sort_list(self._dependency_list) + Diagram._sort_list(self._nesting_list) - def _build_helper(self, input, build_from='string', flag_build_lists=True, + def _build_helper(self, data_in, build_from='string', flag_build_lists=True, flag_reset=False): """Helper function to initialize a `Diagram` object from parsed headers Parameters ---------- - input : CppHeader or str or list(CppHeader) or list(str) + data_in : CppHeader or str or list(CppHeader) or list(str) Input of arbitrary type. The processing depends on the ``build_from`` parameter build_from : str - Determines the type of the ``input`` variable: + Determines the type of the ``data_in`` variable: - ,* ``string``: ``input`` is a string containing C++ header code - ,* ``file``: ``input`` is a filename to parse - ,* ``string_list``: ``input`` is a list of strings containing C++ + ,* ``string``: ``data_in`` is a string containing C++ header code + ,* ``file``: ``data_in`` is a filename to parse + ,* ``string_list``: ``data_in`` is a list of strings containing C++ header code - ,* ``file_list``: ``input`` is a list of filenames to parse + ,* ``file_list``: ``data_in`` is a list of filenames to parse flag_build_lists : bool When True, relationships lists are built and the objects in the @@ -1225,10 +1292,10 @@ class Diagram(object): if flag_reset: self.clear() if build_from in ('string', 'file'): - self.parse_objects(input, build_from) + self.parse_objects(data_in, build_from) elif build_from in ('string_list', 'file_list'): build_from_single = re.sub('_list$', '', build_from) - for single_input in input: + for single_input in data_in: self.parse_objects(single_input, build_from_single) if flag_build_lists: self.build_relationship_lists() @@ -1319,6 +1386,7 @@ class Diagram(object): """ self.build_inheritance_list() self.build_aggregation_list() + self.build_nesting_list() if self._flag_dep: self.build_dependency_list() @@ -1334,17 +1402,32 @@ class Diagram(object): A string containing C++ header code or a filename with C++ header code arg_type : str - It set to ``string``, ``header_file`` is considered to be a string, + If set to ``string``, ``header_file`` is considered to be a string, otherwise, it is assumed to be a filename """ # Parse header file parsed_header = CppHeaderParser.CppHeader(header_file, argType=arg_type) - for container_type, container_iterator, \ - container_handler in CONTAINER_TYPE_MAP: + for container_type, (container_iterator, + container_handler) in CONTAINER_TYPE_MAP.items(): objects = parsed_header.__getattribute__(container_type) for obj in container_iterator(objects): - self._objects.append(container_handler(obj)) + # Parse container + obj_c = container_handler(obj) + self._objects.append(obj_c) + # Look for nested enums + # Find value from iterator (may be a tuple) + if isinstance(obj, tuple) and len(obj) == 2: + obj_n = obj[-1] + else: + obj_n = obj + if 'enums' in obj_n: + for m in MEMBER_PROP_MAP.keys(): + for enum in obj_n['enums'][m]: + enum_c = Enum(enum, parent=obj_c.name) + # Adjust name to reflect nesting + enum_c._name = obj_c.name + '::' + enum_c._name + self._objects.append(enum_c) def _make_class_list(self): """Build list of classes @@ -1355,8 +1438,24 @@ class Diagram(object): Each entry is a dictionary with keys ``name`` (class name) and ``obj`` the instance of the `Class` class """ - return [{'name': obj.get_name(), 'obj': obj} - for obj in self._objects if isinstance(obj, Class)] + return [{'name': obj.name, 'obj': obj} + for obj in self._objects if isinstance(obj, (Class, Enum))] + + def _get_class_list(self): + """Build list of classes in diagram + + Returns + ------- + list + Class object list (returned by :func:`_make_class_list`) + list + Class names + bool + True when at least one container is a namespace + """ + class_list_obj = self._make_class_list() + class_list = [c['name'] for c in class_list_obj] + return class_list_obj, class_list def build_inheritance_list(self): """Build list of inheritance between objects @@ -1371,15 +1470,13 @@ class Diagram(object): """ self._inheritance_list = [] # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Inheritance for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): for parent in obj.build_inheritance_list(): if parent in class_list: @@ -1387,8 +1484,7 @@ class Diagram(object): class_list.index(parent)]['obj'] self._inheritance_list.append( ClassInheritanceRelationship( - parent_obj, obj, - flag_use_namespace=flag_use_namespace)) + parent_obj, obj)) def build_aggregation_list(self): """Build list of aggregation relationships @@ -1403,16 +1499,13 @@ class Diagram(object): for each relationships, using the calculated count. """ self._aggregation_list = [] - # Build list of classes in diagram - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() # Build member type list variable_type_list = {} for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): variable_type_list[obj_name] = obj.build_variable_type_list() # Create aggregation links @@ -1438,8 +1531,7 @@ class Diagram(object): self._aggregation_list.append( ClassAggregationRelationship( obj_class_obj, comp_parent_obj, comp_count, - rel_type=rel_type, - flag_use_namespace=flag_use_namespace)) + rel_type=rel_type)) def build_dependency_list(self): """Build list of dependency between objects @@ -1453,17 +1545,14 @@ class Diagram(object): """ self._dependency_list = [] - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Add all objects name to list objects_name = [] for obj in self._objects: - objects_name.append(obj.get_name()) + objects_name.append(obj.name) # Dependency for obj in self._objects: @@ -1480,14 +1569,31 @@ class Diagram(object): for o in objects_name].index(True) except ValueError: pass - if index != ValueError and \ - method[0] != obj.get_name(): + if index != ValueError and method[0] != obj.name: depend_obj = self._objects[index] self._dependency_list.append( ClassDependencyRelationship( - depend_obj, obj, - flag_use_namespace=flag_use_namespace)) + depend_obj, obj)) + + def build_nesting_list(self): + """Build list of nested objects + + """ + self._nesting_list = [] + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() + + for obj in self._objects: + obj_name = obj.name + if isinstance(obj, (Class, Enum)): + parent = obj._parent + if parent and parent in class_list: + parent_obj = class_list_obj[ + class_list.index(parent)]['obj'] + self._nesting_list.append( + ClassNestingRelationship( + parent_obj, obj)) def _augment_comp(self, c_dict, c_parent, c_child, rel_type='aggregation'): """Increment the aggregation reference count @@ -1533,10 +1639,39 @@ class Diagram(object): object, including objects and object relationships """ template = self._env.get_template(self._template_file) - return template.render(objects=self._objects, + # List namespaces + ns_list = [] + for obj in self._objects: + if obj._namespace and obj._namespace not in ns_list: + ns_list.append(obj._namespace) + # Ensure nested namespaces are processed first + ns_list = sorted(ns_list, key=lambda ns: len(ns.split('::')), + reverse=True) + # Create namespace objects (flat map) + ns_obj_map = {ns: Namespace(ns) for ns in ns_list} + # Build list of objects + objects_out = [] + # 1. Place objects in namespace container or in output list + for obj in self._objects: + if obj._namespace: + ns_obj_map[obj._namespace].append(obj) + else: + objects_out.append(obj) + # 2. Add namespaces: collapse nested namespaces and add top level + # namespaces to output list + for ns in ns_list: + ns_name_parts = ns.split('::') + if len(ns_name_parts) > 1: + ns_parent = '::'.join(ns_name_parts[:-1]) + ns_obj_map[ns_parent].append(ns_obj_map[ns]) + else: + objects_out.append(ns_obj_map[ns]) + # Render + return template.render(objects=objects_out, inheritance_list=self._inheritance_list, aggregation_list=self._aggregation_list, dependency_list=self._dependency_list, + nesting_list=self._nesting_list, flag_dep=self._flag_dep) #+END_SRC @@ -1579,8 +1714,8 @@ def _cleanup_type(type_str): def _cleanup_namespace(ns_str): """Cleanup string representing a C++ namespace - Cleanup simply consists in removing leading and trailing colon characters - (``:``), and ``<>`` blocks. + Cleanup simply consists in removing ``<>`` blocks and trailing ``:`` + characters. Parameters ---------- @@ -1592,10 +1727,9 @@ def _cleanup_namespace(ns_str): str The namespace string after cleanup """ - return re.sub('<([^>]+)>', r'\1', - re.sub('(.+)<[^>]+>', r'\1', - re.sub('^:+', '', - re.sub(':+$', '', ns_str)))) + return re.sub(':+$', '', + re.sub('<([^>]+)>', r'\1', + re.sub('(.+)<[^>]+>', r'\1', ns_str))) #+END_SRC The ~_cleanup_single_line~ function transforms a multiline input string into a @@ -1683,10 +1817,31 @@ def wrap_namespace(input_str, namespace): ``input_str`` wrapped in ``namespace`` block """ return 'namespace {} {{\n'.format(namespace) + \ - '\n'.join([re.sub('^', '\t', line) + '\n'.join([re.sub('^', '\t', line) if line else line for line in input_str.splitlines()]) + \ '\n}\n' +def get_namespace_link_name(namespace): + """Generate namespace string for link + + Parameters + ---------- + namespace : str + Namespace name (in the form ``nested::ns``) + + Returns + ------- + str + The namespace name formatted for use in links + (e.g. ``nested.nested::ns``) + """ + if not namespace: + return '' + ns_list = namespace.split('::') + ns_list_out = [ns_list[0], ] + for ni, ns in enumerate(ns_list[1:]): + ns_list_out.append('{}::{}'.format(ns_list_out[ni - 1], ns)) + return '.'.join(ns_list_out) #+END_SRC @@ -1783,6 +1938,13 @@ default template is as follows: {% endblock %} {% endif %} +{% block nested %} +/' Nested objects '/ +{% for link in nesting_list %} +{{ link.render() }} +{% endfor %} +{% endblock %} + @enduml #+end_src @@ -2038,7 +2200,7 @@ class TestContainer: c_type = "container_type" c_name = "container_name" c_obj = hpp2plantuml.hpp2plantuml.Container(c_type, c_name) - nt.assert_equal(c_obj.get_name(), c_name) + nt.assert_equal(c_obj.name, c_name) nt.assert_equal(c_obj.render(), 'container_type container_name {\n}\n') def test_comparison_keys(self): @@ -2055,7 +2217,7 @@ class TestContainer: c_obj_list.sort(key=lambda obj: obj.comparison_keys()) for i in range(len(c_list)): - nt.assert_equal(c_obj_list[i].get_name(), + nt.assert_equal(c_obj_list[i].name, c_list[ref_sort_idx[i]][1]) #+END_SRC @@ -2157,7 +2319,7 @@ Table{{{tt}}}[[tbl-unittest-class]]. It includes templates and abstract classes | "class Test\n{\npublic:\nvirtual int func() = 0; };" | "abstract class Test {\n\t+{abstract} func() : int\n}\n" | | "template class Test{\nT* func(T& arg); };" | "class Test > {\n\t-func(T& arg) : T*\n}\n" | | "template class Test{\nvirtual T* func(T& arg)=0; };" | "abstract class Test > {\n\t-{abstract} func(T& arg) : T*\n}\n" | -| "namespace Interface {\nclass Test {\nprotected:\nint & member; };};" | "namespace Interface {\n\tclass Test {\n\t\t#member : int&\n\t}\n}\n" | +| "namespace Interface {\nclass Test {\nprotected:\nint & member; };};" | "class Test {\n\t#member : int&\n}\n" | #+NAME: test-unit-class #+BEGIN_SRC python :var test_list_class=tbl-unittest-class @@ -2222,16 +2384,16 @@ relationships (with and without count). #+NAME: tbl-unittest-link #+CAPTION: List of test segments and corresponding PlantUML strings. -| C++ | plantuml | -|------------------------------------------------------+----------------------------------| -| "class A{};\nclass B : A{};" | "A <@-- B\n" | -| "class A{};\nclass B : public A{};" | "A <@-- B\n" | -| "class B{};\nclass A{B obj;};" | "A *-- B\n" | -| "class B{};\nclass A{B* obj;};" | "A o-- B\n" | -| "class B{};\nclass A{B * obj_ptr; B* ptr;};" | "A \"2\" o-- B\n" | -| "class A{};\nclass B{void Method(A* obj);};" | "A <.. B\n" | -| "namespace T {class A{}; class B: A{};};" | "namespace T {\n\tA <@-- B\n}\n" | -| "namespace T {\nclass A{};};\nclass B{T::A* _obj;};" | ".B o-- T.A\n" | +| C++ | plantuml | +|------------------------------------------------------+---------------------| +| "class A{};\nclass B : A{};" | ".A <@-- .B\n" | +| "class A{};\nclass B : public A{};" | ".A <@-- .B\n" | +| "class B{};\nclass A{B obj;};" | ".A *-- .B\n" | +| "class B{};\nclass A{B* obj;};" | ".A o-- .B\n" | +| "class B{};\nclass A{B * obj_ptr; B* ptr;};" | ".A \"2\" o-- .B\n" | +| "class A{};\nclass B{void Method(A* obj);};" | ".A <.. .B\n" | +| "namespace T {class A{}; class B: A{};};" | "T.A <@-- T.B\n" | +| "namespace T {\nclass A{};};\nclass B{T::A* _obj;};" | ".B o-- T.A\n" | #+NAME: test-unit-link @@ -2332,6 +2494,7 @@ namespace Interface { bool _flag; Class01* _obj; T _var; + Enum01 _val; }; class Class04_derived : public Class04 { @@ -2345,6 +2508,15 @@ namespace Interface { struct Struct { int a; }; + enum Enum { A, B }; + + namespace NestedNamespace { + class Class04_ns : private Class04_derived { + protected: + Struct _s; + Enum _e; + }; + }; }; // Anonymous union (issue #9) @@ -2364,7 +2536,6 @@ union { #+END_SRC - *** DONE Reference output :PROPERTIES: :CUSTOM_ID: sec-test-system-ref @@ -2373,7 +2544,6 @@ union { Following is the reference output for the input header files defined [[#sec-test-system-hpp][earlier]]. The comparison takes into account the white space, indentation, etc. - #+NAME: puml-simple-classes #+BEGIN_SRC plantuml :mkdirp yes :tangle tests/simple_classes.puml @startuml @@ -2421,26 +2591,6 @@ class Class03 > { } -namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } -} - - -namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } -} - - enum Enum01 { VALUE_0 VALUE_1 @@ -2448,32 +2598,59 @@ enum Enum01 { } -namespace Interface { - class Struct { - +a : int - } -} - - -class anon-union-1::anon-struct-1 { +class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } -class anon-union-1::anon-struct-2 { +class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } -class anon-union-1 { +class anon_union_1 { +vec : float } +namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } +} + + @@ -2482,9 +2659,10 @@ class anon-union-1 { .Class01 <|-- .Class02 -namespace Interface { - Class04 <|-- Class04_derived -} +Interface.Class04 <|-- Interface.Class04_derived + + +Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -2501,6 +2679,15 @@ namespace Interface { Interface.Class04 o-- .Class01 +Interface.Class04 *-- .Enum01 + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + @@ -2513,6 +2700,20 @@ Interface.Class04 <.. .Class03 + +/' Nested objects '/ + +.Class02 +-- .Class02::ClassNested + + +.anon_union_1 +-- .anon_union_1::anon_struct_1 + + +.anon_union_1 +-- .anon_union_1::anon_struct_2 + + + + @enduml #+END_SRC @@ -2563,26 +2764,6 @@ class Class03 > { } -namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } -} - - -namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } -} - - enum Enum01 { VALUE_0 VALUE_1 @@ -2590,32 +2771,59 @@ enum Enum01 { } -namespace Interface { - class Struct { - +a : int - } -} - - -class anon-union-1::anon-struct-1 { +class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } -class anon-union-1::anon-struct-2 { +class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } -class anon-union-1 { +class anon_union_1 { +vec : float } +namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } +} + + @@ -2624,9 +2832,10 @@ class anon-union-1 { .Class01 <|-- .Class02 -namespace Interface { - Class04 <|-- Class04_derived -} +Interface.Class04 <|-- Interface.Class04_derived + + +Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -2643,6 +2852,29 @@ namespace Interface { Interface.Class04 o-- .Class01 +Interface.Class04 *-- .Enum01 + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + + + + + +/' Nested objects '/ + +.Class02 +-- .Class02::ClassNested + + +.anon_union_1 +-- .anon_union_1::anon_struct_1 + + +.anon_union_1 +-- .anon_union_1::anon_struct_2 + @@ -3682,7 +3914,6 @@ encapsulated in the following source block. #+END_SRC - * Notes :noexport: - [-] Add tests @@ -3709,6 +3940,19 @@ encapsulated in the following source block. - [X] creates documentation rst (and README.rst) - [X] Link to org-mode rst documentation from index.rst -> Use only org-mode rst? +** v0.8 + +*** Bug fixes + +- Fix ignored enums when nested in classes +- Fix missing aggregation link with enums + +*** Features + +- Add nesting links +- Refactor namespace support +- Prevent dashes in object name (e.g. anonymous union/structure) + ** v0.7 *** DONE Handle =delete= keyword diff --git a/src/hpp2plantuml/__init__.py b/src/hpp2plantuml/__init__.py index 420e5e2..05b5da9 100644 --- a/src/hpp2plantuml/__init__.py +++ b/src/hpp2plantuml/__init__.py @@ -106,7 +106,7 @@ __title__ = "hpp2plantuml" __description__ = "Convert C++ header files to PlantUML" -__version__ = '0.7.1' +__version__ = '0.8.0' __uri__ = "https://github.com/thibaultmarin/hpp2plantuml" __doc__ = __description__ + " <" + __uri__ + ">" __author__ = "Thibault Marin" diff --git a/src/hpp2plantuml/hpp2plantuml.py b/src/hpp2plantuml/hpp2plantuml.py index 500125b..89e24ca 100644 --- a/src/hpp2plantuml/hpp2plantuml.py +++ b/src/hpp2plantuml/hpp2plantuml.py @@ -22,17 +22,18 @@ 'inherit': '<|--', 'aggregation': 'o--', 'composition': '*--', - 'dependency': '<..' + 'dependency': '<..', + 'nesting': '+--' } # Association between object names and objects # - The first element is the object type name in the CppHeader object # - The second element is the iterator used to loop over objects # - The third element is a function returning the corresponding internal object -CONTAINER_TYPE_MAP = [ - ['classes', lambda objs: objs.items(), lambda obj: Class(obj)], - ['enums', lambda objs: objs, lambda obj: Enum(obj)] -] +CONTAINER_TYPE_MAP = { + 'classes': [lambda objs: objs.items(), lambda obj: Class(obj)], + 'enums': [lambda objs: objs, lambda obj: Enum(obj)] +} # %% Base classes @@ -54,11 +55,13 @@ def __init__(self, container_type, name): Object name (with ``<``, ``>`` characters removed) """ self._container_type = container_type - self._name = re.sub('[<>]', '', name) + self._name = re.sub('[<>]', '', re.sub('-', '_', name)) self._member_list = [] - self._namespace = None + self._namespace = '' + self._parent = None - def get_name(self): + @property + def name(self): """Name property accessor Returns @@ -82,10 +85,19 @@ def parse_members(self, header_container): header_container : CppClass or CppEnum Parsed header for container """ - namespace = header_container.get('namespace', None) + namespace = header_container.get('namespace', '') if namespace: - if not header_container.get('parent', None): + parent = header_container.get('parent', None) + # Presence of namespace and parent fields indicates a nested class + if not parent: self._namespace = _cleanup_namespace(namespace) + else: + #self._parent = re.sub('[<>]', '', parent['name']) + self._parent = '::'.join(self._name.split('::')[:-1]) + p = parent + while p.get('parent') is not None: + p = p.get('parent', None) + self._namespace = p['namespace'] self._do_parse_members(header_container) def _do_parse_members(self, header_container): @@ -114,8 +126,6 @@ def render(self): for member in self._member_list: container_str += '\t' + member.render() + '\n' container_str += '}\n' - if self._namespace is not None: - return wrap_namespace(container_str, self._namespace) return container_str def comparison_keys(self): @@ -221,7 +231,7 @@ def __init__(self, header_class): Parameters ---------- - header_class : list (str, CppClass) + header_class : tuple(str, CppClass) Parsed header for class object (two-element list where the first element is the class name and the second element is a CppClass object) @@ -485,7 +495,7 @@ class Enum(Container): This class defines a simple object inherited from the base `Container` class. It simply lists enumerated values. """ - def __init__(self, header_enum): + def __init__(self, header_enum, parent=None): """Constructor Parameters @@ -495,6 +505,8 @@ def __init__(self, header_enum): """ super().__init__('enum', header_enum.get('name', 'empty')) self.parse_members(header_enum) + if parent: + self._parent = parent def _do_parse_members(self, header_enum): """Extract enum values from header @@ -536,6 +548,39 @@ def render(self): """ return self._name +# %% Class object + + +class Namespace(list): + """Representation of C++ namespace + + This class lists other containers or namespaces and wraps the rendered + output in a ``namespace`` block. + """ + def __init__(self, name, *args): + """Constructor + + Parameters + ---------- + name : str + Namespace name + """ + self._name = name + super().__init__(*args) + + def render(self): + """Render namespace content + + Render the elements and wrap the result in a ``namespace`` block + + Returns + ------- + str + String representation of namespace in PlantUML syntax + """ + return wrap_namespace('\n'.join([c.render() + for c in self]), self._name) + # %% Class connections @@ -546,24 +591,23 @@ class ClassRelationship(object): This includes a parent/child pair and a relationship type (e.g. inheritance or aggregation). """ - def __init__(self, link_type, c_parent, c_child, flag_use_namespace=False): + def __init__(self, link_type, c_parent, c_child): """Constructor Parameters ---------- link_type : str Relationship type: ``inherit`` or ``aggregation`` - c_parent : str - Name of parent class - c_child : str - Name of child class + c_parent : Container + Parent container + c_child : Container + Child container """ - self._parent = c_parent.get_name() - self._child = c_child.get_name() + self._parent = c_parent.name + self._child = c_child.name self._link_type = link_type - self._parent_namespace = c_parent._namespace or None - self._child_namespace = c_child._namespace or None - self._flag_use_namespace = flag_use_namespace + self._parent_namespace = c_parent._namespace or '' + self._child_namespace = c_child._namespace or '' def comparison_keys(self): """Order comparison key between `ClassRelationship` objects @@ -578,7 +622,7 @@ def comparison_keys(self): """ return self._parent, self._child, self._link_type - def _render_name(self, class_name, class_namespace, flag_use_namespace): + def _render_name(self, class_name, class_namespace): """Render class name with namespace prefix if necessary Parameters @@ -587,22 +631,13 @@ def _render_name(self, class_name, class_namespace, flag_use_namespace): Name of the class class_namespace : str Namespace or None if the class is defined in the default namespace - flag_use_namespace : bool - When False, do not use the namespace Returns ------- str Class name with appropriate prefix for use with link rendering """ - if not flag_use_namespace: - return class_name - - if class_namespace is None: - prefix = '.' - else: - prefix = class_namespace + '.' - return prefix + class_name + return get_namespace_link_name(class_namespace) + '.' + class_name def render(self): """Render class relationship to string @@ -619,26 +654,14 @@ def render(self): """ link_str = '' - # Wrap the link in namespace block (if both parent and child are in the - # same namespace) - namespace_wrap = None - if self._parent_namespace == self._child_namespace and \ - self._parent_namespace is not None: - namespace_wrap = self._parent_namespace - # Prepend the namespace to the class name - flag_render_namespace = self._flag_use_namespace and not namespace_wrap - parent_str = self._render_name(self._parent, self._parent_namespace, - flag_render_namespace) - child_str = self._render_name(self._child, self._child_namespace, - flag_render_namespace) + parent_str = self._render_name(self._parent, self._parent_namespace) + child_str = self._render_name(self._child, self._child_namespace) # Link string - link_str += parent_str + ' ' + self._render_link_type() + \ - ' ' + child_str + '\n' + link_str += (parent_str + ' ' + self._render_link_type() + ' ' + + child_str + '\n') - if namespace_wrap is not None: - return wrap_namespace(link_str, namespace_wrap) return link_str def _render_link_type(self): @@ -747,6 +770,30 @@ def __init__(self, c_parent, c_child, **kwargs): """ super().__init__('dependency', c_parent, c_child, **kwargs) +# %% Nested class + + +class ClassNestingRelationship(ClassRelationship): + """Nesting relationship + + Dependencies occur when member methods depend on an object of another class + in the diagram. + """ + def __init__(self, c_parent, c_child, **kwargs): + """Constructor + + Parameters + ---------- + c_parent : str + Class corresponding to the type of the member variable in the + nesting relationship + c_child : str + Child (or client) class of the dependency relationship + kwargs : dict + Additional parameters passed to parent class + """ + super().__init__('nesting', c_parent, c_child, **kwargs) + # %% Diagram class @@ -789,6 +836,7 @@ def clear(self): self._inheritance_list = [] self._aggregation_list = [] self._dependency_list = [] + self._nesting_list = [] def _sort_list(input_list): """Sort list using `ClassRelationship` comparison @@ -814,24 +862,25 @@ def sort_elements(self): Diagram._sort_list(self._inheritance_list) Diagram._sort_list(self._aggregation_list) Diagram._sort_list(self._dependency_list) + Diagram._sort_list(self._nesting_list) - def _build_helper(self, input, build_from='string', flag_build_lists=True, + def _build_helper(self, data_in, build_from='string', flag_build_lists=True, flag_reset=False): """Helper function to initialize a `Diagram` object from parsed headers Parameters ---------- - input : CppHeader or str or list(CppHeader) or list(str) + data_in : CppHeader or str or list(CppHeader) or list(str) Input of arbitrary type. The processing depends on the ``build_from`` parameter build_from : str - Determines the type of the ``input`` variable: + Determines the type of the ``data_in`` variable: - * ``string``: ``input`` is a string containing C++ header code - * ``file``: ``input`` is a filename to parse - * ``string_list``: ``input`` is a list of strings containing C++ + * ``string``: ``data_in`` is a string containing C++ header code + * ``file``: ``data_in`` is a filename to parse + * ``string_list``: ``data_in`` is a list of strings containing C++ header code - * ``file_list``: ``input`` is a list of filenames to parse + * ``file_list``: ``data_in`` is a list of filenames to parse flag_build_lists : bool When True, relationships lists are built and the objects in the @@ -844,10 +893,10 @@ def _build_helper(self, input, build_from='string', flag_build_lists=True, if flag_reset: self.clear() if build_from in ('string', 'file'): - self.parse_objects(input, build_from) + self.parse_objects(data_in, build_from) elif build_from in ('string_list', 'file_list'): build_from_single = re.sub('_list$', '', build_from) - for single_input in input: + for single_input in data_in: self.parse_objects(single_input, build_from_single) if flag_build_lists: self.build_relationship_lists() @@ -938,6 +987,7 @@ def build_relationship_lists(self): """ self.build_inheritance_list() self.build_aggregation_list() + self.build_nesting_list() if self._flag_dep: self.build_dependency_list() @@ -953,17 +1003,32 @@ def parse_objects(self, header_file, arg_type='string'): A string containing C++ header code or a filename with C++ header code arg_type : str - It set to ``string``, ``header_file`` is considered to be a string, + If set to ``string``, ``header_file`` is considered to be a string, otherwise, it is assumed to be a filename """ # Parse header file parsed_header = CppHeaderParser.CppHeader(header_file, argType=arg_type) - for container_type, container_iterator, \ - container_handler in CONTAINER_TYPE_MAP: + for container_type, (container_iterator, + container_handler) in CONTAINER_TYPE_MAP.items(): objects = parsed_header.__getattribute__(container_type) for obj in container_iterator(objects): - self._objects.append(container_handler(obj)) + # Parse container + obj_c = container_handler(obj) + self._objects.append(obj_c) + # Look for nested enums + # Find value from iterator (may be a tuple) + if isinstance(obj, tuple) and len(obj) == 2: + obj_n = obj[-1] + else: + obj_n = obj + if 'enums' in obj_n: + for m in MEMBER_PROP_MAP.keys(): + for enum in obj_n['enums'][m]: + enum_c = Enum(enum, parent=obj_c.name) + # Adjust name to reflect nesting + enum_c._name = obj_c.name + '::' + enum_c._name + self._objects.append(enum_c) def _make_class_list(self): """Build list of classes @@ -974,8 +1039,24 @@ def _make_class_list(self): Each entry is a dictionary with keys ``name`` (class name) and ``obj`` the instance of the `Class` class """ - return [{'name': obj.get_name(), 'obj': obj} - for obj in self._objects if isinstance(obj, Class)] + return [{'name': obj.name, 'obj': obj} + for obj in self._objects if isinstance(obj, (Class, Enum))] + + def _get_class_list(self): + """Build list of classes in diagram + + Returns + ------- + list + Class object list (returned by :func:`_make_class_list`) + list + Class names + bool + True when at least one container is a namespace + """ + class_list_obj = self._make_class_list() + class_list = [c['name'] for c in class_list_obj] + return class_list_obj, class_list def build_inheritance_list(self): """Build list of inheritance between objects @@ -990,15 +1071,13 @@ def build_inheritance_list(self): """ self._inheritance_list = [] # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Inheritance for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): for parent in obj.build_inheritance_list(): if parent in class_list: @@ -1006,8 +1085,7 @@ def build_inheritance_list(self): class_list.index(parent)]['obj'] self._inheritance_list.append( ClassInheritanceRelationship( - parent_obj, obj, - flag_use_namespace=flag_use_namespace)) + parent_obj, obj)) def build_aggregation_list(self): """Build list of aggregation relationships @@ -1022,16 +1100,13 @@ def build_aggregation_list(self): for each relationships, using the calculated count. """ self._aggregation_list = [] - # Build list of classes in diagram - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() # Build member type list variable_type_list = {} for obj in self._objects: - obj_name = obj.get_name() + obj_name = obj.name if isinstance(obj, Class): variable_type_list[obj_name] = obj.build_variable_type_list() # Create aggregation links @@ -1057,8 +1132,7 @@ def build_aggregation_list(self): self._aggregation_list.append( ClassAggregationRelationship( obj_class_obj, comp_parent_obj, comp_count, - rel_type=rel_type, - flag_use_namespace=flag_use_namespace)) + rel_type=rel_type)) def build_dependency_list(self): """Build list of dependency between objects @@ -1072,17 +1146,14 @@ def build_dependency_list(self): """ self._dependency_list = [] - # Build list of classes in diagram - class_list_obj = self._make_class_list() - class_list = [c['name'] for c in class_list_obj] - flag_use_namespace = any([c['obj']._namespace for c in class_list_obj]) + class_list_obj, class_list = self._get_class_list() # Create relationships # Add all objects name to list objects_name = [] for obj in self._objects: - objects_name.append(obj.get_name()) + objects_name.append(obj.name) # Dependency for obj in self._objects: @@ -1099,14 +1170,31 @@ def build_dependency_list(self): for o in objects_name].index(True) except ValueError: pass - if index != ValueError and \ - method[0] != obj.get_name(): + if index != ValueError and method[0] != obj.name: depend_obj = self._objects[index] self._dependency_list.append( ClassDependencyRelationship( - depend_obj, obj, - flag_use_namespace=flag_use_namespace)) + depend_obj, obj)) + + def build_nesting_list(self): + """Build list of nested objects + + """ + self._nesting_list = [] + # Build list of classes in diagram + class_list_obj, class_list = self._get_class_list() + + for obj in self._objects: + obj_name = obj.name + if isinstance(obj, (Class, Enum)): + parent = obj._parent + if parent and parent in class_list: + parent_obj = class_list_obj[ + class_list.index(parent)]['obj'] + self._nesting_list.append( + ClassNestingRelationship( + parent_obj, obj)) def _augment_comp(self, c_dict, c_parent, c_child, rel_type='aggregation'): """Increment the aggregation reference count @@ -1152,10 +1240,39 @@ def render(self): object, including objects and object relationships """ template = self._env.get_template(self._template_file) - return template.render(objects=self._objects, + # List namespaces + ns_list = [] + for obj in self._objects: + if obj._namespace and obj._namespace not in ns_list: + ns_list.append(obj._namespace) + # Ensure nested namespaces are processed first + ns_list = sorted(ns_list, key=lambda ns: len(ns.split('::')), + reverse=True) + # Create namespace objects (flat map) + ns_obj_map = {ns: Namespace(ns) for ns in ns_list} + # Build list of objects + objects_out = [] + # 1. Place objects in namespace container or in output list + for obj in self._objects: + if obj._namespace: + ns_obj_map[obj._namespace].append(obj) + else: + objects_out.append(obj) + # 2. Add namespaces: collapse nested namespaces and add top level + # namespaces to output list + for ns in ns_list: + ns_name_parts = ns.split('::') + if len(ns_name_parts) > 1: + ns_parent = '::'.join(ns_name_parts[:-1]) + ns_obj_map[ns_parent].append(ns_obj_map[ns]) + else: + objects_out.append(ns_obj_map[ns]) + # Render + return template.render(objects=objects_out, inheritance_list=self._inheritance_list, aggregation_list=self._aggregation_list, dependency_list=self._dependency_list, + nesting_list=self._nesting_list, flag_dep=self._flag_dep) # %% Cleanup object type string @@ -1184,8 +1301,8 @@ def _cleanup_type(type_str): def _cleanup_namespace(ns_str): """Cleanup string representing a C++ namespace - Cleanup simply consists in removing leading and trailing colon characters - (``:``), and ``<>`` blocks. + Cleanup simply consists in removing ``<>`` blocks and trailing ``:`` + characters. Parameters ---------- @@ -1197,10 +1314,9 @@ def _cleanup_namespace(ns_str): str The namespace string after cleanup """ - return re.sub('<([^>]+)>', r'\1', - re.sub('(.+)<[^>]+>', r'\1', - re.sub('^:+', '', - re.sub(':+$', '', ns_str)))) + return re.sub(':+$', '', + re.sub('<([^>]+)>', r'\1', + re.sub('(.+)<[^>]+>', r'\1', ns_str))) # %% Single line version of string @@ -1264,10 +1380,32 @@ def wrap_namespace(input_str, namespace): ``input_str`` wrapped in ``namespace`` block """ return 'namespace {} {{\n'.format(namespace) + \ - '\n'.join([re.sub('^', '\t', line) + '\n'.join([re.sub('^', '\t', line) if line else line for line in input_str.splitlines()]) + \ '\n}\n' +def get_namespace_link_name(namespace): + """Generate namespace string for link + + Parameters + ---------- + namespace : str + Namespace name (in the form ``nested::ns``) + + Returns + ------- + str + The namespace name formatted for use in links + (e.g. ``nested.nested::ns``) + """ + if not namespace: + return '' + ns_list = namespace.split('::') + ns_list_out = [ns_list[0], ] + for ni, ns in enumerate(ns_list[1:]): + ns_list_out.append('{}::{}'.format(ns_list_out[ni - 1], ns)) + return '.'.join(ns_list_out) + # %% Main function @@ -1328,7 +1466,7 @@ def main(): required=False, default=None, metavar='JINJA-FILE', help='path to jinja2 template file') parser.add_argument('--version', action='version', - version='%(prog)s ' + '0.7.1') + version='%(prog)s ' + '0.8.0') args = parser.parse_args() if len(args.input_files) > 0: CreatePlantUMLFile(args.input_files, args.output_file, diff --git a/src/hpp2plantuml/templates/default.puml b/src/hpp2plantuml/templates/default.puml index 51bdd89..1a2ca18 100644 --- a/src/hpp2plantuml/templates/default.puml +++ b/src/hpp2plantuml/templates/default.puml @@ -33,4 +33,11 @@ {% endblock %} {% endif %} +{% block nested %} +/' Nested objects '/ +{% for link in nesting_list %} +{{ link.render() }} +{% endfor %} +{% endblock %} + @enduml diff --git a/tests/simple_classes.puml b/tests/simple_classes.puml index f669615..bd4037c 100644 --- a/tests/simple_classes.puml +++ b/tests/simple_classes.puml @@ -43,26 +43,6 @@ class Class03 > { } -namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } -} - - -namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } -} - - enum Enum01 { VALUE_0 VALUE_1 @@ -70,32 +50,59 @@ enum Enum01 { } -namespace Interface { - class Struct { - +a : int - } -} - - -class anon-union-1::anon-struct-1 { +class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } -class anon-union-1::anon-struct-2 { +class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } -class anon-union-1 { +class anon_union_1 { +vec : float } +namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } +} + + @@ -104,9 +111,10 @@ class anon-union-1 { .Class01 <|-- .Class02 -namespace Interface { - Class04 <|-- Class04_derived -} +Interface.Class04 <|-- Interface.Class04_derived + + +Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -123,6 +131,15 @@ namespace Interface { Interface.Class04 o-- .Class01 +Interface.Class04 *-- .Enum01 + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + @@ -135,4 +152,18 @@ Interface.Class04 <.. .Class03 + +/' Nested objects '/ + +.Class02 +-- .Class02::ClassNested + + +.anon_union_1 +-- .anon_union_1::anon_struct_1 + + +.anon_union_1 +-- .anon_union_1::anon_struct_2 + + + + @enduml diff --git a/tests/simple_classes_3.hpp b/tests/simple_classes_3.hpp index 4fb2724..1989b75 100644 --- a/tests/simple_classes_3.hpp +++ b/tests/simple_classes_3.hpp @@ -21,6 +21,7 @@ namespace Interface { bool _flag; Class01* _obj; T _var; + Enum01 _val; }; class Class04_derived : public Class04 { @@ -34,6 +35,15 @@ namespace Interface { struct Struct { int a; }; + enum Enum { A, B }; + + namespace NestedNamespace { + class Class04_ns : private Class04_derived { + protected: + Struct _s; + Enum _e; + }; + }; }; // Anonymous union (issue #9) diff --git a/tests/simple_classes_nodep.puml b/tests/simple_classes_nodep.puml index 666cd9f..9e4d2be 100644 --- a/tests/simple_classes_nodep.puml +++ b/tests/simple_classes_nodep.puml @@ -43,26 +43,6 @@ class Class03 > { } -namespace Interface { - class Class04 { - +Class04() - +~Class04() - -_obj : Class01* - -_var : T - -_flag : bool - } -} - - -namespace Interface { - class Class04_derived { - +Class04_derived() - +~Class04_derived() - -_var : int - } -} - - enum Enum01 { VALUE_0 VALUE_1 @@ -70,32 +50,59 @@ enum Enum01 { } -namespace Interface { - class Struct { - +a : int - } -} - - -class anon-union-1::anon-struct-1 { +class anon_union_1::anon_struct_1 { +x : float +y : float +z : float } -class anon-union-1::anon-struct-2 { +class anon_union_1::anon_struct_2 { +phi : float +rho : float +theta : float } -class anon-union-1 { +class anon_union_1 { +vec : float } +namespace Interface { + class Class04 { + +Class04() + +~Class04() + -_obj : Class01* + -_val : Enum01 + -_var : T + -_flag : bool + } + + class Class04_derived { + +Class04_derived() + +~Class04_derived() + -_var : int + } + + enum Enum { + A + B + } + + class Struct { + +a : int + } + + namespace Interface::NestedNamespace { + class Class04_ns { + #_e : Enum + #_s : Struct + } + } +} + + @@ -104,9 +111,10 @@ class anon-union-1 { .Class01 <|-- .Class02 -namespace Interface { - Class04 <|-- Class04_derived -} +Interface.Class04 <|-- Interface.Class04_derived + + +Interface.Class04_derived <|-- Interface.Interface::NestedNamespace.Class04_ns @@ -123,6 +131,29 @@ namespace Interface { Interface.Class04 o-- .Class01 +Interface.Class04 *-- .Enum01 + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Enum + + +Interface.Interface::NestedNamespace.Class04_ns *-- Interface.Struct + + + + + + +/' Nested objects '/ + +.Class02 +-- .Class02::ClassNested + + +.anon_union_1 +-- .anon_union_1::anon_struct_1 + + +.anon_union_1 +-- .anon_union_1::anon_struct_2 + diff --git a/tests/test_hpp2plantuml.py b/tests/test_hpp2plantuml.py index 79a2c11..d9ef61c 100644 --- a/tests/test_hpp2plantuml.py +++ b/tests/test_hpp2plantuml.py @@ -38,7 +38,7 @@ def test_init(self): c_type = "container_type" c_name = "container_name" c_obj = hpp2plantuml.hpp2plantuml.Container(c_type, c_name) - nt.assert_equal(c_obj.get_name(), c_name) + nt.assert_equal(c_obj.name, c_name) nt.assert_equal(c_obj.render(), 'container_type container_name {\n}\n') def test_comparison_keys(self): @@ -55,7 +55,7 @@ def test_comparison_keys(self): c_obj_list.sort(key=lambda obj: obj.comparison_keys()) for i in range(len(c_list)): - nt.assert_equal(c_obj_list[i].get_name(), + nt.assert_equal(c_obj_list[i].name, c_list[ref_sort_idx[i]][1]) test_list_classvar=[["""class Test { @@ -137,10 +137,8 @@ def test_list_entries(self): """], ["""namespace Interface { class Test { protected: -int & member; };};""", """namespace Interface { - class Test { - #member : int& - } +int & member; };};""", """class Test { + #member : int& } """]] # %% Test classes @@ -194,20 +192,18 @@ def test_list_entries(self): input_str)) test_list_link=[["""class A{}; -class B : A{};""", """A <@-- B +class B : A{};""", """.A <@-- .B """], ["""class A{}; -class B : public A{};""", """A <@-- B +class B : public A{};""", """.A <@-- .B """], ["""class B{}; -class A{B obj;};""", """A *-- B +class A{B obj;};""", """.A *-- .B """], ["""class B{}; -class A{B* obj;};""", """A o-- B +class A{B* obj;};""", """.A o-- .B """], ["""class B{}; -class A{B * obj_ptr; B* ptr;};""", """A \"2\" o-- B +class A{B * obj_ptr; B* ptr;};""", """.A \"2\" o-- .B """], ["""class A{}; -class B{void Method(A* obj);};""", """A <.. B -"""], ["namespace T {class A{}; class B: A{};};", """namespace T { - A <@-- B -} +class B{void Method(A* obj);};""", """.A <.. .B +"""], ["namespace T {class A{}; class B: A{};};", """T.A <@-- T.B """], ["""namespace T { class A{};}; class B{T::A* _obj;};""", """.B o-- T.A