File: dox_2_rst.py

package info (click to toggle)
uranium 5.0.0-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,304 kB
  • sloc: python: 31,765; sh: 132; makefile: 12
file content (167 lines) | stat: -rw-r--r-- 6,291 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import argparse
import os
import re
from pathlib import Path


class Dox2Rst:
    REGEX = re.compile(r"(?P<before>[\s\S]*\n)?" +
                       r"(?P<dox>\s*##.*\n(?:\s*#.*)*\n*)" +
                       r"(?P<decorator>(?:\s*@.*\n)*)" +
                       r"(?P<def>\s*(?:def|class).*)\n" +
                       r"(?P<after>[\s\S]*)")

    INDENT_PATTERN = re.compile(r"\s*")
    DOX_CONTINUATION_PREFIX_PATTERN = re.compile(r"( +#) *", re.MULTILINE)

    def convert(self, file_path: str, dry_run: bool = False) -> int:
        print(file_path)
        with open(file_path, "r+") as f:
            contents = f.read()
            change_count = 0

            # Block comments
            changed = True
            while changed:
                contents, changed = self.replace_first_block_comment(contents)
                change_count = change_count + changed  # Increase count by 1 if changed

            # variable / member comments
            changed = True
            while changed:
                contents, changed = self.replace_first_member_comment(contents)
                change_count = change_count + changed  # Increase count by 1 if changed

            if change_count > 0:
                if dry_run:
                    print("======[[ {} ]]======".format(file_path))
                    print(contents)
                else:
                    f.seek(0)
                    f.truncate()
                    f.write(contents)

            return change_count

    def replace_first_block_comment(self, contents: str) -> (str, bool):
        match = self.REGEX.match(contents)
        if match is not None:
            comment_block = match.group("dox")
            comment_block = self.add_indent(comment_block)
            comment_block = self.convert_comment_block(comment_block)
            contents = "{before}{decorator}{definition}\n{rst}\n{after}".format(
                before=match.group("before"),
                decorator=match.group("decorator"),
                definition=match.group("def"),
                rst=comment_block,
                after=match.group("after")
            )
            return contents, True
        else:
            return contents, False

    OPENING_REGEX = re.compile(r"## +")
    PARAM_REGEX = re.compile(r":param \w+")
    RETURN_REGEX = re.compile(r":returns?")
    RST_COMMAND_SUB = "\g<0>:"

    def convert_comment_block(self, dox_block: str):
        """

        :param dox_block:
        """
        indent = self.INDENT_PATTERN.search(dox_block)
        if indent is None:
            indent = ""
        else:
            indent = indent.group()

        # replace opening
        output = re.sub(self.OPENING_REGEX, '"""', dox_block)
        output = re.sub(self.DOX_CONTINUATION_PREFIX_PATTERN, indent, output)
        # replace keyword escapes ie. \return -> :return
        output = output.replace('\\', ":")

        output = output.replace(":code", "")
        output = output.replace(":endcode", "")
        output = re.sub(self.PARAM_REGEX, self.RST_COMMAND_SUB, output)
        output = re.sub(self.RETURN_REGEX, self.RST_COMMAND_SUB, output)
        # Add closing """
        if len(output.splitlines()) > 1:
            output = "{before}{indent}\"\"\"\n".format(before=output, indent=indent)
        else:
            output = output.splitlines()[0]
            output = "{before}\"\"\"\n".format(before=output, indent=indent)

        # ensure separation of subject line and body
        lines = output.splitlines()
        if len(lines) > 1 and re.search(r"\S", lines[1]) is not None:
            lines.insert(1, indent)
            output = "\n".join(lines)

        return output

    MEMBER_COMMENT_REGEX = re.compile(r"(?P<before>[\s\S]*\n)?(?P<dox>\s*##.*\n(?:\s*#.*)*\n*)(?P<var>[\s\w]+=.+\n)(?P<after>[\s\S]*)")

    def replace_first_member_comment(self, contents: str) -> (str, bool):
        match = self.MEMBER_COMMENT_REGEX.match(contents)
        if match is not None:
            comment = match.group("dox")
            comment = self.convert_comment_block(comment)
            contents = "{before}{var}{comment}{after}".format(
                before=match.group("before"),
                var=match.group("var"),
                comment=comment,
                after=match.group("after")
            )
            return contents, True
        else:
            return contents, False

    COMMENT_INDENT_PATTERN = re.compile(r"^\s*#", re.MULTILINE)
    COMMENT_INDENT_SUB = "    \g<0>"

    def add_indent(self, comment_block: str):
        """Add a single level of indent."""

        return re.sub(self.COMMENT_INDENT_PATTERN, self.COMMENT_INDENT_SUB, comment_block)


def main():
    parser = argparse.ArgumentParser("python dox_2_rst.py")
    parser.add_argument("-r", "--recursive", dest="recursive",
                        help="Find python files to convert recursively", default=False, action="store_true")
    parser.add_argument("-d", "--dry", dest="dry_run",
                        help="Print output; do not change files", default=False, action="store_true")
    parser.add_argument("paths", metavar="PATHS", type=str, nargs="+", help="Files or Directories to process")

    args = parser.parse_args()

    glob_func = "rglob" if args.recursive else "glob"

    changed_files_count = 0
    total_changes_count = 0
    python_file_list = []
    for entry in args.paths:
        if not os.path.exists(entry):
            print("WARNING: skipping {}: not found".format(entry))
            continue
        if os.path.isfile(entry):
            python_file_list.append(entry)
        else:
            for path_like in getattr(Path(entry), glob_func)('*.py'):
                python_file_list.append(path_like)

    dox_2_rst = Dox2Rst()
    for file_path in python_file_list:
        comments_changed_count = dox_2_rst.convert(str(file_path), dry_run=args.dry_run)
        if comments_changed_count > 0:
            changed_files_count = changed_files_count + 1
        total_changes_count = total_changes_count + comments_changed_count

    print("Considered {} Python files".format(len(python_file_list)))
    print("Converted {} comments across {} files".format(total_changes_count, changed_files_count))


if __name__ == "__main__":
    main()