Skip to content

Commit

Permalink
docs: Add type hints and more complete docstrings
Browse files Browse the repository at this point in the history
Includes a few style changes suggested by pylint and type safety checks
suggested by mypy

re #81
  • Loading branch information
cvockrodt authored and relekang committed Nov 22, 2018
1 parent 85fe638 commit a6d5e9b
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 83 deletions.
4 changes: 3 additions & 1 deletion semantic_release/__init__.py
@@ -1,11 +1,13 @@
"""Semantic Release
"""
__version__ = '3.11.2'


from .errors import (SemanticReleaseBaseError, ImproperConfigurationError, # noqa
UnknownCommitMessageStyleError) # noqa


def setup_hook(argv):
def setup_hook(argv: list):
"""
A hook to be used in setup.py to enable `python setup.py publish`.
Expand Down
28 changes: 16 additions & 12 deletions semantic_release/ci_checks.py
@@ -1,14 +1,18 @@
"""CI Checks
"""
import os
from typing import Callable

from semantic_release.errors import CiVerificationError


def checker(func):
def checker(func: Callable) -> Callable:
"""
A decorator that will convert AssertionErrors into
CiVerificationError.
:param func: A function that will raise AssertionError
:return: The given function wrapped to raise a CiVerificationError on AssertionError
"""

def func_wrapper(*args, **kwargs):
Expand All @@ -24,7 +28,7 @@ def func_wrapper(*args, **kwargs):


@checker
def travis(branch):
def travis(branch: str):
"""
Performs necessary checks to ensure that the travis build is one
that should create releases.
Expand All @@ -36,7 +40,7 @@ def travis(branch):


@checker
def semaphore(branch):
def semaphore(branch: str):
"""
Performs necessary checks to ensure that the semaphore build is successful,
on the correct branch and not a pull-request.
Expand All @@ -49,7 +53,7 @@ def semaphore(branch):


@checker
def frigg(branch):
def frigg(branch: str):
"""
Performs necessary checks to ensure that the frigg build is one
that should create releases.
Expand All @@ -61,7 +65,7 @@ def frigg(branch):


@checker
def circle(branch):
def circle(branch: str):
"""
Performs necessary checks to ensure that the circle build is one
that should create releases.
Expand All @@ -73,7 +77,7 @@ def circle(branch):


@checker
def gitlab(branch):
def gitlab(branch: str):
"""
Performs necessary checks to ensure that the gitlab build is one
that should create releases.
Expand All @@ -84,7 +88,7 @@ def gitlab(branch):
# TODO - don't think there's a merge request indicator variable


def check(branch='master'):
def check(branch: str = 'master'):
"""
Detects the current CI environment, if any, and performs necessary
environment checks.
Expand All @@ -93,12 +97,12 @@ def check(branch='master'):
"""

if os.environ.get('TRAVIS') == 'true':
return travis(branch)
travis(branch)
elif os.environ.get('SEMAPHORE') == 'true':
return semaphore(branch)
semaphore(branch)
elif os.environ.get('FRIGG') == 'true':
return frigg(branch)
frigg(branch)
elif os.environ.get('CIRCLECI') == 'true':
return circle(branch)
circle(branch)
elif os.environ.get('GITLAB_CI') == 'true':
return gitlab(branch)
gitlab(branch)
15 changes: 9 additions & 6 deletions semantic_release/cli.py
@@ -1,3 +1,5 @@
"""CLI
"""
import os
import sys

Expand All @@ -15,7 +17,7 @@
from .vcs_helpers import (checkout, commit_new_version, get_current_head_hash,
get_repository_owner_and_name, push_new_version, tag_new_version)

