Module jumpscale.sals.testdocs

Expand source code
import pytest
from jumpscale.loader import j

test_template = """{% if new_line %}\n{% endif %}### {{ name }}

{{ body | replace ("  ", "")}}
"""

category_template = """{% if new_line %}\n{% endif %}### {{ category }}


"""
entry_template = """- [{{ name }}]({{ location }})

"""


class Collector:
    def __init__(self, source, target):
        self.tests_docs_locations = []
        self.target = target
        if j.sals.fs.exists(source):
            if not j.sals.fs.is_dir(source):
                source = j.sals.fs.parent(source)
            j.sals.fs.chdir(source)
        self.source = source

    def pytest_collection_modifyitems(self, items):
        """This method is a pytest hook which can modify the items has been collected.

        Args:
            items (list): Tests objects has been collected.
        """
        for item in items:
            self._generate_docs_for_item(item)

    def _remove_destination_if_exists(self, location):
        # Check if the docs exists and remove them in this case to generate a new one.
        if location not in self.tests_docs_locations:
            self.tests_docs_locations.append(location)
            if j.sals.fs.exists(location):
                j.sals.fs.rmtree(location)

    def _write_docs(self, name, doc, location):
        # Write docs.
        j.sals.fs.mkdirs(j.sals.fs.parent(location))
        if j.sals.fs.exists(location):
            target_content = j.sals.fs.read_file(location)
            if name in target_content:
                return 0
            test_doc = j.tools.jinja2.render_template(template_text=test_template, name=name, body=doc, new_line=True)
            return j.sals.fs.write_file(location, test_doc, append=True)
        else:
            test_doc = j.tools.jinja2.render_template(template_text=test_template, name=name, body=doc, new_line=False)
            return j.sals.fs.write_file(location, test_doc)

    def _generate_docs_for_item(self, item):
        """Generate a markdown docs for test scenario.

        Args:
            item (object): Test object that docs will be generated from.
        """
        doc = item._obj.__doc__
        name = item.name
        if not doc:
            j.logger.warning(f"Test {name} doesn't have docstring")
            return

        # Get target docs locations.
        absolute_test_location = str(item.fspath)
        relative_test_location = absolute_test_location.split(self.source)[1][1:]
        relative_test_location = relative_test_location.replace(".py", ".md")
        target_location = j.sals.fs.join_paths(self.target, relative_test_location)

        self._remove_destination_if_exists(target_location)
        wrote = self._write_docs(name, doc, target_location)
        if not wrote:
            return

        self._add_entry_to_main_readme(relative_test_location)

    def _add_entry_to_main_readme(self, relative_location):
        """Add an entry to the tests main README.md use for navigation.

        Args:
            relative_location (str): The relative path of the test file to the main tests README.md
        """
        # Extract category and file name
        location_parts = relative_location.split(j.sals.fs.sep)
        if len(location_parts) == 1:
            category = ""
        else:
            category = location_parts[0].capitalize()
        file_name = location_parts[-1].replace(".md", "")

        # Generate the entry to be added in README.md.
        readme_location = j.sals.fs.join_paths(self.target, "README.md")
        readme = ""
        if j.sals.fs.exists(readme_location):
            readme = j.sals.fs.read_file(readme_location)
        if category:
            category = j.tools.jinja2.render_template(
                template_text=category_template, category=category, new_line=readme
            )
        new_entry = j.tools.jinja2.render_template(
            template_text=entry_template, name=file_name, location=relative_location
        )
        if not new_entry in readme:
            if not category.lstrip() in readme:
                readme = f"{readme}{category}"

            # Get the entry location in readme file.
            first_line_in_category = readme.find(category.lstrip()) + len(category.lstrip())
            last_line_in_category = readme.find("###", first_line_in_category) - 1
            entry_location = first_line_in_category
            if last_line_in_category < 0:
                last_line_in_category = len(readme)
            for line in readme[first_line_in_category:last_line_in_category].splitlines():
                if new_entry > line:
                    entry_location += len(line) + 1  # this one for the new line.
                else:
                    break
            new_readme = f"{readme[:entry_location]}{new_entry}{readme[entry_location:]}"
            j.sals.fs.rmtree(readme_location)
            j.sals.fs.write_file(readme_location, new_readme)


def generate_tests_docs(source, target, clean=False):
    """Generate a markdown docs for tests from its docstring.

    Args:
        source (str): Tests path.
        target (str): Target docs path.
        clean (bool): To clean the target path before start.
    """
    source = j.sals.fs.absolute(source)
    target = j.sals.fs.absolute(target)
    if clean and j.sals.fs.exists(target):
        j.sals.fs.rmtree(target)

    collector = Collector(source=source, target=target)
    pytest.main(["--collect-only", source], plugins=[collector])

Functions

def generate_tests_docs(source, target, clean=False)

Generate a markdown docs for tests from its docstring.

Args

