aboutsummaryrefslogtreecommitdiff
path: root/py_release_tools/commands.py
blob: 1570447674b8a1460abc0ea50d5842c4b5cca817 (plain)
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
import os
import re
import sys
from io import BytesIO
from distutils import log
from distutils.cmd import Command
from subprocess import check_output
from distutils.errors import DistutilsError


def simple_call(cmd):
    return check_output(cmd.split(" "))


class SimpleCommand(Command):
    """Default behavior for simple commands
    """

    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def install_requires(self):
        if self.distribution.install_requires:
            self.distribution.fetch_build_eggs(
                    self.distribution.install_requires)

        if self.distribution.tests_require:
            self.distribution.fetch_build_eggs(
                    self.distribution.tests_require)

    def run(self):
        self.install_requires()
        self._run()


class IncrementSemanticVersion(SimpleCommand):
    """Increment Semantic Version and Commmit to Git

    Version incrementing uses semantic versioning. This command accepts -M or
    --major, -m or --minor to increment a major or minor release. If no flags
    are passed then a patch release is created.
    """

    user_options = [
        ("major", "M", "increment version for major release"),
        ("minor", "m", "increment version for minor release"),
    ]

    boolean_options = ("major", "minor")

    def initialize_options(self):
        self.major = False
        self.minor = False

    def _new_version(self, version):
        major, minor, patch = [int(i) for i in version.split(".")]

        if self.major:
            return "{}.0.0".format(major + 1)
        elif self.minor:
            return "{}.{}.0".format(major, minor + 1)
        else:
            return "{}.{}.{}".format(major, minor, patch + 1)

    def _update_version(self):
        pattern = re.compile('^(\s+)version="([0-9\.]+)"')
        output = BytesIO()

        with open("setup.py", "r") as fp:
            for line in fp:
                result = pattern.match(line)

                if not result:
                    output.write(line)
                else:
                    spaces, version = result.groups()
                    new_version = self._new_version(version)
                    output.write(
                            '{}version="{}",\n'.format(spaces, new_version))

        with open("setup.py", "w") as fp:
            fp.write(output.getvalue())

        return new_version

    def _run(self):
        if simple_call("git status --porcelain").strip():
            raise DistutilsError("Uncommited changes, "
                                 "commit all changes before release")

        new_version = self._update_version()

        check_output([
            "git", "commit", "-a", "-m", "Release {}".format(new_version)])

        simple_call("git tag release-{}".format(new_version))


class GitPush(SimpleCommand):
    """Push changes and tags to git origin
    """

    description = "push changes to git origin"

    def _run(self):
        simple_call("git push origin master")
        simple_call("git push --tags")


class TestsWithCoverage(SimpleCommand):
    """Run Unit Tests with Coverage
    """

    description = "run unit tests with coverage"

    def _run(self):
        from coverage import coverage

        cov = coverage(data_file=".coverage", branch=True,
                       source=self.distribution.packages)
        cov.start()

        # Unittest calls exit. How naughty.
        try:
            self.run_command("test")
        except SystemExit:
            pass

        cov.stop()
        cov.xml_report(outfile="coverage.xml")
        cov.html_report()


class PEP8CheckStyle(SimpleCommand):
    """Run PEP8 Code Style Valiation
    """

    description = "run PEP8 style validations"

    def _run(self):
        from pep8 import StyleGuide

        self.run_command("egg_info")
        files = self.get_finalized_command("egg_info")

        report = StyleGuide().check_files([
            p for p in files.filelist.files if p.endswith(".py")])

        if report.total_errors:
            raise DistutilsError(
                    "Found {} PEP8 violations".format(report.total_errors))
        else:
            log.info("No PEP8 violations found")