_common_options = [
COMMON_OPTIONS = [
click.option('--major', 'force_level', flag_value='major', help='Force major version.'),
click.option('--minor', 'force_level', flag_value='minor', help='Force minor version.'),
click.option('--patch', 'force_level', flag_value='patch', help='Force patch version.'),
Expand All @@ -28,9 +30,9 @@

def common_options(func):
"""
Decorator that adds all the options in _common_options
Decorator that adds all the options in COMMON_OPTIONS
"""
for option in reversed(_common_options):
for option in reversed(COMMON_OPTIONS):
func = option(func)
return func

Expand Down Expand Up @@ -85,6 +87,7 @@ def version(**kwargs):
def changelog(**kwargs):
"""
Generates the changelog since the last release.
:raises ImproperConfigurationError: if there is no current version
"""
current_version = get_current_version()
if current_version is None:
Expand All @@ -95,7 +98,7 @@ def changelog(**kwargs):
)
previous_version = get_previous_version(current_version)

log = generate_changelog(previous_version, current_version)
log = generate_changelog(previous_version, current_version)
for section in CHANGELOG_SECTIONS:
if not log[section]:
continue
Expand Down Expand Up @@ -216,5 +219,5 @@ def cmd_version(**kwargs):
# This will have to be removed if there are ever global options
# that are not valid for a subcommand.
#
args = sorted(sys.argv[1:], key=lambda x: 1 if x.startswith('--') else -1)
main(args=args)
ARGS = sorted(sys.argv[1:], key=lambda x: 1 if x.startswith('--') else -1)
main(args=ARGS)
6 changes: 6 additions & 0 deletions semantic_release/errors.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
"""Custom Errors
"""


class SemanticReleaseBaseError(Exception):
Expand All @@ -19,3 +21,7 @@ class GitError(SemanticReleaseBaseError):

class CiVerificationError(SemanticReleaseBaseError):
pass


class HvcsRepoParseError(SemanticReleaseBaseError):
pass
40 changes: 28 additions & 12 deletions semantic_release/history/__init__.py
@@ -1,16 +1,18 @@
"""History
"""
import re

from typing import Optional
import semver

from ..settings import config
from ..vcs_helpers import get_commit_log, get_last_version
from .logs import evaluate_version_bump # noqa

from ..errors import ImproperConfigurationError
from .parser_angular import parse_commit_message as angular_parser # noqa isort:skip
from .parser_tag import parse_commit_message as tag_parser # noqa isort:skip


def get_current_version_by_tag():
def get_current_version_by_tag() -> str:
"""
Finds the current version of the package in the current working directory.
Check tags rather than config file. return 0.0.0 if fails
Expand All @@ -20,29 +22,42 @@ def get_current_version_by_tag():
version = get_last_version()
if version:
return version
else:
return '0.0.0'
return '0.0.0'


def get_current_version_by_config_file():
def get_current_version_by_config_file() -> str:
"""
Get current version from the version variable defined in the configuration
:return: A string with the current version number
:raises ImproperConfigurationError: if version variable cannot be parsed
"""
filename, variable = config.get('semantic_release',
'version_variable').split(':')
variable = variable.strip()
with open(filename, 'r') as fd:
return re.search(
parts = re.search(
r'^{0}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(variable),
fd.read(),
re.MULTILINE
).group(1)
)
if not parts:
raise ImproperConfigurationError
return parts.group(1)


def get_current_version() -> str:
"""
Get current version from tag or version variable, depending on configuration
def get_current_version():
:return: A string with the current version number
"""
if config.get('semantic_release', 'version_source') == 'tag':
return get_current_version_by_tag()
return get_current_version_by_config_file()


def get_new_version(current_version, level_bump):
def get_new_version(current_version: str, level_bump: str) -> str:
"""
Calculates the next version based on the given bump level with semver.
Expand All @@ -56,11 +71,12 @@ def get_new_version(current_version, level_bump):
return getattr(semver, 'bump_{0}'.format(level_bump))(current_version)


def get_previous_version(version):
def get_previous_version(version: str) -> Optional[str]:
"""
Returns the version prior to the given version.
:param version: A string with the version number.
:return: A string with the previous version number
"""
found_version = False
for commit_hash, commit_message in get_commit_log():
Expand All @@ -76,7 +92,7 @@ def get_previous_version(version):
return get_last_version([version, 'v{}'.format(version)])


def set_new_version(new_version):
def set_new_version(new_version: str) -> bool:
"""
Replaces the version number in the correct place and writes the changed file to disk.
Expand Down
23 changes: 14 additions & 9 deletions semantic_release/history/logs.py
@@ -1,4 +1,7 @@
"""Logs
"""
import re
from typing import Optional

from ..errors import UnknownCommitMessageStyleError
from ..settings import config, current_commit_parser
Expand All @@ -15,7 +18,7 @@
re_breaking = re.compile('BREAKING CHANGE: (.*)')


def evaluate_version_bump(current_version, force=None):
def evaluate_version_bump(current_version: str, force: str = None) -> Optional[str]:
"""
Reads git log since last release to find out if should be a major, minor or patch release.
Expand Down Expand Up @@ -53,7 +56,7 @@ def evaluate_version_bump(current_version, force=None):
return bump


def generate_changelog(from_version, to_version=None):
def generate_changelog(from_version: str, to_version: str = None) -> dict:
"""
Generates a changelog for the given version.
Expand All @@ -63,8 +66,8 @@ def generate_changelog(from_version, to_version=None):
:return: a dict with different changelog sections
"""

changes = {'feature': [], 'fix': [],
'documentation': [], 'refactor': [], 'breaking': []}
changes: dict = {'feature': [], 'fix': [],
'documentation': [], 'refactor': [], 'breaking': []}

found_the_release = to_version is None

Expand All @@ -90,20 +93,22 @@ def generate_changelog(from_version, to_version=None):
changes[message[1]].append((_hash, message[3][0]))

if message[3][1] and 'BREAKING CHANGE' in message[3][1]:
changes['breaking'].append(
re_breaking.match(message[3][1]).group(1))
parts = re_breaking.match(message[3][1])
if parts:
changes['breaking'].append(parts.group(1))

if message[3][2] and 'BREAKING CHANGE' in message[3][2]:
changes['breaking'].append(
re_breaking.match(message[3][2]).group(1))
parts = re_breaking.match(message[3][2])
if parts:
changes['breaking'].append(parts.group(1))

except UnknownCommitMessageStyleError:
pass

return changes


def markdown_changelog(version, changelog, header=False):
def markdown_changelog(version: str, changelog: dict, header: bool = False) -> str:
"""
Generates a markdown version of the changelog. Takes a parsed changelog dict from
generate_changelog.
Expand Down
11 changes: 7 additions & 4 deletions semantic_release/history/parser_angular.py
@@ -1,4 +1,7 @@
"""Angular commit style commit parser
"""
import re
from typing import Tuple

from ..errors import UnknownCommitMessageStyleError
from .parser_helpers import parse_text_block
Expand All @@ -22,20 +25,20 @@
}


def parse_commit_message(message):
def parse_commit_message(message: str) -> Tuple[int, str, str, Tuple[str, str, str]]:
"""
Parses a commit message according to the angular commit guidelines specification.
:param message: A string of a commit message.
:return: A tuple of (level to bump, type of change, scope of change, a tuple with descriptions)
:raises UnknownCommitMessageStyleError: if regular expression matching fails
"""

if not re_parser.match(message):
parsed = re_parser.match(message)
if not parsed:
raise UnknownCommitMessageStyleError(
'Unable to parse the given commit message: {}'.format(message)
)

parsed = re_parser.match(message)
level_bump = 0
if parsed.group('text') and 'BREAKING CHANGE' in parsed.group('text'):
level_bump = 3
Expand Down
6 changes: 5 additions & 1 deletion semantic_release/history/parser_helpers.py
@@ -1,5 +1,9 @@
"""Commit parser helpers
"""
from typing import Tuple

def parse_text_block(text):

def parse_text_block(text: str) -> Tuple[str, str]:
"""
This will take a text block and return a tuple with body and footer,
where footer is defined as the last paragraph.
Expand Down

0 comments on commit a6d5e9b

Please sign in to comment.