mirror of
https://github.com/vale981/releases
synced 2025-03-04 17:21:43 -05:00
Merge branch 'master' into 61-int
This commit is contained in:
commit
3e9710e587
19 changed files with 470 additions and 120 deletions
|
@ -1,14 +1,19 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.6"
|
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.3"
|
|
||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
- "pypy"
|
- "pypy"
|
||||||
#- "pypy3" # Looks like Sphinx (as of 1.4.1) is not pypy3 compat
|
#- "pypy3" # Looks like Sphinx (as of 1.4.1) is not pypy3 compat
|
||||||
|
env:
|
||||||
|
- SPHINX=">=1.3,<1.4"
|
||||||
|
- SPHINX=">=1.4,<1.5"
|
||||||
|
- SPHINX=">=1.5,<1.6"
|
||||||
|
- SPHINX=">=1.6,<1.7"
|
||||||
install:
|
install:
|
||||||
- pip install -r dev-requirements.txt
|
- pip install -r dev-requirements.txt
|
||||||
|
- pip install "sphinx$SPHINX"
|
||||||
script:
|
script:
|
||||||
# Primary test suite
|
# Primary test suite
|
||||||
- inv test
|
- inv test
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2016, Jeff Forcier
|
Copyright (c) 2018, Jeff Forcier
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
11
README.rst
11
README.rst
|
@ -4,9 +4,9 @@
|
||||||
What is Releases?
|
What is Releases?
|
||||||
=================
|
=================
|
||||||
|
|
||||||
Releases is a Python 2+3 compatible `Sphinx <http://sphinx-doc.org>`_ extension
|
Releases is a Python (2.7, 3.4+) compatible `Sphinx <http://sphinx-doc.org>`_
|
||||||
designed to help you keep a source control friendly, merge friendly changelog
|
(1.3+) extension designed to help you keep a source control friendly, merge
|
||||||
file & turn it into useful, human readable HTML output.
|
friendly changelog file & turn it into useful, human readable HTML output.
|
||||||
|
|
||||||
Specifically:
|
Specifically:
|
||||||
|
|
||||||
|
@ -26,6 +26,5 @@ Some background on why this tool was created can be found in `this blog post
|
||||||
For more documentation, please see http://releases.readthedocs.io.
|
For more documentation, please see http://releases.readthedocs.io.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
You can install the `development version
|
You can install the development version via ``pip install -e
|
||||||
<https://github.com/bitprophet/releases/tarball/master#egg=releases-dev>`_
|
git+https://github.com/bitprophet/releases/#egg=releases``.
|
||||||
via ``pip install releases==dev``.
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Task runner
|
# Task runner
|
||||||
invoke>=0.6.0,<2.0
|
invoke>=0.6.0,<2.0
|
||||||
invocations>=0.4.1,<2.0
|
invocations>=0.14,<2.0
|
||||||
# Tests (N.B. integration suite also uses Invoke as above)
|
# Tests (N.B. integration suite also uses Invoke as above)
|
||||||
spec>=0.11.3,<2.0
|
spec>=0.11.3,<2.0
|
||||||
mock==1.0.1
|
mock==1.0.1
|
||||||
|
@ -8,8 +8,7 @@ mock==1.0.1
|
||||||
six>=1.4.1,<2.0
|
six>=1.4.1,<2.0
|
||||||
# Docs
|
# Docs
|
||||||
-e .
|
-e .
|
||||||
sphinx>=1.1,<2.0
|
|
||||||
sphinx_rtd_theme>=0.1.5,<2.0
|
sphinx_rtd_theme>=0.1.5,<2.0
|
||||||
# Builds
|
# Builds
|
||||||
wheel==0.24
|
wheel==0.24
|
||||||
twine==1.5
|
twine==1.11.0
|
||||||
|
|
|
@ -4,6 +4,37 @@ Changelog
|
||||||
|
|
||||||
* :feature:`59` Allow multiple changelog files. Thanks to William Minchin for
|
* :feature:`59` Allow multiple changelog files. Thanks to William Minchin for
|
||||||
the patch.
|
the patch.
|
||||||
|
* :release:`1.4.2 <2018-04-27>`
|
||||||
|
* :support:`74` We never pulled our README into our ``setup.py`` metadata,
|
||||||
|
resulting in a rather sparse PyPI page! This has been fixed. Thanks to Peter
|
||||||
|
Demin for the report.
|
||||||
|
* :release:`1.4.1 <2018-03-28>`
|
||||||
|
* :support:`73` Sphinx 1.7.x changed some semi-public APIs; given this is the
|
||||||
|
second minor release in a row to do so, we're explicitly bracketing our
|
||||||
|
``setup.py`` dependencies to Sphinx >= 1.3 and < 1.7. We expect to bump this
|
||||||
|
up one minor release at a time as we add compatibility back in.
|
||||||
|
* :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>`
|
* :release:`1.2.1 <2016-07-25>`
|
||||||
* :support:`51 backported` Modernize release management so PyPI trove
|
* :support:`51 backported` Modernize release management so PyPI trove
|
||||||
classifiers are more accurate, wheel archives are universal instead of Python
|
classifiers are more accurate, wheel archives are universal instead of Python
|
||||||
|
|
14
integration/_support/unreleased_bugs/changelog.rst
Normal file
14
integration/_support/unreleased_bugs/changelog.rst
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
=========
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. Notably, bug #3 hasn't yet been released in the 1.1 line (but has in 1.0).
|
||||||
|
.. This means it should appear in the '1.1' line bucket after parse_changelog.
|
||||||
|
|
||||||
|
* :release:`1.0.2 <2016-10-18>`
|
||||||
|
* :bug:`3` Aw jeez
|
||||||
|
* :release:`1.1.0 <2016-10-17>`
|
||||||
|
* :feature:`2` Woo
|
||||||
|
* :release:`1.0.1 <2014-01-02>`
|
||||||
|
* :bug:`1` Fix a bug.
|
||||||
|
* :release:`1.0.0 <2014-01-01>`
|
6
integration/_support/unreleased_bugs/index.rst
Normal file
6
integration/_support/unreleased_bugs/index.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
====
|
||||||
|
Test
|
||||||
|
====
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
changelog
|
|
@ -16,7 +16,7 @@ class integration(Spec):
|
||||||
# Dynamic sphinx opt overrides
|
# Dynamic sphinx opt overrides
|
||||||
if opts:
|
if opts:
|
||||||
pairs = map(lambda x: '='.join(x), (opts or {}).items())
|
pairs = map(lambda x: '='.join(x), (opts or {}).items())
|
||||||
flags = map(lambda x: '-D {0}'.format(x), pairs)
|
flags = map(lambda x: '-D {}'.format(x), pairs)
|
||||||
flagstr = ' '.join(flags)
|
flagstr = ' '.join(flags)
|
||||||
else:
|
else:
|
||||||
flagstr = ''
|
flagstr = ''
|
||||||
|
@ -25,8 +25,8 @@ class integration(Spec):
|
||||||
build = os.path.join(folder, '_build')
|
build = os.path.join(folder, '_build')
|
||||||
try:
|
try:
|
||||||
# Build
|
# Build
|
||||||
cmd = 'sphinx-build {2} -c {3} -W {0} {1}'.format(
|
cmd = 'sphinx-build {} -c {} -W {} {}'.format(
|
||||||
folder, build, flagstr, conf)
|
flagstr, conf, folder, build)
|
||||||
result = run(cmd, warn=True, hide=True)
|
result = run(cmd, warn=True, hide=True)
|
||||||
if callable(asserts):
|
if callable(asserts):
|
||||||
asserts(result, build, target)
|
asserts(result, build, target)
|
||||||
|
@ -40,10 +40,10 @@ class integration(Spec):
|
||||||
|
|
||||||
def _basic_asserts(self, result, build, target):
|
def _basic_asserts(self, result, build, target):
|
||||||
# Check for errors
|
# Check for errors
|
||||||
msg = "Build failed w/ stderr: {0}"
|
msg = "Build failed w/ stderr: {}"
|
||||||
assert result.ok, msg.format(result.stderr)
|
assert result.ok, msg.format(result.stderr)
|
||||||
# Check for vaguely correct output
|
# Check for vaguely correct output
|
||||||
changelog = os.path.join(build, '{0}.html'.format(target))
|
changelog = os.path.join(build, '{}.html'.format(target))
|
||||||
with open(changelog) as fd:
|
with open(changelog) as fd:
|
||||||
text = fd.read()
|
text = fd.read()
|
||||||
assert "1.0.1" in text
|
assert "1.0.1" in text
|
||||||
|
|
73
integration/util.py
Normal file
73
integration/util.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"""
|
||||||
|
Tests for the ``releases.util`` module.
|
||||||
|
|
||||||
|
These are in the integration suite because they deal with on-disk files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from docutils.nodes import document
|
||||||
|
from spec import Spec, ok_, eq_
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
|
||||||
|
from releases.models import Release, Issue
|
||||||
|
from releases.util import get_doctree, parse_changelog
|
||||||
|
|
||||||
|
|
||||||
|
support = os.path.join(os.path.dirname(__file__), '_support')
|
||||||
|
vanilla = os.path.join(support, 'vanilla', 'changelog.rst')
|
||||||
|
unreleased_bugs = os.path.join(support, 'unreleased_bugs', 'changelog.rst')
|
||||||
|
|
||||||
|
class get_doctree_(Spec):
|
||||||
|
def obtains_app_and_doctree_from_filepath(self):
|
||||||
|
app, doctree = get_doctree(vanilla)
|
||||||
|
# Expect doctree & app
|
||||||
|
ok_(doctree)
|
||||||
|
ok_(app)
|
||||||
|
ok_(isinstance(doctree, document))
|
||||||
|
ok_(isinstance(app, Sphinx))
|
||||||
|
# Sanity checks of internal nodes, which should be Releases objects
|
||||||
|
entries = doctree[0][2]
|
||||||
|
ok_(isinstance(entries[0][0][0], Release))
|
||||||
|
bug = entries[1][0][0]
|
||||||
|
ok_(isinstance(bug, Issue))
|
||||||
|
eq_(bug.type, 'bug')
|
||||||
|
eq_(bug.number, '1')
|
||||||
|
|
||||||
|
|
||||||
|
class parse_changelog_(Spec):
|
||||||
|
def yields_releases_dict_from_changelog_path(self):
|
||||||
|
changelog = parse_changelog(vanilla)
|
||||||
|
ok_(changelog)
|
||||||
|
ok_(isinstance(changelog, dict))
|
||||||
|
eq_(
|
||||||
|
set(changelog.keys()),
|
||||||
|
{'1.0.0', '1.0.1', '1.0', 'unreleased_1_feature'},
|
||||||
|
)
|
||||||
|
eq_(len(changelog['1.0.0']), 0)
|
||||||
|
eq_(len(changelog['unreleased_1_feature']), 0)
|
||||||
|
eq_(len(changelog['1.0.1']), 1)
|
||||||
|
issue = changelog['1.0.1'][0]
|
||||||
|
eq_(issue.type, 'bug')
|
||||||
|
eq_(issue.number, '1')
|
||||||
|
eq_(changelog['1.0'], []) # emptied into 1.0.1
|
||||||
|
|
||||||
|
def unreleased_bugfixes_accounted_for(self):
|
||||||
|
changelog = parse_changelog(unreleased_bugs)
|
||||||
|
# Basic assertions
|
||||||
|
v101 = changelog['1.0.1']
|
||||||
|
eq_(len(v101), 1)
|
||||||
|
eq_(v101[0].number, '1')
|
||||||
|
v110 = changelog['1.1.0']
|
||||||
|
eq_(len(v110), 1)
|
||||||
|
eq_(v110[0].number, '2')
|
||||||
|
v102 = changelog['1.0.2']
|
||||||
|
eq_(len(v102), 1)
|
||||||
|
eq_(v102[0].number, '3')
|
||||||
|
# The crux of the matter: 1.0 bucket empty, 1.1 bucket still has bug 3
|
||||||
|
line_10 = changelog['1.0']
|
||||||
|
eq_(len(line_10), 0)
|
||||||
|
line_11 = changelog['1.1']
|
||||||
|
eq_(len(line_11), 1)
|
||||||
|
eq_(line_11[0].number, '3')
|
||||||
|
ok_(line_11[0] is v102[0])
|
|
@ -4,10 +4,12 @@ import sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from docutils import nodes, utils
|
from docutils import nodes, utils
|
||||||
|
from docutils.parsers.rst import roles
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from .models import Issue, ISSUE_TYPES, Release, Version, Spec
|
from .models import Issue, ISSUE_TYPES, Release, Version, Spec
|
||||||
from .line_manager import LineManager
|
from .line_manager import LineManager
|
||||||
|
from ._version import __version__
|
||||||
|
|
||||||
|
|
||||||
def _log(txt, config):
|
def _log(txt, config):
|
||||||
|
@ -44,7 +46,7 @@ def scan_for_spec(keyword):
|
||||||
# First, test for intermediate '1.2+' style
|
# First, test for intermediate '1.2+' style
|
||||||
matches = release_line_re.findall(keyword)
|
matches = release_line_re.findall(keyword)
|
||||||
if matches:
|
if matches:
|
||||||
return Spec(">={0}".format(matches[0]))
|
return Spec(">={}".format(matches[0]))
|
||||||
# Failing that, see if Spec can make sense of it
|
# Failing that, see if Spec can make sense of it
|
||||||
try:
|
try:
|
||||||
return Spec(keyword)
|
return Spec(keyword)
|
||||||
|
@ -78,7 +80,7 @@ def issues_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||||
# TODO: deal with % vs .format()
|
# TODO: deal with % vs .format()
|
||||||
ref = config.releases_issue_uri % issue_no
|
ref = config.releases_issue_uri % issue_no
|
||||||
elif config.releases_github_path:
|
elif config.releases_github_path:
|
||||||
ref = "https://github.com/{0}/issues/{1}".format(
|
ref = "https://github.com/{}/issues/{}".format(
|
||||||
config.releases_github_path, issue_no)
|
config.releases_github_path, issue_no)
|
||||||
# Only generate a reference/link if we were able to make a URI
|
# Only generate a reference/link if we were able to make a URI
|
||||||
if ref:
|
if ref:
|
||||||
|
@ -109,7 +111,7 @@ def issues_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
|
||||||
if part in ('backported', 'major'):
|
if part in ('backported', 'major'):
|
||||||
keyword = part
|
keyword = part
|
||||||
else:
|
else:
|
||||||
err = "Gave unknown keyword {0!r} for issue no. {1}"
|
err = "Gave unknown keyword {!r} for issue no. {}"
|
||||||
raise ValueError(err.format(keyword, issue_no))
|
raise ValueError(err.format(keyword, issue_no))
|
||||||
# Create temporary node w/ data & final nodes to publish
|
# Create temporary node w/ data & final nodes to publish
|
||||||
node = Issue(
|
node = Issue(
|
||||||
|
@ -136,17 +138,17 @@ def release_nodes(text, slug, date, config):
|
||||||
# TODO: % vs .format()
|
# TODO: % vs .format()
|
||||||
uri = config.releases_release_uri % slug
|
uri = config.releases_release_uri % slug
|
||||||
elif config.releases_github_path:
|
elif config.releases_github_path:
|
||||||
uri = "https://github.com/{0}/tree/{1}".format(
|
uri = "https://github.com/{}/tree/{}".format(
|
||||||
config.releases_github_path, slug)
|
config.releases_github_path, slug)
|
||||||
# Only construct link tag if user actually configured release URIs somehow
|
# Only construct link tag if user actually configured release URIs somehow
|
||||||
if uri:
|
if uri:
|
||||||
link = '<a class="reference external" href="{0}">{1}</a>'.format(uri, text)
|
link = '<a class="reference external" href="{}">{}</a>'.format(uri, text)
|
||||||
else:
|
else:
|
||||||
link = text
|
link = text
|
||||||
datespan = ''
|
datespan = ''
|
||||||
if date:
|
if date:
|
||||||
datespan = ' <span style="font-size: 75%;">{0}</span>'.format(date)
|
datespan = ' <span style="font-size: 75%;">{}</span>'.format(date)
|
||||||
header = '<h2 style="margin-bottom: 0.3em;">{0}{1}</h2>'.format(
|
header = '<h2 style="margin-bottom: 0.3em;">{}{}</h2>'.format(
|
||||||
link, datespan)
|
link, datespan)
|
||||||
return nodes.section('',
|
return nodes.section('',
|
||||||
nodes.raw(rawtext='', text=header, format='html'),
|
nodes.raw(rawtext='', text=header, format='html'),
|
||||||
|
@ -188,7 +190,7 @@ def generate_unreleased_entry(header, line, issues, manager, app):
|
||||||
None,
|
None,
|
||||||
app.config
|
app.config
|
||||||
)]
|
)]
|
||||||
log("Creating {0!r} faux-release with {1!r}".format(line, issues))
|
log("Creating {!r} faux-release with {!r}".format(line, issues))
|
||||||
return {
|
return {
|
||||||
'obj': Release(number=line, date=None, nodelist=nodelist),
|
'obj': Release(number=line, date=None, nodelist=nodelist),
|
||||||
'entries': issues,
|
'entries': issues,
|
||||||
|
@ -205,13 +207,13 @@ def append_unreleased_entries(app, manager, releases):
|
||||||
"""
|
"""
|
||||||
for family, lines in six.iteritems(manager):
|
for family, lines in six.iteritems(manager):
|
||||||
for type_ in ('bugfix', 'feature'):
|
for type_ in ('bugfix', 'feature'):
|
||||||
bucket = 'unreleased_{0}'.format(type_)
|
bucket = 'unreleased_{}'.format(type_)
|
||||||
if bucket not in lines: # Implies unstable prehistory + 0.x fam
|
if bucket not in lines: # Implies unstable prehistory + 0.x fam
|
||||||
continue
|
continue
|
||||||
issues = lines[bucket]
|
issues = lines[bucket]
|
||||||
fam_prefix = "{0}.x ".format(family) if len(manager) > 1 else ""
|
fam_prefix = "{}.x ".format(family) if len(manager) > 1 else ""
|
||||||
header = "Next {0}{1} release".format(fam_prefix, type_)
|
header = "Next {}{} release".format(fam_prefix, type_)
|
||||||
line = "unreleased_{0}.x_{1}".format(family, type_)
|
line = "unreleased_{}.x_{}".format(family, type_)
|
||||||
releases.append(
|
releases.append(
|
||||||
generate_unreleased_entry(header, line, issues, manager, app)
|
generate_unreleased_entry(header, line, issues, manager, app)
|
||||||
)
|
)
|
||||||
|
@ -246,7 +248,7 @@ def construct_entry_with_release(focus, issues, manager, log, releases, rest):
|
||||||
missing = [i for i in explicit if i not in issues]
|
missing = [i for i in explicit if i not in issues]
|
||||||
if missing:
|
if missing:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Couldn't find issue(s) #{0} in the changelog!".format(
|
"Couldn't find issue(s) #{} in the changelog!".format(
|
||||||
', '.join(missing)))
|
', '.join(missing)))
|
||||||
# Obtain the explicitly named issues from global list
|
# Obtain the explicitly named issues from global list
|
||||||
entries = []
|
entries = []
|
||||||
|
@ -357,9 +359,9 @@ def construct_entry_without_release(focus, issues, manager, log, rest):
|
||||||
buried = focus.traverse(Issue)
|
buried = focus.traverse(Issue)
|
||||||
if buried:
|
if buried:
|
||||||
msg = """
|
msg = """
|
||||||
Found issue node ({0!r}) buried inside another node:
|
Found issue node ({!r}) buried inside another node:
|
||||||
|
|
||||||
{1}
|
{}
|
||||||
|
|
||||||
Please double-check your ReST syntax! There is probably text in the above
|
Please double-check your ReST syntax! There is probably text in the above
|
||||||
output that will show you which part of your changelog to look at.
|
output that will show you which part of your changelog to look at.
|
||||||
|
@ -529,7 +531,7 @@ def construct_releases(entries, app):
|
||||||
|
|
||||||
reorder_release_entries(releases)
|
reorder_release_entries(releases)
|
||||||
|
|
||||||
return releases
|
return releases, manager
|
||||||
|
|
||||||
|
|
||||||
def construct_nodes(releases):
|
def construct_nodes(releases):
|
||||||
|
@ -588,7 +590,7 @@ class BulletListVisitor(nodes.NodeVisitor):
|
||||||
if not self.found_changelog:
|
if not self.found_changelog:
|
||||||
self.found_changelog = True
|
self.found_changelog = True
|
||||||
# Walk + parse into release mapping
|
# Walk + parse into release mapping
|
||||||
releases = construct_releases(node.children, self.app)
|
releases, _ = construct_releases(node.children, self.app)
|
||||||
# Construct new set of nodes to replace the old, and we're done
|
# Construct new set of nodes to replace the old, and we're done
|
||||||
node.replace_self(construct_nodes(releases))
|
node.replace_self(construct_nodes(releases))
|
||||||
|
|
||||||
|
@ -627,7 +629,7 @@ def setup(app):
|
||||||
('unstable_prehistory', False),
|
('unstable_prehistory', False),
|
||||||
):
|
):
|
||||||
app.add_config_value(
|
app.add_config_value(
|
||||||
name='releases_{0}'.format(key), default=default, rebuild='html'
|
name='releases_{}'.format(key), default=default, rebuild='html'
|
||||||
)
|
)
|
||||||
# if a string is given for `document_name`, convert it to a list
|
# if a string is given for `document_name`, convert it to a list
|
||||||
# done to maintain backwards compatibility
|
# done to maintain backwards compatibility
|
||||||
|
@ -643,7 +645,19 @@ def setup(app):
|
||||||
|
|
||||||
# Register intermediate roles
|
# Register intermediate roles
|
||||||
for x in list(ISSUE_TYPES) + ['issue']:
|
for x in list(ISSUE_TYPES) + ['issue']:
|
||||||
app.add_role(x, issues_role)
|
add_role(app, x, issues_role)
|
||||||
app.add_role('release', release_role)
|
add_role(app, 'release', release_role)
|
||||||
# Hook in our changelog transmutation at appropriate step
|
# Hook in our changelog transmutation at appropriate step
|
||||||
app.connect('doctree-read', generate_changelog)
|
app.connect('doctree-read', generate_changelog)
|
||||||
|
|
||||||
|
# identifies the version of our extension
|
||||||
|
return {'version': __version__}
|
||||||
|
|
||||||
|
|
||||||
|
def add_role(app, name, role_obj):
|
||||||
|
# This (introspecting docutils.parser.rst.roles._roles) is the same trick
|
||||||
|
# Sphinx uses to emit warnings about double-registering; it's a PITA to try
|
||||||
|
# and configure the app early on so it doesn't emit those warnings, so we
|
||||||
|
# instead just...don't double-register. Meh.
|
||||||
|
if name not in roles._roles:
|
||||||
|
app.add_role(name, role_obj)
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
__version_info__ = (1, 2, 1)
|
__version_info__ = (1, 4, 2)
|
||||||
__version__ = '.'.join(map(str, __version_info__))
|
__version__ = '.'.join(map(str, __version_info__))
|
||||||
|
|
|
@ -34,10 +34,7 @@ class LineManager(dict):
|
||||||
if major_number == 0 and self.config.releases_unstable_prehistory:
|
if major_number == 0 and self.config.releases_unstable_prehistory:
|
||||||
keys = ['unreleased']
|
keys = ['unreleased']
|
||||||
# Either way, the buckets default to an empty list
|
# Either way, the buckets default to an empty list
|
||||||
empty = {}
|
self[major_number] = {key: [] for key in keys}
|
||||||
for key in keys:
|
|
||||||
empty[key] = []
|
|
||||||
self[major_number] = empty
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unstable_prehistory(self):
|
def unstable_prehistory(self):
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Issue(nodes.Element):
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
for attr in self._cmp_keys:
|
for attr in self._cmp_keys:
|
||||||
if getattr(self, attr) != getattr(other, attr):
|
if getattr(self, attr, None) != getattr(other, attr, None):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -110,18 +110,18 @@ class Issue(nodes.Element):
|
||||||
# Make sure truly-default spec skips 0.x if prehistory was unstable.
|
# Make sure truly-default spec skips 0.x if prehistory was unstable.
|
||||||
stable_families = manager.stable_families
|
stable_families = manager.stable_families
|
||||||
if manager.config.releases_unstable_prehistory and stable_families:
|
if manager.config.releases_unstable_prehistory and stable_families:
|
||||||
specstr = ">={0}".format(min(stable_families))
|
specstr = ">={}".format(min(stable_families))
|
||||||
if self.is_featurelike:
|
if self.is_featurelike:
|
||||||
# TODO: if app->config-><releases_always_forwardport_features or
|
# TODO: if app->config-><releases_always_forwardport_features or
|
||||||
# w/e
|
# w/e
|
||||||
if True:
|
if True:
|
||||||
specstr = ">={0}".format(max(manager.keys()))
|
specstr = ">={}".format(max(manager.keys()))
|
||||||
else:
|
else:
|
||||||
# Can only meaningfully limit to minor release buckets if they
|
# Can only meaningfully limit to minor release buckets if they
|
||||||
# actually exist yet.
|
# actually exist yet.
|
||||||
buckets = self.minor_releases(manager)
|
buckets = self.minor_releases(manager)
|
||||||
if buckets:
|
if buckets:
|
||||||
specstr = ">={0}".format(max(buckets))
|
specstr = ">={}".format(max(buckets))
|
||||||
return Spec(specstr) if specstr else Spec()
|
return Spec(specstr) if specstr else Spec()
|
||||||
|
|
||||||
def add_to_manager(self, manager):
|
def add_to_manager(self, manager):
|
||||||
|
@ -178,7 +178,7 @@ class Issue(nodes.Element):
|
||||||
elif self.spec:
|
elif self.spec:
|
||||||
flag = self.spec
|
flag = self.spec
|
||||||
if flag:
|
if flag:
|
||||||
flag = ' ({0})'.format(flag)
|
flag = ' ({})'.format(flag)
|
||||||
return '<{issue.type} #{issue.number}{flag}>'.format(issue=self,
|
return '<{issue.type} #{issue.number}{flag}>'.format(issue=self,
|
||||||
flag=flag)
|
flag=flag)
|
||||||
|
|
||||||
|
@ -200,4 +200,4 @@ class Release(nodes.Element):
|
||||||
return int(self.number.split('.')[0])
|
return int(self.number.split('.')[0])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<release {0}>'.format(self.number)
|
return '<release {}>'.format(self.number)
|
||||||
|
|
263
releases/util.py
Normal file
263
releases/util.py
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
"""
|
||||||
|
Utility functions, such as helpers for standalone changelog parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
|
import sphinx
|
||||||
|
from docutils.core import Publisher
|
||||||
|
from docutils.io import NullOutput
|
||||||
|
from docutils.nodes import bullet_list
|
||||||
|
from sphinx.application import Sphinx # not exposed at top level
|
||||||
|
# NOTE: importing these from environment for backwards compat with Sphinx 1.3
|
||||||
|
from sphinx.environment import (
|
||||||
|
SphinxStandaloneReader, SphinxFileInput, SphinxDummyWriter,
|
||||||
|
)
|
||||||
|
# sphinx_domains is only in Sphinx 1.5+, but is presumably necessary from then
|
||||||
|
# onwards.
|
||||||
|
try:
|
||||||
|
from sphinx.util.docutils import sphinx_domains
|
||||||
|
except ImportError:
|
||||||
|
# Just dummy it up.
|
||||||
|
from contextlib import contextmanager
|
||||||
|
@contextmanager
|
||||||
|
def sphinx_domains(env):
|
||||||
|
yield
|
||||||
|
|
||||||
|
from . import construct_releases, setup
|
||||||
|
|
||||||
|
|
||||||
|
def parse_changelog(path):
|
||||||
|
"""
|
||||||
|
Load and parse changelog file from ``path``, returning data structures.
|
||||||
|
|
||||||
|
This function does not alter any files on disk; it is solely for
|
||||||
|
introspecting a Releases ``changelog.rst`` and programmatically answering
|
||||||
|
questions like "are there any unreleased bugfixes for the 2.3 line?" or
|
||||||
|
"what was included in release 1.2.1?".
|
||||||
|
|
||||||
|
For example, answering the above questions is as simple as::
|
||||||
|
|
||||||
|
changelog = parse_changelog("/path/to/changelog")
|
||||||
|
print("Unreleased issues for 2.3.x: {}".format(changelog['2.3']))
|
||||||
|
print("Contents of v1.2.1: {}".format(changelog['1.2.1']))
|
||||||
|
|
||||||
|
:param str path: A relative or absolute file path string.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A dict whose keys map to lists of ``releases.models.Issue`` objects, as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
- Actual releases are full version number keys, such as ``"1.2.1"`` or
|
||||||
|
``"2.0.0"``.
|
||||||
|
- Unreleased bugs (or bug-like issues; see the Releases docs) are
|
||||||
|
stored in minor-release buckets, e.g. ``"1.2"`` or ``"2.0"``.
|
||||||
|
- Unreleased features (or feature-like issues) are found in
|
||||||
|
``"unreleased_N_feature"``, where ``N`` is one of the major release
|
||||||
|
families (so, a changelog spanning only 1.x will only have
|
||||||
|
``unreleased_1_feature``, whereas one with 1.x and 2.x releases will
|
||||||
|
have ``unreleased_1_feature`` and ``unreleased_2_feature``, etc).
|
||||||
|
"""
|
||||||
|
app, doctree = get_doctree(path)
|
||||||
|
# Have to semi-reproduce the 'find first bullet list' bit from main code,
|
||||||
|
# which is unfortunately side-effect-heavy (thanks to Sphinx plugin
|
||||||
|
# design).
|
||||||
|
first_list = None
|
||||||
|
for node in doctree[0]:
|
||||||
|
if isinstance(node, bullet_list):
|
||||||
|
first_list = node
|
||||||
|
break
|
||||||
|
# Initial parse into the structures Releases finds useful internally
|
||||||
|
releases, manager = construct_releases(first_list.children, app)
|
||||||
|
ret = changelog2dict(releases)
|
||||||
|
# Stitch them together into something an end-user would find better:
|
||||||
|
# - nuke unreleased_N.N_Y as their contents will be represented in the
|
||||||
|
# per-line buckets
|
||||||
|
for key in ret.copy():
|
||||||
|
if key.startswith('unreleased'):
|
||||||
|
del ret[key]
|
||||||
|
for family in manager:
|
||||||
|
# - remove unreleased_bugfix, as they are accounted for in the per-line
|
||||||
|
# buckets too. No need to store anywhere.
|
||||||
|
manager[family].pop('unreleased_bugfix', None)
|
||||||
|
# - bring over each major family's unreleased_feature as
|
||||||
|
# unreleased_N_feature
|
||||||
|
unreleased = manager[family].pop('unreleased_feature', None)
|
||||||
|
if unreleased is not None:
|
||||||
|
ret['unreleased_{}_feature'.format(family)] = unreleased
|
||||||
|
# - bring over all per-line buckets from manager (flattening)
|
||||||
|
# Here, all that's left in the per-family bucket should be lines, not
|
||||||
|
# unreleased_*
|
||||||
|
ret.update(manager[family])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_doctree(path):
|
||||||
|
"""
|
||||||
|
Obtain a Sphinx doctree from the RST file at ``path``.
|
||||||
|
|
||||||
|
Performs no Releases-specific processing; this code would, ideally, be in
|
||||||
|
Sphinx itself, but things there are pretty tightly coupled. So we wrote
|
||||||
|
this.
|
||||||
|
|
||||||
|
:param str path: A relative or absolute file path string.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A two-tuple of the generated ``sphinx.application.Sphinx`` app and the
|
||||||
|
doctree (a ``docutils.document`` object).
|
||||||
|
"""
|
||||||
|
root, filename = os.path.split(path)
|
||||||
|
docname, _ = os.path.splitext(filename)
|
||||||
|
# TODO: this only works for top level changelog files (i.e. ones where
|
||||||
|
# their dirname is the project/doc root)
|
||||||
|
app = make_app(srcdir=root)
|
||||||
|
# Create & init a BuildEnvironment. Mm, tasty side effects.
|
||||||
|
app._init_env(freshenv=True)
|
||||||
|
env = app.env
|
||||||
|
# More arity/API changes: Sphinx 1.3/1.4-ish require one to pass in the app
|
||||||
|
# obj in BuildEnvironment.update(); modern Sphinx performs that inside
|
||||||
|
# Application._init_env() (which we just called above) and so that kwarg is
|
||||||
|
# removed from update(). EAFP.
|
||||||
|
kwargs = dict(
|
||||||
|
config=app.config,
|
||||||
|
srcdir=root,
|
||||||
|
doctreedir=app.doctreedir,
|
||||||
|
app=app,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
env.update(**kwargs)
|
||||||
|
except TypeError:
|
||||||
|
# Assume newer Sphinx w/o an app= kwarg
|
||||||
|
del kwargs['app']
|
||||||
|
env.update(**kwargs)
|
||||||
|
# Code taken from sphinx.environment.read_doc; easier to manually call
|
||||||
|
# it with a working Environment object, instead of doing more random crap
|
||||||
|
# to trick the higher up build system into thinking our single changelog
|
||||||
|
# document was "updated".
|
||||||
|
env.temp_data['docname'] = docname
|
||||||
|
env.app = app
|
||||||
|
# NOTE: SphinxStandaloneReader API changed in 1.4 :(
|
||||||
|
reader_kwargs = {
|
||||||
|
'app': app,
|
||||||
|
'parsers': env.config.source_parsers,
|
||||||
|
#'parsers': app.registry.get_source_parsers()
|
||||||
|
}
|
||||||
|
if sphinx.version_info[:2] < (1, 4):
|
||||||
|
del reader_kwargs['app']
|
||||||
|
# This monkeypatches (!!!) docutils to 'inject' all registered Sphinx
|
||||||
|
# domains' roles & so forth. Without this, rendering the doctree lacks
|
||||||
|
# almost all Sphinx magic, including things like :ref: and :doc:!
|
||||||
|
with sphinx_domains(env):
|
||||||
|
reader = SphinxStandaloneReader(**reader_kwargs)
|
||||||
|
pub = Publisher(reader=reader,
|
||||||
|
writer=SphinxDummyWriter(),
|
||||||
|
destination_class=NullOutput)
|
||||||
|
pub.set_components(None, 'restructuredtext', None)
|
||||||
|
pub.process_programmatic_settings(None, env.settings, None)
|
||||||
|
# NOTE: docname derived higher up, from our given path
|
||||||
|
src_path = env.doc2path(docname)
|
||||||
|
source = SphinxFileInput(
|
||||||
|
app,
|
||||||
|
env,
|
||||||
|
source=None,
|
||||||
|
source_path=src_path,
|
||||||
|
encoding=env.config.source_encoding,
|
||||||
|
)
|
||||||
|
pub.source = source
|
||||||
|
pub.settings._source = src_path
|
||||||
|
pub.set_destination(None, None)
|
||||||
|
pub.publish()
|
||||||
|
return app, pub.document
|
||||||
|
|
||||||
|
|
||||||
|
def make_app(**kwargs):
|
||||||
|
"""
|
||||||
|
Create a dummy Sphinx app, filling in various hardcoded assumptions.
|
||||||
|
|
||||||
|
For example, Sphinx assumes the existence of various source/dest
|
||||||
|
directories, even if you're only calling internals that never generate (or
|
||||||
|
sometimes, even read!) on-disk files. This function creates safe temp
|
||||||
|
directories for these instances.
|
||||||
|
|
||||||
|
It also neuters Sphinx's internal logging, which otherwise causes verbosity
|
||||||
|
in one's own test output and/or debug logs.
|
||||||
|
|
||||||
|
All args are stored in a single ``**kwargs``. Aside from the params listed
|
||||||
|
below (all of which are optional), all kwargs given are turned into
|
||||||
|
'releases_xxx' config settings; e.g. ``make_app(foo='bar')`` is like
|
||||||
|
setting ``releases_foo = 'bar'`` in ``conf.py``.
|
||||||
|
|
||||||
|
:param str docname:
|
||||||
|
Override the document name used (mostly for internal testing).
|
||||||
|
|
||||||
|
:param str srcdir:
|
||||||
|
Sphinx source directory path.
|
||||||
|
|
||||||
|
:param str dstdir:
|
||||||
|
Sphinx dest directory path.
|
||||||
|
|
||||||
|
:param str doctreedir:
|
||||||
|
Sphinx doctree directory path.
|
||||||
|
|
||||||
|
:returns: A Sphinx ``Application`` instance.
|
||||||
|
"""
|
||||||
|
srcdir = kwargs.pop('srcdir', mkdtemp())
|
||||||
|
dstdir = kwargs.pop('dstdir', mkdtemp())
|
||||||
|
doctreedir = kwargs.pop('doctreedir', mkdtemp())
|
||||||
|
try:
|
||||||
|
# Sphinx <1.6ish
|
||||||
|
Sphinx._log = lambda self, message, wfile, nonl=False: None
|
||||||
|
# Sphinx >=1.6ish. Technically still lets Very Bad Things through,
|
||||||
|
# unlike the total muting above, but probably OK.
|
||||||
|
logging.getLogger('sphinx').setLevel(logging.ERROR)
|
||||||
|
# App API seems to work on all versions so far.
|
||||||
|
app = Sphinx(
|
||||||
|
srcdir=srcdir,
|
||||||
|
confdir=None,
|
||||||
|
outdir=dstdir,
|
||||||
|
doctreedir=doctreedir,
|
||||||
|
buildername='html',
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
for d in (srcdir, dstdir, doctreedir):
|
||||||
|
# Only remove empty dirs; non-empty dirs are implicitly something
|
||||||
|
# that existed before we ran, and should not be touched.
|
||||||
|
try:
|
||||||
|
os.rmdir(d)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
setup(app)
|
||||||
|
# Mock out the config within. More assumptions by Sphinx :(
|
||||||
|
config = {
|
||||||
|
'releases_release_uri': 'foo_%s',
|
||||||
|
'releases_issue_uri': 'bar_%s',
|
||||||
|
'releases_debug': False,
|
||||||
|
'master_doc': 'index',
|
||||||
|
}
|
||||||
|
# Allow tinkering with document filename
|
||||||
|
if 'docname' in kwargs:
|
||||||
|
app.env.temp_data['docname'] = kwargs.pop('docname')
|
||||||
|
# Allow config overrides via kwargs
|
||||||
|
for name in kwargs:
|
||||||
|
config['releases_{}'.format(name)] = kwargs[name]
|
||||||
|
# Stitch together as the sphinx app init() usually does w/ real conf files
|
||||||
|
app.config._raw_config = config
|
||||||
|
# init_values() requires a 'warn' runner on Sphinx 1.3-1.6, so if we seem
|
||||||
|
# to be hitting arity errors, give it a dummy such callable. Hopefully
|
||||||
|
# calling twice doesn't introduce any wacko state issues :(
|
||||||
|
try:
|
||||||
|
app.config.init_values()
|
||||||
|
except TypeError: # boy I wish Python had an ArityError or w/e
|
||||||
|
app.config.init_values(lambda x: x)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def changelog2dict(changelog):
|
||||||
|
"""
|
||||||
|
Helper turning internal list-o-releases structure into a dict.
|
||||||
|
|
||||||
|
See `parse_changelog` docstring for return value details.
|
||||||
|
"""
|
||||||
|
return {r['obj'].number: r['entries'] for r in changelog}
|
10
setup.py
10
setup.py
|
@ -12,11 +12,15 @@ setup(
|
||||||
name='releases',
|
name='releases',
|
||||||
version=version,
|
version=version,
|
||||||
description='A Sphinx extension for changelog manipulation',
|
description='A Sphinx extension for changelog manipulation',
|
||||||
|
long_description=open("README.rst").read(),
|
||||||
author='Jeff Forcier',
|
author='Jeff Forcier',
|
||||||
author_email='jeff@bitprophet.org',
|
author_email='jeff@bitprophet.org',
|
||||||
url='https://github.com/bitprophet/releases',
|
url='https://github.com/bitprophet/releases',
|
||||||
packages=['releases'],
|
packages=['releases'],
|
||||||
install_requires=['semantic_version<3.0'],
|
install_requires=[
|
||||||
|
'semantic_version<3.0',
|
||||||
|
'sphinx>=1.3,<1.7',
|
||||||
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
@ -26,12 +30,10 @@ setup(
|
||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
'Operating System :: Microsoft :: Windows',
|
'Operating System :: Microsoft :: Windows',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2.6',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3.2',
|
|
||||||
'Programming Language :: Python :: 3.3',
|
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
'Topic :: Software Development',
|
'Topic :: Software Development',
|
||||||
'Topic :: Software Development :: Documentation',
|
'Topic :: Software Development :: Documentation',
|
||||||
'Topic :: Documentation',
|
'Topic :: Documentation',
|
||||||
|
|
6
tasks.py
6
tasks.py
|
@ -1,3 +1,5 @@
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
from invocations import docs
|
from invocations import docs
|
||||||
from invocations.testing import test, integration, watch_tests
|
from invocations.testing import test, integration, watch_tests
|
||||||
from invocations.packaging import release
|
from invocations.packaging import release
|
||||||
|
@ -13,5 +15,9 @@ ns.configure({
|
||||||
'packaging': {
|
'packaging': {
|
||||||
'sign': True,
|
'sign': True,
|
||||||
'wheel': True,
|
'wheel': True,
|
||||||
|
'changelog_file': join(
|
||||||
|
docs.ns.configuration()['sphinx']['source'],
|
||||||
|
'changelog.rst',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
from tempfile import mkdtemp
|
|
||||||
from shutil import rmtree
|
|
||||||
|
|
||||||
from docutils.nodes import (
|
from docutils.nodes import (
|
||||||
list_item, paragraph,
|
list_item, paragraph,
|
||||||
)
|
)
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
from spec import eq_, ok_
|
from spec import eq_, ok_
|
||||||
from sphinx.application import Sphinx
|
|
||||||
import six
|
import six
|
||||||
import sphinx
|
|
||||||
|
|
||||||
from releases import (
|
from releases import (
|
||||||
Issue,
|
Issue,
|
||||||
|
@ -17,57 +12,9 @@ from releases import (
|
||||||
release_role,
|
release_role,
|
||||||
construct_releases,
|
construct_releases,
|
||||||
)
|
)
|
||||||
from releases import setup as releases_setup # avoid unittest crap
|
from releases.util import make_app, changelog2dict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def make_app(**kwargs):
|
|
||||||
"""
|
|
||||||
Create a real Sphinx app, with stupid temp dirs because it assumes.
|
|
||||||
|
|
||||||
Helps catch things like "testing a config option but forgot
|
|
||||||
app.add_config_value()"
|
|
||||||
|
|
||||||
Kwargs (w/ exception of 'docname' which is used for document name if given)
|
|
||||||
are turned into 'releases_xxx' config settings, so e.g.
|
|
||||||
``make_app(foo='bar')`` is like setting ``releases_foo = 'bar'`` in
|
|
||||||
``conf.py``.
|
|
||||||
"""
|
|
||||||
src, dst, doctree = mkdtemp(), mkdtemp(), mkdtemp()
|
|
||||||
try:
|
|
||||||
# STFU Sphinx :(
|
|
||||||
Sphinx._log = lambda self, message, wfile, nonl=False: None
|
|
||||||
app = Sphinx(
|
|
||||||
srcdir=src,
|
|
||||||
confdir=None,
|
|
||||||
outdir=dst,
|
|
||||||
doctreedir=doctree,
|
|
||||||
buildername='html',
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
[rmtree(x) for x in (src, doctree)]
|
|
||||||
releases_setup(app)
|
|
||||||
# Mock out the config within. More horrible assumptions by Sphinx :(
|
|
||||||
config = {
|
|
||||||
'releases_release_uri': 'foo_%s',
|
|
||||||
'releases_issue_uri': 'bar_%s',
|
|
||||||
'releases_debug': False,
|
|
||||||
}
|
|
||||||
# Allow tinkering with document filename
|
|
||||||
if 'docname' in kwargs:
|
|
||||||
app.env.temp_data['docname'] = kwargs.pop('docname')
|
|
||||||
# Allow config overrides via kwargs
|
|
||||||
for name in kwargs:
|
|
||||||
config['releases_{0}'.format(name)] = kwargs[name]
|
|
||||||
# Stitch together as the sphinx app init() usually does w/ real conf files
|
|
||||||
app.config._raw_config = config
|
|
||||||
# init_values() requires a 'warn' runner on Sphinx 1.3+, give it no-op.
|
|
||||||
init_args = []
|
|
||||||
if sphinx.version_info[:2] > (1, 2):
|
|
||||||
init_args = [lambda x: x]
|
|
||||||
app.config.init_values(*init_args)
|
|
||||||
return app
|
|
||||||
|
|
||||||
def inliner(app=None):
|
def inliner(app=None):
|
||||||
app = app or make_app()
|
app = app or make_app()
|
||||||
return Mock(document=Mock(settings=Mock(env=Mock(app=app))))
|
return Mock(document=Mock(settings=Mock(env=Mock(app=app))))
|
||||||
|
@ -138,15 +85,9 @@ def release_list(*entries, **kwargs):
|
||||||
entries.append(release('1.0.0'))
|
entries.append(release('1.0.0'))
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def changelog2dict(changelog):
|
|
||||||
d = {}
|
|
||||||
for r in changelog:
|
|
||||||
d[r['obj'].number] = r['entries']
|
|
||||||
return d
|
|
||||||
|
|
||||||
def releases(*entries, **kwargs):
|
def releases(*entries, **kwargs):
|
||||||
app = kwargs.pop('app', None) or make_app()
|
app = kwargs.pop('app', None) or make_app()
|
||||||
return construct_releases(release_list(*entries, **kwargs), app)
|
return construct_releases(release_list(*entries, **kwargs), app)[0]
|
||||||
|
|
||||||
def setup_issues(self):
|
def setup_issues(self):
|
||||||
self.f = f(12)
|
self.f = f(12)
|
||||||
|
@ -163,8 +104,8 @@ def expect_releases(entries, release_map, skip_initial=False, app=None):
|
||||||
kwargs['app'] = app
|
kwargs['app'] = app
|
||||||
changelog = changelog2dict(releases(*entries, **kwargs))
|
changelog = changelog2dict(releases(*entries, **kwargs))
|
||||||
snapshot = dict(changelog)
|
snapshot = dict(changelog)
|
||||||
err = "Got unexpected contents for {0}: wanted {1}, got {2}"
|
err = "Got unexpected contents for {}: wanted {}, got {}"
|
||||||
err += "\nFull changelog: {3!r}\n"
|
err += "\nFull changelog: {!r}\n"
|
||||||
for rel, issues in six.iteritems(release_map):
|
for rel, issues in six.iteritems(release_map):
|
||||||
found = changelog.pop(rel)
|
found = changelog.pop(rel)
|
||||||
eq_(set(found), set(issues), err.format(rel, issues, found, snapshot))
|
eq_(set(found), set(issues), err.format(rel, issues, found, snapshot))
|
||||||
|
@ -172,4 +113,4 @@ def expect_releases(entries, release_map, skip_initial=False, app=None):
|
||||||
for key in list(changelog.keys()):
|
for key in list(changelog.keys()):
|
||||||
if not changelog[key]:
|
if not changelog[key]:
|
||||||
del changelog[key]
|
del changelog[key]
|
||||||
ok_(not changelog, "Found leftovers: {0}".format(changelog))
|
ok_(not changelog, "Found leftovers: {}".format(changelog))
|
||||||
|
|
|
@ -159,7 +159,7 @@ class organization(Spec):
|
||||||
)
|
)
|
||||||
# Modify 1.0.1 release to be speshul
|
# Modify 1.0.1 release to be speshul
|
||||||
changelog[0][0].append(Text("2, 3"))
|
changelog[0][0].append(Text("2, 3"))
|
||||||
rendered = construct_releases(changelog, make_app())
|
rendered, _ = construct_releases(changelog, make_app())
|
||||||
# 1.0.1 includes just 2 and 3, not bug 1
|
# 1.0.1 includes just 2 and 3, not bug 1
|
||||||
one_0_1 = rendered[3]['entries']
|
one_0_1 = rendered[3]['entries']
|
||||||
one_1_1 = rendered[2]['entries']
|
one_1_1 = rendered[2]['entries']
|
||||||
|
@ -184,7 +184,7 @@ class organization(Spec):
|
||||||
changelog = release_list('1.1.0', f1, f2)
|
changelog = release_list('1.1.0', f1, f2)
|
||||||
# Ensure that 1.1.0 specifies feature 2
|
# Ensure that 1.1.0 specifies feature 2
|
||||||
changelog[0][0].append(Text("2"))
|
changelog[0][0].append(Text("2"))
|
||||||
rendered = changelog2dict(construct_releases(changelog, make_app()))
|
rendered = changelog2dict(construct_releases(changelog, make_app())[0])
|
||||||
# 1.1.0 should have feature 2 only
|
# 1.1.0 should have feature 2 only
|
||||||
assert f2 in rendered['1.1.0']
|
assert f2 in rendered['1.1.0']
|
||||||
assert f1 not in rendered['1.1.0']
|
assert f1 not in rendered['1.1.0']
|
||||||
|
@ -199,7 +199,7 @@ class organization(Spec):
|
||||||
changelog = release_list('1.0.1', b1, b2)
|
changelog = release_list('1.0.1', b1, b2)
|
||||||
# Ensure that 1.0.1 specifies bug 2
|
# Ensure that 1.0.1 specifies bug 2
|
||||||
changelog[0][0].append(Text('2'))
|
changelog[0][0].append(Text('2'))
|
||||||
rendered = construct_releases(changelog, make_app())
|
rendered, _ = construct_releases(changelog, make_app())
|
||||||
# 1.0.1 should have bug 2 only
|
# 1.0.1 should have bug 2 only
|
||||||
assert b2 in rendered[1]['entries']
|
assert b2 in rendered[1]['entries']
|
||||||
assert b1 not in rendered[1]['entries']
|
assert b1 not in rendered[1]['entries']
|
||||||
|
|
|
@ -49,7 +49,7 @@ class presentation(Spec):
|
||||||
release('1.0.2', app=app),
|
release('1.0.2', app=app),
|
||||||
entry(b(15, app=app)),
|
entry(b(15, app=app)),
|
||||||
release('1.0.0'),
|
release('1.0.0'),
|
||||||
], app=app))
|
], app=app)[0])
|
||||||
# Shorthand for "I'll do my own asserts"
|
# Shorthand for "I'll do my own asserts"
|
||||||
if expected is None:
|
if expected is None:
|
||||||
return nodes
|
return nodes
|
||||||
|
|
Loading…
Add table
Reference in a new issue