source : str
Tests path.
target : str
Target docs path.
clean : bool
To clean the target path before start.
Expand source code
def generate_tests_docs(source, target, clean=False):
    """Generate a markdown docs for tests from its docstring.

    Args:
        source (str): Tests path.
        target (str): Target docs path.
        clean (bool): To clean the target path before start.
    """
    source = j.sals.fs.absolute(source)
    target = j.sals.fs.absolute(target)
    if clean and j.sals.fs.exists(target):
        j.sals.fs.rmtree(target)

    collector = Collector(source=source, target=target)
    pytest.main(["--collect-only", source], plugins=[collector])

Classes

class Collector (source, target)
Expand source code
class Collector:
    def __init__(self, source, target):
        self.tests_docs_locations = []
        self.target = target
        if j.sals.fs.exists(source):
            if not j.sals.fs.is_dir(source):
                source = j.sals.fs.parent(source)
            j.sals.fs.chdir(source)
        self.source = source

    def pytest_collection_modifyitems(self, items):
        """This method is a pytest hook which can modify the items has been collected.

        Args:
            items (list): Tests objects has been collected.
        """
        for item in items:
            self._generate_docs_for_item(item)

    def _remove_destination_if_exists(self, location):
        # Check if the docs exists and remove them in this case to generate a new one.
        if location not in self.tests_docs_locations:
            self.tests_docs_locations.append(location)
            if j.sals.fs.exists(location):
                j.sals.fs.rmtree(location)

    def _write_docs(self, name, doc, location):
        # Write docs.
        j.sals.fs.mkdirs(j.sals.fs.parent(location))
        if j.sals.fs.exists(location):
            target_content = j.sals.fs.read_file(location)
            if name in target_content:
                return 0
            test_doc = j.tools.jinja2.render_template(template_text=test_template, name=name, body=doc, new_line=True)
            return j.sals.fs.write_file(location, test_doc, append=True)
        else:
            test_doc = j.tools.jinja2.render_template(template_text=test_template, name=name, body=doc, new_line=False)
            return j.sals.fs.write_file(location, test_doc)

    def _generate_docs_for_item(self, item):
        """Generate a markdown docs for test scenario.

        Args:
            item (object): Test object that docs will be generated from.
        """
        doc = item._obj.__doc__
        name = item.name
        if not doc:
            j.logger.warning(f"Test {name} doesn't have docstring")
            return

        # Get target docs locations.
        absolute_test_location = str(item.fspath)
        relative_test_location = absolute_test_location.split(self.source)[1][1:]
        relative_test_location = relative_test_location.replace(".py", ".md")
        target_location = j.sals.fs.join_paths(self.target, relative_test_location)

        self._remove_destination_if_exists(target_location)
        wrote = self._write_docs(name, doc, target_location)
        if not wrote:
            return

        self._add_entry_to_main_readme(relative_test_location)

    def _add_entry_to_main_readme(self, relative_location):
        """Add an entry to the tests main README.md use for navigation.

        Args:
            relative_location (str): The relative path of the test file to the main tests README.md
        """
        # Extract category and file name
        location_parts = relative_location.split(j.sals.fs.sep)
        if len(location_parts) == 1:
            category = ""
        else:
            category = location_parts[0].capitalize()
        file_name = location_parts[-1].replace(".md", "")

        # Generate the entry to be added in README.md.
        readme_location = j.sals.fs.join_paths(self.target, "README.md")
        readme = ""
        if j.sals.fs.exists(readme_location):
            readme = j.sals.fs.read_file(readme_location)
        if category:
            category = j.tools.jinja2.render_template(
                template_text=category_template, category=category, new_line=readme
            )
        new_entry = j.tools.jinja2.render_template(
            template_text=entry_template, name=file_name, location=relative_location
        )
        if not new_entry in readme:
            if not category.lstrip() in readme:
                readme = f"{readme}{category}"

            # Get the entry location in readme file.
            first_line_in_category = readme.find(category.lstrip()) + len(category.lstrip())
            last_line_in_category = readme.find("###", first_line_in_category) - 1
            entry_location = first_line_in_category
            if last_line_in_category < 0:
                last_line_in_category = len(readme)
            for line in readme[first_line_in_category:last_line_in_category].splitlines():
                if new_entry > line:
                    entry_location += len(line) + 1  # this one for the new line.
                else:
                    break
            new_readme = f"{readme[:entry_location]}{new_entry}{readme[entry_location:]}"
            j.sals.fs.rmtree(readme_location)
            j.sals.fs.write_file(readme_location, new_readme)

Methods

def pytest_collection_modifyitems(self, items)

This method is a pytest hook which can modify the items has been collected.

Args

items : list
Tests objects has been collected.
Expand source code
def pytest_collection_modifyitems(self, items):
    """This method is a pytest hook which can modify the items has been collected.

    Args:
        items (list): Tests objects has been collected.
    """
    for item in items:
        self._generate_docs_for_item(item)