changelog-0.3.4/ 0000755 0000765 0000024 00000000000 12264304433 014212 5 ustar classic staff 0000000 0000000 changelog-0.3.4/changelog/ 0000755 0000765 0000024 00000000000 12264304433 016141 5 ustar classic staff 0000000 0000000 changelog-0.3.4/changelog/__init__.py 0000644 0000765 0000024 00000000063 12264304422 020247 0 ustar classic staff 0000000 0000000 __version__ = '0.3.4'
from .changelog import setup changelog-0.3.4/changelog/changelog.css 0000644 0000765 0000024 00000000141 12264303536 020601 0 ustar classic staff 0000000 0000000 a.changeset-link {
visibility: hidden;
}
li:hover a.changeset-link {
visibility: visible;
}
changelog-0.3.4/changelog/changelog.py 0000644 0000765 0000024 00000032545 12264303363 020454 0 ustar classic staff 0000000 0000000 #! coding: utf-8
import re
from sphinx.util.compat import Directive
from docutils.statemachine import StringList
from docutils import nodes
from sphinx.util.console import bold
import os
from sphinx.util.osutil import copyfile
import textwrap
import itertools
import collections
import sys
py2k = sys.version_info < (3, 0)
if py2k:
import md5
else:
import hashlib as md5
def _is_html(app):
return app.builder.name in ('html', 'readthedocs') # 'readthedocs', classy
def _comma_list(text):
return re.split(r"\s*,\s*", text.strip())
def _parse_content(content):
d = {}
d['text'] = []
idx = 0
for line in content:
idx += 1
m = re.match(r' *\:(.+?)\:(?: +(.+))?', line)
if m:
attrname, value = m.group(1, 2)
d[attrname] = value or ''
else:
break
d["text"] = content[idx:]
return d
class EnvDirective(object):
@property
def env(self):
return self.state.document.settings.env
@classmethod
def changes(cls, env):
return env.temp_data['ChangeLogDirective_changes']
class ChangeLogDirective(EnvDirective, Directive):
has_content = True
default_section = 'misc'
def _organize_by_section(self, changes):
compound_sections = [(s, s.split(" ")) for s in
self.sections if " " in s]
bysection = collections.defaultdict(list)
all_sections = set()
for rec in changes:
if self.version not in rec['versions']:
continue
inner_tag = rec['tags'].intersection(self.inner_tag_sort)
if inner_tag:
inner_tag = inner_tag.pop()
else:
inner_tag = ""
for compound, comp_words in compound_sections:
if rec['tags'].issuperset(comp_words):
bysection[(compound, inner_tag)].append(rec)
all_sections.add(compound)
break
else:
intersect = rec['tags'].intersection(self.sections)
if intersect:
for sec in rec['sorted_tags']:
if sec in intersect:
bysection[(sec, inner_tag)].append(rec)
all_sections.add(sec)
break
else:
bysection[(self.default_section, inner_tag)].append(rec)
return bysection, all_sections
def _setup_run(self):
self.sections = self.env.config.changelog_sections
self.inner_tag_sort = self.env.config.changelog_inner_tag_sort + [""]
if 'ChangeLogDirective_changes' not in self.env.temp_data:
self.env.temp_data['ChangeLogDirective_changes'] = []
self._parsed_content = _parse_content(self.content)
self.version = version = self._parsed_content.get('version', '')
self.env.temp_data['ChangeLogDirective_version'] = version
p = nodes.paragraph('', '',)
self.state.nested_parse(self.content[1:], 0, p)
def run(self):
self._setup_run()
if 'ChangeLogDirective_includes' in self.env.temp_data:
return []
changes = self.changes(self.env)
output = []
id_prefix = "change-%s" % (self.version, )
topsection = self._run_top(id_prefix)
output.append(topsection)
bysection, all_sections = self._organize_by_section(changes)
counter = itertools.count()
sections_to_render = [s for s in self.sections if s in all_sections]
if not sections_to_render:
for cat in self.inner_tag_sort:
append_sec = self._append_node()
for rec in bysection[(self.default_section, cat)]:
rec["id"] = "%s-%s" % (id_prefix, next(counter))
self._render_rec(rec, None, cat, append_sec)
if append_sec.children:
topsection.append(append_sec)
else:
for section in sections_to_render + [self.default_section]:
sec = nodes.section('',
nodes.title(section, section),
ids=["%s-%s" % (id_prefix, section.replace(" ", "-"))]
)
append_sec = self._append_node()
sec.append(append_sec)
for cat in self.inner_tag_sort:
for rec in bysection[(section, cat)]:
rec["id"] = "%s-%s" % (id_prefix, next(counter))
self._render_rec(rec, section, cat, append_sec)
if append_sec.children:
topsection.append(sec)
return output
def _append_node(self):
return nodes.bullet_list()
def _run_top(self, id_prefix):
version = self._parsed_content.get('version', '')
topsection = nodes.section('',
nodes.title(version, version),
ids=[id_prefix]
)
if self._parsed_content.get("released"):
topsection.append(nodes.Text("Released: %s" %
self._parsed_content['released']))
else:
topsection.append(nodes.Text("no release date"))
intro_para = nodes.paragraph('', '')
len_ = -1
for len_, text in enumerate(self._parsed_content['text']):
if ".. change::" in text:
break
# if encountered any text elements that didn't start with
# ".. change::", those become the intro
if len_ > 0:
self.state.nested_parse(self._parsed_content['text'][0:len_], 0,
intro_para)
topsection.append(intro_para)
return topsection
def _render_rec(self, rec, section, cat, append_sec):
para = rec['node'].deepcopy()
text = _text_rawsource_from_node(para)
to_hash = "%s %s" % (self.version, text[0:100])
targetid = "change-%s" % (
md5.md5(to_hash.encode('ascii', 'ignore')
).hexdigest())
targetnode = nodes.target('', '', ids=[targetid])
para.insert(0, targetnode)
permalink = nodes.reference('', '',
nodes.Text(u"¶", u"¶"),
refid=targetid,
classes=['changeset-link', 'headerlink'],
)
para.append(permalink)
if len(rec['versions']) > 1:
backported_changes = rec['sorted_versions'][rec['sorted_versions'].index(self.version) + 1:]
if backported_changes:
backported = nodes.paragraph('')
backported.append(nodes.Text("This change is also ", ""))
backported.append(nodes.strong("", "backported"))
backported.append(nodes.Text(" to: %s" % ", ".join(backported_changes), ""))
para.append(backported)
insert_ticket = nodes.paragraph('')
para.append(insert_ticket)
i = 0
for collection, render, prefix in (
(rec['tickets'], self.env.config.changelog_render_ticket, "#%s"),
(rec['pullreq'], self.env.config.changelog_render_pullreq,
"pull request %s"),
(rec['changeset'], self.env.config.changelog_render_changeset, "r%s"),
):
for refname in collection:
if i > 0:
insert_ticket.append(nodes.Text(", ", ", "))
else:
insert_ticket.append(nodes.Text("References: """))
i += 1
if render is not None:
if isinstance(render, dict):
if ":" in refname:
typ, refval = refname.split(":")
else:
typ = "default"
refval = refname
refuri = render[typ] % refval
else:
refuri = render % refname
node = nodes.reference('', '',
nodes.Text(prefix % refname, prefix % refname),
refuri=refuri
)
else:
node = nodes.Text(prefix % refname, prefix % refname)
insert_ticket.append(node)
if rec['tags']:
tag_node = nodes.strong('',
" ".join("[%s]" % t for t
in
[t1 for t1 in [section, cat]
if t1 in rec['tags']] +
list(rec['tags'].difference([section, cat]))
) + " "
)
para.children[0].insert(0, tag_node)
append_sec.append(
nodes.list_item('',
nodes.target('', '', ids=[rec['id']]),
para
)
)
class ChangeLogImportDirective(EnvDirective, Directive):
has_content = True
def _setup_run(self):
if 'ChangeLogDirective_changes' not in self.env.temp_data:
self.env.temp_data['ChangeLogDirective_changes'] = []
def run(self):
self._setup_run()
# tell ChangeLogDirective we're here, also prevent
# nested .. include calls
if 'ChangeLogDirective_includes' not in self.env.temp_data:
self.env.temp_data['ChangeLogDirective_includes'] = True
p = nodes.paragraph('', '',)
self.state.nested_parse(self.content, 0, p)
del self.env.temp_data['ChangeLogDirective_includes']
return []
class ChangeDirective(EnvDirective, Directive):
has_content = True
def run(self):
content = _parse_content(self.content)
p = nodes.paragraph('', '',)
sorted_tags = _comma_list(content.get('tags', ''))
declared_version = self.env.temp_data['ChangeLogDirective_version']
versions = set(_comma_list(content.get("versions", ""))).difference(['']).\
union([declared_version])
# if we don't refer to any other versions and we're in an include,
# skip
if len(versions) == 1 and 'ChangeLogDirective_includes' in self.env.temp_data:
return []
def int_ver(ver):
out = []
for dig in ver.split("."):
try:
out.append(int(dig))
except ValueError:
out.append(0)
return tuple(out)
rec = {
'tags': set(sorted_tags).difference(['']),
'tickets': set(_comma_list(content.get('tickets', ''))).difference(['']),
'pullreq': set(_comma_list(content.get('pullreq', ''))).difference(['']),
'changeset': set(_comma_list(content.get('changeset', ''))).difference(['']),
'node': p,
'type': "change",
"title": content.get("title", None),
'sorted_tags': sorted_tags,
"versions": versions,
"sorted_versions": list(reversed(sorted(versions, key=int_ver)))
}
self.state.nested_parse(content['text'], 0, p)
ChangeLogDirective.changes(self.env).append(rec)
return []
def _text_rawsource_from_node(node):
src = []
stack = [node]
while stack:
n = stack.pop(0)
if isinstance(n, nodes.Text):
src.append(n.rawsource)
stack.extend(n.children)
return "".join(src)
def _rst2sphinx(text):
return StringList(
[line.strip() for line in textwrap.dedent(text).split("\n")]
)
def make_ticket_link(name, rawtext, text, lineno, inliner,
options={}, content=[]):
env = inliner.document.settings.env
render_ticket = env.config.changelog_render_ticket or "%s"
prefix = "#%s"
if render_ticket:
ref = render_ticket % text
node = nodes.reference(rawtext, prefix % text, refuri=ref, **options)
else:
node = nodes.Text(prefix % text, prefix % text)
return [node], []
def add_stylesheet(app):
app.add_stylesheet('changelog.css')
def copy_stylesheet(app, exception):
app.info(bold('The name of the builder is: %s' % app.builder.name), nonl=True)
if not _is_html(app) or exception:
return
app.info(bold('Copying sphinx_paramlinks stylesheet... '), nonl=True)
source = os.path.abspath(os.path.dirname(__file__))
# the '_static' directory name is hardcoded in
# sphinx.builders.html.StandaloneHTMLBuilder.copy_static_files.
# would be nice if Sphinx could improve the API here so that we just
# give it the path to a .css file and it does the right thing.
dest = os.path.join(app.builder.outdir, '_static', 'changelog.css')
copyfile(os.path.join(source, "changelog.css"), dest)
app.info('done')
def setup(app):
app.add_directive('changelog', ChangeLogDirective)
app.add_directive('change', ChangeDirective)
app.add_directive('changelog_imports', ChangeLogImportDirective)
app.add_config_value("changelog_sections", [], 'env')
app.add_config_value("changelog_inner_tag_sort", [], 'env')
app.add_config_value("changelog_render_ticket", None, 'env')
app.add_config_value("changelog_render_pullreq", None, 'env')
app.add_config_value("changelog_render_changeset", None, 'env')
app.connect('builder-inited', add_stylesheet)
app.connect('build-finished', copy_stylesheet)
app.add_role('ticket', make_ticket_link)
changelog-0.3.4/changelog.egg-info/ 0000755 0000765 0000024 00000000000 12264304433 017633 5 ustar classic staff 0000000 0000000 changelog-0.3.4/changelog.egg-info/dependency_links.txt 0000644 0000765 0000024 00000000001 12264304433 023701 0 ustar classic staff 0000000 0000000
changelog-0.3.4/changelog.egg-info/not-zip-safe 0000644 0000765 0000024 00000000001 12043773146 022067 0 ustar classic staff 0000000 0000000
changelog-0.3.4/changelog.egg-info/PKG-INFO 0000644 0000765 0000024 00000007510 12264304433 020733 0 ustar classic staff 0000000 0000000 Metadata-Version: 1.1
Name: changelog
Version: 0.3.4
Summary: Provides simple Sphinx markup to render changelog displays.
Home-page: http://bitbucket.org/zzzeek/changelog
Author: Mike Bayer
Author-email: mike@zzzcomputing.com
License: MIT
Description: ==========
Changelog
==========
A `Sphinx `_ extension to generate changelog files.
This is an experimental, possibly-not-useful extension that's used by the
`SQLAlchemy `_ project and related projects.
Configuration
=============
A sample configuration in ``conf.py`` looks like this::
extensions = [
# changelog extension
'changelog',
# your other sphinx extensions
# ...
]
# section names - optional
changelog_sections = ["general", "rendering", "tests"]
# tags to sort on inside of sections - also optional
changelog_inner_tag_sort = ["feature", "bug"]
# how to render changelog links - these are plain
# python string templates, ticket/pullreq/changeset number goes
# in "%s"
changelog_render_ticket = "http://bitbucket.org/myusername/myproject/issue/%s"
changelog_render_pullreq = "http://bitbucket.org/myusername/myproject/pullrequest/%s"
changelog_render_changeset = "http://bitbucket.org/myusername/myproject/changeset/%s"
Usage
=====
Changelog introduces the ``changelog`` and ``change`` directives::
====================
Changelog for 1.5.6
====================
.. changelog::
:version: 1.5.6
:released: Sun Oct 12 2008
.. change::
:tags: general
:tickets: 27
Improved the frobnozzle.
.. change::
:tags: rendering, tests
:pullreq: 8
:changeset: a9d7cc0b56c2
Rendering tests now correctly render.
With the above markup, the changes above will be rendered into document sections
per changelog, then each change within organized into paragraphs, including
special markup for tags, tickets mentioned, pull requests, changesets. The entries will
be grouped and sorted by tag according to the configuration of the ``changelog_sections``
and ``changelog_inner_tag_sort`` configurations.
A "compound tag" can also be used, if the configuration has a section like this::
changelog_sections = ["orm declarative", "orm"]
Then change entries which contain both the ``orm`` and ``declarative`` tags will be
grouped under a section called ``orm declarative``, followed by the ``orm`` section where
change entries that only have ``orm`` will be placed.
Other Markup
============
The ``:ticket:`` directive will make use of the ``changelog_render_ticket`` markup
to render a ticket link::
:ticket:`456`
Keywords: Sphinx
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Documentation
changelog-0.3.4/changelog.egg-info/SOURCES.txt 0000644 0000765 0000024 00000000475 12264304433 021525 0 ustar classic staff 0000000 0000000 LICENSE
MANIFEST.in
README.rst
setup.py
changelog/__init__.py
changelog/changelog.css
changelog/changelog.py
changelog.egg-info/PKG-INFO
changelog.egg-info/SOURCES.txt
changelog.egg-info/dependency_links.txt
changelog.egg-info/not-zip-safe
changelog.egg-info/top_level.txt
util/changelog_to_rst.py
util/trac_to_rst.py changelog-0.3.4/changelog.egg-info/top_level.txt 0000644 0000765 0000024 00000000012 12264304433 022356 0 ustar classic staff 0000000 0000000 changelog
changelog-0.3.4/LICENSE 0000644 0000765 0000024 00000002162 12043773062 015223 0 ustar classic staff 0000000 0000000 This is the MIT license: http://www.opensource.org/licenses/mit-license.php
Copyright (C) 2012 by Michael Bayer.
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
changelog-0.3.4/MANIFEST.in 0000644 0000765 0000024 00000000103 12264304367 015750 0 ustar classic staff 0000000 0000000 recursive-include changelog *.py *.css
include README* LICENSE
changelog-0.3.4/PKG-INFO 0000644 0000765 0000024 00000007510 12264304433 015312 0 ustar classic staff 0000000 0000000 Metadata-Version: 1.1
Name: changelog
Version: 0.3.4
Summary: Provides simple Sphinx markup to render changelog displays.
Home-page: http://bitbucket.org/zzzeek/changelog
Author: Mike Bayer
Author-email: mike@zzzcomputing.com
License: MIT
Description: ==========
Changelog
==========
A `Sphinx `_ extension to generate changelog files.
This is an experimental, possibly-not-useful extension that's used by the
`SQLAlchemy `_ project and related projects.
Configuration
=============
A sample configuration in ``conf.py`` looks like this::
extensions = [
# changelog extension
'changelog',
# your other sphinx extensions
# ...
]
# section names - optional
changelog_sections = ["general", "rendering", "tests"]
# tags to sort on inside of sections - also optional
changelog_inner_tag_sort = ["feature", "bug"]
# how to render changelog links - these are plain
# python string templates, ticket/pullreq/changeset number goes
# in "%s"
changelog_render_ticket = "http://bitbucket.org/myusername/myproject/issue/%s"
changelog_render_pullreq = "http://bitbucket.org/myusername/myproject/pullrequest/%s"
changelog_render_changeset = "http://bitbucket.org/myusername/myproject/changeset/%s"
Usage
=====
Changelog introduces the ``changelog`` and ``change`` directives::
====================
Changelog for 1.5.6
====================
.. changelog::
:version: 1.5.6
:released: Sun Oct 12 2008
.. change::
:tags: general
:tickets: 27
Improved the frobnozzle.
.. change::
:tags: rendering, tests
:pullreq: 8
:changeset: a9d7cc0b56c2
Rendering tests now correctly render.
With the above markup, the changes above will be rendered into document sections
per changelog, then each change within organized into paragraphs, including
special markup for tags, tickets mentioned, pull requests, changesets. The entries will
be grouped and sorted by tag according to the configuration of the ``changelog_sections``
and ``changelog_inner_tag_sort`` configurations.
A "compound tag" can also be used, if the configuration has a section like this::
changelog_sections = ["orm declarative", "orm"]
Then change entries which contain both the ``orm`` and ``declarative`` tags will be
grouped under a section called ``orm declarative``, followed by the ``orm`` section where
change entries that only have ``orm`` will be placed.
Other Markup
============
The ``:ticket:`` directive will make use of the ``changelog_render_ticket`` markup
to render a ticket link::
:ticket:`456`
Keywords: Sphinx
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Documentation
changelog-0.3.4/README.rst 0000644 0000765 0000024 00000004752 12043776216 015720 0 ustar classic staff 0000000 0000000 ==========
Changelog
==========
A `Sphinx `_ extension to generate changelog files.
This is an experimental, possibly-not-useful extension that's used by the
`SQLAlchemy `_ project and related projects.
Configuration
=============
A sample configuration in ``conf.py`` looks like this::
extensions = [
# changelog extension
'changelog',
# your other sphinx extensions
# ...
]
# section names - optional
changelog_sections = ["general", "rendering", "tests"]
# tags to sort on inside of sections - also optional
changelog_inner_tag_sort = ["feature", "bug"]
# how to render changelog links - these are plain
# python string templates, ticket/pullreq/changeset number goes
# in "%s"
changelog_render_ticket = "http://bitbucket.org/myusername/myproject/issue/%s"
changelog_render_pullreq = "http://bitbucket.org/myusername/myproject/pullrequest/%s"
changelog_render_changeset = "http://bitbucket.org/myusername/myproject/changeset/%s"
Usage
=====
Changelog introduces the ``changelog`` and ``change`` directives::
====================
Changelog for 1.5.6
====================
.. changelog::
:version: 1.5.6
:released: Sun Oct 12 2008
.. change::
:tags: general
:tickets: 27
Improved the frobnozzle.
.. change::
:tags: rendering, tests
:pullreq: 8
:changeset: a9d7cc0b56c2
Rendering tests now correctly render.
With the above markup, the changes above will be rendered into document sections
per changelog, then each change within organized into paragraphs, including
special markup for tags, tickets mentioned, pull requests, changesets. The entries will
be grouped and sorted by tag according to the configuration of the ``changelog_sections``
and ``changelog_inner_tag_sort`` configurations.
A "compound tag" can also be used, if the configuration has a section like this::
changelog_sections = ["orm declarative", "orm"]
Then change entries which contain both the ``orm`` and ``declarative`` tags will be
grouped under a section called ``orm declarative``, followed by the ``orm`` section where
change entries that only have ``orm`` will be placed.
Other Markup
============
The ``:ticket:`` directive will make use of the ``changelog_render_ticket`` markup
to render a ticket link::
:ticket:`456`
changelog-0.3.4/setup.cfg 0000644 0000765 0000024 00000000073 12264304433 016033 0 ustar classic staff 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
changelog-0.3.4/setup.py 0000644 0000765 0000024 00000002123 12264303730 015721 0 ustar classic staff 0000000 0000000 from setuptools import setup
import os
import re
v = open(os.path.join(os.path.dirname(__file__), 'changelog', '__init__.py'))
VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1)
v.close()
readme = os.path.join(os.path.dirname(__file__), 'README.rst')
setup(name='changelog',
version=VERSION,
description="Provides simple Sphinx markup to render changelog displays.",
long_description=open(readme).read(),
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Documentation',
],
keywords='Sphinx',
author='Mike Bayer',
author_email='mike@zzzcomputing.com',
url='http://bitbucket.org/zzzeek/changelog',
license='MIT',
packages=['changelog'],
include_package_data=True,
zip_safe=False,
)
changelog-0.3.4/util/ 0000755 0000765 0000024 00000000000 12264304433 015167 5 ustar classic staff 0000000 0000000 changelog-0.3.4/util/changelog_to_rst.py 0000644 0000765 0000024 00000013152 12264303640 021063 0 ustar classic staff 0000000 0000000 """Parses a SQLAlchemy-style CHANGES file into changelog format.
This is pretty specific to the files changelog was created for.
"""
import sys
import re
import textwrap
# this is a history file generated from "hg log".
# it's parsed for "tag: " in order to get the dates
# for releases. It relates a tag to a release in CHANGELOG
# using the form tag_X_Y_Z -> X.Y.Z.
TAGFILE = "all_my_tags.txt"
def read_dates():
lines = open(TAGFILE).readlines()
tags = {}
for line in lines:
if line.startswith("changeset:"):
tag = None
elif line.startswith("tag:"):
tag = re.match(r'tag:\s+(.+)', line).group(1)
elif line.startswith("date:") and tag is not None:
date, year = re.match(r'date:\s+(\w+ \w+ \d+) \d+:\d+:\d+ (\d+)', line).group(1, 2)
digits = re.findall(r"(?:^|_)(\d+)", tag)
extra = re.match(".*?(beta\d|rc\d|[a-z]\d)$", tag)
if extra:
extra = extra.group(1)
else:
extra = ""
if len(digits) == 2 and extra:
versions = ".".join(digits) + extra, \
".".join(digits + ["0"]) + extra
else:
versions = (".".join(digits) + extra,)
for version in versions:
tags[version] = "%s %s" % (date, year)
return tags
def raw_blocks(fname):
lines = open(fname).readlines()
version_re = re.compile(r'''
(
^\d+\.\d+ (?: \.\d+ )?
(?:beta\d|rc\d|[a-z]\d?)?
)
\s*(\(.*\))?$''', re.X)
bullet_re = re.compile(r'''
(\s{0,5})-\s(.*)
''', re.X)
normal = 0
in_bullet = 1
state = normal
while lines:
line = lines.pop(0)
if state == normal:
m = version_re.match(line)
if m:
yield "version", m.group(1)
continue
m = bullet_re.match(line)
if m:
state = in_bullet
bullet_indent = len(m.group(1))
bullet_lines = [(" " * (bullet_indent + 2)) + m.group(2) + "\n"]
continue
yield "ignored", line
elif state == in_bullet:
another_bullet = bullet_re.match(line)
version_number = version_re.match(line)
if \
line == "\n" or \
(
not another_bullet and not version_number \
and (
(
bullet_indent and line.startswith(" " * bullet_indent)
) or not bullet_indent
)
):
bullet_lines.append(line)
else:
yield "bullet", textwrap.dedent("".join(bullet_lines))
state = normal
if another_bullet or version_number:
lines.insert(0, line)
continue
def tagged(elements):
current_version = None
current_tags = set()
for type_, content in elements:
if type_ == 'version':
current_tags = set()
current_version = content
elif type_ == 'bullet':
if len(content.split(' ')) < 3 and "ticket" not in content:
current_tags = set([re.sub(r'[\s\:]', '', c.lower()) for c in content.split(' ')])
else:
in_content_tags = set()
tickets = set()
def tag(m):
tag_content = m.group(1)
t_r = re.match(r'(?:ticket:|#)(\d+)', tag_content)
if t_r:
tickets.add(t_r.group(1))
else:
in_content_tags.add(tag_content)
content = re.sub(r'(?:^|\s)\[(.+?)\]', tag, content)
content = re.sub(r'\s(#\d+)', tag, content)
yield {
'version': current_version,
'tags': ", ".join(current_tags.union(in_content_tags)),
'tickets': ", ".join(tickets),
'content': content
}
def emit_rst(records):
current_version = None
versions = read_dates()
current_major_version = None
current_output_file = None
for rec in records:
indented_content = re.compile(r'^', re.M).sub(' ', rec['content'].strip())
if indented_content.endswith(","):
indented_content = indented_content[0:-1] + "."
if rec['version'] != current_version:
current_version = rec['version']
released = versions.get(current_version, '')
major_version = current_version[0:3]
if major_version != current_major_version:
if current_output_file:
current_output_file.close()
current_major_version = major_version
cfile = "changelog_%s.rst" % major_version.replace(".", "")
print "writing %s" % cfile
current_output_file = open(cfile, 'w')
current_output_file.write("""
==============
%s Changelog
==============
""" % major_version)
current_output_file.write(
"""
.. changelog::
:version: %s
:released: %s
""" % (current_version, released)
)
current_output_file.write(
"""
.. change::
:tags: %s
:tickets: %s
%s
""" % (
rec['tags'],
rec['tickets'],
indented_content
)
)
if __name__ == '__main__':
fname = sys.argv[1]
emit_rst(tagged(raw_blocks(fname))) changelog-0.3.4/util/trac_to_rst.py 0000644 0000765 0000024 00000017363 12043773402 020077 0 ustar classic staff 0000000 0000000 """Parses a trac wiki page into rst."""
import sys
import re
import textwrap
def structure(fname):
lines = open(fname).readlines()
plain = 0
bullet = 1
state = plain
current_chunk = []
current_indent = ""
while lines:
line = lines.pop(0).rstrip()
line_indent = re.match(r'^(\s*)', line)
line_indent = len(line_indent.group(1))
bullet_m = re.match(r'^(\s*)\* (.*)', line)
if bullet_m:
if current_chunk:
yield {
"bullet": state == bullet,
"indent": len(current_indent) if state == bullet else 0,
"lines": current_chunk
}
current_chunk = []
current_indent = bullet_m.group(1)
line = bullet_m.group(2)
state = bullet
current_chunk.append(line)
elif state == bullet:
if not line:
current_chunk.append(line)
elif (
line
and (
# line indent is less
line_indent < len(current_indent)
or
# or no indent and previous line was blank
(not line_indent and len(current_indent) == 0 and current_chunk and not current_chunk[-1])
)
):
yield {
"bullet": True,
"indent": len(current_indent),
"lines": current_chunk
}
current_chunk = []
state = plain
current_chunk.append(line)
else:
current_chunk.append(line[len(current_indent):])
#elif not line:
# if current_chunk:
# yield {
# "bullet": state == bullet,
# "indent": len(current_indent) if state == bullet else 0,
# "lines": current_chunk
# }
# current_chunk = []
else:
current_chunk.append(line)
yield {
"bullet": state == bullet,
"indent": len(current_indent) if state == bullet else 0,
"lines": current_chunk
}
def bullet_depth(recs):
bullet_indent = 0
rec_indent = 0
for rec in recs:
if rec['bullet']:
if bullet_indent:
if rec['indent'] > rec_indent:
bullet_indent += 1
elif rec['indent'] < rec_indent:
bullet_indent -= 1
else:
bullet_indent = 1
else:
bullet_indent = 0
rec_indent = rec['indent']
rec['bullet_depth'] = bullet_indent
yield rec
def code(recs):
for rec in recs:
code = False
current_chunk = []
asterisk = rec['bullet']
for line in rec["lines"]:
if re.match(r'^\s*{{{', line):
code = True
if current_chunk:
yield {
'bullet_depth': rec['bullet_depth'],
'lines': current_chunk,
'code': False,
'asterisk': asterisk
}
asterisk = False
current_chunk = []
elif re.match(r'^\s*}}}', line):
code = False
if current_chunk:
yield {
'bullet_depth': rec['bullet_depth'],
'lines': current_chunk,
'code': True
}
current_chunk = []
elif code:
if not re.match(r'^\s*#!', line):
current_chunk.append(line)
elif not line:
if current_chunk:
yield {
'bullet_depth': rec['bullet_depth'],
'lines': current_chunk,
'code': False,
'asterisk': asterisk
}
asterisk = False
current_chunk = []
else:
if line == "----" or \
line.startswith("[[PageOutline"):
continue
line = re.sub(r'(\*\*?\w+)(?!\*)\b', lambda m: "\\%s" % m.group(1), line)
line = re.sub(r'\!(\w+)\b', lambda m: m.group(1), line)
line = re.sub(r"`(.+?)`", lambda m: "``%s``" % m.group(1), line)
line = re.sub(r"'''(.+?)'''", lambda m: "**%s**" % m.group(1).replace("``", ""), line)
line = re.sub(r"''(.+?)'", lambda m: "*%s*" % m.group(1).replace("``", ""), line)
line = re.sub(r'\[(http://\S+) (.*)\]',
lambda m: "`%s <%s>`_" % (m.group(2), m.group(1)),
line
)
line = re.sub(r'#(\d+)', lambda m: ":ticket:`%s`" % m.group(1), line)
if line.startswith("=") and line.endswith("="):
if current_chunk:
yield {
'bullet_depth': rec['bullet_depth'],
'lines': current_chunk,
'code': False,
'asterisk': asterisk,
}
asterisk = False
current_chunk = []
header_lines = output_header(line)
yield {
'bullet_depth': 0,
'lines': header_lines,
'code': False,
'asterisk': False,
'header': True
}
else:
if line or current_chunk:
current_chunk.append(line)
if current_chunk:
yield {
'bullet_depth': rec['bullet_depth'],
'lines': current_chunk,
'code': False,
'asterisk': asterisk
}
def render(recs):
for rec in recs:
bullet_depth = rec['bullet_depth']
bullet_indent = " " * bullet_depth
if rec.get('header', False):
print "\n".join(rec['lines'])
print ""
elif rec['code']:
text = "\n".join(
bullet_indent + " " + line
for line in rec['lines']
)
print bullet_indent + "::\n"
print text
print ""
else:
text = textwrap.dedent("\n".join(rec['lines']))
lines = textwrap.wrap(text, 60 - (2 * bullet_depth))
if rec['asterisk']:
line = lines.pop(0)
print (" " * (bullet_depth - 1)) + "* " + line
print "\n".join(
[bullet_indent + line for line in lines]
)
print ""
def remove_double_blanks(lines):
blank = 0
for line in lines:
for subline in line.split("\n"):
if not subline:
blank += 1
else:
blank = 0
if blank < 2:
yield subline
def output_header(line):
line = line.strip()
m = re.match(r'^(=+) (.*?) =+$', line)
depth = len(m.group(1))
if depth == 1:
return [
"=" * len(m.group(2)),
m.group(2),
"=" * len(m.group(2))
]
char = {
2: "=",
3: "-",
4: "^"
}[depth]
return [
m.group(2),
char * len(m.group(2))
]
if __name__ == '__main__':
fname = sys.argv[1]
# for rec in structure(fname):
# print rec
render(code(bullet_depth(structure(fname))))