././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692121989.626089 changelog-0.6.1/0000755000175100001730000000000014466735606013067 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/LICENSE0000644000175100001730000000216214466735571014076 0ustar00runnerdockerThis 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/MANIFEST.in0000644000175100001730000000010314466735571014620 0ustar00runnerdockerrecursive-include changelog *.py *.css include README* LICENSE ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692121989.626089 changelog-0.6.1/PKG-INFO0000644000175100001730000001155114466735606014167 0ustar00runnerdockerMetadata-Version: 2.1 Name: changelog Version: 0.6.1 Summary: Provides simple Sphinx markup to render changelog displays. Home-page: https://github.com/sqlalchemyorg/changelog Author: Mike Bayer Author-email: mike@zzzcomputing.com License: MIT Keywords: Sphinx 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 License-File: LICENSE ========== Changelog ========== |PyPI| |Python| |Downloads| .. |PyPI| image:: https://img.shields.io/pypi/v/changelog :target: https://pypi.org/project/changelog :alt: PyPI .. |Python| image:: https://img.shields.io/pypi/pyversions/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Python Version .. |Downloads| image:: https://img.shields.io/pypi/dm/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Downloads 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"] # section css classes - optional changelog_caption_class = "caption" # tags to sort on inside of sections - also optional changelog_inner_tag_sort = ["feature", "bug"] # whether sections should be hidden from tags list changelog_hide_sections_from_tags = False # whether tags should be hidden from entries changelog_hide_tags_in_entry = False # 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` Other things not documented yet =============================== * the ``:version:`` directive, which indicates a changelog entry should be listed in other versions as well * the ``.. changelog_imports::`` directive - reads other changelog.rst files looking for ``:version:`` directives which apply to this changelog file, adding those entries to the changelog entries in this file * the ``:include_notes_from:`` symbol - imports all the .rst files in a directory into the current one so that changes can be one-per-file, makes git merges possible * the ``changelog release-notes`` command that at release time gathers up the above-mentioned change-per-file .rst files and renders them into the main changelog.rst file, running "git rm" on the individual files * the changelog.rst -> markdown converter, used for web guis that want changelog sections written in markdown * the changelog.rst -> stream per changelog markdown API function, which can for example stream the changelogs per release to the github releases API ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/README.rst0000644000175100001730000001030714466735571014560 0ustar00runnerdocker========== Changelog ========== |PyPI| |Python| |Downloads| .. |PyPI| image:: https://img.shields.io/pypi/v/changelog :target: https://pypi.org/project/changelog :alt: PyPI .. |Python| image:: https://img.shields.io/pypi/pyversions/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Python Version .. |Downloads| image:: https://img.shields.io/pypi/dm/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Downloads 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"] # section css classes - optional changelog_caption_class = "caption" # tags to sort on inside of sections - also optional changelog_inner_tag_sort = ["feature", "bug"] # whether sections should be hidden from tags list changelog_hide_sections_from_tags = False # whether tags should be hidden from entries changelog_hide_tags_in_entry = False # 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` Other things not documented yet =============================== * the ``:version:`` directive, which indicates a changelog entry should be listed in other versions as well * the ``.. changelog_imports::`` directive - reads other changelog.rst files looking for ``:version:`` directives which apply to this changelog file, adding those entries to the changelog entries in this file * the ``:include_notes_from:`` symbol - imports all the .rst files in a directory into the current one so that changes can be one-per-file, makes git merges possible * the ``changelog release-notes`` command that at release time gathers up the above-mentioned change-per-file .rst files and renders them into the main changelog.rst file, running "git rm" on the individual files * the changelog.rst -> markdown converter, used for web guis that want changelog sections written in markdown * the changelog.rst -> stream per changelog markdown API function, which can for example stream the changelogs per release to the github releases API ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692121989.626089 changelog-0.6.1/changelog/0000755000175100001730000000000014466735606015016 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/__init__.py0000644000175100001730000000007414466735571017131 0ustar00runnerdocker__version__ = "0.6.1" from .sphinxext import setup # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/changelog.css0000644000175100001730000000015314466735571017457 0ustar00runnerdockera.changelog-reference { visibility: hidden; } li:hover a.changelog-reference { visibility: visible; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/cmd.py0000644000175100001730000000607314466735571016142 0ustar00runnerdockerimport argparse import os import re import shutil import sys import tempfile from . import mdwriter def release_notes_into_changelog_file(target_filename, version, release_date): """Read changelog fragment files and render them into a single .rst file. remove the fragment files afterwards using git rm. The fragment files are located by looking for ':include_notes_from:' directives in the given changelog file. """ output = tempfile.NamedTemporaryFile( mode="w", delete=False, encoding="utf-8" ) with open(target_filename) as handle: for line in handle: m = re.match(r".*:version: %s" % version, line) if m: output.write(line) output.write(" :released: %s\n" % release_date) continue m = re.match(r".*:include_notes_from: (.+)", line) if m: notes_dir = os.path.join( os.path.dirname(target_filename), m.group(1) ) for fname in os.listdir(notes_dir): if not fname.endswith(".rst"): continue fname_path = os.path.join(notes_dir, fname) output.write("\n") with open(fname_path) as inner: for inner_line in inner: output.write((" " + inner_line).rstrip() + "\n") os.system("git rm %s" % fname_path) else: output.write(line) output.close() shutil.move(output.name, target_filename) def main(argv=None): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() subparser = subparsers.add_parser( "release-notes", help="Merge notes files into changelog and git rm" ) subparser.add_argument("filename", help="target changelog filename") subparser.add_argument( "version", help="version string as it appears in changelog" ) subparser.add_argument("date", help="full text of datestamp to insert") subparser.set_defaults( cmd=( release_notes_into_changelog_file, ["filename", "version", "date"], ) ) subparser = subparsers.add_parser( "generate-md", help="Generate file into markdown" ) subparser.add_argument("filename", help="target changelog filename") subparser.add_argument("-c", "--config", help="path to conf.py") subparser.add_argument( "-v", "--version", type=str, help="render changelog only for version given", ) subparser.add_argument( "-s", "--sections-only", action="store_true", help="render changelogs as top level sections", ) subparser.set_defaults( cmd=( mdwriter.render_changelog_as_md, ["filename", "config", "version", "sections_only"], ) ) options = parser.parse_args(argv) fn, argnames = options.cmd fn(*[getattr(options, name) for name in argnames]) if __name__ == "__main__": main(sys.argv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/docutils.py0000644000175100001730000003354714466735571017233 0ustar00runnerdockerimport collections from distutils.version import LooseVersion import hashlib as md5 import os import re import sys import warnings from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst import directives from docutils.parsers.rst import roles from . import generate_rst from .environment import Environment py3k = sys.version_info >= (3, 0) 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 "" elif idx == 1 and line: # accomodate a unique value on the edge of .. change:: continue else: break d["text"] = content[idx:] return d class EnvDirective(object): @property def env(self): return Environment.from_document_settings(self.state.document.settings) @classmethod def get_changes_list(cls, env, hash_on_version): key = ("ChangeLogDirective_changes", hash_on_version) if key not in env.temp_data: env.temp_data[key] = collections.OrderedDict() return env.temp_data[key] class ChangeLogDirective(EnvDirective, Directive): """Implement the ``.. changelog::`` directive. """ has_content = True default_section = "misc" def run(self): self._parse() if not ChangeLogImportDirective.in_include_directive(self.env): return generate_rst.render_changelog(self) else: return [] def _parse(self): # 1. pull in global configuration from conf.py self.sections = self.env.changelog_sections self.caption_classes = self.env.changelog_caption_class.split(" ") self.inner_tag_sort = self.env.changelog_inner_tag_sort + [""] self.hide_sections_from_tags = bool( self.env.changelog_hide_sections_from_tags ) self.hide_tags_in_entry = bool(self.env.changelog_hide_tags_in_entry) # 2. examine top level directives inside the .. changelog:: # directive. version, release date self._parsed_content = parsed = _parse_content(self.content) self.version = version = parsed.get("version", "") self.release_date = parsed.get("released", None) self.is_released = bool(self.release_date) self.env.temp_data["ChangeLogDirective"] = self content = self.content # 3. read extra per-file included notes if "include_notes_from" in parsed: if content.items and content.items[0]: source = content.items[0][0] # seems we are now getting strings like: # changelog/changelog_11.rst source = source.split(" ")[0] path = os.path.join( os.path.dirname(source), parsed["include_notes_from"] ) else: path = parsed["include_notes_from"] if not os.path.exists(path): raise Exception("included nodes path %s does not exist" % path) files = [ fname for fname in os.listdir(path) if fname.endswith(".rst") ] for fname in self.env.status_iterator( files, "reading changelog note files (version %s)..." % version ): fpath = os.path.join(path, fname) with open(fpath) as handle: content.append("", path, 0) for num, line in enumerate(handle): if not py3k: line = line.decode("utf-8") if "\t" in line: warnings.warn( "file %s has a tab in it! please " "convert to spaces." % fname ) line = line.replace("\t", " ") line = line.rstrip() content.append(line, path, num) # 4. parse the content of the .. changelog:: directive. This # is where we parse individual .. change:: directives and construct # a list of items, stored in the env via self.get_changes_list(env) p = nodes.paragraph("", "") self.state.nested_parse(content[1:], 0, p) class ChangeLogImportDirective(EnvDirective, Directive): """Implement the ``.. changelog_imports::`` directive. Here, we typically load in other changelog.rst files which may feature elements that also apply to our current changelog.rst file, when they specify the ``:version:`` modifier. """ has_content = True @classmethod def in_include_directive(cls, env): return "ChangeLogDirective_includes" in env.temp_data def run(self): # tell ChangeLogDirective we're here, also prevent # nested .. include calls if not self.in_include_directive(self.env): 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 SeeAlsoDirective(EnvDirective, Directive): """implement a quick version of Sphinx "seealso" when running outside of sphinx.""" has_content = True def run(self): text = "\n".join(self.content) ad = nodes.admonition(rawsource=text) st = nodes.strong() st.append(nodes.Text("See also:")) ad.append(st) ad.append(nodes.Text("\n")) self.state.nested_parse(self.content, 0, ad) return [ad] class ChangeDirective(EnvDirective, Directive): """Implement the ``.. change::`` directive. """ has_content = True def run(self): # don't do anything if we're not inside of a version if "ChangeLogDirective" not in self.env.temp_data: return [] content = _parse_content(self.content) sorted_tags = _comma_list(content.get("tags", "")) changelog_directive = self.env.temp_data["ChangeLogDirective"] declared_version = changelog_directive.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 ChangeLogImportDirective.in_include_directive(self.env): return [] body_paragraph = nodes.paragraph( "", "", classes=changelog_directive.caption_classes ) self.state.nested_parse(content["text"], 0, body_paragraph) raw_text = _text_rawsource_from_node(body_paragraph) tickets = set(_comma_list(content.get("tickets", ""))).difference([""]) pullreq = set(_comma_list(content.get("pullreq", ""))).difference([""]) tags = set(sorted_tags).difference([""]) for hash_on_version in versions: issue_hash = _get_robust_version_hash( raw_text, hash_on_version, tickets, tags ) rec = ChangeLogDirective.get_changes_list( changelog_directive.env, hash_on_version ).setdefault(issue_hash, {}) if not rec: sorted(versions, key=_str_version_as_tuple) rec.update( { "hash": issue_hash, "render_for_version": hash_on_version, "tags": tags, "tickets": tickets, "pullreq": pullreq, "changeset": set( _comma_list(content.get("changeset", "")) ).difference([""]), "node": body_paragraph, "raw_text": raw_text, "type": "change", "title": content.get("title", None), "sorted_tags": sorted_tags, "versions": versions, "version_to_hash": { version: _get_legacy_version_hash( raw_text, version ) for version in versions }, "source_versions": [declared_version], "sorted_versions": list( reversed( sorted(versions, key=_str_version_as_tuple) ) ), } ) else: # This seems to occur repeated times for each included # changelog, not clear if sphinx has changed the scope # of self.env to lead to this occurring more often self.env.log_debug( "Merging changelog record '%s' from version(s) %s " "with that of version %s", _quick_rec_str(rec), ", ".join(rec["source_versions"]), declared_version, ) rec["source_versions"].append(declared_version) assert rec["raw_text"] == raw_text assert rec["tags"] == tags assert rec["render_for_version"] == hash_on_version rec["tickets"].update(tickets) rec["pullreq"].update(pullreq) rec["changeset"].update( set(_comma_list(content.get("changeset", ""))).difference( [""] ) ) rec["versions"].update(versions) rec["version_to_hash"].update( { version: _get_legacy_version_hash(raw_text, version) for version in versions } ) rec["sorted_versions"] = list( reversed( sorted(rec["versions"], key=_str_version_as_tuple) ) ) return [] def _quick_rec_str(rec): """try to print an identifiable description of a record""" if rec["tickets"]: return "[tickets: %s]" % ", ".join(rec["tickets"]) else: return "%s..." % rec["raw_text"][0:25] def _get_legacy_version_hash(raw_text, version): # this needs to stay like this for link compatibility # with thousands of already-published changelogs to_hash = "%s %s" % (version, raw_text[0:100]) return md5.md5(to_hash.encode("ascii", "ignore")).hexdigest() def _get_robust_version_hash(raw_text, version, tickets, tags): # this needs to stay like this for link compatibility # with thousands of already-published changelogs to_hash = "%s %s %s %s" % ( version, ", ".join(tickets), ", ".join(tags), raw_text, ) return md5.md5(to_hash.encode("ascii", "ignore")).hexdigest() def _text_rawsource_from_node(node): src = [] stack = [node] while stack: n = stack.pop(0) if isinstance(n, nodes.Text): src.append(str(n)) stack.extend(n.children) return "".join(src) _VERSION_IDS = {} def _str_version_as_tuple(ver): if ver in _VERSION_IDS: return _VERSION_IDS[ver] result = LooseVersion(ver) _VERSION_IDS[ver] = result return result def make_ticket_link( name, rawtext, text, lineno, inliner, options={}, content=[] ): env = Environment.from_document_settings(inliner.document.settings) render_ticket = env.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 make_generic_attrref( name, rawtext, text, lineno, inliner, options={}, content=[] ): text = text.lstrip(".") lt = nodes.literal(rawtext=rawtext) lt.append(nodes.Text(text)) return [lt], [] def make_generic_funcref( name, rawtext, text, lineno, inliner, options={}, content=[] ): if not text.endswith("()"): text += "()" return make_generic_attrref( name, rawtext, text, lineno, inliner, options, content ) def make_generic_docref( name, rawtext, text, lineno, inliner, options={}, content=[] ): lt = nodes.literal(rawtext=rawtext) lt.append(nodes.Text(text)) return [lt], [] def setup_docutils(): """register docutils directives and roles assuming Sphinx is not in use.""" directives.register_directive("changelog", ChangeLogDirective) directives.register_directive("change", ChangeDirective) directives.register_directive( "changelog_imports", ChangeLogImportDirective ) directives.register_directive("seealso", SeeAlsoDirective) roles.register_canonical_role("ticket", make_ticket_link) # sphinx autodoc stuff we don't have in this context roles.register_canonical_role("func", make_generic_funcref) roles.register_canonical_role("class", make_generic_attrref) roles.register_canonical_role("paramref", make_generic_attrref) roles.register_canonical_role("attr", make_generic_attrref) roles.register_canonical_role("mod", make_generic_attrref) roles.register_canonical_role("meth", make_generic_funcref) roles.register_canonical_role("obj", make_generic_attrref) roles.register_canonical_role("exc", make_generic_attrref) roles.register_canonical_role("doc", make_generic_docref) roles.register_canonical_role("ref", make_generic_docref) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/environment.py0000644000175100001730000000572314466735571017744 0ustar00runnerdockerimport logging import sys LOG = logging.getLogger(__name__) class Environment(object): __slots__ = () env_classes = () @classmethod def register(cls, env_class): cls.env_classes = (env_class,) + cls.env_classes @classmethod def from_document_settings(cls, settings): for cls in cls.env_classes: e = cls.from_document_settings(settings) if e is not None: return e raise NotImplementedError("TODO") @property def temp_data(self): raise NotImplementedError() @property def changelog_sections(self): raise NotImplementedError() @property def changelog_caption_class(self): raise NotImplementedError() @property def changelog_inner_tag_sort(self): raise NotImplementedError() @property def changelog_hide_sections_from_tags(self): raise NotImplementedError() @property def changelog_render_ticket(self): raise NotImplementedError() @property def changelog_render_pullreq(self): raise NotImplementedError() @property def changelog_render_changeset(self): raise NotImplementedError() def status_iterator(self, elements, message): raise NotImplementedError() class DefaultEnvironment(Environment): @classmethod def from_document_settings(cls, settings): return settings.changelog_env def __init__(self, config_filename=None): self._temp_data = {} self.config = {} if config_filename is not None: exec(open(config_filename).read(), self.config) def log_debug(self, msg, *args): LOG.debug(msg, *args) @property def temp_data(self): return self._temp_data @property def changelog_sections(self): return self.config.get("changelog_sections", []) @property def changelog_caption_class(self): return self.config.get("changelog_caption_class", "caption") @property def changelog_inner_tag_sort(self): return self.config.get("changelog_inner_tag_sort", []) @property def changelog_hide_sections_from_tags(self): return self.config.get("changelog_hide_sections_from_tags", []) @property def changelog_hide_tags_in_entry(self): return self.config.get("changelog_hide_tags_in_entry", []) @property def changelog_render_ticket(self): return self.config.get("changelog_render_ticket", "ticket:%s") @property def changelog_render_pullreq(self): return self.config.get("changelog_render_pullreq", "pullreq:%s") @property def changelog_render_changeset(self): return self.config.get("changelog_render_changeset", "changeset:%s") def status_iterator(self, elements, message): for i, element in enumerate(elements, 1): percent = (i / len(elements)) * 100 sys.stderr.write(message + "...[%d%%] %s\n" % (percent, element)) yield element ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/generate_rst.py0000644000175100001730000001722414466735571020061 0ustar00runnerdocker#! coding: utf-8 import collections import itertools from docutils import nodes def render_changelog(changelog_directive): changes = changelog_directive.get_changes_list( changelog_directive.env, changelog_directive.version ).values() output = [] id_prefix = "change-%s" % (changelog_directive.version,) topsection = _run_top(changelog_directive, id_prefix) output.append(topsection) bysection, all_sections = _organize_by_section( changelog_directive, changes ) counter = itertools.count() sections_to_render = [ s for s in changelog_directive.sections if s in all_sections ] if not sections_to_render: for cat in changelog_directive.inner_tag_sort: append_sec = _append_node(changelog_directive) for rec in bysection[(changelog_directive.default_section, cat)]: rec["id"] = "%s-%s" % (id_prefix, next(counter)) _render_rec(changelog_directive, rec, None, cat, append_sec) if append_sec.children: topsection.append(append_sec) else: for section in sections_to_render + [ changelog_directive.default_section ]: sec = nodes.section( "", nodes.title(section, section), ids=["%s-%s" % (id_prefix, section.replace(" ", "-"))], ) append_sec = _append_node(changelog_directive) sec.append(append_sec) for cat in changelog_directive.inner_tag_sort: for rec in bysection[(section, cat)]: rec["id"] = "%s-%s" % (id_prefix, next(counter)) _render_rec( changelog_directive, rec, section, cat, append_sec ) if append_sec.children: topsection.append(sec) return output def _organize_by_section(changelog_directive, changes): compound_sections = [ (s, s.split(" ")) for s in changelog_directive.sections if " " in s ] bysection = collections.defaultdict(list) all_sections = set() for rec in changes: assert changelog_directive.version == rec["render_for_version"] inner_tag = rec["tags"].intersection( changelog_directive.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(changelog_directive.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[ (changelog_directive.default_section, inner_tag) ].append(rec) return bysection, all_sections def _append_node(changelog_directive): return nodes.bullet_list() def _run_top(changelog_directive, id_prefix): version = changelog_directive._parsed_content.get("version", "") topsection = nodes.section( "", nodes.title(version, version, classes=["release-version"]), ids=[id_prefix], version_string=version, ) if changelog_directive._parsed_content.get("released"): topsection.append( nodes.Text( "Released: %s" % changelog_directive._parsed_content["released"] ) ) else: topsection.append(nodes.Text("no release date")) intro_para = nodes.paragraph("", "") len_ = -1 for len_, text in enumerate(changelog_directive._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: changelog_directive.state.nested_parse( changelog_directive._parsed_content["text"][0:len_], 0, intro_para ) topsection.append(intro_para) return topsection def _render_rec(changelog_directive, rec, section, cat, append_sec): para = rec["node"].deepcopy() targetid = "change-%s" % ( rec["version_to_hash"][changelog_directive.version], ) targetnode = nodes.target("", "", ids=[targetid]) sections = section.split(" ") if section else [] section_tags = [tag for tag in sections if tag in rec["tags"]] category_tags = [cat] if cat in rec["tags"] else [] other_tags = list( sorted(rec["tags"].difference(section_tags + category_tags)) ) all_items = [] if not changelog_directive.hide_sections_from_tags: all_items.extend(section_tags) all_items.extend(category_tags) all_items.extend(other_tags) all_items = all_items or ["no_tags"] permalink = nodes.reference( "", "", nodes.Text(u"¶", u"¶"), refid=targetid, classes=["changelog-reference", "headerlink"], ) if not changelog_directive.hide_tags_in_entry: tag_node = nodes.strong("", " ".join("[%s]" % t for t in all_items)) targetnode.insert(0, nodes.Text(" ", " ")) targetnode.insert(0, tag_node) targetnode.append(permalink) para.insert(0, targetnode) if len(rec["versions"]) > 1: backported_changes = rec["sorted_versions"][ rec["sorted_versions"].index(changelog_directive.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) if changelog_directive.hide_tags_in_entry: para.append(permalink) insert_ticket = nodes.paragraph("") para.append(insert_ticket) i = 0 for collection, render, prefix in ( ( rec["tickets"], changelog_directive.env.changelog_render_ticket, "#%s", ), ( rec["pullreq"], changelog_directive.env.changelog_render_pullreq, "pull request %s", ), ( rec["changeset"], changelog_directive.env.changelog_render_changeset, "r%s", ), ): for refname in sorted(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) append_sec.append( nodes.list_item("", nodes.target("", "", ids=[rec["id"]]), para) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/mdwriter.py0000644000175100001730000002202614466735571017230 0ustar00runnerdockerimport io from docutils import nodes from docutils import writers from docutils.core import publish_file from docutils.core import publish_string from .docutils import setup_docutils from .environment import DefaultEnvironment from .environment import Environment class Writer(writers.Writer): supported = ("markdown",) def __init__(self, limit_version=None, receive_sections=None): super(Writer, self).__init__() self.limit_version = limit_version self.receive_sections = receive_sections def translate(self): translator = MarkdownTranslator( self.document, self.limit_version, self.receive_sections ) self.document.walkabout(translator) self.output = translator.output_buf.getvalue() class MarkdownTranslator(nodes.NodeVisitor): def __init__(self, document, limit_version, receive_sections): super(MarkdownTranslator, self).__init__(document) self.buf = self.output_buf = io.StringIO() self.limit_version = limit_version self.receive_sections = receive_sections self.section_level = 1 self.stack = [] self._standalone_section_display = ( self.limit_version or self.receive_sections ) if self._standalone_section_display: self.disable_writing() def enable_writing(self): self.buf = self.output_buf def disable_writing(self): self.buf = io.StringIO() def _detect_section_was_squashed_into_subtitle(self, document_node): # docutils converts a single section we generated into a "subtitle" # and squashes the section object # http://docutils.sourceforge.net/FAQ.html\ # #how-can-i-indicate-the-document-title-subtitle for subnode in document_node: if "version_string" in subnode.attributes: if isinstance(subnode, nodes.subtitle): return subnode elif isinstance(subnode, nodes.section): return False else: raise NotImplementedError( "detected version_string in unexpected node type: %s" % subnode ) def visit_document(self, node): self.document = node self.env = Environment.from_document_settings(self.document.settings) subtitle_node = self._detect_section_was_squashed_into_subtitle(node) if subtitle_node: version = subtitle_node.attributes["version_string"] rebuild_our_lost_section = nodes.section( "", nodes.title(version, version, classes=["release-version"]), **subtitle_node.attributes ) # note we are taking the nodes from the document and moving them # into the new section. nodes have a "parent" which means that # parent changes, which means we are mutating the internal # state of the document node. so use deepcopy() to avoid this # side effect; deepcopy() for these nodes seems to not be a # performance problem. rebuild_our_lost_section.extend([n.deepcopy() for n in node[2:]]) rebuild_our_lost_section.walkabout(self) raise nodes.SkipNode() def visit_standalone_version_node(self, node, version_string): """visit a section or document that has a changelog version string at the top""" if self.limit_version and self.limit_version != version_string: return self.section_level = 1 self.enable_writing() if self.receive_sections: self.buf = io.StringIO() def depart_standalone_version_node(self, node, version_string): """depart a section or document that has a changelog version string at the top""" if self.limit_version and self.limit_version != version_string: return if self.receive_sections: self.receive_sections(version_string, self.buf.getvalue()) self.disable_writing() def visit_section(self, node): if ( "version_string" in node.attributes and self._standalone_section_display ): self.visit_standalone_version_node( node, node.attributes["version_string"] ) else: self.section_level += 1 def depart_section(self, node): if ( "version_string" in node.attributes and self._standalone_section_display ): self.depart_standalone_version_node( node, node.attributes["version_string"] ) else: self.section_level -= 1 def visit_strong(self, node): self.buf.write("**") def depart_strong(self, node): self.buf.write("**") def visit_emphasis(self, node): self.buf.write("*") def depart_emphasis(self, node): self.buf.write("*") def visit_literal(self, node): self.buf.write("`") def visit_Text(self, node): self.buf.write(node.astext()) def depart_Text(self, node): pass def depart_paragraph(self, node): self.buf.write("\n\n") def depart_literal(self, node): self.buf.write("`") def visit_title(self, node): self.buf.write( "\n%s %s\n\n" % ("#" * self.section_level, node.astext()) ) raise nodes.SkipNode() def visit_changeset_link(self, node): # it would be nice to have an absolutely link to the HTML # hosted changelog but this requires being able to generate # the absolute link from the document filename and all that. # it can perhaps be sent on the commandline raise nodes.SkipNode() def depart_changeset_link(self, node): pass def visit_reference(self, node): if "changelog-reference" in node.attributes["classes"]: self.visit_changeset_link(node) else: self.buf.write("[") def depart_reference(self, node): if "changelog-reference" in node.attributes["classes"]: self.depart_changeset_link(node) else: self.buf.write("](%s)" % node.attributes["refuri"]) def visit_admonition(self, node): # "seealsos" typically have internal sphinx references so at the # moment we're not prepared to look those up, future version can # perhaps use sphinx object lookup raise nodes.SkipNode() def visit_list_item(self, node): self.stack.append(self.buf) self.buf = io.StringIO() def depart_list_item(self, node): popped = self.buf self.buf = self.stack.pop(-1) indent_level = len(self.stack) indent_string = " " * 4 * indent_level value = popped.getvalue().strip() lines = value.split("\n") self.buf.write("\n" + indent_string + "- ") line = lines.pop(0) self.buf.write(line + "\n") for line in lines: self.buf.write(indent_string + " " + line + "\n") def _visit_generic_node(self, node): pass def __getattr__(self, name): if not name.startswith("_"): return self._visit_generic_node else: raise AttributeError(name) def stream_changelog_sections( target_filename, config_filename, receive_sections, version=None ): """Send individual changelog sections to a callable, one per version. The callable accepts two arguments, the string version number of the changelog section, and the markdown-formatted content of the changelog section. Used for APIs that receive changelog sections per version. """ Environment.register(DefaultEnvironment) setup_docutils() with open(target_filename, encoding="utf-8") as handle: publish_string( handle.read(), source_path=target_filename, writer=Writer( limit_version=version, receive_sections=receive_sections ), settings_overrides={ "changelog_env": DefaultEnvironment(config_filename), "report_level": 3, }, ) def render_changelog_as_md( target_filename, config_filename, version, sections_only ): Environment.register(DefaultEnvironment) setup_docutils() if sections_only: def receive_sections(version_string, text): print(text) else: receive_sections = None writer = Writer(limit_version=version, receive_sections=receive_sections) settings_overrides = { "changelog_env": DefaultEnvironment(config_filename), "report_level": 3, } with open(target_filename, encoding="utf-8") as handle: if receive_sections: publish_string( handle.read(), source_path=target_filename, writer=writer, settings_overrides=settings_overrides, ) else: publish_file( handle, writer=writer, settings_overrides=settings_overrides ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/changelog/sphinxext.py0000644000175100001730000001000214466735571017414 0ustar00runnerdockerimport os from sphinx.util import logging from sphinx.util.console import bold from sphinx.util.osutil import copyfile from .docutils import ChangeDirective from .docutils import ChangeLogDirective from .docutils import ChangeLogImportDirective from .docutils import make_ticket_link from .environment import Environment try: from sphinx.util.display import status_iterator except ImportError: from sphinx.util import status_iterator LOG = logging.getLogger(__name__) def _is_html(app): return app.builder.name in ("html", "readthedocs") class SphinxEnvironment(Environment): __slots__ = ("sphinx_env",) @classmethod def from_document_settings(cls, settings): return SphinxEnvironment(settings.env) def __init__(self, sphinx_env): self.sphinx_env = sphinx_env def log_debug(self, msg, *args): LOG.debug(msg, *args) @property def temp_data(self): return self.sphinx_env.temp_data @property def changelog_sections(self): return self.sphinx_env.config.changelog_sections @property def changelog_caption_class(self): return self.sphinx_env.config.changelog_caption_class @property def changelog_inner_tag_sort(self): return self.sphinx_env.config.changelog_inner_tag_sort @property def changelog_hide_sections_from_tags(self): return self.sphinx_env.config.changelog_hide_sections_from_tags @property def changelog_hide_tags_in_entry(self): return self.sphinx_env.config.changelog_hide_tags_in_entry @property def changelog_render_ticket(self): return self.sphinx_env.config.changelog_render_ticket @property def changelog_render_pullreq(self): return self.sphinx_env.config.changelog_render_pullreq @property def changelog_render_changeset(self): return self.sphinx_env.config.changelog_render_changeset def status_iterator(self, elements, message): return status_iterator( elements, message, "purple", length=len(elements), verbosity=self.sphinx_env.app.verbosity, ) def add_stylesheet(app): # changed in 1.8 from add_stylesheet() # https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_css_file app.add_css_file("changelog.css") def copy_stylesheet(app, exception): LOG.info( bold("The name of the builder is: %s" % app.builder.name), nonl=True ) if not _is_html(app) or exception: return LOG.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) LOG.info("done") def setup(app): Environment.register(SphinxEnvironment) 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_caption_class", "caption", "env") app.add_config_value("changelog_inner_tag_sort", [], "env") app.add_config_value("changelog_hide_sections_from_tags", False, "env") app.add_config_value("changelog_hide_tags_in_entry", False, "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) return {"parallel_read_safe": True, "parallel_write_safe": True} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692121989.626089 changelog-0.6.1/changelog.egg-info/0000755000175100001730000000000014466735606016510 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/PKG-INFO0000644000175100001730000001155114466735605017607 0ustar00runnerdockerMetadata-Version: 2.1 Name: changelog Version: 0.6.1 Summary: Provides simple Sphinx markup to render changelog displays. Home-page: https://github.com/sqlalchemyorg/changelog Author: Mike Bayer Author-email: mike@zzzcomputing.com License: MIT Keywords: Sphinx 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 License-File: LICENSE ========== Changelog ========== |PyPI| |Python| |Downloads| .. |PyPI| image:: https://img.shields.io/pypi/v/changelog :target: https://pypi.org/project/changelog :alt: PyPI .. |Python| image:: https://img.shields.io/pypi/pyversions/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Python Version .. |Downloads| image:: https://img.shields.io/pypi/dm/changelog :target: https://pypi.org/project/changelog :alt: PyPI - Downloads 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"] # section css classes - optional changelog_caption_class = "caption" # tags to sort on inside of sections - also optional changelog_inner_tag_sort = ["feature", "bug"] # whether sections should be hidden from tags list changelog_hide_sections_from_tags = False # whether tags should be hidden from entries changelog_hide_tags_in_entry = False # 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` Other things not documented yet =============================== * the ``:version:`` directive, which indicates a changelog entry should be listed in other versions as well * the ``.. changelog_imports::`` directive - reads other changelog.rst files looking for ``:version:`` directives which apply to this changelog file, adding those entries to the changelog entries in this file * the ``:include_notes_from:`` symbol - imports all the .rst files in a directory into the current one so that changes can be one-per-file, makes git merges possible * the ``changelog release-notes`` command that at release time gathers up the above-mentioned change-per-file .rst files and renders them into the main changelog.rst file, running "git rm" on the individual files * the changelog.rst -> markdown converter, used for web guis that want changelog sections written in markdown * the changelog.rst -> stream per changelog markdown API function, which can for example stream the changelogs per release to the github releases API ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/SOURCES.txt0000644000175100001730000000065614466735605020402 0ustar00runnerdockerLICENSE MANIFEST.in README.rst setup.cfg setup.py changelog/__init__.py changelog/changelog.css changelog/cmd.py changelog/docutils.py changelog/environment.py changelog/generate_rst.py changelog/mdwriter.py changelog/sphinxext.py changelog.egg-info/PKG-INFO changelog.egg-info/SOURCES.txt changelog.egg-info/dependency_links.txt changelog.egg-info/entry_points.txt changelog.egg-info/not-zip-safe changelog.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/dependency_links.txt0000644000175100001730000000000114466735605022555 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/entry_points.txt0000644000175100001730000000006114466735605022002 0ustar00runnerdocker[console_scripts] changelog = changelog.cmd:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/not-zip-safe0000644000175100001730000000000114466735605020735 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121989.0 changelog-0.6.1/changelog.egg-info/top_level.txt0000644000175100001730000000001214466735605021232 0ustar00runnerdockerchangelog ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1692121989.626089 changelog-0.6.1/setup.cfg0000644000175100001730000000050214466735606014705 0ustar00runnerdocker[flake8] show-source = true enable-extensions = G ignore = A003, D, E203,E305,E711,E712,E721,E722,E741, F821 N801,N802,N806, RST304,RST303,RST299,RST399, W503,W504 exclude = .venv,.git,.tox,dist,doc,*egg,build import-order-style = google application-import-names = changelog [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692121977.0 changelog-0.6.1/setup.py0000644000175100001730000000227014466735571014603 0ustar00runnerdockerimport os import re from setuptools import setup 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="https://github.com/sqlalchemyorg/changelog", license="MIT", packages=["changelog"], include_package_data=True, zip_safe=False, entry_points={"console_scripts": ["changelog = changelog.cmd:main"]}, )