...
 
Commits (13)
Copyright (c) 2015, Jeff Forcier
Copyright (c) 2017, Jeff Forcier
All rights reserved.
Redistribution and use in source and binary forms, with or without
......
Metadata-Version: 1.1
Name: releases
Version: 1.0.0
Version: 1.4.0
Summary: A Sphinx extension for changelog manipulation
Home-page: https://github.com/bitprophet/releases
Author: Jeff Forcier
Author-email: jeff@bitprophet.org
License: UNKNOWN
Description-Content-Type: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Unix
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Documentation
Classifier: Topic :: Documentation :: Sphinx
.. image:: https://secure.travis-ci.org/bitprophet/releases.png?branch=master
:target: https://travis-ci.org/bitprophet/releases
What is Releases?
=================
Releases is a `Sphinx <http://sphinx-doc.org>`_ extension designed to help you
keep a source control friendly, merge friendly changelog file & turn it into
useful, human readable HTML output.
Releases is a Python (2.7, 3.4+) compatible `Sphinx <http://sphinx-doc.org>`_
(1.3+) extension designed to help you keep a source control friendly, merge
friendly changelog file & turn it into useful, human readable HTML output.
Specifically:
......@@ -20,10 +23,8 @@ Specifically:
Some background on why this tool was created can be found in `this blog post
<http://bitprophet.org/blog/2013/09/14/a-better-changelog/>`_.
For more documentation, including detailed installation and usage information,
please see http://releases.readthedocs.org.
For more documentation, please see http://releases.readthedocs.io.
.. note::
You can install the `development version
<https://github.com/bitprophet/releases/tarball/master#egg=releases-dev>`_
via ``pip install releases==dev``.
You can install the development version via ``pip install -e
git+https://github.com/bitprophet/releases/#egg=releases``.
python-releases (1.0.0-2) UNRELEASED; urgency=medium
python-releases (1.4.0-1) unstable; urgency=medium
[ Stefano Rivera ]
* Team upload.
* New upstream release.
* Update copyright.
* Verify upstream signature in uscan.
* Update X-Python3-Version, upstream declares >= 3.4 support.
* Declare Rules-Requires-Root: no.
* Bump debhelper compat to 10.
* Bump Standards-Version to 4.1.3, no changes needed.
* New upstream requirement: python3-semantic-version.
* Build docs under Python 3.
[ Ondřej Nový ]
* d/control: Set Vcs-* to salsa.debian.org
* d/copyright: Use https protocol in Format field
* d/watch: Use https protocol
-- Ondřej Nový <onovy@debian.org> Tue, 13 Feb 2018 10:03:33 +0100
-- Stefano Rivera <stefanor@debian.org> Fri, 16 Mar 2018 15:54:24 -0700
python-releases (1.0.0-1) unstable; urgency=medium
......
......@@ -3,20 +3,22 @@ Section: python
Priority: optional
Maintainer: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Uploaders: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
Build-Depends: debhelper (>= 9),
Build-Depends: debhelper (>= 10),
dh-python,
python-all,
python-setuptools,
python-sphinx (>= 1.3.1-3~),
python-sphinx-rtd-theme (>= 0.1.8-2~),
python3-all,
python3-setuptools
Standards-Version: 3.9.6
python3-semantic-version,
python3-setuptools,
python3-sphinx (>= 1.3.1-3~),
python3-sphinx-rtd-theme (>= 0.1.8-2~)
Standards-Version: 4.1.3
X-Python-Version: >= 2.7
X-Python3-Version: >= 3.2
X-Python3-Version: >= 3.4
Vcs-Git: https://salsa.debian.org/python-team/modules/python-releases.git
Vcs-Browser: https://salsa.debian.org/python-team/modules/python-releases
Homepage: https://github.com/bitprophet/releases
Rules-Requires-Root: no
Package: python-releases
Architecture: all
......
......@@ -3,9 +3,8 @@ Upstream-Name: releases
Source: https://github.com/bitprophet/releases
Files: *
Copyright: 2014, 2015 Jeff Forcier
Copyright: 2014-2017, Jeff Forcier
License: BSD-2-clause
Copyright (c) 2014, Jeff Forcier
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
......@@ -31,6 +30,7 @@ License: BSD-2-clause
Files: debian/*
Copyright: 2014 Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
2016 Jan Dittberner <jandd@debian.org>
2018 Stefano Rivera <stefanor@debian.org>
License: GPL-3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3,
......
From 9f5ee4d88503024479c3d6828c59aebb4b48090b Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jandd@debian.org>
Date: Sat, 30 Jan 2016 18:54:34 +0100
Subject: Remove travis-ci image and link to avoid privacy breach
......@@ -8,7 +7,7 @@ Subject: Remove travis-ci image and link to avoid privacy breach
1 file changed, 3 deletions(-)
diff --git a/README.rst b/README.rst
index 6e79e84..f427115 100644
index bdaac6b..cab592a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,3 @@
......
From b0a4c7c816eb3338112b999e203db3fcbd611e8f Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jandd@debian.org>
Date: Sat, 30 Jan 2016 19:58:55 +0100
Subject: Remove calculated date from docs/conf.py to support reproducible
......
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
u6pINQ==
=7x3O
-----END PGP PUBLIC KEY BLOCK-----
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \
https://pypi.debian.net/releases/releases-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
# Task runner
invoke>=0.6.0
invocations>=0.4.1
invoke>=0.6.0,<2.0
invocations>=0.14,<2.0
# Tests (N.B. integration suite also uses Invoke as above)
spec>=0.11.3
spec>=0.11.3,<2.0
mock==1.0.1
# Just for tests...heh
six>=1.4.1
six>=1.4.1,<2.0
# Docs
-e .
sphinx>=1.1
sphinx_rtd_theme>=0.1.5
sphinx_rtd_theme>=0.1.5,<2.0
# Builds
wheel==0.24
twine==1.5
......@@ -2,6 +2,64 @@
Changelog
=========
* :release:`1.4.0 <2017-10-20>`
* :support:`-` Drop Python 2.6 and 3.3 support, to correspond with earlier
changes in Sphinx and most other public Python projects.
* :bug:`- major` Identified a handful of issues with our Sphinx pin &
subsequently, internal changes in Sphinx 1.6 which broke (and/or appear to
break, such as noisy warnings) our own behavior. These have (hopefully) all
been fixed.
* :release:`1.3.2 <2017-10-19>`
* :support:`68 backported` Update packaging requirements to allow for
``sphinx>=1.3,<2``. Thanks to William Minchin.
* :release:`1.3.1 <2017-05-18>`
* :bug:`60` Report extension version to Sphinx for improved Sphinx debug
output. Credit: William Minchin.
* :bug:`66` (via :issue:`67`) Deal with some Sphinx 1.6.1 brokenness causing
``AttributeError`` by leveraging ``getattr()``'s default-value argument.
Thanks to Ian Cordasco for catch & patch.
* :release:`1.3.0 <2016-12-09>`
* :feature:`-` Add ``releases.util``, exposing (among other things) a highly
useful ``parse_changelog(path)`` function that returns a user-facing dict
representing a parsed changelog. Allows users to examine their changelogs
programmatically and answer questions like "do I have any outstanding bugs in
the 1.1 release line?".
* :release:`1.2.1 <2016-07-25>`
* :support:`51 backported` Modernize release management so PyPI trove
classifiers are more accurate, wheel archives are universal instead of Python
2 only, and release artifacts are GPG signed.
* :bug:`56` Fix exceptions that occurred when no release/issue link options
were configured. Now those options are truly optional: release version and
issue number text will simply display normally instead of as hyperlinks.
Thanks to André Caron for the report.
* :bug:`36` Changelogs with no releases whatsoever should still be viable
instead of raising exceptions. This is now happily the case. All items in
such changelogs will end up in a single "unreleased features" list, just as
with regular prehistory entries. Thanks to Steve Ivy for initial report and
André Caron for additional feedback.
* :release:`1.2.0 <2016-05-20>`
* :bug:`- major` Fix formatting of release header dates; a "75% text size"
style rule has had an uncaught typo for some time.
* :bug:`55 major` Non-annotated changelog line items (which implicitly become
bugs) were incorrectly truncating their contents in some situations
(basically, any time they included non-regular-text elements like monospace,
bold etc). This has been fixed.
* :feature:`19` Add ``unstable_prehistory`` option/mode for changelogs whose
0.x release cycle is "rapid" or "unstable" and doesn't closely follow normal
semantic version-driven organization. See :ref:`unstable-prehistory`.
* :bug:`53 major` Tweak newly-updated models so bugfix items prior to an
initial release are considered 'major bugs' so they get rolled into that
initial release (instead of causing a ``ValueError``).
* :release:`1.1.0 <2016-04-28>`
* :feature:`45` Add support for major version transitions (e.g. 1.0 to 2.0).
.. note::
This adds a new install-time dependency: the `semantic_version library
<https://python-semanticversion.readthedocs.io>`_. It's pure Python, so
installation should be trivial.
* :bug:`44 major` Update one of our internal docutils-related classes for
compatibility with Sphinx 1.4.x. Thanks to Gabi Davar for catch & patch.
* :release:`1.0.0 <2015-11-05>`
* :feature:`42` For readability, issues within each release so they are
displayed in feature->bug->support order.
......
This diff is collapsed.
from datetime import datetime
import os
import sys
......@@ -10,7 +11,7 @@ source_suffix = '.rst'
master_doc = 'index'
project = u'Releases'
year = 2015
year = datetime.now().year
copyright = u'%d Jeff Forcier' % year
# Ensure project directory is on PYTHONPATH for version, autodoc access
......
......@@ -31,6 +31,15 @@ Specifically:
for an example.
* You may optionally set ``releases_debug = True`` to see debug output
while building your docs.
* If your changelog includes "simple" pre-1.0 releases derived from a
single branch (i.e. without stable release lines & semantic versioning)
you may want to set ``releases_unstable_prehistory = True``.
* This is also useful if you've just imported a non-Releases changelog,
where your issues are all basic list-items and you don't want to go
through and add bug/feature/support/etc roles.
* See :ref:`the appropriate conceptual docs <unstable-prehistory>` for
details on this behavior.
* Create a Sphinx document named ``changelog.rst`` containing a bulleted list
somewhere at its topmost level.
......@@ -79,10 +88,21 @@ Specifically:
releases '1.1.1' and '1.2.0' will cause it to appear in '1.2.0'
**only**.
* ``(N.N+)`` where ``N.N`` is a valid release line, e.g. ``1.1`` or
``2.10``: Given on *bug* issues to denote minimum release line. E.g.
when actively backporting most bugs to release lines 1.2, 1.3 and
1.4, you might specify ``:bug:`55 (1.3+)``` to note that bug 55 only
applies to releases in 1.3 and above - not 1.2.
``2.10``: Given on issues (usually *bugs*) to denote minimum release
line. E.g. when actively backporting most bugs to release lines 1.2,
1.3 and 1.4, you might specify ``:bug:`55 (1.3+)``` to note that bug
55 only applies to releases in 1.3 and above - not 1.2.
* A `semantic version range spec covering minor+major version numbers
<https://python-semanticversion.readthedocs.io/en/latest/reference.html#version-specifications-the-spec-class>`_
such as ``(<2.0)`` or ``(>=1.0,<3.1)``. A more powerful version of
``(N.N+)`` allowing annotation of issues belonging to specific major
versions.
.. note::
It is possible to give *both* a regular keyword
(``backported``/``major``) *and* a spec (``(N.N+)``/``(>=1.0)``) in
the same issue. However, giving two keywords or two specs at the same
time makes no sense & is not allowed.
* Regular Sphinx content may be given after issue roles and will be preserved
as-is when rendering. For example, in ``:bug:`123` Fixed a bug, thanks
......@@ -99,7 +119,7 @@ Specifically:
Then build your docs; in the rendered output, ``changelog.html`` should show
issues grouped by release, as per the above rules. Examples: `Releases' own
rendered changelog
<http://releases.readthedocs.org/en/latest/changelog.html>`_, `Fabric's
<http://releases.readthedocs.io/en/latest/changelog.html>`_, `Fabric's
rendered changelog <http://www.fabfile.org/changelog.html>`_.
......
Metadata-Version: 1.1
Name: releases
Version: 1.4.0
Summary: A Sphinx extension for changelog manipulation
Home-page: https://github.com/bitprophet/releases
Author: Jeff Forcier
Author-email: jeff@bitprophet.org
License: UNKNOWN
Description-Content-Type: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Unix
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Documentation
Classifier: Topic :: Documentation :: Sphinx
LICENSE
MANIFEST.in
README.rst
dev-requirements.txt
setup.cfg
setup.py
tasks.py
docs/changelog.rst
docs/concepts.rst
docs/conf.py
docs/index.rst
docs/usage.rst
releases/__init__.py
releases/_version.py
releases/line_manager.py
releases/models.py
releases/util.py
releases.egg-info/PKG-INFO
releases.egg-info/SOURCES.txt
releases.egg-info/dependency_links.txt
releases.egg-info/requires.txt
releases.egg-info/top_level.txt
tests/_util.py
tests/organization.py
tests/presentation.py
\ No newline at end of file
semantic_version<3.0
sphinx<2,>=1.3
This diff is collapsed.
__version_info__ = (1, 0, 0)
__version_info__ = (1, 4, 0)
__version__ = '.'.join(map(str, __version_info__))
# TODO: un-subclass dict in favor of something more explicit, once all regular
# dict-like access has been factored out into methods
class LineManager(dict):
"""
Manages multiple release lines/families as well as related config state.
"""
def __init__(self, app):
"""
Initialize new line manager dict.
:param app: The core Sphinx app object. Mostly used for config.
"""
super(LineManager, self).__init__()
self.app = app
@property
def config(self):
"""
Return Sphinx config object.
"""
return self.app.config
def add_family(self, major_number):
"""
Expand to a new release line with given ``major_number``.
This will flesh out mandatory buckets like ``unreleased_bugfix`` and do
other necessary bookkeeping.
"""
# Normally, we have separate buckets for bugfixes vs features
keys = ['unreleased_bugfix', 'unreleased_feature']
# But unstable prehistorical releases roll all up into just
# 'unreleased'
if major_number == 0 and self.config.releases_unstable_prehistory:
keys = ['unreleased']
# Either way, the buckets default to an empty list
self[major_number] = {key: [] for key in keys}
@property
def unstable_prehistory(self):
"""
Returns True if 'unstable prehistory' behavior should be applied.
Specifically, checks config & whether any non-0.x releases exist.
"""
return (
self.config.releases_unstable_prehistory
and not self.has_stable_releases
)
@property
def stable_families(self):
"""
Returns release family numbers which aren't 0 (i.e. prehistory).
"""
return [x for x in self if x != 0]
@property
def has_stable_releases(self):
"""
Returns whether stable (post-0.x) releases seem to exist.
"""
nonzeroes = self.stable_families
# Nothing but 0.x releases -> yup we're prehistory
if not nonzeroes:
return False
# Presumably, if there's >1 major family besides 0.x, we're at least
# one release into the 1.0 (or w/e) line.
if len(nonzeroes) > 1:
return True
# If there's only one, we may still be in the space before its N.0.0 as
# well; we can check by testing for existence of bugfix buckets
return any(
x for x in self[nonzeroes[0]] if not x.startswith('unreleased')
)
from functools import reduce
from operator import xor
from docutils import nodes
from semantic_version import Version as StrictVersion, Spec
import six
class Version(StrictVersion):
"""
Version subclass toggling ``partial=True`` by default.
"""
def __init__(self, version_string, partial=True):
super(Version, self).__init__(version_string, partial)
# Issue type list (keys) + color values
......@@ -10,10 +23,24 @@ ISSUE_TYPES = {
class Issue(nodes.Element):
# Technically, we just need number, but heck, you never know...
_cmp_keys = ('type', 'number', 'backported', 'major')
@property
def type(self):
return self['type_']
@property
def is_featurelike(self):
if self.type == 'bug':
return self.major
else:
return not self.backported
@property
def is_buglike(self):
return not self.is_featurelike
@property
def backported(self):
return self.get('backported', False)
......@@ -27,8 +54,120 @@ class Issue(nodes.Element):
return self.get('number', None)
@property
def line(self):
return self.get('line', None)
def spec(self):
return self.get('spec', None)
def __eq__(self, other):
for attr in self._cmp_keys:
if getattr(self, attr, None) != getattr(other, attr, None):
return False
return True
def __hash__(self):
return reduce(xor, [hash(getattr(self, x)) for x in self._cmp_keys])
def minor_releases(self, manager):
"""
Return all minor release line labels found in ``manager``.
"""
# TODO: yea deffo need a real object for 'manager', heh. E.g. we do a
# very similar test for "do you have any actual releases yet?"
# elsewhere. (This may be fodder for changing how we roll up
# pre-major-release features though...?)
return [
key for key, value in six.iteritems(manager)
if any(x for x in value if not x.startswith('unreleased'))
]
def default_spec(self, manager):
"""
Given the current release-lines structure, return a default Spec.
Specifics:
* For feature-like issues, only the highest major release is used, so
given a ``manager`` with top level keys of ``[1, 2]``, this would
return ``Spec(">=2")``.
* When ``releases_always_forwardport_features`` is ``True``, that
behavior is nullified, and this function always returns the empty
``Spec`` (which matches any and all versions/lines).
* For bugfix-like issues, we only consider major release families which
have actual releases already.
* Thus the core difference here is that features are 'consumed' by
upcoming major releases, and bugfixes are not.
* When the ``unstable_prehistory`` setting is ``True``, the default
spec starts at the oldest non-zero release line. (Otherwise, issues
posted after prehistory ends would try being added to the 0.x part of
the tree, which makes no sense in unstable-prehistory mode.)
"""
# TODO: I feel like this + the surrounding bits in add_to_manager()
# could be consolidated & simplified...
specstr = ""
# Make sure truly-default spec skips 0.x if prehistory was unstable.
stable_families = manager.stable_families
if manager.config.releases_unstable_prehistory and stable_families:
specstr = ">={}".format(min(stable_families))
if self.is_featurelike:
# TODO: if app->config-><releases_always_forwardport_features or
# w/e
if True:
specstr = ">={}".format(max(manager.keys()))
else:
# Can only meaningfully limit to minor release buckets if they
# actually exist yet.
buckets = self.minor_releases(manager)
if buckets:
specstr = ">={}".format(max(buckets))
return Spec(specstr) if specstr else Spec()
def add_to_manager(self, manager):
"""
Given a 'manager' structure, add self to one or more of its 'buckets'.
"""
# Derive version spec allowing us to filter against major/minor buckets
spec = self.spec or self.default_spec(manager)
# Only look in appropriate major version/family; if self is an issue
# declared as living in e.g. >=2, this means we don't even bother
# looking in the 1.x family.
families = [Version(str(x)) for x in manager]
versions = list(spec.filter(families))
for version in versions:
family = version.major
# Within each family, we further limit which bugfix lines match up
# to what self cares about (ignoring 'unreleased' until later)
candidates = [
Version(x)
for x in manager[family]
if not x.startswith('unreleased')
]
# Select matching release lines (& stringify)
buckets = []
bugfix_buckets = [str(x) for x in spec.filter(candidates)]
# Add back in unreleased_* as appropriate
# TODO: probably leverage Issue subclasses for this eventually?
if self.is_buglike:
buckets.extend(bugfix_buckets)
# Don't put into JUST unreleased_bugfix; it implies that this
# major release/family hasn't actually seen any releases yet
# and only exists for features to go into.
if bugfix_buckets:
buckets.append('unreleased_bugfix')
# Obtain list of minor releases to check for "haven't had ANY
# releases yet" corner case, in which case ALL issues get thrown in
# unreleased_feature for the first release to consume.
# NOTE: assumes first release is a minor or major one,
# but...really? why would your first release be a bugfix one??
no_releases = not self.minor_releases(manager)
if self.is_featurelike or self.backported or no_releases:
buckets.append('unreleased_feature')
# Now that we know which buckets are appropriate, add ourself to
# all of them. TODO: or just...do it above...instead...
for bucket in buckets:
manager[family][bucket].append(self)
def __repr__(self):
flag = ''
......@@ -36,10 +175,10 @@ class Issue(nodes.Element):
flag = 'backported'
elif self.major:
flag = 'major'
elif self.line:
flag = self.line + '+'
elif self.spec:
flag = self.spec
if flag:
flag = ' ({0})'.format(flag)
flag = ' ({})'.format(flag)
return '<{issue.type} #{issue.number}{flag}>'.format(issue=self,
flag=flag)
......@@ -49,5 +188,16 @@ class Release(nodes.Element):
def number(self):
return self['number']
@property
def minor(self):
# TODO: use Version
return '.'.join(self.number.split('.')[:-1])
@property
def family(self):
# TODO: use Version.major
# TODO: and probs just rename to .major, 'family' is dumb tbh
return int(self.number.split('.')[0])
def __repr__(self):
return '<release {0}>'.format(self.number)
return '<release {}>'.format(self.number)
This diff is collapsed.
[bdist_wheel]
[flake8]
exclude = docs,.git,build,dist
ignore = E124,E125,E128,E261,E301,E302,E303
max-line-length = 79
[wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
......@@ -16,20 +16,26 @@ setup(
author_email='jeff@bitprophet.org',
url='https://github.com/bitprophet/releases',
packages=['releases'],
install_requires=[
'semantic_version<3.0',
'sphinx>=1.3,<2',
],
classifiers=[
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Unix',
'Operating System :: POSIX',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Topic :: Software Development',
'Topic :: Software Development :: Build Tools',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Unix',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development',
'Topic :: Software Development :: Documentation',
'Topic :: Documentation',
'Topic :: Documentation :: Sphinx',
],
)
from os.path import join
from invocations import docs
from invocations.testing import test
from invocations.testing import test, integration, watch_tests
from invocations.packaging import release
from invoke import Collection
from invoke import run
from invoke import task
@task(help={
'pty': "Whether to run tests under a pseudo-tty",
ns = Collection(test, integration, watch_tests, release, docs)
ns.configure({
'tests': {
'package': 'releases',
},
'packaging': {
'sign': True,
'wheel': True,
'changelog_file': join(
docs.ns.configuration()['sphinx']['source'],
'changelog.rst',
),
},
})
def integration(pty=True):
"""Runs integration tests."""
cmd = 'inv test -o --tests=integration'
run(cmd + ('' if pty else ' --no-pty'), pty=pty)
ns = Collection(test, integration, release, docs)
from docutils.nodes import (
list_item, paragraph,
)
from mock import Mock
from spec import eq_, ok_
import six
from releases import (
Issue,
issues_role,
Release,
release_role,
construct_releases,
)
from releases.util import make_app, changelog2dict
def inliner(app=None):
app = app or make_app()
return Mock(document=Mock(settings=Mock(env=Mock(app=app))))
# Obtain issue() object w/o wrapping all parse steps
def issue(type_, number, **kwargs):
text = str(number)
if kwargs.get('backported', False):
text += " backported"
if kwargs.get('major', False):
text += " major"
if kwargs.get('spec', None):
text += " (%s)" % kwargs['spec']
app = kwargs.get('app', None)
return issues_role(
name=type_,
rawtext='',
text=text,
lineno=None,
inliner=inliner(app=app),
)[0][0]
# Even shorter shorthand!
def b(number, **kwargs):
return issue('bug', str(number), **kwargs)
def f(number, **kwargs):
return issue('feature', str(number), **kwargs)
def s(number, **kwargs):
return issue('support', str(number), **kwargs)
def entry(i):
"""
Easy wrapper for issue/release objects.
Default is to give eg an issue/release object that gets wrapped in a LI->P.
May give your own (non-issue/release) object to skip auto wrapping. (Useful
since entry() is often called a few levels deep.)
"""
if not isinstance(i, (Issue, Release)):
return i
return list_item('', paragraph('', '', i))
def release(number, **kwargs):
app = kwargs.get('app', None)
nodes = release_role(
name=None,
rawtext='',
text='%s <2013-11-20>' % number,
lineno=None,
inliner=inliner(app=app),
)[0]
return list_item('', paragraph('', '', *nodes))
def release_list(*entries, **kwargs):
skip_initial = kwargs.pop('skip_initial', False)
entries = list(entries) # lol tuples
# Translate simple objs into changelog-friendly ones
for index, item in enumerate(entries):
if isinstance(item, six.string_types):
entries[index] = release(item)
else:
entries[index] = entry(item)
# Insert initial/empty 1st release to start timeline
if not skip_initial:
entries.append(release('1.0.0'))
return entries
def releases(*entries, **kwargs):
app = kwargs.pop('app', None) or make_app()
return construct_releases(release_list(*entries, **kwargs), app)[0]
def setup_issues(self):
self.f = f(12)
self.s = s(5)
self.b = b(15)
self.mb = b(200, major=True)
self.bf = f(27, backported=True)
self.bs = s(29, backported=True)
def expect_releases(entries, release_map, skip_initial=False, app=None):
kwargs = {'skip_initial': skip_initial}
# Let high level tests tickle config settings via make_app()
if app is not None:
kwargs['app'] = app
changelog = changelog2dict(releases(*entries, **kwargs))
snapshot = dict(changelog)
err = "Got unexpected contents for {}: wanted {}, got {}"
err += "\nFull changelog: {!r}\n"
for rel, issues in six.iteritems(release_map):
found = changelog.pop(rel)
eq_(set(found), set(issues), err.format(rel, issues, found, snapshot))
# Sanity: ensure no leftover issue lists exist (empty ones are OK)
for key in list(changelog.keys()):
if not changelog[key]:
del changelog[key]
ok_(not changelog, "Found leftovers: {}".format(changelog))
from spec import Spec, eq_
from docutils.nodes import (
reference, bullet_list, list_item, literal, raw, paragraph, Text
)
from releases import (
Issue,
construct_releases,
construct_nodes,
)
from _util import (
b, f, s,
entry,
make_app,
release,
releases,
setup_issues,
)
def _obj2name(obj):
cls = obj if isinstance(obj, type) else obj.__class__
return cls.__name__.split('.')[-1]
def _expect_type(node, cls):
type_ = _obj2name(node)
name = _obj2name(cls)
msg = "Expected %r to be a %s, but it's a %s" % (node, name, type_)
assert isinstance(node, cls), msg
class presentation(Spec):
"""
Expansion/extension of docutils nodes (rendering)
"""
def setup(self):
setup_issues(self)
def _generate(self, *entries, **kwargs):
raw = kwargs.pop('raw', False)
nodes = construct_nodes(releases(*entries, **kwargs))
# By default, yield the contents of the bullet list.
return nodes if raw else nodes[0][1][0]
def _test_link(self, kwargs, type_, expected):
app = make_app(**kwargs)
nodes = construct_nodes(construct_releases([
release('1.0.2', app=app),
entry(b(15, app=app)),
release('1.0.0'),
], app=app)[0])
# Shorthand for "I'll do my own asserts"
if expected is None:
return nodes
if type_ == 'release':
header = nodes[0][0][0].astext()
assert expected in header
elif type_ == 'issue':
link = nodes[0][1][0][0][2]
eq_(link['refuri'], expected)
else:
raise Exception("Gave unknown type_ kwarg to _test_link()!")
def issues_with_numbers_appear_as_number_links(self):
self._test_link({}, 'issue', 'bar_15')
def releases_appear_as_header_links(self):
self._test_link({}, 'release', 'foo_1.0.2')
def links_will_use_github_option_if_defined(self):
kwargs = {
'release_uri': None,
'issue_uri': None,
'github_path': 'foo/bar',
}
for type_, expected in (
('issue', 'https://github.com/foo/bar/issues/15'),
('release', 'https://github.com/foo/bar/tree/1.0.2'),
):
self._test_link(kwargs, type_, expected)
def issue_links_prefer_explicit_setting_over_github_setting(self):
kwargs = {
'release_uri': None,
'issue_uri': 'explicit_issue_%s',
'github_path': 'foo/bar',
}
self._test_link(kwargs, 'issue', 'explicit_issue_15')
def release_links_prefer_explicit_setting_over_github_setting(self):
kwargs = {
'release_uri': 'explicit_release_%s',
'issue_uri': None,
'github_path': 'foo/bar',
}
self._test_link(kwargs, 'release', 'explicit_release_1.0.2')
def completely_blank_uri_settings_does_not_asplode(self):
kwargs = {
'release_uri': None,
'issue_uri': None,
'github_path': None,
}
# Get nodes for direct inspection
nodes = self._test_link(kwargs, 'release', None)
# Ensure release entry still displays release version.
# (These are curently constructed as raw text nodes so no other great
# way to test this. Meh.)
text = nodes[0][0][0].astext()
assert '>1.0.2 <span' in text
# Ensure issues still display issue number. (Ditto.)
text = nodes[0][1][0][0].astext()
assert '>Bug</span>] #15:' in text
def _assert_prefix(self, entries, expectation):
assert expectation in self._generate(*entries)[0][0][0]
def bugs_marked_as_bugs(self):
self._assert_prefix(['1.0.2', self.b], 'Bug')
def features_marked_as_features(self):
self._assert_prefix(['1.1.0', self.f], 'Feature')
def support_marked_as_support(self):
self._assert_prefix(['1.1.0', self.s], 'Support')
def dashed_issues_appear_as_unlinked_issues(self):
node = self._generate('1.0.2', b('-'))
assert not isinstance(node[0][2], reference)
def zeroed_issues_appear_as_unlinked_issues(self):
node = self._generate('1.0.2', b(0))
assert not isinstance(node[0][2], reference)
def un_prefixed_list_items_appear_as_unlinked_bugs(self):
fake = list_item('', paragraph('', '',
Text("fixes an issue in "), literal('', 'methodname')))
node = self._generate('1.0.2', fake)
# [<raw prefix>, <inline colon>, <inline space>, <text>, <monospace>]
eq_(len(node[0]), 5)
assert 'Bug' in str(node[0][0])
assert 'fixes an issue' in str(node[0][3])
assert 'methodname' in str(node[0][4])
def un_prefixed_list_items_get_no_prefix_under_unstable_prehistory(self):
app = make_app(unstable_prehistory=True)
fake = list_item('', paragraph('', '', raw('', 'whatever')))
node = self._generate('0.1.0', fake, app=app, skip_initial=True)
# [<raw bug text>]
eq_(len(node[0]), 1)
assert 'Bug' not in str(node[0][0])
assert 'whatever' in str(node[0][0])
def issues_remain_wrapped_in_unordered_list_nodes(self):
node = self._generate('1.0.2', self.b, raw=True)[0][1]
_expect_type(node, bullet_list)
_expect_type(node[0], list_item)
def release_headers_have_local_style_tweaks(self):
node = self._generate('1.0.2', self.b, raw=True)[0][0]
_expect_type(node, raw)
# Header w/ bottom margin
assert '<h2 style="margin-bottom' in str(node)
# Date span w/ font-size
assert '<span style="font-size' in str(node)
def descriptions_are_preserved(self):
# Changelog containing an issue item w/ trailing node
issue = list_item('',
paragraph('', '', self.b.deepcopy(), raw('', 'x')),
)
# Trailing nodes should appear post-processing after the link/etc
rest = self._generate('1.0.2', issue)[0]
eq_(len(rest), 5)
_expect_type(rest[4], raw)
eq_(rest[4].astext(), 'x')
def complex_descriptions_are_preserved(self):
# Complex 'entry' mapping to an outer list_item (list) containing two
# paragraphs, one w/ the real issue + desc, another simply a 2nd text
# paragraph. Using 'raw' nodes for filler as needed.
issue = list_item('',
paragraph('', '', self.b.deepcopy(), raw('', 'x')),
paragraph('', '', raw('', 'y')),
)
li = self._generate('1.0.2', issue)
# Expect that the machinery parsing issue nodes/nodelists, is not
# discarding our 2nd 'paragraph'
eq_(len(li), 2)
p1, p2 = li
# Last item in 1st para is our 1st raw node
_expect_type(p1[4], raw)
eq_(p1[4].astext(), 'x')
# Only item in 2nd para is our 2nd raw node
_expect_type(p2[0], raw)
eq_(p2[0].astext(), 'y')
def descriptions_are_parsed_for_issue_roles(self):
item = list_item('',
paragraph('', '', self.b.deepcopy(), s(5))
)
para = self._generate('1.0.2', item)[0]
# Sanity check - in a broken parsing scenarion, the 4th child will be a
# raw issue object
assert not isinstance(para[4], Issue)
# First/primary link
_expect_type(para[2], reference)
eq_(para[2].astext(), '#15')
assert 'Bug' in para[0].astext()
# Second/inline link
_expect_type(para[6], reference)
eq_(para[6].astext(), '#5')
assert 'Support' in para[4].astext()
def unreleased_buckets_omit_major_version_when_only_one_exists(self):
result = self._generate(b(1), raw=True)[0][0][0]
html = str(result) # since repr() from test-fail hides actual text
assert "Next bugfix release" in html
def unreleased_buckets_display_major_version_when_multiple(self):
entries = (
b(3), # should appear in unreleased bugs for 2.x
'2.0.0',
b(2), # should appear in unreleased bugs for 1.x
'1.0.1',
b(1),
)
# Expectation: [2.x unreleased, 1.x unreleased, 1.0.1]
two_x, one_x, _ = self._generate(*entries, raw=True)
html = str(two_x[0][0])
assert "Next 2.x bugfix release" in html
html = str(one_x[0][0])
assert "Next 1.x bugfix release" in html
def unreleased_displays_version_when_only_some_lines_displayed(self):
# I.e. if there's unreleased 1.x stuff but no unreleased 2.x, still
# display the "1.x".
entries = (
# Note lack of any bugfixes post 2.0.0
'2.0.0',
b(2),
'1.0.1',
b(1),
)
# Expectation: [1.x unreleased, 1.0.1] - no 2.x.
result = self._generate(*entries, raw=True)
eq_(len(result), 2)
html = str(result[0][0][0])
assert "Next 1.x bugfix release" in html
def unstable_prehistory_active_means_only_one_unreleased_release(self):
app = make_app(unstable_prehistory=True)
entries = (b(2), f(3), '0.1.0', b(1))
result = self._generate(*entries, app=app, raw=True, skip_initial=True)
html = str(result[0][0][0])
assert "Next release" in html