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