news-to-specfile-changelog 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #! /usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # bin/news-to-specfile-changelog
  5. # Part of ComixCursors, a desktop cursor theme.
  6. #
  7. # Copyright © 2010–2013 Ben Finney <ben+opendesktop@benfinney.id.au>
  8. #
  9. # This work is free software: you can redistribute it and/or modify it
  10. # under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 3 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This work is distributed in the hope that it will be useful, but
  15. # WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  17. # General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with this work. If not, see <http://www.gnu.org/licenses/>.
  21. """ Render the NEWS file to a specfile changelog field value.
  22. The project NEWS file is a reStructuredText document, with each
  23. section describing a version of the project. The document is
  24. intended to be readable as-is by end users.
  25. This program transforms the document to a compact list of changes,
  26. suitable for the “changelog” field of an RPM specfile.
  27. Requires:
  28. * Docutils <http://docutils.sourceforge.net/>
  29. """
  30. import datetime
  31. import textwrap
  32. from docutils.core import publish_cmdline, default_description
  33. import docutils.nodes
  34. import docutils.writers
  35. class SpecChangelogWriter(docutils.writers.Writer):
  36. """ Docutils writer to produce a changelog field for RPM spec files.
  37. """
  38. supported = ('spec_changelog')
  39. """ Formats this writer supports. """
  40. def __init__(self):
  41. docutils.writers.Writer.__init__(self)
  42. self.translator_class = SpecChangelogTranslator
  43. def translate(self):
  44. visitor = self.translator_class(self.document)
  45. self.document.walkabout(visitor)
  46. self.output = visitor.astext()
  47. class NewsEntry(object):
  48. """ An individual entry from the NEWS document. """
  49. def __init__(
  50. self, released=None, version=None, maintainer=None, body=None):
  51. self.released = released
  52. self.version = version
  53. self.maintainer = maintainer
  54. self.body = body
  55. def as_specfile_changelog_entry(self):
  56. """ Format the news entry as an RPM specfile changelog entry. """
  57. # Reference: <URL:http://fedoraproject.org/wiki/PackagingGuidelines>
  58. released_timestamp_text = self.released.strftime("%a %b %d %Y")
  59. header = " ".join([
  60. "*", released_timestamp_text, self.maintainer, "-", self.version])
  61. text = "\n".join([header, self.body])
  62. return text
  63. def get_name_for_field_body(node):
  64. """ Return the text of the field name of a field body node. """
  65. field_node = node.parent
  66. field_name_node_index = field_node.first_child_matching_class(
  67. docutils.nodes.field_name)
  68. field_name_node = field_node.children[field_name_node_index]
  69. field_name = unicode(field_name_node.children[0])
  70. return field_name
  71. class InvalidFormatError(ValueError):
  72. """ Raised when the document is not a valid NEWS document. """
  73. def news_timestamp_to_datetime(text):
  74. """ Return a datetime value from the news entry timestamp. """
  75. if text == "FUTURE":
  76. timestamp = datetime.datetime.max
  77. else:
  78. timestamp = datetime.datetime.strptime(text, "%Y-%m-%d")
  79. return timestamp
  80. class SpecChangelogTranslator(docutils.nodes.SparseNodeVisitor):
  81. """ Translator from document nodes to a changelog for RPM spec files. """
  82. wrap_width = 78
  83. field_convert_funcs = {
  84. 'released': news_timestamp_to_datetime,
  85. 'maintainer': unicode,
  86. }
  87. def __init__(self, document):
  88. docutils.nodes.NodeVisitor.__init__(self, document)
  89. self.settings = document.settings
  90. self.current_field_name = None
  91. self.body = u""
  92. self.indent_width = 0
  93. self.initial_indent = u""
  94. self.subsequent_indent = u""
  95. self.current_entry = None
  96. def astext(self):
  97. """ Return the translated document as text. """
  98. return self.body
  99. def append_to_current_entry(self, text):
  100. if self.current_entry is not None:
  101. if self.current_entry.body is not None:
  102. self.current_entry.body += text
  103. def visit_Text(self, node):
  104. raw_text = node.astext()
  105. text = textwrap.fill(
  106. raw_text,
  107. width=self.wrap_width,
  108. initial_indent=self.initial_indent,
  109. subsequent_indent=self.subsequent_indent)
  110. self.append_to_current_entry(text)
  111. def depart_Text(self, node):
  112. pass
  113. def visit_comment(self, node):
  114. raise docutils.nodes.SkipNode
  115. def depart_comment(self, node):
  116. pass
  117. def visit_field_body(self, node):
  118. convert_func = self.field_convert_funcs[self.current_field_name]
  119. attr_name = self.current_field_name
  120. attr_value = convert_func(node.astext())
  121. setattr(self.current_entry, attr_name, attr_value)
  122. raise docutils.nodes.SkipNode
  123. def depart_field_body(self, node):
  124. pass
  125. def visit_field_list(self, node):
  126. section_node = node.parent
  127. if not isinstance(section_node, docutils.nodes.section):
  128. raise InvalidFormatError(
  129. "Unexpected field list within " + repr(section_node))
  130. def depart_field_list(self, node):
  131. self.current_field_name = None
  132. if self.current_entry is not None:
  133. self.current_entry.body = u""
  134. def visit_field_name(self, node):
  135. field_name = node.astext()
  136. if field_name.lower() not in ("released", "maintainer"):
  137. raise InvalidFormatError(
  138. "Unexpected field name %(field_name)s" % vars())
  139. self.current_field_name = field_name.lower()
  140. raise docutils.nodes.SkipNode
  141. def depart_field_name(self, node):
  142. pass
  143. def adjust_indent_width(self, delta):
  144. self.indent_width += delta
  145. self.subsequent_indent = u" " * self.indent_width
  146. self.initial_indent = self.subsequent_indent
  147. def visit_list_item(self, node):
  148. self.adjust_indent_width(+2)
  149. self.initial_indent = self.subsequent_indent[:-2]
  150. self.append_to_current_entry(self.initial_indent + "- ")
  151. def depart_list_item(self, node):
  152. self.adjust_indent_width(-2)
  153. self.append_to_current_entry("\n")
  154. def visit_section(self, node):
  155. self.current_entry = NewsEntry()
  156. def depart_section(self, node):
  157. self.body += self.current_entry.as_specfile_changelog_entry() + "\n"
  158. self.current_entry = None
  159. def visit_title(self, node):
  160. title_text = node.astext()
  161. words = title_text.split(" ")
  162. self.current_entry.version = words[-1]
  163. def depart_title(self, node):
  164. pass
  165. description = (
  166. u"Render the NEWS file to a specfile changelog field value."
  167. + u" " + default_description)
  168. publish_cmdline(writer=SpecChangelogWriter(), description=description)