diff --git a/.gitignore b/.gitignore index 078e30a..e98c1fa 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ cython_debug/ # ---> Ansible *.retry +src/ansible_module/module_utils diff --git a/plugins/module_utils/generic.py b/plugins/module_utils/generic.py index 65a891b..5896ae0 100644 --- a/plugins/module_utils/generic.py +++ b/plugins/module_utils/generic.py @@ -1,30 +1,69 @@ import builtins import pathlib import warnings -from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union +from typing import Any, Dict, Literal, NotRequired, Optional, Required, Sequence, Tuple, Type, TypedDict __all__ = ( "Types", "SYSTEMD_SERVICE_CONFIG", "SYSTEMD_NETWORK_CONFIG", "SYSTEMD_CONFIG_ROOT", + "FILE_COMMON_ARGS", "systemdbool", "AnsibleParameter", + "AnsibleReturnParameter", ) SYSTEMD_CONFIG_ROOT = pathlib.Path("/etc/systemd") SYSTEMD_NETWORK_CONFIG = SYSTEMD_CONFIG_ROOT / "network" SYSTEMD_SERVICE_CONFIG = SYSTEMD_CONFIG_ROOT / "system" +FILE_COMMON_ARGS = frozenset(("owner", "group", "mode", "seuser", "serole", "setype", "selevel", "unsafe_writes")) -AnsibleParameter = Dict[str, Any] +AnsibleType = Literal[ + "str", + "bool", + "int", + "float", + "path", + "raw", + "jsonarg", + "json", + "bytes", + "dict", + "list", + "bits", +] +ReturnOptions = Literal["always", "changed", "success"] + + +class AnsibleParameter(TypedDict, total=False): + description: Required[str | list[str]] + required: NotRequired[bool] + default: NotRequired[Any] + type: Required[AnsibleType] + choices: NotRequired[list[Any] | tuple[Any]] + elements: NotRequired["AnsibleParameter" | AnsibleType] + aliases: NotRequired[list[str]] + version_added: NotRequired[str] + options: NotRequired[dict[str, "AnsibleParameter"]] + + +class AnsibleReturnParameter(TypedDict, total=False): + description: Required[str | list[str]] + type: Required[AnsibleType] + returned: NotRequired[ReturnOptions] + elements: NotRequired[AnsibleType] + sample: NotRequired[list[Any] | Any] + version_added: NotRequired[str] + contains: NotRequired[dict[str, "AnsibleReturnParameter"]] def wrap_func(func, **updates): def wrapper(*args, **kwargs): return func(*args, **kwargs) - attrs = set(('__module__', '__name__', '__qualname__', '__doc__', '__annotations__', '__type_params__')).difference( + attrs = {'__module__', '__name__', '__qualname__', '__doc__', '__annotations__', '__type_params__'}.difference( updates.keys() ) for attr in attrs: @@ -44,29 +83,14 @@ def wrap_func(func, **updates): GENERIC_DOC = """Returns an dictionary for the Ansible {type} type.""" -def default(name: str): - def wrapped( - required: bool = False, - help: str | list[str] | None = None, - choices: Optional[Sequence] = None, - default: Optional[Any] = None, - ) -> AnsibleParameter: - option: AnsibleParameter = dict(type=name, required=required) - if choices is not None: - option["choices"] = choices - if default is not None: - option["default"] = default - if help is not None and isinstance(help, str): - option["description"] = help.split("\n") - elif help is not None: - option["description"] = help - return option - - return wrapped - - -class meta(type): +class TypeBase(type): def __new__(cls, clsname, bases, attrs): + if "_default" not in attrs: + raise TypeError( + f"The class {clsname} must define an wrapper function called _default that returns the default type" + ) + default_type = attrs["_default"] + del attrs["_default"] types = frozenset( ( "str", @@ -85,19 +109,38 @@ class meta(type): ) for attr in types - set(attrs.keys()): attrs[attr] = wrap_func( - default(attr), __doc__=GENERIC_DOC.format(type=attr), __name__=attr, __qualname__=f"{clsname}.{attr}" + default_type(attr), + __doc__=GENERIC_DOC.format(type=attr), + __name__=attr, + __qualname__=f"{clsname}.{attr}", ) attrs["__slots__"] = () return super().__new__(cls, clsname, bases, attrs) -class Types(metaclass=meta): +class Types(metaclass=TypeBase): + @staticmethod + def _default(name: AnsibleType): + def wrapped( + help: str | list[str], + required: bool = False, + choices: Optional[Sequence] = None, + default: Optional[Any] = None, + ) -> AnsibleParameter: + option = AnsibleParameter(type=name, required=required, description=help) + if choices is not None: + option["choices"] = tuple(choices) + if default is not None: + option["default"] = default + return option + + return wrapped @staticmethod def list( # type: ignore[misc] - elements: Union[Type[object], str, AnsibleParameter], + elements: Type[object] | AnsibleType | AnsibleParameter, + help: str | list[str], required: bool = False, - help: str | list[str] | None = None, default: list[Any] | None = None, ) -> AnsibleParameter: """Wrapper for the Ansible list type @@ -110,12 +153,16 @@ class Types(metaclass=meta): """ if required and default: raise ValueError("required and default are not allowed") - option: AnsibleParameter = dict(type="list", required=required) + option: AnsibleParameter = AnsibleParameter( + type="list", + required=required, + description=help, + ) if not isinstance(elements, (str, dict)): - option["elements"] = elements.__name__ + option["elements"] = elements.__name__ # type:ignore[reportGeneralTypeIssue] elif isinstance(elements, dict): option["elements"] = elements["type"] - if elements["type"] == "dict": + if elements["type"] == "dict" and "options" in elements: option["options"] = dict() for name, value in elements["options"].items(): option["options"][name] = value @@ -135,7 +182,7 @@ class Types(metaclass=meta): return option @staticmethod - def dict(required: bool = False, help: str | builtins.list[str] | None = None, **options: AnsibleParameter) -> AnsibleParameter: # type: ignore[misc] + def dict(help: str | builtins.list[str], required: bool = False, **options: AnsibleParameter) -> AnsibleParameter: # type: ignore[misc] """Wrapper for the Ansible dict type Args: @@ -143,7 +190,7 @@ class Types(metaclass=meta): help: an helptext for the ansible-doc options: The individual options that this parameter has """ - option: AnsibleParameter = dict(type="dict", required=required) + option: AnsibleParameter = AnsibleParameter(type="dict", description=help, required=required) option["options"] = options if help is not None and isinstance(help, str): option["description"] = help.split("\n") @@ -152,7 +199,70 @@ class Types(metaclass=meta): return option -def systemdbool(b: Union[bool, str]) -> str: +class ReturnTypes(metaclass=TypeBase): + @staticmethod + def _default(name: AnsibleType): + def wrapped( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ) -> AnsibleReturnParameter: + option = AnsibleReturnParameter(type=name, description=help) + if returned is not None: + option["returned"] = returned + if sample is not None: + option["sample"] = sample + if version_added is not None: + option["version_added"] = version_added + return option + + return wrapped + + @staticmethod + def list( + help: str | list[str], + elements: AnsibleType | AnsibleReturnParameter | Type[object], + returned: ReturnOptions | None = None, + sample: builtins.list[Any] | None = None, + version_added: str | None = None, + ) -> AnsibleReturnParameter: + option = AnsibleReturnParameter(description=help, type="list") + if returned is not None: + option["returned"] = returned + if isinstance(elements, str): + option["elements"] = elements + elif isinstance(elements, dict): + option["elements"] = elements["type"] + if "contains" in elements: + option["contains"] = elements["contains"] + else: + option["elements"] = elements.__name__ # type:ignore[reportGeneralTypeIssue] + if sample is not None: + option["sample"] = sample + if version_added is not None: + option["version_added"] = version_added + return option + + @staticmethod + def dict( + help: str | builtins.list[str], + contains: dict[str, AnsibleReturnParameter], + returned: ReturnOptions | None = None, + sample: builtins.list[Any] | None = None, + version_added: str | None = None, + ): + option = AnsibleReturnParameter(description=help, type="dict", contains=contains) + if returned is not None: + option["contains"] = contains + if sample is not None: + option["sample"] = sample + if version_added is not None: + option["version_added"] = version_added + return option + + +def systemdbool(b: bool | str) -> str: """Converts values into things systemd can parse""" if b is True: return "yes" @@ -166,10 +276,14 @@ def modspec( mutually_exclusive: Sequence[Tuple[str, ...]] = (), required_together: Sequence[Tuple[str, ...]] = (), required_one_of: Sequence[Tuple[str, ...]] = (), - required_if: Sequence[Union[Tuple[str, Any, Tuple[str, ...]], Tuple[str, Any, Tuple[str, ...], bool]]] = (), - required_by: Dict[str, Union[str, Tuple[str, ...]]] = {}, + required_if: Sequence[Tuple[str, Any, Tuple[str, ...]] | Tuple[str, Any, Tuple[str, ...], bool]] = (), + required_by: Dict[str, str | Tuple[str, ...]] = {}, supports_check_mode: bool = False, add_file_common_args: bool = False, + deprecated: bool = False, + version_added: str | None = None, + notes: list[str] | None = None, + extends_documentation_fragment: list[str] | None = None, ) -> Dict[str, Any]: # pragma: nocover """Wrapper to properly Type the module specs""" return dict( @@ -181,6 +295,10 @@ def modspec( required_by=required_by, add_file_common_args=add_file_common_args, supports_check_mode=supports_check_mode, + deprecated=deprecated, + version_added=version_added, + notes=notes, + extends_documentation_fragment=extends_documentation_fragment, ) diff --git a/plugins/module_utils/generic.pyi b/plugins/module_utils/generic.pyi index 4dae026..d499cc1 100644 --- a/plugins/module_utils/generic.pyi +++ b/plugins/module_utils/generic.pyi @@ -1,24 +1,62 @@ from pathlib import PosixPath -from typing import Any, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Literal, NotRequired, Optional, Required, Sequence, Tuple, Type, TypedDict __all__ = [ - 'Types', - 'SYSTEMD_SERVICE_CONFIG', - 'SYSTEMD_NETWORK_CONFIG', - 'SYSTEMD_CONFIG_ROOT', - 'systemdbool', - 'AnsibleParameter', + "Types", + "SYSTEMD_SERVICE_CONFIG", + "SYSTEMD_NETWORK_CONFIG", + "SYSTEMD_CONFIG_ROOT", + "FILE_COMMON_ARGS", + "systemdbool", + "AnsibleParameter", + "AnsibleReturnParameter", ] SYSTEMD_CONFIG_ROOT: PosixPath SYSTEMD_NETWORK_CONFIG: PosixPath SYSTEMD_SERVICE_CONFIG: PosixPath -AnsibleParameter = dict[str, Any] +FILE_COMMON_ARGS: frozenset[str] -class meta(type): +AnsibleType = Literal[ + "str", + "bool", + "int", + "float", + "path", + "raw", + "jsonarg", + "json", + "bytes", + "dict", + "list", + "bits", +] +ReturnOptions = Literal["always", "changed", "success"] + +class AnsibleParameter(TypedDict, total=False): + description: Required[str | list[str]] + required: NotRequired[bool] + default: NotRequired[Any] + type: Required[AnsibleType] + choices: NotRequired[list[Any] | tuple[Any]] + elements: NotRequired["AnsibleParameter" | AnsibleType] + aliases: NotRequired[list[str]] + version_added: NotRequired[str] + options: NotRequired[dict[str, "AnsibleParameter"]] + +class AnsibleReturnParameter(TypedDict, total=False): + description: Required[str | list[str]] + type: Required[AnsibleType] + returned: NotRequired[ReturnOptions] + elements: NotRequired[AnsibleType] + sample: NotRequired[list[Any] | Any] + version_added: NotRequired[str] + contains: NotRequired[dict[str, "AnsibleReturnParameter"]] + +class TypeBase(type): def __new__(cls, clsname, bases, attrs): ... -class Types(metaclass=meta): +class Types(metaclass=TypeBase): @staticmethod def str( required: bool = False, @@ -101,6 +139,94 @@ class Types(metaclass=meta): required: bool = False, help: str | list[str] | None = None, **options: AnsibleParameter ) -> AnsibleParameter: ... +class ReturnTypes(metaclass=TypeBase): + @staticmethod + def list( + help: str | list[str], + elements: AnsibleType | AnsibleReturnParameter | Type[object], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def dict( + help: str | list[str], + contains: dict[str, AnsibleReturnParameter], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def str( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def bool( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def int( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def float( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def path( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def raw( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def jsonarg( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def json( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def bytes( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + @staticmethod + def bits( + help: str | list[str], + returned: ReturnOptions | None = None, + sample: list[Any] | None = None, + version_added: str | None = None, + ): ... + def systemdbool(b: bool | str) -> str: ... def joindict(*items: dict) -> dict: ... def modspec( @@ -108,8 +234,12 @@ def modspec( mutually_exclusive: Sequence[Tuple[str, ...]] = (), required_together: Sequence[Tuple[str, ...]] = (), required_one_of: Sequence[Tuple[str, ...]] = (), - required_if: Sequence[Union[Tuple[str, Any, Tuple[str, ...]], Tuple[str, Any, Tuple[str, ...], bool]]] = (), - required_by: Dict[str, Union[str, Tuple[str, ...]]] = {}, + required_if: Sequence[Tuple[str, Any, Tuple[str, ...]] | Tuple[str, Any, Tuple[str, ...], bool]] = (), + required_by: Dict[str, str | Tuple[str, ...]] = {}, supports_check_mode: bool = False, add_file_common_args: bool = False, + deprecated: bool = False, + version_added: str | None = None, + notes: list[str] | None = None, + extends_documentation_fragment: list[str] | None = None, ) -> Dict[str, Any]: ... diff --git a/plugins/module_utils/module.py b/plugins/module_utils/module.py index 71baaaf..01c73f2 100644 --- a/plugins/module_utils/module.py +++ b/plugins/module_utils/module.py @@ -1,11 +1,12 @@ -import pathlib -from copy import deepcopy -from typing import Any, Callable, ClassVar, Dict, NoReturn, Optional, Type, TypedDict, TypeVar, Union, overload, Generic -import ansible.module_utils.basic as basic import os +import pathlib import shutil +from copy import deepcopy +from typing import Any, Callable, ClassVar, Dict, Generic, NoReturn, Optional, Type, TypedDict, TypeVar, overload -from .generic import AnsibleParameter, Types, systemdbool +import ansible.module_utils.basic as basic + +from .generic import AnsibleParameter, AnsibleReturnParameter, Types, modspec, systemdbool __all__ = ( "AnsibleModule", @@ -25,28 +26,28 @@ class TypedDiff(Generic[T], TypedDict, total=False): after_header: str -def docify(input: Union[dict, AnsibleParameter]) -> dict: +def docify(input: AnsibleParameter) -> dict: options = dict() for name, help in input.items(): - options[name] = dict(type=help["type"]) - if "description" in help: - if isinstance(help["description"], str): - help["description"] = help["description"].split("\n") - options[name]["description"] = help["description"] - if "required" in help and help["required"]: + options[name] = dict(type=help["type"]) # type: ignore[reportIndexIssue] + if "description" in help: # type: ignore[reportOperatorIssue] + if isinstance(help["description"], str): # type: ignore[reportIndexIssue] + help["description"] = help["description"].split("\n") # type: ignore[reportIndexIssue] + options[name]["description"] = help["description"] # type: ignore[reportIndexIssue] + if "required" in help and help["required"]: # type: ignore[reportOperatorIssue] options[name]["required"] = True else: options[name]["required"] = False - if help["type"] == "list": - options[name]["elements"] = help["elements"] + if help["type"] == "list": # type: ignore[reportOperatorIssue] + options[name]["elements"] = help["elements"] # type: ignore[reportIndexIssue] if not options[name]["required"]: options[name]["default"] = [] - if "default" in help: - options[name]["default"] = help["default"] - if "options" in help and help["options"] != {}: - options[name]["options"] = docify(help["options"]) - if "choices" in help and len(help["choices"]) > 0: - options[name]["choices"] = tuple(help["choices"]) + if "default" in help: # type: ignore[reportOperatorIssue] + options[name]["default"] = help["default"] # type: ignore[reportIndexIssue] + if "options" in help and help["options"] != {}: # type: ignore[reportOperatorIssue] + options[name]["options"] = docify(help["options"]) # type: ignore[reportIndexIssue] + if "choices" in help and len(help["choices"]) > 0: # type: ignore[reportOperatorIssue] + options[name]["choices"] = tuple(help["choices"]) # type: ignore[reportIndexIssue] return options @@ -61,6 +62,10 @@ class AnsibleModule(object): result: dict #: the specification of the arguments. Subclasses that are usable Modules must set this value. module_spec: ClassVar[dict] + + #: The specification of return arguments + return_spec: ClassVar[dict[str, AnsibleReturnParameter]] + #: This is set by classes that define common things for their subclasses, like behaviour of the run and check methods. This is used by `SystemdUnitModule` _common_args: ClassVar[dict[str, Any]] = dict() @@ -69,7 +74,11 @@ class AnsibleModule(object): """params is an wrapper for the module.params""" return self.module.params # type: ignore - def __init__(self): + def __init__(self, documentation=False): + """ + Args: + documentation: Only true if the module is initialized for documentation. The Ansible Module is None is this case + """ self.result = dict(changed=False) specs = dict() specs.update(deepcopy(self._common_args)) @@ -78,9 +87,13 @@ class AnsibleModule(object): specs["argument_spec"].update(modspec["argument_spec"]) del modspec["argument_spec"] specs.update(modspec) - self.modspec = specs - self.module = basic.AnsibleModule(**specs) - self.tmpdir = pathlib.Path(self.module.tmpdir) + self.modspec = specs.copy() + specs.pop("deprecated", None) + specs.pop("notes", None) + specs.pop("version_added", None) + if not documentation: + self.module = basic.AnsibleModule(**specs) + self.tmpdir = pathlib.Path(self.module.tmpdir) def set(self, key: str, value): """sets an value for the result""" @@ -179,21 +192,50 @@ class AnsibleModule(object): doc = cls.__doc__ if doc is None: doc = "" - specs = dict() - if "argument_spec" in cls._common_args: # pragma: nocover - specs.update(cls._common_args["argument_spec"]) - if "argument_spec" in cls.module_spec: # pragma: nocover - specs.update(cls.module_spec["argument_spec"]) - options = docify(specs) + mod = cls(documentation=True) + options = docify(mod.modspec["argument_spec"]) docu = doc.split("\n") + documentation: dict[str, Any] = dict( + module=cls.name, + short_description=docu[0], + description=docu, + options=options, + ) + if mod.modspec.get("extends_documentation_fragment", None) is not None or mod.modspec.get( + "add_file_common_args", False + ): + documentation["extends_documentation_fragment"] = [] + if mod.modspec.get("extends_documentation_fragment", None) is not None: + documentation["extends_documentation_fragment"].extend(mod.modspec["extends_documentation_fragment"]) + if mod.modspec.get("add_file_common_args", False): + documentation["extends_documentation_fragment"].append("ansible.builtin.files") + if mod.modspec.get("deprecated", False): + documentation["deprecated"] = True + if mod.modspec.get("notes", None) is not None: + documentation["notes"] = mod.modspec["notes"] + if mod.modspec.get("version_added", None) is not None: + documentation["version_added"] = mod.modspec["version_added"] + return str( yaml.safe_dump( - dict( - module=cls.name, - short_description=docu[0], - description=docu, - options=options, - ), + documentation, + stream=None, + explicit_start=True, + ) + ) + + @classmethod + def returns(cls) -> str: + """This returns the string for the RETURNS String of the module documentation""" + try: + import yaml + except ImportError: # pragma: nocover + return "---\n" + if not hasattr(cls, "return_spec"): + return "--\n" + return str( + yaml.safe_dump( + cls.returns, stream=None, explicit_start=True, ) diff --git a/pyproject.toml b/pyproject.toml index 0ec116c..e23da19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,13 @@ name = "ansible-module" dynamic = ["version"] description = 'Helps with developing modules for ansible in an easier manner' readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.11" license = "MIT" keywords = [] authors = [{ name = "Sebastian Tobie", email = "sebastian@sebastian-tobie.de" }] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", diff --git a/src/ansible_module/__init__.py b/src/ansible_module/__init__.py index cd1ee63..3d18726 100644 --- a/src/ansible_module/__init__.py +++ b/src/ansible_module/__init__.py @@ -1 +1 @@ -__version__ = "0.4.4" +__version__ = "0.5.0" diff --git a/src/ansible_module/update_doc.py b/src/ansible_module/update_doc.py index b001b52..245e2fe 100755 --- a/src/ansible_module/update_doc.py +++ b/src/ansible_module/update_doc.py @@ -4,10 +4,15 @@ import pathlib import re import sys +from .module_utils.module import AnsibleModule + sys.path.append(".") mindocstring = "DOCUMENTATION = ''''''" moduledir = pathlib.Path("plugins/modules") -regex = re.compile("DOCUMENTATION *= *r?(?P\"{3}|'{3})(---)?.*?(?P=quote)", re.MULTILINE | re.DOTALL) +regex = re.compile( + "(?PDOCUMENTATION|RETURNS) *= *r?(?P\"{3}|'{3})(---)?.*?(?P=quote)", + re.MULTILINE | re.DOTALL, +) def main() -> None: @@ -20,6 +25,7 @@ def main() -> None: if modfile.name in ("__init__.py", "__pycache__", "unit.py.example"): continue mod = importlib.import_module(".".join((modfile.parts[:-1]) + (modfile.stem,))) + module: AnsibleModule if hasattr(mod, "Module"): module = mod.Module elif hasattr(mod, "__module_name__"): @@ -29,6 +35,7 @@ def main() -> None: continue try: moddoc = module.doc() + returns = module.returns() except AttributeError: print("Broken module. skipping {}".format(modfile)) continue @@ -36,16 +43,37 @@ def main() -> None: print("Error in documentation of module {}: {}".format(modfile, e)) continue moddata = modfile.read_text() - match = regex.search(moddata) - if not match: + changed = False + start = 0 + while True: + match = regex.search(moddata, pos=start) + if not match: + break + changed = True + type = match.group("type") + content = "" + if type == "DOCUMENTATION": + content = moddoc + elif type == "RETURNS": + content = returns + else: + Exception("Please update the script: Unknown Type") + after = moddata[match.end() :] + moddata = "{pre}{type} = r{quote}{doc}{quote}".format( + pre=moddata[: match.start()], + type=type, + quote=match.group("quote"), + doc=content, + ) + start = len(moddata) + moddata = f"{moddata}{after}" + if changed is False: print( "no Documentation set for module {}. Please add at least \"{}\" to the file".format( modfile.stem, mindocstring ) ) continue - newmod = "{pre}DOCUMENTATION = {quote}{doc}{quote}{post}".format( - pre=moddata[: match.start()], quote=match.group("quote"), doc=moddoc, post=moddata[match.end() :] - ) - modfile.write_text(newmod) + + modfile.write_text(moddata) print("updated the documentation of module {}".format(module.name))