Skip to content

Commit

Permalink
Merge pull request #7 from lgarber-akamai/fix/improve-inject
Browse files Browse the repository at this point in the history
new: (BREAKING) Improve documentation injection logic and add support for additional constants
  • Loading branch information
lgarber-akamai authored Feb 10, 2023
2 parents 586a5df + 608ae3e commit 1cc0634
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 35 deletions.
96 changes: 85 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ansible-specdoc

A utility for dynamically generating documentation from an Ansible module's spec. This is primarily designed for the [Linode Ansible Collection](https://github.com/linode/ansible_linode).
A utility for dynamically generating documentation from an Ansible module's spec.

This project was primarily designed for the [Linode Ansible Collection](https://github.com/linode/ansible_linode).

## Usage

Expand All @@ -9,7 +11,7 @@ ansible-specdoc [-h] [-s] [-n MODULE_NAME] [-i INPUT_FILE] [-o OUTPUT_FILE] [-f

Generate Ansible Module documentation from spec.

optional arguments:
options:
-h, --help show this help message and exit
-s, --stdin Read the module from stdin.
-n MODULE_NAME, --module-name MODULE_NAME
Expand All @@ -20,14 +22,54 @@ optional arguments:
The file to output the documentation to.
-f {yaml,json,jinja2}, --output_format {yaml,json,jinja2}
The output format of the documentation.
-j, --inject Inject the output documentation into the `DOCUMENTATION` field of input module.
-j, --inject Inject the output documentation into the `DOCUMENTATION`, `RETURN`, and `EXAMPLES` fields of input module.
-t TEMPLATE_FILE, --template_file TEMPLATE_FILE
The file to use as the template for templated formats.
```

## Specification Format
---

#### Generating a templated documentation file:

```shell
ansible-specdoc -f jinja2 -t path/to/my/template.md.j2 -i path/to/my/module.py -o path/to/output/file.md
```

---

#### Dynamically generating and injecting documentation back into module constants:

```shell
ansible-specdoc -j -i path/to/my/module.py
```

NOTE: Documentation string injection requires that you have `DOCUMENTATION`, `RETURN`, and `EXAMPLES` constants defined in your module.

---

#### Generating a raw documentation string (not recommended):

```shell
ansible-specdoc -f yaml -i path/to/my/module.py
```

## Implementation

### Module Metadata
### Importing SpecDoc Classes

All of the `ansible-specdoc` classes can be imported into an Ansible module using the following statement:

```python
from ansible_specdoc.objects import *
```

Alternatively, only specific classes can be imported using the following statement:

```python
from ansible_specdoc.objects import SpecDocMeta, SpecField, SpecReturnValue, FieldType, DeprecationInfo
```

### Declaring Module Metadata
The `ansible-specdoc` specification format requires that each module exports a `SPECDOC_META` object with the following structure:

```python
Expand All @@ -49,13 +91,10 @@ SPECDOC_META = SpecDocMeta(
)
```

### Argument Specification

Certain fields may automatically be passed into the Ansible-compatible spec dict.

Spec fields may additional metadata that will appear in the documentation.
### Declaring Argument Specification

For example:
Each `SpecField` object translates to a parameter that can be rendered into documentation and passed into Ansible for specification.
These fields should be declared in a dict format as shown below:

```python
module_spec = {
Expand All @@ -67,4 +106,39 @@ module_spec = {
}
```

This dict should be passed into the `options` field of the `SPECDOC_META` declaration.

### Passing Specification to Ansible

In order to retrieve the Ansible-compatible spec dict, use the `SPECDOC_META.ansible_spec` property.

### Other Notes

To prevent `ansible-specdoc` from executing module code, please ensure that all module logic executes using the following pattern:

```python
if __name__ == '__main__':
main()
```

---

To deprecate a module, specify the `deprecated` field as follows:

```python
SPECDOC_META = SpecDocMeta(
...
deprecated=DeprecationInfo(
alternative='my.new.module',
removed_in='1.0.0',
why='Reason for deprecation'
)
)
```

When deprecating a module, you will also need to update your `meta/runtime.yml` file.
Please refer to the [official Ansible deprecation documentation](https://docs.ansible.com/ansible/latest/dev_guide/module_lifecycle.html#deprecating-modules-and-plugins-in-a-collection) for more details.

## Templates

This repository provides an [example Markdown template](./template/module.md.j2) that can be used in conjunction with the `-t` argument.
47 changes: 33 additions & 14 deletions ansible_specdoc/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pathlib
import sys
from types import ModuleType
from typing import Optional
from typing import Optional, Tuple

import jinja2
import yaml
Expand Down Expand Up @@ -80,10 +80,21 @@ def __generate_doc_dict(self):
result['module'] = self._module_name
return result

def __generate_ansible_doc_dicts(self):
documentation, returns, examples = self._metadata.ansible_doc
documentation['module'] = self._module_name
return documentation, returns, examples

def generate_yaml(self) -> str:
"""Generates a YAML documentation string"""
return yaml.dump(self.__generate_doc_dict())

def generate_ansible_doc_yaml(self) -> Tuple[str, str, str]:
"""Generates YAML documentation strings for all Ansible documentation fields."""
documentation, returns, examples = self.__generate_ansible_doc_dicts()

return yaml.dump(documentation), yaml.dump(returns), yaml.dump(examples, sort_keys=False)

def generate_json(self) -> str:
"""Generates a JSON documentation string"""
return json.dumps(self.__generate_doc_dict())
Expand Down Expand Up @@ -120,13 +131,13 @@ def __init__(self):
type=str, help='The file to output the documentation to.')

self._parser.add_argument('-f', '--output_format',
type=str, default='yaml',
type=str,
choices=['yaml', 'json', 'jinja2'],
help='The output format of the documentation.')

self._parser.add_argument('-j', '--inject',
help='Inject the output documentation into the `DOCUMENTATION` '
'field of input module.',
help='Inject the output documentation into the `DOCUMENTATION`, '
'`RETURN`, and `EXAMPLES` fields of input module.',
action='store_true')

self._parser.add_argument('-t', '--template_file',
Expand All @@ -138,18 +149,22 @@ def __init__(self):
self._mod = SpecDocModule()
self._output = ''

@staticmethod
def _inject_docs(module_content: str, docs_content: str) -> str:
def _inject_docs(self, module_content: str) -> str:
"""Injects docs_content into the DOCUMENTATION field of module_content"""

doc, returns, examples = self._mod.generate_ansible_doc_yaml()

red = RedBaron(module_content)

doc_field = red.find('name', value='DOCUMENTATION')
if doc_field is None or doc_field.parent is None:
raise Exception('failed to inject documentation: '
'an empty DOCUMENTATION field must be specified')
to_inject = [['DOCUMENTATION', doc], ['RETURN', returns], ['EXAMPLES', examples]]

for field_info in to_inject:
doc_field = red.find('name', value=field_info[0])
if doc_field is None or doc_field.parent is None:
raise Exception('failed to inject documentation: '
f'an empty {field_info[0]} field must be specified')

doc_field.parent.value.value = f'\'\'\'\n{docs_content}\'\'\''
doc_field.parent.value.value = f'\'\'\'\n{field_info[1]}\'\'\''

return red.dumps()

Expand Down Expand Up @@ -206,6 +221,10 @@ def _load_input_source(self):
self._parser.error('No input source specified')

def _process_docs(self):
# We'll handle the output logic elsewhere
if self._args.inject:
return

if self._args.output_format == 'yaml':
self._output = self._mod.generate_yaml()
return
Expand All @@ -230,11 +249,11 @@ def _try_inject_original_file(self):
if not self._args.inject:
return

if self._args.output_format not in {'yaml'}:
self._parser.error(f'Format {self._args.output_format} is not supported for --inject.')
if self._args.output_format is not None:
self._parser.error('No format should be declared when using --inject.')

with open(self._args.input_file, 'r+') as file:
injected_module = self._inject_docs(file.read(), self._output)
injected_module = self._inject_docs(file.read())
file.seek(0)
file.write(injected_module)
file.truncate()
Expand Down
Loading

0 comments on commit 1cc0634

Please sign in to comment.