diff options
Diffstat (limited to 'py_release_tools/commands.py')
-rw-r--r-- | py_release_tools/commands.py | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/py_release_tools/commands.py b/py_release_tools/commands.py new file mode 100644 index 0000000..e57f99c --- /dev/null +++ b/py_release_tools/commands.py | |||
@@ -0,0 +1,158 @@ | |||
1 | import os | ||
2 | import re | ||
3 | import sys | ||
4 | from io import BytesIO | ||
5 | from distutils import log | ||
6 | from distutils.cmd import Command | ||
7 | from subprocess import check_output | ||
8 | from distutils.errors import DistutilsError | ||
9 | |||
10 | |||
11 | def simple_call(cmd): | ||
12 | return check_output(cmd.split(" ")) | ||
13 | |||
14 | |||
15 | class SimpleCommand(Command): | ||
16 | """Default behavior for simple commands | ||
17 | """ | ||
18 | |||
19 | user_options = [] | ||
20 | |||
21 | def initialize_options(self): | ||
22 | pass | ||
23 | |||
24 | def finalize_options(self): | ||
25 | pass | ||
26 | |||
27 | def install_requires(self): | ||
28 | if self.distribution.install_requires: | ||
29 | self.distribution.fetch_build_eggs( | ||
30 | self.distribution.install_requires) | ||
31 | |||
32 | if self.distribution.tests_require: | ||
33 | self.distribution.fetch_build_eggs( | ||
34 | self.distribution.tests_require) | ||
35 | |||
36 | def run(self): | ||
37 | self.install_requires() | ||
38 | self._run() | ||
39 | |||
40 | |||
41 | class IncrementSemanticVersion(SimpleCommand): | ||
42 | """Increment Semantic Version and Commmit to Git | ||
43 | |||
44 | Version incrementing uses semantic versioning. This command accepts -M or | ||
45 | --major, -m or --minor to increment a major or minor release. If no flags | ||
46 | are passed then a patch release is created. | ||
47 | """ | ||
48 | |||
49 | user_options = [ | ||
50 | ("major", "M", "increment version for major release"), | ||
51 | ("minor", "m", "increment version for minor release"), | ||
52 | ] | ||
53 | |||
54 | boolean_options = ("major", "minor") | ||
55 | |||
56 | def initialize_options(self): | ||
57 | self.major = False | ||
58 | self.minor = False | ||
59 | |||
60 | def _new_version(self, version): | ||
61 | major, minor, patch = [int(i) for i in version.split(".")] | ||
62 | |||
63 | if self.major: | ||
64 | return "{}.0.0".format(major + 1) | ||
65 | elif self.minor: | ||
66 | return "{}.{}.0".format(major, minor + 1) | ||
67 | else: | ||
68 | return "{}.{}.{}".format(major, minor, patch + 1) | ||
69 | |||
70 | def _update_version(self): | ||
71 | pattern = re.compile('^(\s+)version="([0-9\.]+)"') | ||
72 | output = BytesIO() | ||
73 | |||
74 | with open("setup.py", "r") as fp: | ||
75 | for line in fp: | ||
76 | result = pattern.match(line) | ||
77 | |||
78 | if not result: | ||
79 | output.write(line) | ||
80 | else: | ||
81 | spaces, version = result.groups() | ||
82 | new_version = self._new_version(version) | ||
83 | output.write( | ||
84 | '{}version="{}",\n'.format(spaces, new_version)) | ||
85 | |||
86 | with open("setup.py", "w") as fp: | ||
87 | fp.write(output.getvalue()) | ||
88 | |||
89 | return new_version | ||
90 | |||
91 | def _run(self): | ||
92 | if simple_call("git status --porcelain").strip(): | ||
93 | raise DistutilsError("Uncommited changes, " | ||
94 | "commit all changes before release") | ||
95 | |||
96 | new_version = self._update_version() | ||
97 | |||
98 | check_output([ | ||
99 | "git", "commit", "-m", "Release {}".format(new_version)]) | ||
100 | |||
101 | simple_call("git tag release-{}".format(new_version)) | ||
102 | |||
103 | |||
104 | class GitPush(SimpleCommand): | ||
105 | """Push changes and tags to git origin | ||
106 | """ | ||
107 | |||
108 | description = "push changes to git origin" | ||
109 | |||
110 | def _run(self): | ||
111 | simple_call("git push origin master") | ||
112 | simple_call("git push --tags") | ||
113 | |||
114 | |||
115 | class TestsWithCoverage(SimpleCommand): | ||
116 | """Run Unit Tests with Coverage | ||
117 | """ | ||
118 | |||
119 | description = "run unit tests with coverage" | ||
120 | |||
121 | def _run(self): | ||
122 | from coverage import coverage | ||
123 | |||
124 | cov = coverage(data_file=".coverage", branch=True, | ||
125 | source=self.distribution.packages) | ||
126 | cov.start() | ||
127 | |||
128 | # Unittest calls exit. How naughty. | ||
129 | try: | ||
130 | self.run_command("test") | ||
131 | except SystemExit: | ||
132 | pass | ||
133 | |||
134 | cov.stop() | ||
135 | cov.xml_report(outfile="coverage.xml") | ||
136 | cov.html_report() | ||
137 | |||
138 | |||
139 | class PEP8CheckStyle(SimpleCommand): | ||
140 | """Run PEP8 Code Style Valiation | ||
141 | """ | ||
142 | |||
143 | description = "run PEP8 style validations" | ||
144 | |||
145 | def _run(self): | ||
146 | from pep8 import StyleGuide | ||
147 | |||
148 | self.run_command("egg_info") | ||
149 | files = self.get_finalized_command("egg_info") | ||
150 | |||
151 | report = StyleGuide().check_files([ | ||
152 | p for p in files.filelist.files if p.endswith(".py")]) | ||
153 | |||
154 | if report.total_errors: | ||
155 | raise DistutilsError( | ||
156 | "Found {} PEP8 violations".format(report.total_errors)) | ||
157 | else: | ||
158 | log.info("No PEP8 violations found") | ||