parse-changelog 1.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ 1.0.10
@@ -0,0 +1,11 @@
1
+ import os
2
+
3
+ def get_version():
4
+ dir_path = os.path.dirname(os.path.realpath(__file__))
5
+ try:
6
+ with open(os.path.join(dir_path, "VERSION"), encoding="utf-8") as vf:
7
+ version = vf.read().strip()
8
+ except Exception: #pylint: disable=broad-exception-caught
9
+ version = "0.0.0"
10
+
11
+ return version
@@ -0,0 +1,292 @@
1
+ #!python
2
+ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
3
+ from collections import OrderedDict
4
+ from datetime import datetime
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ import tempfile
10
+
11
+
12
+ def add_release(changelog_file, new_release, release_date=None):
13
+ """
14
+ Add a new release to the changelog, using the current content of the Unreleased section.
15
+
16
+ Args:
17
+ changelog_file: (str) changelog file to parse
18
+ new_release: (str) version of the release to create
19
+ release_date: (datetime) date of the release (default to today)
20
+ """
21
+ if not release_date:
22
+ release_date = datetime.now().strftime("%Y-%m-%d")
23
+
24
+ new_release_title = f"[{new_release}] - {release_date}"
25
+
26
+ changelog = parse(changelog_file)
27
+
28
+ if new_release in changelog:
29
+ print("Release already exists in changelog")
30
+ return
31
+
32
+ changelog[new_release] = {
33
+ "title": new_release_title,
34
+ "content": changelog["prerelease"]["content"],
35
+ "version": new_release,
36
+ "date": release_date
37
+ }
38
+ # Move the new release into the correct spot - move new release to the beginning, and then put prerelease and intro at the beginning
39
+ changelog.move_to_end(new_release, last=False)
40
+ changelog.move_to_end("prerelease", last=False)
41
+ changelog.move_to_end("introduction", last=False)
42
+
43
+ changelog["prerelease"]["content"] = ""
44
+
45
+ print(json.dumps(changelog, indent=2))
46
+ write_changelog(changelog_file, changelog)
47
+
48
+
49
+ def parse(changelog_file):
50
+ """
51
+ Parse the changelog into JSON
52
+
53
+ Args:
54
+ changelog_file: (str) changelog file to parse
55
+ """
56
+
57
+ #
58
+ # Mode 2 - parse the changelog into a list of releases
59
+ #
60
+ try:
61
+ with open(changelog_file, "r", encoding="utf-8") as cf:
62
+ changelog = cf.read()
63
+ except FileNotFoundError:
64
+ print(f"Could not find {changelog_file}")
65
+ sys.exit(1)
66
+
67
+ # This parser is extremely simple and makes many assumptions about the structure of the document.
68
+ # Instead of parsing headings into a generic tree structure, assume the changelog format where there is a single
69
+ # heading1 node, a list of heading2 nodes that refer to releases, and 0 or more heading3 nodes per release that
70
+ # refer to types of changes
71
+ release_list = OrderedDict()
72
+ current_release = None
73
+ current_content = []
74
+ found_heading1 = False
75
+ version = "unknown"
76
+ rel_date = "unknown"
77
+ for line in changelog.split("\n"):
78
+ if line.startswith("#"):
79
+ heading_level = line.count("#")
80
+ title = line.strip("#").strip()
81
+
82
+ # Main heading, there should only be one
83
+ if heading_level == 1:
84
+ assert title.lower() == "changelog", "The top level heading1 must be named Changelog"
85
+ assert not found_heading1, "There can only be one heading1"
86
+ found_heading1 = True
87
+ continue
88
+
89
+ # Heading2, this indicates a release section
90
+ if heading_level == 2:
91
+ # A new release means stop parsing for the previous release
92
+ # Add the previous release to the document
93
+ if current_release:
94
+ if version != "unknown":
95
+ section_key = version
96
+ else:
97
+ section_key = current_release
98
+ release_list[section_key] = {
99
+ "title": current_release,
100
+ "content": "\n".join(current_content).rstrip(),
101
+ "version": version,
102
+ "date": rel_date}
103
+ else:
104
+ release_list["introduction"] = {
105
+ "title": "Changelog",
106
+ "content": "\n".join(current_content).rstrip(),
107
+ "version": "",
108
+ "date": ""
109
+ }
110
+
111
+ current_release = title
112
+ current_content = []
113
+ # Special case for Unreleased section
114
+ if title.lower() == "[unreleased]":
115
+ version = "prerelease"
116
+ rel_date = "unreleased"
117
+ else:
118
+ # Parse version and release date from the section title
119
+ m = re.match(
120
+ r"\[(\d+\.\d+\.\d+)[-\.+0-9a-zA-Z]*\]\s+-\s+(\d{4}-\d{2}-\d{2})", title)
121
+ if m:
122
+ version = m.group(1)
123
+ rel_date = m.group(2)
124
+ else:
125
+ version = "unknown"
126
+ rel_date = "unknown"
127
+
128
+ continue
129
+
130
+ # Any other level is content inside a release
131
+ if heading_level >= 3:
132
+ current_content.append(line)
133
+ continue
134
+
135
+ # Anything other than a release line, add to the current content
136
+ current_content.append(line)
137
+
138
+ # Add the last section we found
139
+ if current_release:
140
+ if version != "unknown":
141
+ section_key = version
142
+ else:
143
+ section_key = current_release
144
+ release_list[section_key] = {
145
+ "title": current_release,
146
+ "content": "\n".join(current_content).rstrip(),
147
+ "version": version,
148
+ "date": rel_date}
149
+
150
+ return release_list
151
+
152
+
153
+ def pretty_print(changelog_file, show_unreleased):
154
+ """
155
+ Print changelog as JSON
156
+
157
+ Args:
158
+ changelog_file: (str) changelog file to parse
159
+ show_unreleased: (bool) only show the unreleased section
160
+ """
161
+ changelog = parse(changelog_file)
162
+
163
+ # Only show the unreleased changes section
164
+ if show_unreleased:
165
+ print(changelog.get("prerelease", {}).get("content", ""))
166
+ return
167
+
168
+ # Show what we parsed, in JSON format
169
+ print(json.dumps(changelog, indent=2))
170
+
171
+
172
+ def add_change(changelog_file, change_desc, change_type):
173
+ change_type = change_type.title()
174
+ if not change_desc.startswith("*"):
175
+ change_desc = "* " + change_desc
176
+ change_desc.rstrip()
177
+
178
+ changelog = parse(changelog_file)
179
+ prerelease = changelog.get("prerelease", {})
180
+ if not prerelease:
181
+ prerelease = {
182
+ "title": "[Unreleased]",
183
+ "content": "",
184
+ "version": "prerelease",
185
+ "date": "unreleased"
186
+ }
187
+
188
+ # Insert the new change at the appropriate section
189
+ content = prerelease.get("content", "")
190
+ new_content = ""
191
+ found = False
192
+ for line in content.split("\n"):
193
+ if re.search(r"^###\s+" + change_type, line, re.IGNORECASE):
194
+ found = True
195
+ # insert the new change description
196
+ if not new_content.endswith("\n"):
197
+ new_content += "\n"
198
+ new_content += line
199
+ if not new_content.endswith("\n"):
200
+ new_content += "\n"
201
+ new_content += change_desc + "\n"
202
+ else:
203
+ new_content += line
204
+ if not new_content.endswith("\n"):
205
+ new_content += "\n"
206
+ prerelease["content"] = new_content.rstrip()
207
+ if not found:
208
+ prerelease["content"] += f"\n### {change_type}\n{change_desc}"
209
+
210
+
211
+ changelog["prerelease"] = prerelease
212
+
213
+ print(json.dumps(changelog, indent=2))
214
+ write_changelog(changelog_file, changelog)
215
+
216
+
217
+ def write_changelog(changelog_file, content):
218
+ """
219
+ Write out a changelog from the JSON structure
220
+
221
+ Args:
222
+ changelog_file: (str) changelog file to parse
223
+ content: (OrderedDict) content to write to the changelog
224
+ """
225
+ target_path = os.path.abspath(changelog_file)
226
+ target_dir = os.path.dirname(target_path) or "."
227
+ new_changelog = None
228
+ try:
229
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=target_dir, delete=False) as tmp:
230
+ new_changelog = tmp.name
231
+ tmp.write("# Changelog\n")
232
+ intro = content.pop("introduction", {}).get("content", "")
233
+ tmp.write(f"{intro}\n")
234
+ tmp.write("\n")
235
+
236
+ for idx, heading in enumerate(content):
237
+ tmp.write(f"## {content[heading]['title']}\n")
238
+ if content[heading]["content"]:
239
+ tmp.write(content[heading]["content"] + "\n")
240
+ if idx < len(content)-1:
241
+ tmp.write("\n")
242
+ os.replace(new_changelog, target_path)
243
+ finally:
244
+ # Make sure intermediate file is removed
245
+ if new_changelog:
246
+ try:
247
+ os.remove(new_changelog)
248
+ except FileNotFoundError:
249
+ pass
250
+
251
+
252
+ def get_version():
253
+ try:
254
+ import parse_changelog
255
+ return parse_changelog.get_version()
256
+ except ImportError:
257
+ return "0.0.0"
258
+
259
+
260
+ if __name__ == "__main__":
261
+ change_types = ["added", "changed", "deprecated", "removed", "fixed", "security"]
262
+
263
+ parser = ArgumentParser(description="Parse and update changelog files",
264
+ formatter_class=ArgumentDefaultsHelpFormatter)
265
+ # Common args
266
+ parser.add_argument("--changelog", "-c", dest="changelog_file", default="CHANGELOG.md",
267
+ help="the changelog file to parse")
268
+ parser.add_argument("--version", "-v", action="version",
269
+ version=get_version())
270
+ # Args for parsing
271
+ parsing_group = parser.add_argument_group(title="Parsing")
272
+ parsing_group.add_argument("--show-unreleased", "-u", dest="show_unreleased", action="store_true",
273
+ help="When parsing, only show the changes from the Unreleased section")
274
+ # Args for creating a release
275
+ release_group = parser.add_argument_group("Adding a Release")
276
+ release_group.add_argument("--release", "-r", dest="new_release", metavar="X.Y.Z",
277
+ help="Create a new release by moving the Unreleased section to the specified new release section")
278
+ release_group.add_argument("--date", "-d", dest="release_date", metavar="YYYY-MM-DD",
279
+ help="Use this date for the new release, instead of today")
280
+ # Args for adding changes
281
+ change_group = parser.add_argument_group("Adding a change")
282
+ change_group.add_argument("--add-change", "-a", dest="change_desc", metavar="TEXT", help="One-line description of the change")
283
+ change_group.add_argument("--type", "-t", dest="change_type", metavar="TYPE", choices=change_types, help=f"The type of change {change_types}")
284
+
285
+ args = parser.parse_args()
286
+
287
+ if args.new_release:
288
+ add_release(args.changelog_file, args.new_release, args.release_date)
289
+ elif args.change_desc:
290
+ add_change(args.changelog_file, args.change_desc, args.change_type)
291
+ else:
292
+ pretty_print(args.changelog_file, args.show_unreleased)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Carl Seelye
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.2
2
+ Name: parse-changelog
3
+ Version: 1.0.10
4
+ Summary: A very simplistic changelog parser/updater
5
+ Home-page: https://github.com/cseelye/parse-changelog
6
+ Author: Carl Seelye
7
+ Author-email: cseelye@gmail.com
8
+ License: MIT
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: description
14
+ Dynamic: description-content-type
15
+ Dynamic: home-page
16
+ Dynamic: license
17
+ Dynamic: summary
18
+
19
+ # parse-changelog
20
+ This is a simplified changelog updater/parser, made for CI pipeline use. It has these abilities:
21
+ 1. Add a new change to the changelog, in the Unreleased section
22
+ 1. Add a new release into the changelog, using the list of changes from the Unreleased section
23
+ 1. Parse the releases in the changelog into a JSON structure.
24
+
25
+ Changelogs must be in the format documented by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), see the details
26
+ below.
27
+
28
+ Release history: [parse-changelog's CHANGELOG](https://github.com/cseelye/parse-changelog/blob/main/CHANGELOG.md)
29
+
30
+ ## Adding a Release
31
+ This mode is invoked by using the `--release` arg and will parse the changelog and insert a new release at the beginning
32
+ of the list of releases, using the changes from the Unreleased section. You can optionally specify a release date, or by
33
+ default use todays date.
34
+
35
+ The changelog must use the format `## [Unreleased]` (case insensitive) for this parser to find it.
36
+ For example, here is the diff generated by adding a new release(`--release 1.0.2`):
37
+ ```
38
+ ## [Unreleased]
39
+ +
40
+ +## [1.0.2] - 2022-10-20
41
+ * Great new stuff
42
+
43
+ ## [1.0.1] - 2022-10-08
44
+ ```
45
+
46
+ ## Adding a Change
47
+ This mode is invoked with the `--add-change` and `--type` args. It will parse the changelog to find the Unreleased
48
+ section and any changes already there, find the correct change type heading, and add the new change.
49
+ For example, here is the diff generated by adding a new fix (`--add-change "Fix that annoying bug" --type fixed`):
50
+ ```
51
+ ## [Unreleased]
52
+ * Great new stuff
53
+ +### Fixed
54
+ +* Fix that annoying bug
55
+
56
+ ## [1.0.1] - 2022-10-08
57
+ ```
58
+
59
+ ## Changelog Parsing
60
+ This mode will parse the changelog into JSON and print it to the screen. It finds all of
61
+ the heading2 entries (lines starting with `##`) and assumes each of those is a release. For each release, it parses
62
+ the release title into version and date, and collects the content of the release as a single string. Each release
63
+ heading must be of the format `## [release_version] - YYYY-MM-DD`, where `release_version` matches SemVer version string
64
+ spec, and `YYYY-MM-DD` is a valid year-month-day. The one exception to this format is the special release heading for
65
+ unreleased changes, which must match `## [Unreleased]`. The content of the release is parsed as a single string and not
66
+ interpreted in any way.
67
+
68
+ The top heading1 and project description are parsed into a special section named "introduction".
69
+
70
+ For example, given the following changelog:
71
+ ```
72
+ # Changelog
73
+
74
+ My Project Name
75
+
76
+ All notable changes to this project will be documented in this file.
77
+
78
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
79
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
80
+
81
+ ## [Unreleased]
82
+ * Great new stuff
83
+
84
+ ## [1.0.1] - 2022-10-08
85
+ ### Fixed
86
+ * Minor bug that we missed
87
+ ### Changed
88
+ * New name for an artifact
89
+
90
+ ## [1.0.0] - 2022-09-01
91
+ Initial release
92
+ * Some cool feature
93
+ * Other interesting stuff
94
+ ```
95
+
96
+ It will be parsed into this JSON:
97
+
98
+ ```
99
+ {
100
+ "introduction": {
101
+ "title": "Changelog",
102
+ "content": "\nMy Project Name\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).",
103
+ "version": "",
104
+ "date": ""
105
+ },
106
+ "prerelease": {
107
+ "title": "[Unreleased]",
108
+ "content": "* Great new stuff",
109
+ "version": "prerelease",
110
+ "date": "unreleased"
111
+ },
112
+ "[1.0.1] - 2022-10-08": {
113
+ "title": "[1.0.1] - 2022-10-08",
114
+ "content": "### Fixed\n* Minor bug that we missed\n### Changed\n* New name for an artifact",
115
+ "version": "1.0.1",
116
+ "date": "2022-10-08"
117
+ },
118
+ "[1.0.0] - 2022-09-01": {
119
+ "title": "[1.0.0] - 2022-09-01",
120
+ "content": "Initial release\n* Some cool feature\n* Other interesting stuff",
121
+ "version": "1.0.0",
122
+ "date": "2022-09-01"
123
+ }
124
+ }
125
+ ```
@@ -0,0 +1,8 @@
1
+ parse_changelog/VERSION,sha256=ACUb7h0D9ljCMkkmeFqZbdBA3_Fr-ZOyxSCiDtu6csg,7
2
+ parse_changelog/__init__.py,sha256=C1Qe0BVPxa61b441fKYGAf8Qd7XyHgBYDwR8F-RJxlA,324
3
+ parse_changelog-1.0.10.data/scripts/parse-changelog,sha256=Dt5md5ZmhQ9Cge6v3l--X7rL1NK2wiy8HMDUSXdqdz8,10762
4
+ parse_changelog-1.0.10.dist-info/LICENSE,sha256=Imudq40fjwY04eXGz5ObRd0xy51LT7X_jEphYX-9Y8Q,1068
5
+ parse_changelog-1.0.10.dist-info/METADATA,sha256=BfG0kAk8APC9W4wInj2Bjgq9BhhH5n7Qsh5i7N67Fjg,4383
6
+ parse_changelog-1.0.10.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
7
+ parse_changelog-1.0.10.dist-info/top_level.txt,sha256=scKFCMsZgB5m8oBmtrjV8iybGgm8pXaWF0XqfC6oLuY,16
8
+ parse_changelog-1.0.10.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (76.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ parse_changelog