changelog-0.3.4/0000755000076500000240000000000012264304433014212 5ustar classicstaff00000000000000changelog-0.3.4/changelog/0000755000076500000240000000000012264304433016141 5ustar classicstaff00000000000000changelog-0.3.4/changelog/__init__.py0000644000076500000240000000006312264304422020247 0ustar classicstaff00000000000000__version__ = '0.3.4' from .changelog import setupchangelog-0.3.4/changelog/changelog.css0000644000076500000240000000014112264303536020601 0ustar classicstaff00000000000000a.changeset-link { visibility: hidden; } li:hover a.changeset-link { visibility: visible; } changelog-0.3.4/changelog/changelog.py0000644000076500000240000003254512264303363020454 0ustar classicstaff00000000000000#! 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/0000755000076500000240000000000012264304433017633 5ustar classicstaff00000000000000changelog-0.3.4/changelog.egg-info/dependency_links.txt0000644000076500000240000000000112264304433023701 0ustar classicstaff00000000000000 changelog-0.3.4/changelog.egg-info/not-zip-safe0000644000076500000240000000000112043773146022067 0ustar classicstaff00000000000000 changelog-0.3.4/changelog.egg-info/PKG-INFO0000644000076500000240000000751012264304433020733 0ustar classicstaff00000000000000Metadata-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.txt0000644000076500000240000000047512264304433021525 0ustar classicstaff00000000000000LICENSE 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.pychangelog-0.3.4/changelog.egg-info/top_level.txt0000644000076500000240000000001212264304433022356 0ustar classicstaff00000000000000changelog changelog-0.3.4/LICENSE0000644000076500000240000000216212043773062015223 0ustar classicstaff00000000000000This 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.in0000644000076500000240000000010312264304367015750 0ustar classicstaff00000000000000recursive-include changelog *.py *.css include README* LICENSE changelog-0.3.4/PKG-INFO0000644000076500000240000000751012264304433015312 0ustar classicstaff00000000000000Metadata-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.rst0000644000076500000240000000475212043776216015720 0ustar classicstaff00000000000000========== 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.cfg0000644000076500000240000000007312264304433016033 0ustar classicstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 changelog-0.3.4/setup.py0000644000076500000240000000212312264303730015721 0ustar classicstaff00000000000000from 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/0000755000076500000240000000000012264304433015167 5ustar classicstaff00000000000000changelog-0.3.4/util/changelog_to_rst.py0000644000076500000240000001315212264303640021063 0ustar classicstaff00000000000000"""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.py0000644000076500000240000001736312043773402020077 0ustar classicstaff00000000000000"""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))))