forked from marksteve/bump
-
Notifications
You must be signed in to change notification settings - Fork 9
/
bump.py
191 lines (167 loc) · 5.51 KB
/
bump.py
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import configparser
import os
import re
import sys
import click
import toml
from first import first
from packaging.utils import canonicalize_version
pattern = re.compile(r"((?:__)?version(?:__)? ?= ?[\"'])(.+?)([\"'])")
class Config:
def __init__(self):
self.ini_config = configparser.RawConfigParser()
self.ini_config.read([".bump", "setup.cfg"])
self.toml_config = {}
if os.path.exists("pyproject.toml"):
self.toml_config = (
toml.load("pyproject.toml").get("tool", {}).get("bump", {})
)
def get(self, key, coercer=str, default=None):
candidate = self.toml_config.get(key)
if candidate is not None:
# No coercion needed for TOML, since values are strongly typed.
return candidate
if coercer is str:
return self.ini_config.get("bump", key, fallback=default)
elif coercer is bool:
return self.ini_config.getboolean("bump", key, fallback=default)
else:
raise ValueError(f"invalid coercer: {coercer}")
class SemVer(object):
def __init__(self, major=0, minor=0, patch=0, pre=None, local=None):
self.major = major
self.minor = minor
self.patch = patch
self.pre = pre
self.local = local
def __repr__(self):
# TODO: this is broken
return "<SemVer {}>".format(
", ".join(["{}={}".format(n, getattr(self, n)) for n in self.__slots__])
)
def __str__(self):
version_string = ".".join(map(str, [self.major, self.minor, self.patch]))
if self.pre:
version_string += "-" + self.pre
if self.local:
version_string += "+" + self.local
return version_string
@classmethod
def parse(cls, version):
major = minor = patch = 0
local = pre = None
local_split = version.split("+")
if len(local_split) > 1:
version, local = local_split
pre_split = version.split("-", 1)
if len(pre_split) > 1:
version, pre = pre_split
major_split = version.split(".", 1)
if len(major_split) > 1:
major, version = major_split
minor_split = version.split(".", 1)
if len(minor_split) > 1:
minor, version = minor_split
if version:
patch = version
else:
minor = version
else:
major = version
return cls(
major=int(major), minor=int(minor), patch=int(patch), pre=pre, local=local
)
def bump(
self, major=False, minor=False, patch=False, pre=None, local=None, reset=False
):
if major:
self.major += 1
if reset:
self.minor = 0
self.patch = 0
if minor:
self.minor += 1
if reset:
self.patch = 0
if patch:
self.patch += 1
if pre:
self.pre = pre
if local:
self.local = local
if not (major or minor or patch or pre or local):
self.patch += 1
class NoVersionFound(Exception):
pass
def find_version(input_string):
match = first(pattern.findall(input_string))
if match is None:
raise NoVersionFound
return match[1]
@click.command()
@click.option(
"--major",
"-M",
"major",
flag_value=True,
default=None,
help="Bump major number. Ex.: 1.2.3 -> 2.2.3",
)
@click.option(
"--minor",
"-m",
"minor",
flag_value=True,
default=None,
help="Bump minor number. Ex.: 1.2.3 -> 1.3.3",
)
@click.option(
"--patch",
"-p",
"patch",
flag_value=True,
default=None,
help="Bump patch number. Ex.: 1.2.3 -> 1.2.4",
)
@click.option(
"--reset",
"-r",
"reset",
flag_value=True,
default=None,
help="Reset subversions. Ex.: Major bump from 1.2.3 will be 2.0.0 instead of 2.2.3",
)
@click.option("--pre", help="Set the pre-release identifier")
@click.option("--local", help="Set the local version segment")
@click.option(
"--canonicalize", flag_value=True, default=None, help="Canonicalize the new version"
)
@click.argument("input", type=click.File("rb"), default=None, required=False)
@click.argument("output", type=click.File("wb"), default=None, required=False)
def main(input, output, major, minor, patch, reset, pre, local, canonicalize):
config = Config()
major = major or config.get("major", coercer=bool, default=False)
minor = minor or config.get("minor", coercer=bool, default=False)
patch = patch or config.get("patch", coercer=bool, default=False)
reset = reset or config.get("reset", coercer=bool, default=False)
input = input or click.File("rb")(config.get("input", default="setup.py"))
output = output or click.File("wb")(input.name)
canonicalize = canonicalize or config.get(
"canonicalize", coercer=bool, default=False
)
contents = input.read().decode("utf-8")
try:
version_string = find_version(contents)
except NoVersionFound:
click.echo("No version found in ./{}.".format(input.name))
sys.exit(1)
version = SemVer.parse(version_string)
version.bump(major, minor, patch, pre, local, reset)
version_string = str(version)
if canonicalize:
version_string = canonicalize_version(version_string)
new = pattern.sub(r"\g<1>{}\g<3>".format(version_string), contents)
output.write(new.encode())
click.echo(version_string)
if __name__ == "__main__":
main()