Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for diagrams and dot files generation #99

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 97 additions & 46 deletions .idea/workspace.xml

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ theme:
icon:
repo: fontawesome/brands/github


palette:
- scheme: slate
# Palette toggle for dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: orange
accent: orange
toggle:
icon: material/brightness-4
name: Switch to light mode

extra:
social:
Expand Down Expand Up @@ -74,13 +80,25 @@ plugins:
OPTIMIZE_OUTPUT_JAVA: True
JAVADOC_AUTOBRIEF: True
EXTRACT_ALL: True

GENERATE_HTML: YES
HAVE_DOT: YES
DOT_IMAGE_FORMAT: jpg
# HAVE_DOT: YES
UML_LOOK: YES
animal:
src-dirs: demo-projects/animal
full-doc: True
doxy-cfg:
FILE_PATTERNS: "*.cpp *.h*"
EXAMPLE_PATH: examples
RECURSIVE: True
GENERATE_HTML: YES
HAVE_DOT: YES
# DOT_IMAGE_FORMAT: jpg
DOT_IMAGE_FORMAT: svg
# INTERACTIVE_SVG: YES
# UML_LOOK: YES
save-api: .mkdoxy
full-doc: True
debug: False
Expand Down
3 changes: 2 additions & 1 deletion mkdoxy/doxygen.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
from pathlib import PurePath
from xml.etree import ElementTree

from mkdoxy.cache import Cache
Expand All @@ -11,7 +12,7 @@


class Doxygen:
def __init__(self, index_path: str, parser: XmlParser, cache: Cache):
def __init__(self, index_path: PurePath, parser: XmlParser, cache: Cache):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Low code quality found in Doxygen.__init__ - 23% (low-code-quality)


ExplanationThe quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

self.debug = parser.debug
path_xml = os.path.join(index_path, "index.xml")
if self.debug:
Expand Down
19 changes: 19 additions & 0 deletions mkdoxy/generatorAuto.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ def save(self, path: str, output: str):
with open(os.path.join(self.tempDoxyDir, pathRel), "w", encoding="utf-8") as file:
file.write(output)

def save_image(self, path: str, image_source_link: str):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the file copying logic in save_image to improve code maintainability and robustness.

The addition of the save_image method introduces some complexity and potential maintenance challenges. Here are a few suggestions to streamline this functionality:

  1. Consider abstracting the file copying logic into a separate method, such as copy_file, to reduce code duplication and improve readability. This method can ensure the destination directory exists and then perform the copy operation, encapsulating these steps away from the higher-level logic of save_image.

  2. The deeply nested code within save_image makes it a bit harder to follow. Simplifying this by using the proposed copy_file method could make the overall flow more understandable.

  3. It's important to handle potential errors that can occur during file operations, such as permissions issues or disk space limitations. While the current code doesn't explicitly address these, relying on built-in functions like shutil.copy within a copy_file method could offer more robustness by leveraging their internal error handling.

  4. The commented-out code and somewhat duplicated path construction logic could be cleaned up to further improve the maintainability of this code. Removing unused code and possibly centralizing path construction could be beneficial.

By addressing these points, the code would not only become cleaner and more maintainable but also potentially more robust against common file operation errors.

# copy image from image_source_link to mkdocs
source = os.path.join(self.tempDoxyDir, "html", image_source_link)
if not os.path.exists(source):
return
destination = os.path.join(self.siteDir, self.apiPath, path, image_source_link)
os.makedirs(os.path.dirname(destination), exist_ok=True)
with open(source, "rb") as fsrc:
with open(destination, "wb") as fdst:
fdst.write(fsrc.read())

def fullDoc(self, defaultTemplateConfig: dict):
self.annotated(self.doxygen.root.children, defaultTemplateConfig)
self.fileindex(self.doxygen.files.children, defaultTemplateConfig)
Expand Down Expand Up @@ -246,6 +257,14 @@ def hierarchy(self, nodes: [Node], config: dict = None):

def member(self, node: Node, config: dict = None):
path = node.filename
refid = node.refid

if node.has_inheritance_graph:
self.save_image(refid, node.inheritance_graph)
if node.has_collaboration_graph:
self.save_image(refid, node.collaboration_graph)
# if node.has_directory_dependency:
# self.save_image(refid, node.directory_dependency)

