changelet 0.1.0__tar.gz
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.
- changelet-0.1.0/LICENSE +25 -0
- changelet-0.1.0/PKG-INFO +82 -0
- changelet-0.1.0/README.md +43 -0
- changelet-0.1.0/changelet/__init__.py +5 -0
- changelet-0.1.0/changelet/command/__init__.py +18 -0
- changelet-0.1.0/changelet/command/bump.py +134 -0
- changelet-0.1.0/changelet/command/check.py +30 -0
- changelet-0.1.0/changelet/command/create.py +74 -0
- changelet-0.1.0/changelet/config.py +107 -0
- changelet-0.1.0/changelet/entry.py +120 -0
- changelet-0.1.0/changelet/github.py +94 -0
- changelet-0.1.0/changelet/main.py +61 -0
- changelet-0.1.0/changelet/pr.py +23 -0
- changelet-0.1.0/changelet.egg-info/PKG-INFO +82 -0
- changelet-0.1.0/changelet.egg-info/SOURCES.txt +29 -0
- changelet-0.1.0/changelet.egg-info/dependency_links.txt +1 -0
- changelet-0.1.0/changelet.egg-info/entry_points.txt +2 -0
- changelet-0.1.0/changelet.egg-info/requires.txt +18 -0
- changelet-0.1.0/changelet.egg-info/top_level.txt +1 -0
- changelet-0.1.0/pyproject.toml +16 -0
- changelet-0.1.0/setup.cfg +4 -0
- changelet-0.1.0/setup.py +61 -0
- changelet-0.1.0/tests/test_command.py +32 -0
- changelet-0.1.0/tests/test_command_bump.py +299 -0
- changelet-0.1.0/tests/test_command_check.py +40 -0
- changelet-0.1.0/tests/test_command_create.py +92 -0
- changelet-0.1.0/tests/test_config.py +175 -0
- changelet-0.1.0/tests/test_entry.py +226 -0
- changelet-0.1.0/tests/test_github.py +176 -0
- changelet-0.1.0/tests/test_main.py +76 -0
- changelet-0.1.0/tests/test_pr.py +29 -0
changelet-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Ross McFarland & the octoDNS Maintainers
|
|
4
|
+
Copyright (c) 2017 GitHub, Inc.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person
|
|
7
|
+
obtaining a copy of this software and associated documentation
|
|
8
|
+
files (the "Software"), to deal in the Software without
|
|
9
|
+
restriction, including without limitation the rights to use,
|
|
10
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the
|
|
12
|
+
Software is furnished to do so, subject to the following
|
|
13
|
+
conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
22
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
23
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
24
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
25
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
changelet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: changelet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ChangeLog and Release Management Tooling
|
|
5
|
+
Home-page: https://github.com/octodns/changelet
|
|
6
|
+
Author: Ross McFarland
|
|
7
|
+
Author-email: rwmcfa1@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: PyYaml
|
|
13
|
+
Requires-Dist: semver>=3.0.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-network; extra == "dev"
|
|
18
|
+
Requires-Dist: black<25.0.0,>=24.3.0; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=0.7.0; extra == "dev"
|
|
20
|
+
Requires-Dist: isort>=5.11.5; extra == "dev"
|
|
21
|
+
Requires-Dist: pyflakes>=2.2.0; extra == "dev"
|
|
22
|
+
Requires-Dist: readme_renderer[md]>=26.0; extra == "dev"
|
|
23
|
+
Requires-Dist: twine>=3.4.2; extra == "dev"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
27
|
+
Requires-Dist: pytest-network; extra == "test"
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: provides-extra
|
|
36
|
+
Dynamic: requires-dist
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
## ChangeLog and Release Management Tooling
|
|
41
|
+
|
|
42
|
+
A simple standalone Python module for CHANGELOG and release management. Spun out of [octoDNS](https://github.com/octodns/octodns/).
|
|
43
|
+
|
|
44
|
+
### Installation
|
|
45
|
+
|
|
46
|
+
#### Command line
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
pip install changelet
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Usage
|
|
53
|
+
|
|
54
|
+
For help with using the command
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
changelet --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
In most cases the only command you're likely to encounter is `create`. It will be used to add a changelog entry to your branch/PR.
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
changelet create --type (TYPE) Short description of your change to be included in the changelog
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The options for type are
|
|
67
|
+
* `major` - rare for non-maintainers, a change that will break backwards compatibility and require users to take care when updating
|
|
68
|
+
* `minor` - adds new functionality that is either self contained or done in a completely backwards compatible manner
|
|
69
|
+
* `patch` - fixes an issue or bug with existing functionality
|
|
70
|
+
* `none` - change that should not be included in the CHANGELOG and will not directly impact users, e.g. documentation, README, tooling, ...
|
|
71
|
+
|
|
72
|
+
### Using changlet
|
|
73
|
+
|
|
74
|
+
Currently the tooling has only been tested with repositories in the octoDNS org, but it should be usable elsewhere. You'll want to have a look at the following for how to incorporate it into your repo and workflows.
|
|
75
|
+
|
|
76
|
+
* [script/changelog](script/changelog)
|
|
77
|
+
* [.git_hooks_pre-commit](.git_hooks_pre-commit) (and [script/bootstrap](script/bootstrap) which installs it)
|
|
78
|
+
* [.github/workflows/changelog.yml](.github/workflows/changelog.yml)
|
|
79
|
+
|
|
80
|
+
### Development
|
|
81
|
+
|
|
82
|
+
See the [/script/](/script/) directory for some tools to help with the development process. They generally follow the [Script to rule them all](https://github.com/github/scripts-to-rule-them-all) pattern. Most useful is `./script/bootstrap` which will create a venv and install both the runtime and development related requirements. It will also hook up a pre-commit hook that covers most of what's run by CI.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## ChangeLog and Release Management Tooling
|
|
2
|
+
|
|
3
|
+
A simple standalone Python module for CHANGELOG and release management. Spun out of [octoDNS](https://github.com/octodns/octodns/).
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
#### Command line
|
|
8
|
+
|
|
9
|
+
```console
|
|
10
|
+
pip install changelet
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Usage
|
|
14
|
+
|
|
15
|
+
For help with using the command
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
changelet --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
In most cases the only command you're likely to encounter is `create`. It will be used to add a changelog entry to your branch/PR.
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
changelet create --type (TYPE) Short description of your change to be included in the changelog
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The options for type are
|
|
28
|
+
* `major` - rare for non-maintainers, a change that will break backwards compatibility and require users to take care when updating
|
|
29
|
+
* `minor` - adds new functionality that is either self contained or done in a completely backwards compatible manner
|
|
30
|
+
* `patch` - fixes an issue or bug with existing functionality
|
|
31
|
+
* `none` - change that should not be included in the CHANGELOG and will not directly impact users, e.g. documentation, README, tooling, ...
|
|
32
|
+
|
|
33
|
+
### Using changlet
|
|
34
|
+
|
|
35
|
+
Currently the tooling has only been tested with repositories in the octoDNS org, but it should be usable elsewhere. You'll want to have a look at the following for how to incorporate it into your repo and workflows.
|
|
36
|
+
|
|
37
|
+
* [script/changelog](script/changelog)
|
|
38
|
+
* [.git_hooks_pre-commit](.git_hooks_pre-commit) (and [script/bootstrap](script/bootstrap) which installs it)
|
|
39
|
+
* [.github/workflows/changelog.yml](.github/workflows/changelog.yml)
|
|
40
|
+
|
|
41
|
+
### Development
|
|
42
|
+
|
|
43
|
+
See the [/script/](/script/) directory for some tools to help with the development process. They generally follow the [Script to rule them all](https://github.com/github/scripts-to-rule-them-all) pattern. Most useful is `./script/bootstrap` which will create a venv and install both the runtime and development related requirements. It will also hook up a pre-commit hook that covers most of what's run by CI.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from os.path import abspath, basename, join
|
|
9
|
+
from sys import exit, path
|
|
10
|
+
|
|
11
|
+
from semver import Version
|
|
12
|
+
|
|
13
|
+
from changelet.entry import Entry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_current_version(module_name, directory='.'):
|
|
17
|
+
path.append(directory)
|
|
18
|
+
module = import_module(module_name)
|
|
19
|
+
return Version.parse(module.__version__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_new_version(current_version, entries):
|
|
23
|
+
try:
|
|
24
|
+
bump_type = entries[0].type
|
|
25
|
+
except IndexError:
|
|
26
|
+
return None
|
|
27
|
+
if bump_type == 'major':
|
|
28
|
+
return current_version.bump_major()
|
|
29
|
+
elif bump_type == 'minor':
|
|
30
|
+
return current_version.bump_minor()
|
|
31
|
+
elif bump_type == 'patch':
|
|
32
|
+
return current_version.bump_patch()
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def version(value):
|
|
37
|
+
return Version.parse(value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Bump:
|
|
41
|
+
name = 'bump'
|
|
42
|
+
description = (
|
|
43
|
+
'Builds a changelog update and calculates a new version number.'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def configure(self, parser):
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
'--version',
|
|
49
|
+
type=version,
|
|
50
|
+
required=False,
|
|
51
|
+
help='Use the supplied version number for the bump',
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
'--make-changes',
|
|
55
|
+
action='store_true',
|
|
56
|
+
help='Write changelog update and bump version number',
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
'title', nargs='*', help='A short title/quip for the release title'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def exit(self, code):
|
|
63
|
+
exit(code)
|
|
64
|
+
|
|
65
|
+
def run(self, args, config, root='.'):
|
|
66
|
+
buf = StringIO()
|
|
67
|
+
|
|
68
|
+
module_name = basename(abspath(root)).replace('-', '_')
|
|
69
|
+
|
|
70
|
+
buf.write('## ')
|
|
71
|
+
current_version = _get_current_version(module_name)
|
|
72
|
+
|
|
73
|
+
entries = sorted(Entry.load_all(config), reverse=True)
|
|
74
|
+
|
|
75
|
+
new_version = (
|
|
76
|
+
args.version
|
|
77
|
+
if args.version
|
|
78
|
+
else _get_new_version(current_version, entries)
|
|
79
|
+
)
|
|
80
|
+
if not new_version:
|
|
81
|
+
print('No changelog entries found that would bump, nothing to do')
|
|
82
|
+
return self.exit(1)
|
|
83
|
+
buf.write(str(new_version))
|
|
84
|
+
buf.write(' - ')
|
|
85
|
+
buf.write(datetime.now().strftime('%Y-%m-%d'))
|
|
86
|
+
if args.title:
|
|
87
|
+
buf.write(' - ')
|
|
88
|
+
buf.write(' '.join(args.title))
|
|
89
|
+
buf.write('\n')
|
|
90
|
+
|
|
91
|
+
current_type = None
|
|
92
|
+
for entry in entries:
|
|
93
|
+
type = entry.type
|
|
94
|
+
if type == 'none':
|
|
95
|
+
# these aren't included in the listing
|
|
96
|
+
continue
|
|
97
|
+
if type != current_type:
|
|
98
|
+
buf.write('\n')
|
|
99
|
+
buf.write(type.capitalize())
|
|
100
|
+
buf.write(':\n')
|
|
101
|
+
current_type = type
|
|
102
|
+
buf.write(entry.markdown)
|
|
103
|
+
|
|
104
|
+
buf.write('\n')
|
|
105
|
+
|
|
106
|
+
buf.write('\n')
|
|
107
|
+
|
|
108
|
+
buf = buf.getvalue()
|
|
109
|
+
if not args.make_changes:
|
|
110
|
+
print(f'New version number {new_version}\n')
|
|
111
|
+
print(buf)
|
|
112
|
+
self.exit(0)
|
|
113
|
+
else:
|
|
114
|
+
changelog = join(root, 'CHANGELOG.md')
|
|
115
|
+
with open(changelog) as fh:
|
|
116
|
+
existing = fh.read()
|
|
117
|
+
|
|
118
|
+
with open(changelog, 'w') as fh:
|
|
119
|
+
fh.write(buf)
|
|
120
|
+
fh.write(existing)
|
|
121
|
+
|
|
122
|
+
init = join(root, module_name, '__init__.py')
|
|
123
|
+
with open(init) as fh:
|
|
124
|
+
existing = fh.read()
|
|
125
|
+
|
|
126
|
+
with open(init, 'w') as fh:
|
|
127
|
+
fh.write(
|
|
128
|
+
existing.replace(str(current_version), str(new_version))
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
for entry in entries:
|
|
132
|
+
entry.remove()
|
|
133
|
+
|
|
134
|
+
return new_version, buf
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from sys import argv, exit, stderr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Check:
|
|
9
|
+
name = 'check'
|
|
10
|
+
description = (
|
|
11
|
+
'Checks to see if the current branch contains a changelog entry'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def configure(self, parser):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def exit(self, code):
|
|
18
|
+
exit(code)
|
|
19
|
+
|
|
20
|
+
def run(self, args, config):
|
|
21
|
+
if config.provider.changelog_entries_in_branch(
|
|
22
|
+
root=config.root, directory=config.directory
|
|
23
|
+
):
|
|
24
|
+
return self.exit(0)
|
|
25
|
+
|
|
26
|
+
print(
|
|
27
|
+
f'PR is missing required changelog file, run {argv[0]} create',
|
|
28
|
+
file=stderr,
|
|
29
|
+
)
|
|
30
|
+
self.exit(1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from os.path import join
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from changelet.entry import Entry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Create:
|
|
12
|
+
name = 'create'
|
|
13
|
+
description = 'Creates a new changelog entry.'
|
|
14
|
+
|
|
15
|
+
def configure(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
'-t',
|
|
18
|
+
'--type',
|
|
19
|
+
choices=('none', 'patch', 'minor', 'major'),
|
|
20
|
+
required=True,
|
|
21
|
+
help='''The scope of the change.
|
|
22
|
+
|
|
23
|
+
* patch - This is a bug fix
|
|
24
|
+
* minor - This adds new functionality or makes changes in a fully backwards
|
|
25
|
+
compatible way
|
|
26
|
+
* major - This includes substantial new functionality and/or changes that break
|
|
27
|
+
compatibility and may require careful migration
|
|
28
|
+
* none - This change does not need to be mentioned in the changelog
|
|
29
|
+
|
|
30
|
+
See https://semver.org/ for more info''',
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
'-p',
|
|
34
|
+
'--pr',
|
|
35
|
+
type=int,
|
|
36
|
+
help='Manually override the PR number for the change, maintainer use only.',
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
'-a',
|
|
40
|
+
'--add',
|
|
41
|
+
action='store_true',
|
|
42
|
+
default=False,
|
|
43
|
+
help='`git add` the newly created changelog entry',
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
'description',
|
|
47
|
+
metavar='change-description',
|
|
48
|
+
nargs='+',
|
|
49
|
+
help='''A short description of the changes in this PR, suitable as an entry in
|
|
50
|
+
CHANGELOG.md. Should be a single line. Can option include simple markdown formatting
|
|
51
|
+
and links.''',
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def run(self, args, config):
|
|
55
|
+
filename = join(config.directory, f'{uuid4().hex}.md')
|
|
56
|
+
entry = Entry(
|
|
57
|
+
type=args.type,
|
|
58
|
+
description=' '.join(args.description),
|
|
59
|
+
pr=args.pr,
|
|
60
|
+
filename=filename,
|
|
61
|
+
)
|
|
62
|
+
entry.save()
|
|
63
|
+
|
|
64
|
+
if args.add:
|
|
65
|
+
config.provider.add_file(entry.filename)
|
|
66
|
+
print(
|
|
67
|
+
f'Created {entry.filename}, it has been staged and should be committed to your branch.'
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
print(
|
|
71
|
+
f'Created {entry.filename}, it can be further edited and should be committed to your branch.'
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return entry
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from os.path import isfile, join
|
|
7
|
+
from sys import version_info
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from yaml import safe_load as yaml_load
|
|
11
|
+
|
|
12
|
+
# https://pypi.org/project/tomli/#intro
|
|
13
|
+
# based on code in black.file
|
|
14
|
+
if version_info >= (3, 11): # pragma: no cover
|
|
15
|
+
try:
|
|
16
|
+
from tomllib import load as toml_load
|
|
17
|
+
except ImportError:
|
|
18
|
+
# Help users on older alphas
|
|
19
|
+
if not TYPE_CHECKING:
|
|
20
|
+
from tomli import load as toml_load
|
|
21
|
+
else: # pragma: no cover
|
|
22
|
+
from tomli import load as toml_load
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
DEFAULT_ROOT = ''
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def build(cls, **kwargs):
|
|
30
|
+
# create w/defaults
|
|
31
|
+
config = Config()
|
|
32
|
+
|
|
33
|
+
root = kwargs.get('root', cls.DEFAULT_ROOT)
|
|
34
|
+
|
|
35
|
+
# override w/toml, if applicable
|
|
36
|
+
pyproject_toml_filename = join(root, 'pyproject.toml')
|
|
37
|
+
if isfile(pyproject_toml_filename):
|
|
38
|
+
config.load_pyproject_toml(pyproject_toml_filename)
|
|
39
|
+
|
|
40
|
+
# explicit yaml file
|
|
41
|
+
yaml_filename = kwargs.get('config')
|
|
42
|
+
if not yaml_filename:
|
|
43
|
+
# default yaml file
|
|
44
|
+
yaml_filename = join(root, '.changelet.yaml')
|
|
45
|
+
# override w/yaml, if applicable
|
|
46
|
+
if isfile(yaml_filename):
|
|
47
|
+
config.load_yaml(yaml_filename)
|
|
48
|
+
|
|
49
|
+
# override root w/command line arg, if applicable
|
|
50
|
+
try:
|
|
51
|
+
config.root = kwargs['root']
|
|
52
|
+
except KeyError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# override directory w/command line arg, if applicable
|
|
56
|
+
try:
|
|
57
|
+
config.directory = kwargs['directory']
|
|
58
|
+
except KeyError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
return config
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
root=DEFAULT_ROOT,
|
|
66
|
+
directory='.changelog',
|
|
67
|
+
provider={'class': 'changelet.github.GitHubCli'},
|
|
68
|
+
):
|
|
69
|
+
self.root = root
|
|
70
|
+
self.directory = directory
|
|
71
|
+
|
|
72
|
+
# will instantiate & configure
|
|
73
|
+
self.provider = provider
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def provider(self):
|
|
77
|
+
if self._provider_config is not None:
|
|
78
|
+
value = dict(self._provider_config)
|
|
79
|
+
klass = value.pop('class')
|
|
80
|
+
if isinstance(klass, str):
|
|
81
|
+
module, klass = klass.rsplit('.', 1)
|
|
82
|
+
module = import_module(module)
|
|
83
|
+
klass = getattr(module, klass)
|
|
84
|
+
self._provider = klass(**value)
|
|
85
|
+
self._provider_config = None
|
|
86
|
+
return self._provider
|
|
87
|
+
|
|
88
|
+
@provider.setter
|
|
89
|
+
def provider(self, value):
|
|
90
|
+
self._provider_config = value
|
|
91
|
+
|
|
92
|
+
def load_pyproject_toml(self, filename):
|
|
93
|
+
with open(filename, 'rb') as fh:
|
|
94
|
+
config = toml_load(fh).get('tool', {}).get('changelet')
|
|
95
|
+
if isinstance(config, dict):
|
|
96
|
+
for k, v in config.items():
|
|
97
|
+
setattr(self, k, v)
|
|
98
|
+
|
|
99
|
+
def load_yaml(self, filename=None):
|
|
100
|
+
with open(filename, 'rb') as fh:
|
|
101
|
+
config = yaml_load(fh)
|
|
102
|
+
if isinstance(config, dict):
|
|
103
|
+
for k, v in config.items():
|
|
104
|
+
setattr(self, k, v)
|
|
105
|
+
|
|
106
|
+
def __repr__(self):
|
|
107
|
+
return f'Config<root={self.root}, directory={self.directory}, provider={self.provider}>'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from os import listdir, makedirs, remove
|
|
8
|
+
from os.path import dirname, isdir, join
|
|
9
|
+
|
|
10
|
+
from yaml import safe_load
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EntryType(Enum):
|
|
14
|
+
NONE = 'none'
|
|
15
|
+
PATCH = 'patch'
|
|
16
|
+
MINOR = 'minor'
|
|
17
|
+
MAJOR = 'major'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Entry:
|
|
21
|
+
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
22
|
+
ORDERING = {'major': 3, 'minor': 2, 'patch': 1, 'none': 0, '': 0}
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def load(self, filename, config):
|
|
26
|
+
with open(filename, 'r') as fh:
|
|
27
|
+
pieces = fh.read().split('---\n')
|
|
28
|
+
data = safe_load(pieces[1])
|
|
29
|
+
description = pieces[2]
|
|
30
|
+
if description[-1] == '\n':
|
|
31
|
+
description = description[:-1]
|
|
32
|
+
if 'pr' in data:
|
|
33
|
+
pr = config.provider.pr_by_id(
|
|
34
|
+
root=config.root, directory=config.directory, id=data['pr']
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
pr = config.provider.pr_by_filename(
|
|
38
|
+
root=config.root,
|
|
39
|
+
directory=config.directory,
|
|
40
|
+
filename=filename,
|
|
41
|
+
)
|
|
42
|
+
return Entry(
|
|
43
|
+
filename=filename,
|
|
44
|
+
type=data['type'],
|
|
45
|
+
description=description,
|
|
46
|
+
pr=pr,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def load_all(cls, config):
|
|
51
|
+
directory = config.directory
|
|
52
|
+
entries = []
|
|
53
|
+
if isdir(directory):
|
|
54
|
+
for filename in sorted(listdir(directory)):
|
|
55
|
+
if not filename.endswith('.md'):
|
|
56
|
+
continue
|
|
57
|
+
filename = join(directory, filename)
|
|
58
|
+
entries.append(Entry.load(filename, config))
|
|
59
|
+
return entries
|
|
60
|
+
|
|
61
|
+
def __init__(self, type, description, pr=None, filename=None):
|
|
62
|
+
self.type = type
|
|
63
|
+
self.description = description
|
|
64
|
+
self.pr = pr
|
|
65
|
+
self.filename = filename
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def _ordering(self):
|
|
69
|
+
return (
|
|
70
|
+
self.ORDERING[self.type],
|
|
71
|
+
self.pr.merged_at if self.pr else self.EPOCH,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def save(self, filename=None):
|
|
75
|
+
if filename is None:
|
|
76
|
+
filename = self.filename
|
|
77
|
+
directory = dirname(filename)
|
|
78
|
+
if not isdir(directory):
|
|
79
|
+
makedirs(directory)
|
|
80
|
+
with open(filename, 'w') as fh:
|
|
81
|
+
fh.write('---\ntype: ')
|
|
82
|
+
fh.write(self.type)
|
|
83
|
+
if self.pr:
|
|
84
|
+
fh.write('\npr: ')
|
|
85
|
+
fh.write(str(self.pr.id))
|
|
86
|
+
fh.write('\n---\n')
|
|
87
|
+
fh.write(self.description)
|
|
88
|
+
self.filename = filename
|
|
89
|
+
|
|
90
|
+
def remove(self):
|
|
91
|
+
if not self.filename:
|
|
92
|
+
return False
|
|
93
|
+
remove(self.filename)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def text(self):
|
|
98
|
+
if self.pr:
|
|
99
|
+
return f'* {self.description} - {self.pr.plain}'
|
|
100
|
+
return f'* {self.description}'
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def markdown(self):
|
|
104
|
+
if self.pr:
|
|
105
|
+
return f'* {self.description} - {self.pr.markdown}'
|
|
106
|
+
return f'* {self.description}'
|
|
107
|
+
|
|
108
|
+
def copy(self):
|
|
109
|
+
return Entry(
|
|
110
|
+
type=self.type,
|
|
111
|
+
description=self.description,
|
|
112
|
+
pr=self.pr,
|
|
113
|
+
filename=self.filename,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def __lt__(self, other):
|
|
117
|
+
return self._ordering < other._ordering
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f'Entry<{self.type}, {self.description[:16]}, {self.filename}, {self.pr}>'
|