Skip to content

docs

trestle.core.commands.author.docs ¤

Trestle author docs sub-command.

logger ¤

Classes¤

Docs (AuthorCommonCommand) ¤

Markdown governed documents - enforcing consistent markdown across a set of files.

Source code in trestle/core/commands/author/docs.py
class Docs(AuthorCommonCommand):
    """Markdown governed documents - enforcing consistent markdown across a set of files."""

    name = 'docs'

    template_name = 'template.md'

    def _init_arguments(self) -> None:
        self.add_argument(
            author_const.GH_SHORT, author_const.GH_LONG, help=author_const.GH_HELP, default=None, type=str
        )
        self.add_argument(
            author_const.SHORT_HEADER_VALIDATE,
            author_const.LONG_HEADER_VALIDATE,
            help=author_const.HEADER_VALIDATE_HELP,
            action='store_true'
        )
        self.add_argument(
            author_const.SHORT_TEMPLATE_VERSION,
            author_const.LONG_TEMPLATE_VERSION,
            help=author_const.TEMPLATE_VERSION_HELP,
            action='store'
        )
        self.add_argument(
            author_const.HOV_SHORT, author_const.HOV_LONG, help=author_const.HOV_HELP, action='store_true'
        )
        self.add_argument(
            author_const.SHORT_IGNORE, author_const.LONG_IGNORE, help=author_const.IGNORE_HELP, default=None, type=str
        )
        self.add_argument(
            author_const.RECURSE_SHORT, author_const.RECURSE_LONG, help=author_const.RECURSE_HELP, action='store_true'
        )
        self.add_argument(author_const.MODE_ARG_NAME, choices=author_const.MODE_CHOICES)
        tn_help_str = '\n'.join(
            [
                'The name of the the task to be governed.',
                ''
                'The template file is at .trestle/author/[task-name]/template.md',
                'Note that by default this will automatically enforce the task.'
            ]
        )

        self.add_argument(
            author_const.TASK_NAME_SHORT, author_const.TASK_NAME_LONG, help=tn_help_str, required=True, type=str
        )
        self.add_argument(
            author_const.SHORT_README_VALIDATE,
            author_const.LONG_README_VALIDATE,
            help=author_const.README_VALIDATE_HELP,
            action='store_true'
        )

        self.add_argument(
            author_const.TEMPLATE_TYPE_VALIDATE_SHORT,
            author_const.TEMPLATE_TYPE_VALIDATE_LONG,
            help=author_const.TEMPLATE_TYPE_VALIDATE_HELP,
            action='store_true'
        )

    def _run(self, args: argparse.Namespace) -> int:
        try:
            status = 1
            if self._initialize(args):
                return status

            if args.mode == 'create-sample':
                status = self.create_sample()

            elif args.mode == 'template-validate':
                status = self.template_validate(
                    args.governed_heading,
                    args.header_validate,
                    args.header_only_validate,
                )
            elif args.mode == 'setup':
                status = self.setup_template_governed_docs(args.template_version)
            elif args.mode == 'validate':
                # mode is validate
                status = self.validate(
                    args.governed_heading,
                    args.header_validate,
                    args.header_only_validate,
                    args.recurse,
                    args.readme_validate,
                    args.template_version,
                    args.ignore,
                    args.validate_template_type
                )

            return status

        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(e, logger, 'Error occurred when running trestle author docs')

    def setup_template_governed_docs(self, template_version: str) -> int:
        """Create structure to allow markdown template enforcement.

        Returns:
            Unix return code.
        """
        if not self.task_path.exists():
            self.task_path.mkdir(exist_ok=True, parents=True)
        elif self.task_path.is_file():
            raise TrestleError(f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.')
        if not self.template_dir.exists():
            self.template_dir.mkdir(exist_ok=True, parents=True)
        elif self.template_dir.is_file():
            raise TrestleError(f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.')
        logger.debug(self.template_dir)
        if not self._validate_template_dir():
            raise TrestleError('Aborting setup')
        template_file = self.template_dir / self.template_name
        if template_file.is_file():
            return CmdReturnCodes.SUCCESS.value
        TemplateVersioning.write_versioned_template('template.md', self.template_dir, template_file, template_version)
        logger.info(f'Template file setup for task {self.task_name} at {self.rel_dir(template_file)}')
        logger.info(f'Task directory is {self.rel_dir(self.task_path)}')
        return CmdReturnCodes.SUCCESS.value

    def create_sample(self) -> int:
        """Presuming the template exists, copy into a sample markdown file with an index."""
        template_file = self.template_dir / self.template_name

        if not self._validate_template_dir():
            raise TrestleError('Aborting setup')
        if not template_file.is_file():
            raise TrestleError('No template file ... exiting.')

        index = 0
        while True:
            candidate_task = self.task_path / f'{self.task_name}_{index:03d}.md'
            if candidate_task.is_file():
                index = index + 1
            else:
                shutil.copy(str(template_file), str(candidate_task))
                break
        return CmdReturnCodes.SUCCESS.value

    def template_validate(self, heading: Optional[str], validate_header: bool, validate_only_header: bool) -> int:
        """Validate that the template is acceptable markdown."""
        template_file = self.template_dir / self.template_name
        if not self._validate_template_dir():
            raise TrestleError(f'Aborting setup, template directory {self.template_dir} is invalid.')
        if not template_file.is_file():
            raise TrestleError(f'Required template file: {self.rel_dir(template_file)} does not exist. Exiting.')
        try:
            md_api = MarkdownAPI()
            validate_body = False if validate_only_header else True
            md_api.load_validator_with_template(
                template_file, validate_header or validate_only_header, validate_body, heading, True
            )
        except Exception as ex:
            raise TrestleError(f'Template for task {self.task_name} failed to validate due to {ex}')

        logger.info(f'TEMPLATES VALID: {self.task_name}')
        return CmdReturnCodes.SUCCESS.value

    def _validate_template_dir(self) -> bool:
        """Template directory should only have template file."""
        for child in file_utils.iterdir_without_hidden_files(self.template_dir):
            # Only allowable template file in the directory is the template directory.
            if child.name != self.template_name and child.name.lower() != 'readme.md':
                logger.warning(f'Unknown file: {child.name} in template directory {self.rel_dir(self.template_dir)}')
                return False
        return True

    def _validate_dir(
        self,
        governed_heading: str,
        md_dir: pathlib.Path,
        validate_header: bool,
        validate_only_header: bool,
        recurse: bool,
        readme_validate: bool,
        template_version: Optional[str] = None,
        ignore: Optional[str] = None,
        validate_by_type_field: bool = False
    ) -> int:
        """
        Validate md files in a directory with option to recurse.

        Template version will be fetched from the instance header.
        """
        # status is a linux returncode
        status = 0
        for item_path in md_dir.iterdir():
            if file_utils.is_local_and_visible(item_path):
                if item_path.is_file():
                    if not item_path.suffix == const.MARKDOWN_FILE_EXT:
                        logger.info(
                            f'Unexpected file {self.rel_dir(item_path)} in folder {self.rel_dir(md_dir)}, skipping.'
                        )
                        continue
                    if not readme_validate and item_path.name.lower() == 'readme.md':
                        continue

                    if ignore:
                        p = re.compile(ignore)
                        matched = p.match(item_path.parts[-1])
                        if matched is not None:
                            logger.info(f'Ignoring file {item_path} from validation.')
                            continue

                    md_api = MarkdownAPI()
                    if template_version != '':
                        template_file = self.template_dir / self.template_name
                    else:
                        instance_version = md_api.processor.fetch_value_from_header(
                            item_path, author_const.TEMPLATE_VERSION_HEADER
                        )
                        if instance_version is None:
                            instance_version = '0.0.1'
                        versione_template_dir = TemplateVersioning.get_versioned_template_dir(
                            self.template_dir, instance_version
                        )
                        # checks on naming template name out of type header if needed
                        if validate_by_type_field:
                            # get template name out of its type which essentially needs to be the same
                            template_name = md_api.processor.fetch_value_from_header(
                                item_path, author_const.TEMPLATE_TYPE_HEADER
                            )
                            # throw an error if template type is not present
                            if template_name is None:
                                logger.error(
                                    f'INVALID: Instance file {item_path} does not have'
                                    f' {author_const.TEMPLATE_TYPE_HEADER}'
                                    ' field in its header and can not be validated using optional parameter validate'
                                    ' template type field'
                                )
                                status = 1
                                return status
                            template_name = template_name + '.md'
                            template_file = versione_template_dir / template_name
                        else:  # continues regular flow without template type
                            template_file = versione_template_dir / self.template_name
                    if not template_file.is_file():
                        raise TrestleError(
                            f'Required template file: {self.rel_dir(template_file)} does not exist. Exiting.'
                        )
                    md_api.load_validator_with_template(
                        template_file, validate_header, not validate_only_header, governed_heading
                    )
                    if not md_api.validate_instance(item_path):
                        logger.info(f'INVALID: {self.rel_dir(item_path)}')
                        status = 1
                    else:
                        logger.info(f'VALID: {self.rel_dir(item_path)}')
                elif recurse:
                    if ignore:
                        p = re.compile(ignore)
                        if len(list(filter(p.match, str(item_path.relative_to(md_dir)).split('/')))) > 0:
                            logger.info(f'Ignoring directory {item_path} from validation.')
                            continue
                    rc = self._validate_dir(
                        governed_heading,
                        item_path,
                        validate_header,
                        validate_only_header,
                        recurse,
                        readme_validate,
                        template_version,
                        ignore,
                        validate_by_type_field
                    )
                    if rc != 0:
                        status = rc

        return status

    def validate(
        self,
        governed_heading: str,
        validate_header: bool,
        validate_only_header: bool,
        recurse: bool,
        readme_validate: bool,
        template_version: str,
        ignore: str,
        validate_by_type_field: bool
    ) -> int:
        """
        Validate task.

        Args:
            governed_heading: A heading for which structural enforcement (see online docs).
            validate_header: Whether or not to validate the key structure of the yaml header to the markdown document.
            validate_only_header: Whether to validate just the yaml header.
            recurse: Whether to allow validated files to be in a directory tree.
            readme_validate: Whether to validate readme files, otherwise they will be ignored.

        Returns:
            Return code to be used for the command.
        """
        if not self.task_path.is_dir():
            raise TrestleError(f'Task directory {self.rel_dir(self.task_path)} does not exist. Exiting validate.')

        return self._validate_dir(
            governed_heading,
            self.task_path,
            validate_header,
            validate_only_header,
            recurse,
            readme_validate,
            template_version,
            ignore,
            validate_by_type_field
        )
name ¤
template_name ¤
Methods¤
create_sample(self) ¤

Presuming the template exists, copy into a sample markdown file with an index.

Source code in trestle/core/commands/author/docs.py
def create_sample(self) -> int:
    """Presuming the template exists, copy into a sample markdown file with an index."""
    template_file = self.template_dir / self.template_name

    if not self._validate_template_dir():
        raise TrestleError('Aborting setup')
    if not template_file.is_file():
        raise TrestleError('No template file ... exiting.')

    index = 0
    while True:
        candidate_task = self.task_path / f'{self.task_name}_{index:03d}.md'
        if candidate_task.is_file():
            index = index + 1
        else:
            shutil.copy(str(template_file), str(candidate_task))
            break
    return CmdReturnCodes.SUCCESS.value
setup_template_governed_docs(self, template_version) ¤

Create structure to allow markdown template enforcement.

Returns:

Type Description
int

Unix return code.

Source code in trestle/core/commands/author/docs.py
def setup_template_governed_docs(self, template_version: str) -> int:
    """Create structure to allow markdown template enforcement.

    Returns:
        Unix return code.
    """
    if not self.task_path.exists():
        self.task_path.mkdir(exist_ok=True, parents=True)
    elif self.task_path.is_file():
        raise TrestleError(f'Task path: {self.rel_dir(self.task_path)} is a file not a directory.')
    if not self.template_dir.exists():
        self.template_dir.mkdir(exist_ok=True, parents=True)
    elif self.template_dir.is_file():
        raise TrestleError(f'Template path: {self.rel_dir(self.template_dir)} is a file not a directory.')
    logger.debug(self.template_dir)
    if not self._validate_template_dir():
        raise TrestleError('Aborting setup')
    template_file = self.template_dir / self.template_name
    if template_file.is_file():
        return CmdReturnCodes.SUCCESS.value
    TemplateVersioning.write_versioned_template('template.md', self.template_dir, template_file, template_version)
    logger.info(f'Template file setup for task {self.task_name} at {self.rel_dir(template_file)}')
    logger.info(f'Task directory is {self.rel_dir(self.task_path)}')
    return CmdReturnCodes.SUCCESS.value
template_validate(self, heading, validate_header, validate_only_header) ¤

Validate that the template is acceptable markdown.

Source code in trestle/core/commands/author/docs.py
def template_validate(self, heading: Optional[str], validate_header: bool, validate_only_header: bool) -> int:
    """Validate that the template is acceptable markdown."""
    template_file = self.template_dir / self.template_name
    if not self._validate_template_dir():
        raise TrestleError(f'Aborting setup, template directory {self.template_dir} is invalid.')
    if not template_file.is_file():
        raise TrestleError(f'Required template file: {self.rel_dir(template_file)} does not exist. Exiting.')
    try:
        md_api = MarkdownAPI()
        validate_body = False if validate_only_header else True
        md_api.load_validator_with_template(
            template_file, validate_header or validate_only_header, validate_body, heading, True
        )
    except Exception as ex:
        raise TrestleError(f'Template for task {self.task_name} failed to validate due to {ex}')

    logger.info(f'TEMPLATES VALID: {self.task_name}')
    return CmdReturnCodes.SUCCESS.value
validate(self, governed_heading, validate_header, validate_only_header, recurse, readme_validate, template_version, ignore, validate_by_type_field) ¤

Validate task.

Parameters:

Name Type Description Default
governed_heading str

A heading for which structural enforcement (see online docs).

required
validate_header bool

Whether or not to validate the key structure of the yaml header to the markdown document.

required
validate_only_header bool

Whether to validate just the yaml header.

required
recurse bool

Whether to allow validated files to be in a directory tree.

required
readme_validate bool

Whether to validate readme files, otherwise they will be ignored.

required

Returns:

Type Description
int

Return code to be used for the command.

Source code in trestle/core/commands/author/docs.py
def validate(
    self,
    governed_heading: str,
    validate_header: bool,
    validate_only_header: bool,
    recurse: bool,
    readme_validate: bool,
    template_version: str,
    ignore: str,
    validate_by_type_field: bool
) -> int:
    """
    Validate task.

    Args:
        governed_heading: A heading for which structural enforcement (see online docs).
        validate_header: Whether or not to validate the key structure of the yaml header to the markdown document.
        validate_only_header: Whether to validate just the yaml header.
        recurse: Whether to allow validated files to be in a directory tree.
        readme_validate: Whether to validate readme files, otherwise they will be ignored.

    Returns:
        Return code to be used for the command.
    """
    if not self.task_path.is_dir():
        raise TrestleError(f'Task directory {self.rel_dir(self.task_path)} does not exist. Exiting validate.')

    return self._validate_dir(
        governed_heading,
        self.task_path,
        validate_header,
        validate_only_header,
        recurse,
        readme_validate,
        template_version,
        ignore,
        validate_by_type_field
    )

handler: python