output = self.generatorBase.member(node, config)
self.save(path, output)
Expand Down
23 changes: 22 additions & 1 deletion mkdoxy/generatorBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from jinja2 import Template
from jinja2.exceptions import TemplateError
from mkdocs import exceptions
from pprint import pformat

import mkdoxy
from mkdoxy.constants import Kind
Expand Down Expand Up @@ -94,10 +95,30 @@ def render(self, tmpl: Template, data: dict) -> str:
@param data (dict): Data to render the template.
@return (str): Rendered template.
"""

def print_node_content(node: Node, level=0, max_depth=1):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider simplifying the render method by maintaining the use of Jinja2 Environment and separating the print_node_content logic.

The refactoring introduces a more complex approach by removing the use of Environment from Jinja2 and directly creating Template instances. This change loses the benefits of a shared environment, such as centralized configuration and filter management. Additionally, the introduction of the print_node_content function within the render method significantly increases its complexity by adding recursive functionality directly inside it. This not only makes the method harder to understand but also violates the Single Responsibility Principle.

Moreover, the method now passes additional functions (pformat, vars, print_node_content) as variables to the template, increasing the complexity of the data managed within the template and the coupling between the Python code and the Jinja2 template.

A more maintainable approach would be to keep the use of an Environment for managing templates, which allows for a cleaner and more centralized configuration. Also, consider keeping template-related logic separate from data processing logic, such as the content inspection and formatting done by print_node_content. This function, if necessary, could be better placed outside the render method, possibly as a utility function or within the data model itself, to keep the rendering logic clean and focused solely on template rendering.

This approach would not only simplify the current implementation but also enhance the maintainability and readability of the code by clearly separating concerns and utilizing Jinja2 features more effectively.

if level > max_depth:
return "" # Stop recursion when max depth is exceeded

indent = "\n" + " " * (level * 4) + "- " # Indentation for better readability
# node_representation = f"{indent}Node at Level {level}: {pformat(vars(node))}\n"
node_representation = f"{indent}Node at Level {level}: {node.name}\n"
# print all attributes of the node
for key, value in vars(node).items():
if key == "children":
continue
node_representation += f"{indent} {key}: {pformat(value)}\n"

# Assuming each node has a list or iterable of child nodes in an attribute like `children`
for child in getattr(node, "children", []):
node_representation += print_node_content(child, level + 1, max_depth)

return node_representation
Comment on lines +111 to +128
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider limiting the recursion depth for large node trees.

The print_node_content function recursively prints node content. For very large node trees, consider adding a mechanism to limit the recursion depth to prevent potential stack overflow.


try:
# if self.debug:
# print('Generating', path) # TODO: add path to data
rendered: str = tmpl.render(data)
rendered: str = tmpl.render(data, pformat=pformat, vars=vars, print_node_content=print_node_content)
return rendered
except TemplateError as e:
raise Exception(str(e)) from e
Expand Down
27 changes: 27 additions & 0 deletions mkdoxy/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def __init__(
self._initializer = Property.Initializer(self._xml, parser, self._kind)
self._definition = Property.Definition(self._xml, parser, self._kind)
self._programlisting = Property.Programlisting(self._xml, parser, self._kind)
self._inheritancegraph = Property.InheritanceGraph(self._xml, parser, self._kind)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider encapsulating the new graph-related functionality into a separate class.

The introduction of _inheritancegraph, _collaborationgraph, and the commented-out _directorydependency properties, along with their associated logic, significantly increases the complexity of this class. This expansion not only adds more responsibilities to the class but also enlarges its public interface with additional conditional checks and methods. It's generally beneficial to adhere to the Single Responsibility Principle to keep classes focused and maintainable.

Consider encapsulating the new graph-related functionality into a separate class. This would streamline the responsibilities of the original class, making it easier to understand and maintain, while also providing a dedicated space for managing graph properties and any future expansions in this area. Encapsulation would also address the issue of the commented-out code by providing a clear context for its inclusion or future development.

A separate GraphProperties class could manage the inheritance and collaboration graphs, and potentially the directory dependency, should it be reintroduced. This approach would simplify the original class's interface and reduce the cognitive load required to understand its behavior, leading to a cleaner, more modular design.

self._collaborationgraph = Property.CollaborationGraph(self._xml, parser, self._kind)
# self._directorydependency = Property.DirectoryDependency(self._xml, parser, self._kind)

def __repr__(self):
return f"Node: {self.name} refid: {self._refid}"
Expand Down Expand Up @@ -828,6 +831,30 @@ def has_programlisting(self) -> bool:
def programlisting(self) -> str:
return self._programlisting.md()

@property
def has_inheritance_graph(self) -> bool:
return self._inheritancegraph.has()

@property
def inheritance_graph(self) -> str:
return self._inheritancegraph.md()

@property
def has_collaboration_graph(self) -> bool:
return self._collaborationgraph.has()

@property
def collaboration_graph(self) -> str:
return self._collaborationgraph.md()

# @property
# def has_directory_dependency(self) -> bool:
# return self._directorydependency.has()
#
# @property
# def directory_dependency(self) -> str:
# return self._directorydependency.md()

@property
def is_resolved(self) -> bool:
return True
Expand Down
49 changes: 49 additions & 0 deletions mkdoxy/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,52 @@ def md(self, plain: bool = False) -> str:

def has(self) -> bool:
return self.xml.find("programlisting") is not None

class InheritanceGraph:
def __init__(self, xml: Element, parser: XmlParser, kind: Kind):
self.xml = xml
self.parser = parser
self.kind = kind
self.refid = self.xml.attrib.get("id") if self.xml is not None else None
Comment on lines +348 to +352
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code_refinement): Consider extracting common initialization logic to a base class.

Both InheritanceGraph and CollaborationGraph classes have similar init methods. Extracting this to a base class could reduce code duplication.


def md(self, plain: bool = False) -> str:
return f"{self.refid}__inherit__graph.svg"

def plain(self) -> str:
return self.md(plain=True)

def has(self) -> bool:
return self.kind.is_class() and self.refid is not None and self.xml.find("inheritancegraph") is not None

class CollaborationGraph:
def __init__(self, xml: Element, parser: XmlParser, kind: Kind):
self.xml = xml
self.parser = parser
self.kind = kind
self.refid = self.xml.attrib.get("id") if self.xml is not None else None

def md(self, plain: bool = False) -> str:
return f"{self.refid}__coll__graph.svg"

def plain(self) -> str:
return self.md(plain=True)

def has(self) -> bool:
return self.kind.is_class() and self.refid is not None and self.xml.find("collaborationgraph") is not None

# class DirectoryDependency:
# def __init__(self, xml: Element, parser: XmlParser, kind: Kind):
# self.xml = xml
# self.parser = parser
# self.kind = kind
# self.refid = self.xml.attrib.get("id") if self.xml is not None else None
#
# def md(self, plain: bool = False) -> str:
# return f"{self.refid}_dep.svg"
#
# def plain(self) -> str:
# return self.md(plain=True)
#
# def has(self) -> bool:
# return (self.refid is not None and
# (self.kind.is_dir()))
28 changes: 28 additions & 0 deletions mkdoxy/templates/member.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ implements: True
---
{% filter indent(config.get('indent_level', 0), True) %}


{#{{ print_node_content(node) }}#}

# {{node.kind.value|title}} {{node.name_long}}
{% if node.has_templateparams %}
**template <{{node.templateparams}}>**
Expand All @@ -31,6 +34,31 @@ implements: True
{% endfor -%}
{%- endif %}


{% if node.has_inheritance_graph %}
### Inheritance diagram:
{#{{ node.inheritance_graph }}#}
<p align="center" markdown>
![alt]({{ node.inheritance_graph }})
</p>
{% endif %}

{% if node.has_collaboration_graph %}
### Collaboration diagram:
{#{{ node.collaboration_graph }}#}
<p align="center" markdown>
![alt]({{ node.collaboration_graph }})
</p>
{% endif %}

{#{% if node.has_directory_dependency %}#}
{#### Directory dependency graph:#}
{#{{ node.directory_dependency }}#}
{#<p align="center" markdown>#}
{# ![alt]({{ node.directory_dependency }})#}
{#</p>#}
{#{% endif %}#}

{% if node.has_base_classes %}
Inherits the following classes:
{%- for base in node.base_classes -%}
Expand Down
Loading