From 37d9ed4ae3e0b0193f990363df9dcea191475b35 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 12 Dec 2015 19:37:57 -0800 Subject: Initial import --- .gitignore | 14 ++++ README.rst | 112 ++++++++++++++++++++++++++++++ py_release_tools/__init__.py | 0 py_release_tools/commands.py | 158 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 40 +++++++++++ 5 files changed, 324 insertions(+) create mode 100644 .gitignore create mode 100644 README.rst create mode 100644 py_release_tools/__init__.py create mode 100644 py_release_tools/commands.py create mode 100755 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..548304c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.pyc +.pip/ +.pip2/ +.pip27/ +.pip3/ +.pip32/ +.pip34/ +.pip35/ +build/ +coverage.xml +dist/ +htmlcov/ +*.egg-info/ +.coverage/ diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..85f4e6a --- /dev/null +++ b/README.rst @@ -0,0 +1,112 @@ +====================== +Python Releasing Tools +====================== + +This code is licensed under the MIT license. + +This is a suite of tools that are useful for releasing Python code for public +or private use. There are setuptools extensions to validate various conventions +and to perform release activities. + +Installation and Usage +====================== +This package is generally not installed directly but instead listed in the +``setup_requires`` option of the setuptools ``setup`` function. For example:: + + from setuptools import setup + + setup( + name="my_clever_package", + version="1.0.0", + setup_requires=[ + "py_release_tools", + ] + ) + +Commands are exported as distuils commands and should be automatically +available provided the package is installed. The tool provides the commands +documented below and are generally run by defining a ``release`` `alias +`_ +in the project's ``setup.cfg`` file. The author's typical project ``setup.cfg`` +contains these aliases:: + + [aliases] + validate = cover_tests pep8 + release = validate increment_semver git_push sdist upload + release_major = validate increment_semver -M git_push sdist upload + release_minor = validate increment_semver -m git_push sdist upload + +Commands +======== +increment_semver +---------------- +This command will update the ``setup.py`` file version number following the +rules of `Semantic Versioning (semver) `_. This command will +re-write and commit the project's ``setup.py`` file. It assumes that the +version line is formatted as such, with some amount of leading whitespace:: + + version="1.20.1" + +It will rewrite all lines that look like this in the file. + +The version format is:: + + MAJOR.MINOR.PATCH + +For more information check out the semver docs. + +Version generation increments a version component by one. By default a patch +version is generated. Passing the ``-m`` or ``--minor`` flags to the command +will increment the minor version and set the patch version to zero. Passing +``-M`` or ``--major`` will increment the major version and set both the minor +and patch versions to zero. + +This command will also create a tag in the git repository of format +``release-{semver}``. + +git_push +-------- +This command runs a ``git push`` command to push the ``master`` branch to the +remote ``origin``. The command will also push tags. If your git repository +doesn't use these naming conventions the command will fail. + +cover_tests +----------- +This command will setup python +`coverage `_ monitoring and invoke the +setuptools ``test`` target. Coverage data will be written to ``.coverage`` in +the same directory as the ``setup.py`` file. + +This command will also generate a Cobertura coverage report as ``coverage.xml`` +and an HTML report in the ``htmlcov`` folder. + +Failure of the tests will cause a failure of the build so it is suitable to use +this command as a replacement for the builtin ``test`` command. This command +also suppresses the system exit that the builtin ``test`` command generates so +other commands can be chained after this one. + +pep8 +---- +This command will run a `PEP8 `_ +code style validation on all Python files in the project, including the +setup.py file. + +Contributing +============ +If you would like to contribute to Pydora please visit the project's +`GitHub page `_ and open a pull +request with your changes. To have the best experience contributing, please: + +* Don't break backwards compatibility of public interfaces +* Write tests for your new feature/bug fix +* Ensure that existing tests pass +* Update the readme/docstrings, if necessary +* Follow the coding style of the current code-base +* Ensure that your code is PEP8 compliant +* Validate that your changes work with Python 2.7+ and 3.x + +All code is reviewed before acceptance and changes may be requested to better +follow the conventions of the existing API. + +he build system runs ``python setup.py validate`` on all supported Python +versions. You can, and should, run this on your pull request before submitting. diff --git a/py_release_tools/__init__.py b/py_release_tools/__init__.py new file mode 100644 index 0000000..e69de29 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 @@ +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", "-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") diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..a422483 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name="py_release_tools", + version="0.1.0", + description="Python release tools", + long_description=open("README.rst", "r").read(), + author="Mike Crute", + author_email="mcrute@gmail.com", + url="https://github.com/mcrute/py_release_tools", + packages=find_packages(), + install_requires=[ + "pep8>=1.6.2", + "mock>=1.0.1", + "coverage>=4.0.3", + ], + entry_points={ + "distutils.commands": [ + ("increment_semver = " + "py_release_tools.commands:IncrementSemanticVersion"), + "git_push = py_release_tools.commands:GitPush", + "cover_tests = py_release_tools.commands:TestsWithCoverage", + "pep8 = py_release_tools.commands:PEP8CheckStyle", + ] + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Framework :: Setuptools Plugin", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Testing", + ] +) -- cgit v1.2.3