releases-2.1.1/0000755000175000001440000000000014423015165014301 5ustar jforcierusers00000000000000releases-2.1.1/setup.py0000644000175000001440000000452214373504447016030 0ustar jforcierusers00000000000000#!/usr/bin/env python from setuptools import setup # Version info -- read without importing _locals = {} with open("releases/_version.py") as fp: exec(fp.read(), None, _locals) version = _locals["__version__"] setup( name="releases", version=version, description="A Sphinx extension for changelog manipulation", long_description=open("README.rst").read(), author="Jeff Forcier", author_email="jeff@bitprophet.org", url="https://github.com/bitprophet/releases", project_urls={ "Docs": "https://releases.readthedocs.io", "Source": "https://github.com/bitprophet/releases", "Changelog": "https://releases.readthedocs.io/en/latest/changelog.html", # noqa "CI": "https://app.circleci.com/pipelines/github/bitprophet/releases", }, packages=["releases"], install_requires=[ # We mostly still work on Sphinx>=1.8, but a number of transitive # dependencies do not, and trying to square that circle is definitely # not worth the effort at this time. PRs that can pass the entire test # matrix are welcome, if you disagree! "sphinx>=4", # Continuing to pin an old semantic_version until I have time to update # and finish the branch I made for # https://github.com/bitprophet/releases/pull/86#issuecomment-580037996 "semantic_version<2.7", ], python_requires=">=3.6", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: MacOS :: MacOS X", "Operating System :: Unix", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Software Development", "Topic :: Software Development :: Documentation", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", ], ) releases-2.1.1/releases.egg-info/0000755000175000001440000000000014423015165017576 5ustar jforcierusers00000000000000releases-2.1.1/releases.egg-info/top_level.txt0000644000175000001440000000001114423015165022320 0ustar jforcierusers00000000000000releases releases-2.1.1/releases.egg-info/PKG-INFO0000644000175000001440000001020314423015165020667 0ustar jforcierusers00000000000000Metadata-Version: 1.2 Name: releases Version: 2.1.1 Summary: A Sphinx extension for changelog manipulation Home-page: https://github.com/bitprophet/releases Author: Jeff Forcier Author-email: jeff@bitprophet.org License: UNKNOWN Project-URL: Docs, https://releases.readthedocs.io Project-URL: Source, https://github.com/bitprophet/releases Project-URL: Changelog, https://releases.readthedocs.io/en/latest/changelog.html Project-URL: CI, https://app.circleci.com/pipelines/github/bitprophet/releases Description: |version| |python| |license| |ci| |coverage| .. |version| image:: https://img.shields.io/pypi/v/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Package Version .. |python| image:: https://img.shields.io/pypi/pyversions/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Python Version .. |license| image:: https://img.shields.io/pypi/l/releases :target: https://github.com/bitprophet/releases/blob/main/LICENSE :alt: PyPI - License .. |ci| image:: https://img.shields.io/circleci/build/github/bitprophet/releases/main :target: https://app.circleci.com/pipelines/github/bitprophet/releases :alt: CircleCI .. |coverage| image:: https://img.shields.io/codecov/c/gh/bitprophet/releases :target: https://app.codecov.io/gh/bitprophet/releases :alt: Codecov What is Releases? ================= Releases is a `Sphinx `_ extension designed to help you keep a source control friendly, merge friendly changelog file & turn it into useful, human readable HTML output. It's compatible with Python 3.6+, and may work on Sphinx versions as far back as 1.8.x, though 4.x and up are recommended and generally all we will support. Specifically: * The source format (kept in your Sphinx tree as ``changelog.rst``) is a stream-like timeline that plays well with source control & only requires one entry per change (even for changes that exist in multiple release lines). * The output (when you have the extension installed and run your Sphinx build command) is a traditional looking changelog page with a section for every release; multi-release issues are copied automatically into each release. * By default, feature and support issues are only displayed under feature releases, and bugs are only displayed under bugfix releases. This can be overridden on a per-issue basis. Some background on why this tool was created can be found in `this blog post `_. For more documentation, please see http://releases.readthedocs.io. For a roadmap, see the maintainer's `roadmap page `_. .. note:: You can install the development version via ``pip install -e git+https://github.com/bitprophet/releases/#egg=releases``. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Documentation Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.6 releases-2.1.1/releases.egg-info/SOURCES.txt0000644000175000001440000000076614423015165021473 0ustar jforcierusers00000000000000LICENSE MANIFEST.in README.rst dev-requirements.txt setup.py tasks.py docs/changelog.rst docs/concepts.rst docs/conf.py docs/index.rst docs/usage.rst releases/__init__.py releases/_version.py releases/line_manager.py releases/models.py releases/util.py releases.egg-info/PKG-INFO releases.egg-info/SOURCES.txt releases.egg-info/dependency_links.txt releases.egg-info/requires.txt releases.egg-info/top_level.txt tests/_util.py tests/conftest.py tests/organization.py tests/presentation.py tests/util.pyreleases-2.1.1/releases.egg-info/dependency_links.txt0000644000175000001440000000000114423015165023644 0ustar jforcierusers00000000000000 releases-2.1.1/releases.egg-info/requires.txt0000644000175000001440000000003714423015165022176 0ustar jforcierusers00000000000000sphinx>=4 semantic_version<2.7 releases-2.1.1/dev-requirements.txt0000644000175000001440000000046714423011045020341 0ustar jforcierusers00000000000000# Task runner invoke>=1.7.3 invocations>=3 # Tests (N.B. integration suite also uses Invoke as above) pytest-relaxed>=2 pytest-cov>=2.4 pytest>=4.6.9 coverage==4.4.2 # Docs (including self, because dogfood) -e . sphinx_rtd_theme>=0.1.5,<2.0 # Formatting flake8==5.0.4 black==22.8.0 # Miscellany icecream==2.1.3 releases-2.1.1/tasks.py0000644000175000001440000000101414302472547016003 0ustar jforcierusers00000000000000from os.path import join from invocations import docs, checks, ci from invocations.pytest import test, integration, coverage from invocations.packaging import release from invoke import Collection ns = Collection(test, integration, coverage, release, docs, ci, checks.blacken) ns.configure( { "packaging": { "sign": True, "wheel": True, "changelog_file": join( docs.ns.configuration()["sphinx"]["source"], "changelog.rst" ), }, } ) releases-2.1.1/PKG-INFO0000644000175000001440000001020314423015165015372 0ustar jforcierusers00000000000000Metadata-Version: 1.2 Name: releases Version: 2.1.1 Summary: A Sphinx extension for changelog manipulation Home-page: https://github.com/bitprophet/releases Author: Jeff Forcier Author-email: jeff@bitprophet.org License: UNKNOWN Project-URL: Docs, https://releases.readthedocs.io Project-URL: Source, https://github.com/bitprophet/releases Project-URL: Changelog, https://releases.readthedocs.io/en/latest/changelog.html Project-URL: CI, https://app.circleci.com/pipelines/github/bitprophet/releases Description: |version| |python| |license| |ci| |coverage| .. |version| image:: https://img.shields.io/pypi/v/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Package Version .. |python| image:: https://img.shields.io/pypi/pyversions/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Python Version .. |license| image:: https://img.shields.io/pypi/l/releases :target: https://github.com/bitprophet/releases/blob/main/LICENSE :alt: PyPI - License .. |ci| image:: https://img.shields.io/circleci/build/github/bitprophet/releases/main :target: https://app.circleci.com/pipelines/github/bitprophet/releases :alt: CircleCI .. |coverage| image:: https://img.shields.io/codecov/c/gh/bitprophet/releases :target: https://app.codecov.io/gh/bitprophet/releases :alt: Codecov What is Releases? ================= Releases is a `Sphinx `_ extension designed to help you keep a source control friendly, merge friendly changelog file & turn it into useful, human readable HTML output. It's compatible with Python 3.6+, and may work on Sphinx versions as far back as 1.8.x, though 4.x and up are recommended and generally all we will support. Specifically: * The source format (kept in your Sphinx tree as ``changelog.rst``) is a stream-like timeline that plays well with source control & only requires one entry per change (even for changes that exist in multiple release lines). * The output (when you have the extension installed and run your Sphinx build command) is a traditional looking changelog page with a section for every release; multi-release issues are copied automatically into each release. * By default, feature and support issues are only displayed under feature releases, and bugs are only displayed under bugfix releases. This can be overridden on a per-issue basis. Some background on why this tool was created can be found in `this blog post `_. For more documentation, please see http://releases.readthedocs.io. For a roadmap, see the maintainer's `roadmap page `_. .. note:: You can install the development version via ``pip install -e git+https://github.com/bitprophet/releases/#egg=releases``. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Documentation Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.6 releases-2.1.1/README.rst0000644000175000001440000000441214302472547016000 0ustar jforcierusers00000000000000|version| |python| |license| |ci| |coverage| .. |version| image:: https://img.shields.io/pypi/v/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Package Version .. |python| image:: https://img.shields.io/pypi/pyversions/releases :target: https://pypi.org/project/releases/ :alt: PyPI - Python Version .. |license| image:: https://img.shields.io/pypi/l/releases :target: https://github.com/bitprophet/releases/blob/main/LICENSE :alt: PyPI - License .. |ci| image:: https://img.shields.io/circleci/build/github/bitprophet/releases/main :target: https://app.circleci.com/pipelines/github/bitprophet/releases :alt: CircleCI .. |coverage| image:: https://img.shields.io/codecov/c/gh/bitprophet/releases :target: https://app.codecov.io/gh/bitprophet/releases :alt: Codecov What is Releases? ================= Releases is a `Sphinx `_ extension designed to help you keep a source control friendly, merge friendly changelog file & turn it into useful, human readable HTML output. It's compatible with Python 3.6+, and may work on Sphinx versions as far back as 1.8.x, though 4.x and up are recommended and generally all we will support. Specifically: * The source format (kept in your Sphinx tree as ``changelog.rst``) is a stream-like timeline that plays well with source control & only requires one entry per change (even for changes that exist in multiple release lines). * The output (when you have the extension installed and run your Sphinx build command) is a traditional looking changelog page with a section for every release; multi-release issues are copied automatically into each release. * By default, feature and support issues are only displayed under feature releases, and bugs are only displayed under bugfix releases. This can be overridden on a per-issue basis. Some background on why this tool was created can be found in `this blog post `_. For more documentation, please see http://releases.readthedocs.io. For a roadmap, see the maintainer's `roadmap page `_. .. note:: You can install the development version via ``pip install -e git+https://github.com/bitprophet/releases/#egg=releases``. releases-2.1.1/docs/0000755000175000001440000000000014423015165015231 5ustar jforcierusers00000000000000releases-2.1.1/docs/conf.py0000644000175000001440000000117714423013133016527 0ustar jforcierusers00000000000000from datetime import datetime import os import sys import sphinx_rtd_theme extensions = [] templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" project = "Releases" copyright = f"{datetime.now().year} Jeff Forcier" # Ensure project directory is on PYTHONPATH for version, autodoc access sys.path.insert(0, os.path.abspath(os.path.join(os.getcwd(), ".."))) exclude_patterns = ["_build"] # RTD theme html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Dogfood extensions.append("releases") releases_github_path = "bitprophet/releases" releases_supported_versions = [2] releases-2.1.1/docs/index.rst0000644000175000001440000000024214253147100017064 0ustar jforcierusers00000000000000======== Releases ======== .. include:: ../README.rst Table of Contents ================= .. toctree:: :maxdepth: 2 concepts usage changelog releases-2.1.1/docs/changelog.rst0000644000175000001440000002760214423015162017716 0ustar jforcierusers00000000000000========= Changelog ========= - :release:`2.1.1 <2023-04-28>` - :release:`2.0.1 <2023-04-28>` - :bug:`-` Fix up an internal utility which monkeypatches a Sphinx/docutils internal, so that it accepts arbitrary args/kwargs instead of exploding on newer Sphinxes. - :release:`2.1.0 <2023-02-24>` - :feature:`-` Add a new configuration setting, ``releases_supported_versions``, allowing you to limit how many "Next 1.x feature release" (or bugfix, etc) sections appear at the top of your changelog. - :feature:`-` Allow controlling the name of your development branch for source code links (eg "Next 1.x feature release" section headers) via the new ``releases_development_branch`` config option. - :release:`2.0.0 <2022-12-31>` - :bug:`- major` Changelog transformation sometimes failed to occur when running under a 'single HTML file' Sphinx builder (eg ``singlehtml``), which resulted in 'unknown node' errors. This has been fixed. - :support:`-` Administrivia overhaul: enhanced README, packaging metadata cleaned up/expanded, CI moved to Circle-CI, renamed dev branch to ``main``, and more besides. - :support:`-` The ``releases_release_uri``/``releases_issue_uri`` settings now allow modern (``.format``/f-strings) string formatting, in addition to the old ``%s``-based interpolation. - :support:`-` Dropped support for Python 2.7, Python 3.4, and Python 3.5, to align slightly better with upstream (and ecosystem) EOLs. - :bug:`- major` Don't make tmpdirs in ``releases.util.make_app`` when being given explicit directory args. - :support:`-` Dropped support for Sphinx <4. We tried to support 1.8+, but too many transitive dependencies have clearly "moved on" and cause various cells in the test matrix to fail hard. .. warning:: This change is backwards incompatible if your Sphinx docs don't already work on recent Sphinx versions. - :support:`-` Migrated the test suite to use ``pytest-relaxed`` (and thus pytest) instead of ``spec``. - :release:`1.6.3 <2020-01-10>` - :support:`87 backported` (via :issue:`88`) Our upper Sphinx version limit was mostly defensive and at this point is just too old to even build on ReadTheDocs successfully. Removed it for the time being. Thanks to José Sánchez-Gallego for the nudge and the patch. - :release:`1.6.2 <2020-01-10>` - :release:`1.5.2 <2020-01-10>` - :support:`84 backported` Pin us to ``semantic-version<2.7`` as they changed their APIs (...) in that version in a way that breaks us. We do not need anything they've added recently, so the conservative response is to just pin until we truly need to upgrade. Thanks to everyone who reported and submitted workarounds for this. - :release:`1.6.1 <2018-07-02>` - :release:`1.5.1 <2018-07-02>` - :support:`73 backported` (via :issue:`77`) Add support for Sphinx 1.7.x (while retaining support for previous versions of Sphinx). Thanks to Miro Hrončok for the patch. - :release:`1.6.0 <2018-06-20>` - :feature:`75` Update ``releases.util.parse_changelog`` so it hands kwargs into ``releases.util.make_app``, which in turn now accepts a ``load_extensions`` argument triggering loading of one's configured extensions. This is only of interest if you're using ``parse_changelog`` directly; it does not impact normal Releases usage. - :release:`1.5.0 <2018-05-02>` - :feature:`59` Allow multiple changelog files -- ``releases_document_name`` may now optionally be a list of strings instead of a single string. Thanks to William Minchin for the patch. - :release:`1.4.2 <2018-04-27>` - :support:`74 backported` We never pulled our README into our ``setup.py`` metadata, resulting in a rather sparse PyPI page! This has been fixed. Thanks to Peter Demin for the report. - :release:`1.4.1 <2018-03-28>` - :support:`73 backported` Sphinx 1.7.x changed some semi-public APIs; given this is the second minor release in a row to do so, we're explicitly bracketing our ``setup.py`` dependencies to Sphinx >= 1.3 and < 1.7. We expect to bump this up one minor release at a time as we add compatibility back in. - :release:`1.4.0 <2017-10-20>` - :support:`-` Drop Python 2.6 and 3.3 support, to correspond with earlier changes in Sphinx and most other public Python projects. - :bug:`- major` Identified a handful of issues with our Sphinx pin & subsequently, internal changes in Sphinx 1.6 which broke (and/or appear to break, such as noisy warnings) our own behavior. These have (hopefully) all been fixed. - :release:`1.3.2 <2017-10-19>` - :support:`68 backported` Update packaging requirements to allow for ``sphinx>=1.3,<2``. Thanks to William Minchin. - :release:`1.3.1 <2017-05-18>` - :bug:`60` Report extension version to Sphinx for improved Sphinx debug output. Credit: William Minchin. - :bug:`66` (via :issue:`67`) Deal with some Sphinx 1.6.1 brokenness causing ``AttributeError`` by leveraging ``getattr()``'s default-value argument. Thanks to Ian Cordasco for catch & patch. - :release:`1.3.0 <2016-12-09>` - :feature:`-` Add ``releases.util``, exposing (among other things) a highly useful ``parse_changelog(path)`` function that returns a user-facing dict representing a parsed changelog. Allows users to examine their changelogs programmatically and answer questions like "do I have any outstanding bugs in the 1.1 release line?". - :release:`1.2.1 <2016-07-25>` - :support:`51 backported` Modernize release management so PyPI trove classifiers are more accurate, wheel archives are universal instead of Python 2 only, and release artifacts are GPG signed. - :bug:`56` Fix exceptions that occurred when no release/issue link options were configured. Now those options are truly optional: release version and issue number text will simply display normally instead of as hyperlinks. Thanks to André Caron for the report. - :bug:`36` Changelogs with no releases whatsoever should still be viable instead of raising exceptions. This is now happily the case. All items in such changelogs will end up in a single "unreleased features" list, just as with regular prehistory entries. Thanks to Steve Ivy for initial report and André Caron for additional feedback. - :release:`1.2.0 <2016-05-20>` - :bug:`- major` Fix formatting of release header dates; a "75% text size" style rule has had an uncaught typo for some time. - :bug:`55 major` Non-annotated changelog line items (which implicitly become bugs) were incorrectly truncating their contents in some situations (basically, any time they included non-regular-text elements like monospace, bold etc). This has been fixed. - :feature:`19` Add ``unstable_prehistory`` option/mode for changelogs whose 0.x release cycle is "rapid" or "unstable" and doesn't closely follow normal semantic version-driven organization. See :ref:`unstable-prehistory`. - :bug:`53 major` Tweak newly-updated models so bugfix items prior to an initial release are considered 'major bugs' so they get rolled into that initial release (instead of causing a ``ValueError``). - :release:`1.1.0 <2016-04-28>` - :feature:`45` Add support for major version transitions (e.g. 1.0 to 2.0). .. note:: This adds a new install-time dependency: the `semantic_version library `_. It's pure Python, so installation should be trivial. - :bug:`44 major` Update one of our internal docutils-related classes for compatibility with Sphinx 1.4.x. Thanks to Gabi Davar for catch & patch. - :release:`1.0.0 <2015-11-05>` - :feature:`42` For readability, issues within each release so they are displayed in feature->bug->support order. - :feature:`41` Clean up changelog discovery so one can have comments, paragraphs or other non-bullet-list elements above or below the changelog. Thanks to Rodrigue Cloutier for the original request/patch. - :release:`0.7.0 <2014-09-04>` - :bug:`30 major` Add LICENSE (plus a handful of other administrative files) to a ``MANIFEST.in`` so sdists pick it up. Thanks to Zygmunt Krynicki for catch & original patch (:issue:`33`). - :feature:`21` Allow duplicate issue numbers; not allowing them was technically an implementation detail. Thanks to Dorian Puła for the patch. - :release:`0.6.1 <2014-04-06>` - :bug:`-` Fix a silly issue with the new feature from :issue:`22` where it accidentally referred to the Sphinx document *title* instead of the document *filename*. - :release:`0.6.0 <2014-04-03>` - :feature:`22` Make the document name used as the changelog - previously hardcoded as ``changelog`` (``.rst``) - configurable. Thanks to James Mills for the feature request. - :feature:`26` Allow specifying Github path shorthand config option instead of explicit release/issue URL strings. - :release:`0.5.3 <2014-03-15>` - :bug:`25` Empty/no-issue line items broke at some point; fixed. - :bug:`24` Broke inline issue parsing; fixed now. - :release:`0.5.2 <2014-03-13>` - :bug:`23` Rework implementation to deal with issue descriptions that span more than one paragraph - subsequent paragraphs/blocks were not being displayed prior. - :release:`0.5.1 <2014-02-11>` - :bug:`-` Fix silly bug in :issue:`20` that cropped up on Python 3.x. - :release:`0.5.0 <2014-02-11>` - :feature:`20` Allow specifying minimum release line in bugfixes that don't apply to all active lines (e.g. because they pertain to a recently added feature.) - :release:`0.4.0 <2013-12-24>` - :feature:`17` Allow releases to explicitly define which issues they include. Useful for overriding default assumptions (e.g. a special bugfix release from an otherwise dormant line.) - :release:`0.3.1 <2013-12-18>` - :bug:`16` Fix some edge cases regarding release ordering & unreleased issue display. Includes splitting unreleased display info into two 'Next release' pseudo-release entries. - :support:`15` Add :doc:`/concepts` to flesh out some assumptions not adequately explained in :doc:`/usage`. - :release:`0.3.0 <2013-11-21>` - :feature:`11` Fix up styling so changelogs don't look suboptimal under `the new Read The Docs theme `_. Still looks OK under their old theme too! - :support:`0` Move to actual Sphinx docs so we can use ourselves. - :support:`0` Created a basic test suite to protect against regressions. - :bug:`9 major` Clean up additional 'unreleased' display/organization behavior, including making sure ALL unreleased issues show up as 'unreleased'. Thanks to Donald Stufft for the report. - :feature:`1` (also :issue:`3`, :issue:`10`) Allow using ``-`` or ``0`` as a dummy issue 'number', which will result in no issue number/link being displayed. Thanks to Markus Zapke-Gründemann and Hynek Schlawack for patches & discussion. * This feature lets you categorize changes that aren't directly related to issues in your tracker. It's an improvement over, and replacement for, the previous "vanilla bullet list items are treated as bugs" behavior. * Said behavior (non-role-prefixed bullet list items turning into regular bugs) is being retained as there's not a lot to gain from deactivating it. - :release:`0.2.4 <2013.10.04>` - :support:`0 backported` Handful of typos, doc tweaks & addition of a .gitignore file. Thanks to Markus Zapke-Gründemann. - :bug:`0` Fix duplicate display of "bare" (not prefixed with an issue role) changelog entries. Thanks again to Markus. - :support:`0 backported` Edited the README/docs to be clearer about how Releases works/operates. - :support:`0 backported` Explicitly documented how non-role-prefixed line items are preserved. - :bug:`0` Updated non-role-prefixed line items so they get prefixed with a '[Bug]' signifier (since they are otherwise treated as bugfix items.) - :release:`0.2.3 <2013.09.16>` - :bug:`0` Fix a handful of bugs in release assignment logic. - :release:`0.2.2 <2013.09.15>` - :bug:`0` Ensured Python 3 compatibility. - :release:`0.2.1 <2013.09.15>` - :bug:`0` Fixed a stupid bug causing invalid issue hyperlinks. - :release:`0.2.0 <2013.09.15>` - :feature:`0` Basic functionality. releases-2.1.1/docs/concepts.rst0000644000175000001440000004522514374005426017615 0ustar jforcierusers00000000000000======== Concepts ======== This page contains conceptual info about how Releases organizes and thinks about issues and releases. For details on formatting/config options/etc (e.g. so you can interpret the examples below), see :doc:`/usage`. Issue and release types ======================= * Issues are always one of three types: **features**, **bug fixes** or **support items**. * **Features** are (typically larger) changes adding new behavior. * **Bug fixes** are (typically minor) changes addressing incorrect behavior, crashes, etc. * **Support items** vary in size but are usually non-code-related changes, such as documentation or packaging updates. * Releases also happen to come in three flavors: * **Major releases** are backwards incompatible releases, often with large/sweeping changes to a codebase. * They increment the first version number only, e.g. ``1.0.0``. * **Feature releases** (sometimes called **minor** or **secondary**) are backwards compatible with the previous major release, and focus on adding new functionality (code, or support, or both.) They sometimes include major/complex bug fixes which are too risky to include in a bugfix release. * The second version number is incremented for these, e.g. ``1.1.0``. * **Bugfix releases** (sometimes called **tertiary**) focus on fixing incorrect behavior while minimizing the risk of creating more bugs. Rarely, they will include small new features deemed important enough to backport from their 'native' feature release. * These releases increment the third/final version number, e.g. ``1.1.1``. Release organization ==================== We parse changelog timelines so the resulting per-release issue lists honor the above descriptions. Here are the core rules, with examples. See :doc:`/usage` for details on formatting/etc. * **By default, bugfixes go into bugfix releases, features and support items go into feature releases.** * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :support:`4` Updated our test runner * :bug:`3` Another bugfix * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.1.0``: feature #2, support #4 * ``1.0.1``: bug #1, bug #3 * **Bugfixes are assumed to backport to all stable release lines by default, and are displayed as such.** However, this can be overridden on a per-release and/or per-bug basis - see later bullet points. * Input:: * :release:`1.1.1 ` * :release:`1.0.2 ` * :bug:`3` Fixed another bug, onoes * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.1.1``: bug #3 * ``1.0.2``: bug #3 * ``1.1.0``: feature #2 * ``1.0.1``: bug #1 * **Bugfixes marked 'major' go into feature releases instead.** In other words, they're displayed as bugs, but organized as features. * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`3 major` Big bugfix with lots of changes * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.1.0``: feature #2, bug #3 * ``1.0.1``: bug #1 * **Features or support items marked 'backported' appear in both bugfix and feature releases.** In other words, they're displayed as feature/support items, but organized as a combination feature/support *and* bug item. * Input:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`4` Fixed another bug * :feature:`3` Regular feature * :feature:`2 backported` Small new feature worth backporting * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Result: * ``1.1.0``: feature #2, feature #3 * ``1.0.1``: bug #1, feature #2, bug #4 * **Releases implicitly include all issues from their own, and prior, release lines.** (Again, unless the release explicitly states otherwise - see below.) * For example, in the below changelog (remembering that changelogs are written in descending order from newest to oldest entry) the code released as ``1.1.0`` includes the changes from bugs #1 and #3, in addition to its explicitly stated contents of feature #2:: * :release:`1.1.0 ` * :release:`1.0.1 ` * :bug:`3` Another bugfix * :feature:`2` Implemented new feature * :bug:`1` Fixed a bug * :release:`1.0.0 ` * Again, to be explicit, the rendered changelog displays this breakdown: * ``1.1.0``: feature #2 * ``1.0.1``: bug #1, bug #3 But it's *implied* that ``1.1.0`` includes the contents of ``1.0.1`` because it released afterwards/simultaneously and is a higher release line. * **Releases may be told explicitly which issues to include** (using a comma-separated list.) This is useful for the rare bugfix that gets backported beyond the actively supported release lines. For example, below shows a project whose lifecycle is "release 1.0; release 1.1 and drop active support for 1.0; put out a special 1.0.x release." Without the explicit issue list for 1.0.1, Releases would roll up all bugfixes, including the two that didn't actually apply to the 1.0 line. * Input:: * :release:`1.0.1 ` 1, 5 * :release:`1.1.1 ` * :bug:`5` Bugfix that applied back to 1.0. * :bug:`4` Bugfix that didn't apply to 1.0, only 1.1 * :bug:`3` Bugfix that didn't apply to 1.0, only 1.1 * :release:`1.1.0 ` * :feature:`2` Implemented new feature * :bug:`1` Fixed a 1.0.0 bug * :release:`1.0.0 ` * Result: * ``1.1.0``: feature #2 * ``1.1.1``: bugs #3, #4 and #5 * ``1.0.1``: bugs #1 and #5 only * **Bugfix issues may be told explicitly which release line they 'start' in.** This is useful for bugs that don't go back all the way to the oldest actively supported line - it keeps them from showing up in "too-old" releases. The below example includes a project actively supporting 1.5, 1.6 and 1.7 release lines, with a couple of bugfixes that only applied to 1.6+. * Input:: * :release:`1.7.1 ` * :release:`1.6.2 ` * :release:`1.5.3 ` * :bug:`50` Bug applying to all lines * :bug:`42 (1.6+)` A bug only applying to the new feature in 1.6 * :release:`1.7.0 ` * :release:`1.6.1 ` * :release:`1.5.2 ` * :feature:`25` Another new feature * :bug:`35` Bug that applies to all lines * :bug:`34` Bug that applies to all lines * :release:`1.6.0 ` * :release:`1.5.1 ` * :feature:`22` Some new feature * :bug:`20` Bugfix * :release:`1.5.0 ` * Result: * ``1.7.1``: bugs #50 and #42 * ``1.6.2``: bugs #50 and #42 * ``1.5.3``: bug #50 only * ``1.7.0``: feature #25 * ``1.6.1``: bugs #34, #35 * ``1.5.2``: bugs #34, #35 * ``1.6.0``: feature #22 * ``1.5.1``: bug #20 * **Bugs listed before the first release are treated as though they have the 'major' keyword.** This is chiefly because it makes no sense to have a "bugfix release" as one's first-ever release - you can't fix something that's not public! Then once the changelog parser passes that initial release, normal rules start to apply again. * Input:: * :release:`0.1.1` * :bug:`3` The feature had bugs :( * :release:`0.1.0 ` * :feature:`2` Our first ever feature * :bug:`1` Explicitly marked bug, even though that is silly * Implicit issue/entry here (becomes a bug by default) * Result: * ``0.1.1``: bug #3 only, since it's the only bug after the first release. * ``0.1.0``: everything else - the implicit bug, the explicit bug #1, and the feature #2. Major releases ============== Major releases introduce additional concerns to changelog organization on top of those above. Users whose software tends to just "roll forwards" without keeping older stable branches alive for bugfix releases, will likely not need to do much. However, when your support window stretches across major version boundaries, telling Releases which issues belong to which major version (or versions plural) becomes a bit more work. There are two main rules to keep in mind when dealing with "mixed" major versions: * **All issues encountered after a major release** are considered associated with that major release line **by default**. * **All feature-like items (features, support, major bugs) encountered just prior to a major release** are considered part of the major release itself. * To force association with a **different major release** (or set of major releases), issues may **specify a 'version spec'** annotation. Here's some examples to clarify. "Rolling" releases ------------------ This example has no mixing of release lines, just moving from 1.x to 2.x. 1.x is effectively abandoned. (Hope 2.x is an easy upgrade...) Note how features 4 and 5, because they are encountered prior to 2.0.0, are attached to it automatically. Input:: * :release:`2.1.0 ` * :release:`2.0.1 ` * :feature:`7` Yet another new feature * :bug:`6` A bug :( * :release:`2.0.0 ` * :feature:`5` Another (backwards incompatible) feature! * :feature:`4` A (backwards incompatible) feature! * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`3` New feature * :bug:`2` Another bug * :bug:`1` An bug * :release:`1.0.0 ` Result: * ``2.1.0``: feature #7 * ``2.0.1``: bug #6 * ``2.0.0``: feature #4, feature #5 * ``1.1.0``: feature #3 * ``1.0.1``: bug #1, bug #2 Pretty simple, nothing actually new here. Mostly-compatible 2.0 with continued maint for 1.x -------------------------------------------------- This maintainer is a bit more conscientious/masochistic and wants to keep users of 1.x happy for a while after 2.0 launches. The timeline is very similar to the previous example, but in this scenario, all issues developed on the 1.x branch are forward-ported to 2.x, because 2.x wasn't a huge departure from 1.x. To signify this, post-2.0 issues that were developed initially for 1.x, are annotated with ``(1.0+)``, telling Releases to add them to all releases above 1.0, instead of just the most recent major release (2.0):: * :release:`2.1.0 ` * :release:`2.0.1 ` * :release:`1.2.0 ` * :release:`1.1.1 ` * :release:`1.0.2 ` * :bug:`9` A 2.0-only bugfix. * :feature:`8` A 2.0-only feature. * :feature:`7 (1.0+)` Yet another new feature * :bug:`6 (1.0+)` A bug :( * :release:`2.0.0 ` * :feature:`5` Another (backwards incompatible) feature! * :feature:`4` A (backwards incompatible) feature! * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`3` New feature * :bug:`2` Another bug * :bug:`1` An bug * :release:`1.0.0 ` Result: * ``2.1.0``: feature #7, feature #8 * ``2.0.1``: bug #6, bug #9 * ``1.2.0``: feature #7, but not feature #8 * ``1.1.1``: bug #6, but not bug #9 * ``1.0.2``: bug #6, but not bug #9 * ``2.0.0``: feature #4, feature #5 * ``1.1.0``: feature #3 * ``1.0.1``: bug #1, bug #2 Some issues forward-ported, others not -------------------------------------- This time, some issues remain 1.x-specific as they don't apply to 2.x for whatever reason. The simple "X.Y+" format doesn't let us declare this, so we use one you're familiar with from packaging systems like ``setuptools``/``pip``: * ``(<2.0)`` signifies "only included in releases lower than 2.0" * ``(>=2.0)`` says "only include in release lines 2.0 and higher" (thus applying to 2.1, 2.2, 3.0, 4.0 etc). * This is identical to saying ``(2.0+)``; the ``+`` version is just a convenient / backwards compatible shorthand. * ``(>=2.0,<3.0)`` limits an issue to *just* the 2.x line, preventing its inclusion in 1.x, 3.x or anything else. * And so on; see the documentation for the ``Spec`` class at https://python-semanticversion.readthedocs.io for details. * To be clear, **you may put any combination of major+minor version number in these annotations**, just as with the simpler ``(1.5+)`` style format. * This is mostly applicable to bugs or backported issues. Features, support items and major bugs only need to inform Releases about major release lines. Armed with this more powerful syntax, we can limit some issues just to the 1.x line:: * :release:`2.1.0 ` * :release:`2.0.1 ` * :release:`1.2.0 ` * :release:`1.1.1 ` * :release:`1.0.2 ` * :feature:`9 (>=1.0)` A new feature that works with both versions (using the more explicit version of "1.0+") * :feature:`8` A new feature that only works on 2.x (no annotation needed) * :bug:`7 (<2.0)` A bug only affecting 1.x * :bug:`6 (1.0+)` A bug affecting all versions * :release:`2.0.0 ` * :feature:`5` Another (backwards incompatible) feature! * :feature:`4` A (backwards incompatible) feature! * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`3` New feature * :bug:`2` Another bug * :bug:`1` An bug * :release:`1.0.0 ` Result: * ``2.1.0``: feature #8, feature #9 * ``2.0.1``: bug #6 (but not #7) * ``1.2.0``: feature #9 (but not #8) * ``1.1.1``: bug #6, bug #7 * ``1.0.2``: bug #6, bug #7 * ``2.0.0``: feature #4, feature #5 * ``1.1.0``: feature #3 * ``1.0.1``: bug #1, bug #2 Mixed-but-exclusive features prior to a new major release --------------------------------------------------------- This example illustrates a corner case where one is actively maintaining a "current" 1.x line at the same time as releasing the new 2.x line. Unlike the earlier examples, this one has both "2.0-only" *and* "1.0-only" features in the run-up to 2.0.0 (plus bugs). In this scenario, the non-annotated features are automatically assigned to the 2.0 major version, even though the 1.2.0 release technically came out "before" 2.0.0. As long as no non-release line items appear between 1.2.0 and 2.0.0, the system will behave as if 2.0.0 was the "primary" next release, with 1.2.0 only capturing features explicitly annotated as being "<2.0" (or similar). .. note:: This behavior holds true even if the adjacent release line-items have different dates; the heuristic is solely about their placement in the changelog list. Note also how bugs found in this window just prior to 2.0.0, remain associated with the 1.x line that they are fixing; it wouldn't make sense to publish a bugfix for unreleased functionality. Changelog:: * :release:`2.0.0 ` * :release:`1.2.0 ` * :release:`1.1.1 ` * :bug:`6` A bug found after 1.1.0 came out * :feature:`5 (<2.0)` A 1.0-only feature! * :feature:`4` A (backwards incompatible) feature! * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`3` New feature * :bug:`2` Another bug * :bug:`1` An bug * :release:`1.0.0 ` Result: * ``2.0.0``: feature #4 (but not feature #5) * ``1.2.0``: feature #5 (but not feature #4) * ``1.1.1``: bug 6 * ``1.1.0``: feature #3 * ``1.0.1``: bug #1, bug #2 .. _unstable-prehistory: "Unstable prehistory" mode ========================== All of the above assumes a mature, semantic-versioning-enabled project, where you have stable release lines as well as a feature development 'trunk' branch. This doesn't always describe young projects, however - before one's 1.0.0, semantic versioning may not apply strongly or at all. When the ``releases_unstable_prehistory`` option is enabled (it's off by default for backwards compatibility reasons), changelog parsing/organizing behaves differently, until releases other than ``0.x.x`` are encountered: * All issues, regardless of type, are assigned to the very next release; there's no organizing along minor release lines, no 'major' bugs are necessary, nor are 'backported' features. * Unmarked line-items - which are normally considered to be bugs - are displayed without any classification (i.e. they don't get a 'Bug' prefix). * This is mostly to enable the types of "pre-Releases" changelogs wherein *all* line items lack issue-type role prefixes. * If your changelog *does* include explicit role prefixes (``:bug:``, ``:feature:`` etc) they are left untouched & will still visually appear as the indicated type. Example ------- Here's an example of what this option means. Take the following changelog:: * :release:`0.2.1 ` * Bugfix #7 * Feature #6, but meh, we arbitrarily are gonna call the next release a tertiary one anyways * Bugfix #5 * :release:`0.2.0 ` * Medium bugfix #4 * Tiny bugfix #3 * Feature #2 * :release:`0.1.0 ` * It works! First public release. Under normal Releases behavior this wouldn't match what the author clearly intends - all of these line items lack roles, so they'd all be "bugs", and then none of them would get inserted into 0.1.0 or 0.2.0 which are feature releases. With ``releases_unstable_prehistory`` enabled, we instead get: * ``0.2.1``: bugfix 5, feature 6, bugfix 7 * ``0.2.0``: feature 2, bugfix 3, bugfix 4 * ``0.1.0``: the beginning-of-time "it works!" note Crossing the 1.0 boundary ------------------------- As mentioned, even when this option is enabled, the 1.0.0 release (or whichever release is the first not beginning with ``0.``) implicitly deactivates this behavior. All subsequent issues then follow the behavior outlined in the rest of the document: bugfixes only go in tertiary releases, features only go in minor releases, etc. Another explicit example - this changelog (which is even more arbitrary with its versioning prior to 1.0):: * :release:`1.1.0 ` * :release:`1.0.1 ` * :feature:`8` A new, backwards compatible feature, hooray * :bug:`7` First post-1.0 bugfix! * :release:`1.0.0 ` * Bug #6 * Feature #5 * `0.5.0` * Feature #4 * Bug #3 * Bug #2 * `0.1.0` * Feature #1 The resulting changelog is organized like so: * ``1.1.0``: Feature #8 * ``1.0.1``: Bug #7 - no features, this is the first "real" bugfix release * ``1.0.0``: Bug #6, feature #5 - this is the last "unstable" release rolling up all prior issues. * ``0.5.0``: Bug #2, bug #3, feature #4 * ``0.1.0``: Feature #1 releases-2.1.1/docs/usage.rst0000644000175000001440000001771514423013133017073 0ustar jforcierusers00000000000000===== Usage ===== To use Releases, mimic the format seen in `its own changelog `_ or in `Fabric's changelog `_. Specifically: * Install ``releases`` and update your Sphinx ``conf.py`` to include it in the ``extensions`` list setting: ``extensions = ['releases']``. * Also set the ``releases_release_uri`` and ``releases_issue_uri`` top level options - they determine the targets of the issue & release links in the HTML output. Both must include a ``{number}`` slug (for use with `str.format`) where the release/issue number should go; the older ``%s`` style is also acceptable. * Alternately, if your project is hosted on Github, set the ``releases_github_path`` setting instead, to e.g. ``account/project``. Releases will then use an appropriate Github URL for both releases and issues. * If ``releases_release_uri`` or ``releases_issue_uri`` are *also* configured, they will be preferred over ``releases_github_path``. (If only one is configured, the other link type will continue using ``releases_github_path``.) * See `Fabric's docs/conf.py `_ for an example. * You may optionally set ``releases_debug = True`` to see debug output while building your docs. * If your changelog includes "simple" pre-1.0 releases derived from a single branch (i.e. without stable release lines & semantic versioning) you may want to set ``releases_unstable_prehistory = True``. * This is also useful if you've just imported a non-Releases changelog, where your issues are all basic list-items and you don't want to go through and add bug/feature/support/etc roles. * See :ref:`the appropriate conceptual docs ` for details on this behavior. * If your development branch which is linked to for unreleased changelog items, does not match the current default (``master`` as of version 2.0, to be changed to ``main`` in 3.0), you may override it via ``releases_development_branch``. * Projects with a long history of major release versions may want to specify which of them get 'unreleased' entries at the top of the changelog; set ``releases_supported_versions`` to a list of major version numbers, eg ``releases_supported_versions = [2, 3]`` to drop any "Next 1.x (feature|bugfix)" buckets. * Create a Sphinx document named ``changelog.rst`` containing a bulleted list somewhere at its topmost level. * If you wish to use a different document name, use another config option (as per previous bullet point), ``releases_document_name``. E.g. ``releases_document_name = "CHANGES"`` would cause Releases to mutate a file called ``CHANGES.rst`` instead of ``changelog.rst``. * It is possible to target multiple changelog files for mutation by setting ``releases_document_name`` to a list of strings instead of a single string, e.g. ``releases_document_name = ['project_1/changelog', 'project_2/changes', 'changelog']``. * Releases only modifies the bulleted list in these files and does not touch other elements; this allows you to place paragraphs, comments etc at the top (or bottom) of the document. * List items are to be ordered chronologically with the newest ones on top. * As you fix issues, put them on the top of the list. * As you cut releases, put those on the top of the list and they will include the issues below them. * Issues with no releases above them will end up in a specially marked "Unreleased" section of the rendered changelog. * Bullet list items should use the ``support``, ``feature`` or ``bug`` roles to mark issues, or ``release`` to mark a release. These special roles must be the first element in each list item. * Line-items that do not start with any issue role will be considered bugs (both in terms of inclusion in releases, and formatting) and, naturally, will not be given a hyperlink. * Issue roles are of the form ``:type:`number[ keyword]```. Specifically: * ``number`` is used to generate the link to the actual issue in your issue tracker (going by the ``releases_issue_uri`` option). It's used for both the link target & (part of) the link text. * If ``number`` is given as ``-`` or ``0`` (as opposed to a "real" issue number), no issue link will be generated. You can use this for items without a related issue. * Keywords are optional and may be one of: * ``backported``: Given on *support* or *feature* issues to denote backporting to bugfix releases; such issues will show up in both release types. E.g. placing ``:support:`123 backported``` in your changelog below releases '1.1.1' and '1.2.0' will cause it to appear in both of those releases' lists. * ``major``: Given on *bug* issues to denote inclusion in feature, instead of bugfix, releases. E.g. placing ``:bug:`22 major``` below releases '1.1.1' and '1.2.0' will cause it to appear in '1.2.0' **only**. * ``(N.N+)`` where ``N.N`` is a valid release line, e.g. ``1.1`` or ``2.10``: Given on issues (usually *bugs*) to denote minimum release line. E.g. when actively backporting most bugs to release lines 1.2, 1.3 and 1.4, you might specify ``:bug:`55 (1.3+)``` to note that bug 55 only applies to releases in 1.3 and above - not 1.2. * A `semantic version range spec covering minor+major version numbers `_ such as ``(<2.0)`` or ``(>=1.0,<3.1)``. A more powerful version of ``(N.N+)`` allowing annotation of issues belonging to specific major versions. .. note:: It is possible to give *both* a regular keyword (``backported``/``major``) *and* a spec (``(N.N+)``/``(>=1.0)``) in the same issue. However, giving two keywords or two specs at the same time makes no sense & is not allowed. * Regular Sphinx content may be given after issue roles and will be preserved as-is when rendering. For example, in ``:bug:`123` Fixed a bug, thanks `@somebody`!``, the rendered changelog will preserve/render "Fixed a bug, thanks ``@somebody``!" after the issue link. * Release roles are of the form ``:release:`number ```. * You may place a comma-separated (whitespace optional) list of issue numbers after the release role, and this will limit the issues included in that release to that explicit list. * Otherwise, releases include all relevant issues as outlined above and in :doc:`/concepts`. Then build your docs; in the rendered output, ``changelog.html`` should show issues grouped by release, as per the above rules. Examples: `Releases' own rendered changelog `_, `Fabric's rendered changelog `_. Optional styling additions ========================== If you have any nontrivial changelog entries (e.g. whose description spans multiple paragraphs or includes their own bulleted lists, etc) you may run into `docutils' rather enthusiastic bulleted list massaging `_ which can then make your releases look different from one another. To help combat this, it may be useful to add the following rule to the Sphinx theme you're using:: div#changelog > div.section > ul > li > p:only-child { margin-bottom: 0; } .. note:: Some themes, like `Alabaster `_, may already include this style rule. releases-2.1.1/setup.cfg0000644000175000001440000000004614423015165016122 0ustar jforcierusers00000000000000[egg_info] tag_build = tag_date = 0 releases-2.1.1/LICENSE0000644000175000001440000000244214253147100015304 0ustar jforcierusers00000000000000Copyright (c) 2020, Jeff Forcier All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. releases-2.1.1/MANIFEST.in0000644000175000001440000000026514253147100016036 0ustar jforcierusers00000000000000include LICENSE include dev-requirements.txt include tasks.py recursive-include docs * recursive-exclude docs/_build * recursive-include tests * recursive-exclude tests *.pyc *.pyo releases-2.1.1/tests/0000755000175000001440000000000014423015165015443 5ustar jforcierusers00000000000000releases-2.1.1/tests/_util.py0000644000175000001440000000665414347504500017145 0ustar jforcierusers00000000000000from docutils.nodes import list_item, paragraph from unittest.mock import Mock from releases import ( Issue, issues_role, Release, release_role, construct_releases, ) from releases.util import make_app, changelog2dict def inliner(app=None): app = app or make_app() return Mock(document=Mock(settings=Mock(env=Mock(app=app)))) # Obtain issue() object w/o wrapping all parse steps def issue(type_, number, **kwargs): text = str(number) if kwargs.get("backported", False): text += " backported" if kwargs.get("major", False): text += " major" if kwargs.get("spec", None): text += f" ({kwargs['spec']})" app = kwargs.get("app", None) return issues_role( name=type_, rawtext="", text=text, lineno=None, inliner=inliner(app=app), )[0][0] # Even shorter shorthand! def b(number, **kwargs): return issue("bug", str(number), **kwargs) def f(number, **kwargs): return issue("feature", str(number), **kwargs) def s(number, **kwargs): return issue("support", str(number), **kwargs) def entry(i): """ Easy wrapper for issue/release objects. Default is to give eg an issue/release object that gets wrapped in a LI->P. May give your own (non-issue/release) object to skip auto wrapping. (Useful since entry() is often called a few levels deep.) """ if not isinstance(i, (Issue, Release)): return i return list_item("", paragraph("", "", i)) def release(number, **kwargs): app = kwargs.get("app", None) nodes = release_role( name=None, rawtext="", text=f"{number} <2013-11-20>", lineno=None, inliner=inliner(app=app), )[0] return list_item("", paragraph("", "", *nodes)) def release_list(*entries, **kwargs): skip_initial = kwargs.pop("skip_initial", False) entries = list(entries) # lol tuples # Translate simple objs into changelog-friendly ones for index, item in enumerate(entries): if isinstance(item, str): entries[index] = release(item) else: entries[index] = entry(item) # Insert initial/empty 1st release to start timeline if not skip_initial: entries.append(release("1.0.0")) return entries def releases(*entries, **kwargs): app = kwargs.pop("app", None) or make_app() return construct_releases(release_list(*entries, **kwargs), app)[0] def setup_issues(self): self.f = f(12) self.s = s(5) self.b = b(15) self.mb = b(200, major=True) self.bf = f(27, backported=True) self.bs = s(29, backported=True) def expect_releases(entries, release_map, skip_initial=False, app=None): kwargs = {"skip_initial": skip_initial} # Let high level tests tickle config settings via make_app() if app is not None: kwargs["app"] = app changelog = changelog2dict(releases(*entries, **kwargs)) snapshot = dict(changelog) err = "Got unexpected contents for {}: wanted {}, got {}" err += "\nFull changelog: {!r}\n" for rel, issues in release_map.items(): found = changelog.pop(rel) msg = err.format(rel, issues, found, snapshot) assert set(found) == set(issues), msg # Sanity: ensure no leftover issue lists exist (empty ones are OK) for key in list(changelog.keys()): if not changelog[key]: del changelog[key] assert not changelog, "Found leftovers: {}".format(changelog) releases-2.1.1/tests/organization.py0000644000175000001440000004521314423013133020517 0ustar jforcierusers00000000000000from pytest import skip from pytest_relaxed import raises from docutils.nodes import list_item, raw, paragraph, Text from releases import Issue, construct_releases from _util import ( b, f, s, changelog2dict, expect_releases, make_app, release_list, releases, setup_issues, ) class organization: """ Organization of issues into releases (parsing) """ def setup_method(self): setup_issues(self) def _expect_entries(self, all_entries, in_, not_in): # Grab 2nd release as 1st is the empty 'beginning of time' one entries = releases(*all_entries)[1]["entries"] assert len(entries) == len(in_) for x in in_: assert x in entries for x in not_in: assert x not in entries def feature_releases_include_features_and_support_not_bugs(self): self._expect_entries( ["1.1.0", self.f, self.b, self.s], [self.f, self.s], [self.b] ) def feature_releases_include_major_bugs(self): self._expect_entries( ["1.1.0", self.f, self.b, self.mb], [self.f, self.mb], [self.b] ) def bugfix_releases_include_bugs(self): self._expect_entries( ["1.0.2", self.f, self.b, self.mb], [self.b], [self.mb, self.f] ) def bugfix_releases_include_backported_features(self): self._expect_entries( ["1.0.2", self.bf, self.b, self.s], [self.b, self.bf], [self.s] ) def bugfix_releases_include_backported_support(self): self._expect_entries( ["1.0.2", self.f, self.b, self.s, self.bs], [self.b, self.bs], [self.s, self.f], ) def backported_features_also_appear_in_feature_releases(self): entries = ("1.1.0", "1.0.2", self.bf, self.b, self.s) # Ensure bf (backported feature) is in BOTH 1.0.2 AND 1.1.0 expected = {"1.0.2": [self.bf, self.b], "1.1.0": [self.bf, self.s]} expect_releases(entries, expected) def unmarked_bullet_list_items_treated_as_bugs(self): fake = list_item("", paragraph("", "", raw("", "whatever"))) changelog = releases("1.0.2", self.f, fake) entries = changelog[1]["entries"] assert len(entries) == 1 assert self.f not in entries assert isinstance(entries[0], Issue) assert entries[0].number is None def unreleased_items_go_in_unreleased_releases(self): changelog = releases(self.f, self.b) # Should have two unreleased lists, one feature w/ feature, one bugfix # w/ bugfix. bugfix, feature = changelog[1:] assert len(feature["entries"]) == 1 assert len(bugfix["entries"]) == 1 assert self.f in feature["entries"] assert self.b in bugfix["entries"] assert feature["obj"].number == "unreleased_1.x_feature" assert bugfix["obj"].number == "unreleased_1.x_bugfix" def issues_consumed_by_releases_are_not_in_unreleased(self): changelog = releases("1.0.2", self.f, self.b, self.s, self.bs) release = changelog[1]["entries"] unreleased = changelog[-1]["entries"] assert self.b in release assert self.b not in unreleased def oddly_ordered_bugfix_releases_and_unreleased_list(self): # Release set up w/ non-contiguous feature+bugfix releases; catches # funky problems with 'unreleased' buckets b2 = b(2) f3 = f(3) changelog = releases("1.1.1", "1.0.2", self.f, b2, "1.1.0", f3, self.b) assert f3 in changelog[1]["entries"] assert b2 in changelog[2]["entries"] assert b2 in changelog[3]["entries"] def release_line_bugfix_specifier(self): b50 = b(50) b42 = b(42, spec="1.1+") f25 = f(25) b35 = b(35) b34 = b(34) f22 = f(22) b20 = b(20) c = changelog2dict( releases( "1.2.1", "1.1.2", "1.0.3", b50, b42, "1.2.0", "1.1.1", "1.0.2", f25, b35, b34, "1.1.0", "1.0.1", f22, b20, ) ) for rel, issues in ( ("1.0.1", [b20]), ("1.1.0", [f22]), ("1.0.2", [b34, b35]), ("1.1.1", [b34, b35]), ("1.2.0", [f25]), ("1.0.3", [b50]), # the crux - is not b50 + b42 ("1.1.2", [b50, b42]), ("1.2.1", [b50, b42]), ): err = "Expected {} to contain {!r}, but it contained {!r}" got, expected = set(c[rel]), set(issues) assert got == expected, err.format(rel, expected, got) def releases_can_specify_issues_explicitly(self): # Build regular list-o-entries b2 = b(2) b3 = b(3) changelog = release_list( "1.0.1", "1.1.1", b3, b2, self.b, "1.1.0", self.f ) # Modify 1.0.1 release to be speshul changelog[0][0].append(Text("2, 3")) rendered, _ = construct_releases(changelog, make_app()) # 1.0.1 includes just 2 and 3, not bug 1 one_0_1 = rendered[3]["entries"] one_1_1 = rendered[2]["entries"] assert self.b not in one_0_1 assert b2 in one_0_1 assert b3 in one_0_1 # 1.1.1 includes all 3 (i.e. the explicitness of 1.0.1 didn't affect # the 1.1 line bucket.) assert self.b in one_1_1 assert b2 in one_1_1 assert b3 in one_1_1 def explicit_release_list_split_works_with_unicode(self): changelog = release_list("1.0.1", b(17)) changelog[0][0].append(Text(str("17"))) # When using naive method calls, this explodes construct_releases(changelog, make_app()) def explicit_feature_release_features_are_removed_from_unreleased(self): f1 = f(1) f2 = f(2) changelog = release_list("1.1.0", f1, f2) # Ensure that 1.1.0 specifies feature 2 changelog[0][0].append(Text("2")) rendered = changelog2dict(construct_releases(changelog, make_app())[0]) # 1.1.0 should have feature 2 only assert f2 in rendered["1.1.0"] assert f1 not in rendered["1.1.0"] # unreleased feature list should still get/see feature 1 assert f1 in rendered["unreleased_1.x_feature"] # now-released feature 2 should not be in unreleased_feature assert f2 not in rendered["unreleased_1.x_feature"] class unsupported_families_not_included_in_unreleased: _entries = ( f(7), # should appear in unreleased features for 3.x b(6), # should appear in unreleased bugs for 3.x "3.0.0", f(5), # should appear in unreleased features for 2.x b(4), # should appear in unreleased bugs for 2.x "2.0.0", f(3), # should appear in unreleased features for 1.x b(2), # should appear in unreleased bugs for 1.x "1.0.1", b(1), # prehistory ) def no_actual_hiding_when_given_but_contains_all_families(self): # Expectation: everything families = [1, 2, 3] for option in (None, families): # Also test default None changelog = release_list(*self._entries) releases = changelog2dict( construct_releases( changelog, make_app(supported_versions=option) )[0] ) for major in families: for type_ in ("bugfix", "feature"): assert f"unreleased_{major}.x_{type_}" in releases def one_old_family_hidden(self): changelog = release_list(*self._entries) releases = changelog2dict( construct_releases( changelog, make_app(supported_versions=[2, 3]) )[0] ) for type_ in ("bugfix", "feature"): assert f"unreleased_1.x_{type_}" not in releases assert f"unreleased_2.x_{type_}" in releases assert f"unreleased_3.x_{type_}" in releases def multiple_old_families_hidden(self): changelog = release_list(*self._entries) releases = changelog2dict( construct_releases( changelog, make_app(supported_versions=[3]) )[0] ) for type_ in ("bugfix", "feature"): assert f"unreleased_1.x_{type_}" not in releases assert f"unreleased_2.x_{type_}" not in releases assert f"unreleased_3.x_{type_}" in releases def in_between_family_hidden_for_mysterious_reasons(self): changelog = release_list(*self._entries) releases = changelog2dict( construct_releases( changelog, make_app(supported_versions=[1, 3]) )[0] ) for type_ in ("bugfix", "feature"): assert f"unreleased_1.x_{type_}" in releases assert f"unreleased_2.x_{type_}" not in releases assert f"unreleased_3.x_{type_}" in releases def explicit_bugfix_releases_get_removed_from_unreleased(self): b1 = b(1) b2 = b(2) changelog = release_list("1.0.1", b1, b2) # Ensure that 1.0.1 specifies bug 2 changelog[0][0].append(Text("2")) rendered, _ = construct_releases(changelog, make_app()) # 1.0.1 should have bug 2 only assert b2 in rendered[1]["entries"] assert b1 not in rendered[1]["entries"] # unreleased bug list should still get/see bug 1 assert b1 in rendered[2]["entries"] @raises(ValueError) def explicit_releases_error_on_unfound_issues(self): # Just a release - result will have 1.0.0, 1.0.1, and unreleased changelog = release_list("1.0.1") # No issues listed -> this clearly doesn't exist in any buckets changelog[1][0].append(Text("25")) # This should asplode construct_releases(changelog, make_app()) def duplicate_issue_numbers_adds_two_issue_items(self): test_changelog = releases("1.0.1", self.b, self.b) test_changelog = changelog2dict(test_changelog) assert len(test_changelog["1.0.1"]) == 2 def duplicate_zeroes_dont_error(self): cl = releases("1.0.1", b(0), b(0)) cl = changelog2dict(cl) assert len(cl["1.0.1"]) == 2 def issues_are_sorted_by_type_within_releases(self): b1 = b(123, major=True) b2 = b(124, major=True) s1 = s(25) s2 = s(26) f1 = f(3455) f2 = f(3456) # Semi random definitely-not-in-desired-order order changelog = changelog2dict(releases("1.1", b1, s1, s2, f1, b2, f2)) # Order should be feature, bug, support. While it doesn't REALLY # matter, assert that within each category the order matches the old # 'reverse chronological' order. assert changelog["1.1"], [f2, f1, b2, b1, s2 == s1] def rolling_release_works_without_annotation(self): b1 = b(1) b2 = b(2) f3 = f(3) f4 = f(4) f5 = f(5) b6 = b(6) f7 = f(7) entries = ( "2.1.0", "2.0.1", f7, b6, "2.0.0", f5, f4, "1.1.0", "1.0.1", f3, b2, b1, ) expected = { "1.0.1": [b1, b2], "1.1.0": [f3], "2.0.0": [f4, f5], "2.0.1": [b6], "2.1.0": [f7], } expect_releases(entries, expected) def plus_annotations_let_old_lines_continue_getting_released(self): b9 = b(9) f8 = f(8) f7 = f(7, spec="1.0+") b6 = b(6, spec="1.0+") f5 = f(5) f4 = f(4) f3 = f(3) b2 = b(2) b1 = b(1) entries = ( "2.1.0", "2.0.1", "1.2.0", "1.1.1", "1.0.2", b9, f8, f7, b6, "2.0.0", f5, f4, "1.1.0", "1.0.1", f3, b2, b1, ) expected = { "2.1.0": [f7, f8], "2.0.1": [b6, b9], "1.2.0": [f7], # but not f8 "1.1.1": [b6], # but not b9 "1.0.2": [b6], # but not b9 "2.0.0": [f4, f5], "1.1.0": [f3], "1.0.1": [b1, b2], } expect_releases(entries, expected) def semver_spec_annotations_allow_preventing_forward_porting(self): f9 = f(9, spec=">=1.0") f8 = f(8) b7 = b(7, spec="<2.0") b6 = b(6, spec="1.0+") f5 = f(5) f4 = f(4) f3 = f(3) b2 = b(2) b1 = b(1) entries = ( "2.1.0", "2.0.1", "1.2.0", "1.1.1", "1.0.2", f9, f8, b7, b6, "2.0.0", f5, f4, "1.1.0", "1.0.1", f3, b2, b1, ) expected = { "2.1.0": [f8, f9], "2.0.1": [b6], # (but not #7) "1.2.0": [f9], # (but not #8) "1.1.1": [b6, b7], "1.0.2": [b6, b7], "2.0.0": [f4, f5], "1.1.0": [f3], "1.0.1": [b1, b2], } expect_releases(entries, expected) def bugs_before_major_releases_associate_with_previous_release_only(self): b1 = b(1) b2 = b(2) f3 = f(3) f4 = f(4) f5 = f(5, spec="<2.0") b6 = b(6) entries = ( "2.0.0", "1.2.0", "1.1.1", b6, f5, f4, "1.1.0", "1.0.1", f3, b2, b1, ) expected = { "2.0.0": [f4], # but not f5 "1.2.0": [f5], # but not f4 "1.1.1": [b6], "1.1.0": [f3], "1.0.1": [b1, b2], } expect_releases(entries, expected) def semver_double_ended_specs_work_when_more_than_two_major_versions(self): skip() def can_disable_default_pin_to_latest_major_version(self): skip() def features_before_first_release_function_correctly(self): f0 = f(0) b1 = b(1) f2 = f(2) entries = ("0.2.0", f2, "0.1.1", b1, "0.1.0", f0) expected = {"0.1.0": [f0], "0.1.1": [b1], "0.2.0": [f2]} # Make sure to skip typically-implicit 1.0.0 release. # TODO: consider removing that entirely; arguably needing it is a bug? expect_releases(entries, expected, skip_initial=True) def all_bugs_before_first_release_act_featurelike(self): b1 = b(1) f2 = f(2) b3 = b(3) implicit = list_item("", paragraph("", "", raw("", "whatever"))) changelog = changelog2dict( releases("0.1.1", b3, "0.1.0", f2, b1, implicit, skip_initial=True) ) first = changelog["0.1.0"] second = changelog["0.1.1"] assert b1 in first assert f2 in first assert len(first) == 3 # Meh, hard to assert about the implicit one assert second == [b3] def specs_and_keywords_play_together_nicely(self): b1 = b(1) b2 = b(2, major=True, spec="1.0+") f3 = f(3) # Feature copied to both 1.x and 2.x branches f4 = f(4, spec="1.0+") # Support item backported to bugfix line + 1.17 + 2.0.0 s5 = s(5, spec="1.0+", backported=True) entries = ("2.0.0", "1.17.0", "1.16.1", s5, f4, f3, b2, b1, "1.16.0") expected = { "1.16.1": [b1, s5], # s5 backported ok "1.17.0": [b2, f4, s5], # s5 here too, plus major bug b2 "2.0.0": [b2, f3, f4, s5], # all featurelike items here } expect_releases(entries, expected) def changelogs_without_any_releases_display_unreleased_normally(self): changelog = releases(self.f, self.b, skip_initial=True) # Ensure only the two unreleased 'releases' showed up assert len(changelog) == 2 # And assert that both items appeared in one of them (since there's no # real releases at all, the bugfixes are treated as 'major' bugs, as # per concepts doc.) bugfix, feature = changelog assert len(feature["entries"]) == 2 assert len(bugfix["entries"]) == 0 class unstable_prehistory: def _expect_releases(self, *args, **kwargs): """ expect_releases() wrapper setting unstable_prehistory by default """ kwargs["app"] = make_app(unstable_prehistory=True) return expect_releases(*args, **kwargs) def all_issue_types_rolled_up_together(self): # Pre-1.0-only base case entries = ("0.1.1", f(4), b(3), "0.1.0", f(2), b(1)) expected = {"0.1.1": [b(3), f(4)], "0.1.0": [b(1), f(2)]} self._expect_releases(entries, expected, skip_initial=True) def does_not_affect_releases_after_1_0(self): # Mixed changelog crossing 1.0 boundary entries = ( "1.1.0", "1.0.1", f(6), b(5), "1.0.0", f(4), b(3), "0.1.0", f(2), b(1), ) expected = { "1.1.0": [f(6)], "1.0.1": [b(5)], "1.0.0": [b(3), f(4)], "0.1.0": [b(1), f(2)], } self._expect_releases(entries, expected, skip_initial=True) def doesnt_care_if_you_skipped_1_0_entirely(self): # Mixed changelog where 1.0 is totally skipped and one goes to 2.0 entries = ( "2.1.0", "2.0.1", f(6), b(5), "2.0.0", f(4), b(3), "0.1.0", f(2), b(1), ) expected = { "2.1.0": [f(6)], "2.0.1": [b(5)], "2.0.0": [b(3), f(4)], "0.1.0": [b(1), f(2)], } self._expect_releases(entries, expected, skip_initial=True) def explicit_unstable_releases_still_eat_their_issues(self): # I.e. an 0.x.y releases using explicit issue listings, works # correctly - the explicitly listed issues don't appear in nearby # implicit releases. skip() releases-2.1.1/tests/conftest.py0000644000175000001440000000014614423013133017634 0ustar jforcierusers00000000000000from icecream import ic, install as install_ic install_ic() ic.configureOutput(includeContext=True) releases-2.1.1/tests/presentation.py0000644000175000001440000002464314423013133020532 0ustar jforcierusers00000000000000from docutils.nodes import ( reference, bullet_list, list_item, literal, raw, paragraph, Text, ) from releases import Issue, construct_releases, construct_nodes from _util import b, f, s, entry, make_app, release, releases, setup_issues def _obj2name(obj): cls = obj if isinstance(obj, type) else obj.__class__ return cls.__name__.split(".")[-1] def _expect_type(node, cls): type_ = _obj2name(node) name = _obj2name(cls) msg = f"Expected {node!r} to be a {name}, but it's a {type_}" assert isinstance(node, cls), msg class presentation: """ Expansion/extension of docutils nodes (rendering) """ def setup_method(self): setup_issues(self) def _generate(self, *entries, **kwargs): raw = kwargs.pop("raw", False) nodes = construct_nodes(releases(*entries, **kwargs)) # By default, yield the contents of the bullet list. return nodes if raw else nodes[0][1][0] def _test_link(self, kwargs, type_, expected, entries=None): app = make_app(**kwargs) # Lazy-evaluated entries: (callable, *args) entries = entries or [ (release, "1.0.2"), (b, 15), (release, "1.0.0"), ] nodes = construct_nodes( construct_releases( [entry(x[0](*x[1:], app=app)) for x in entries], app=app, )[0] ) # Shorthand for "I'll do my own asserts" if expected is None: return nodes if type_ == "release": header = nodes[0][0][0].astext() assert expected in header elif type_ == "issue": link = nodes[0][1][0][0][2] assert link["refuri"] == expected else: raise Exception("Gave unknown type_ kwarg to _test_link()!") def issues_with_numbers_appear_as_number_links(self): self._test_link({}, "issue", "bar_15") def releases_appear_as_header_links(self): self._test_link({}, "release", "foo_1.0.2") def links_will_use_github_option_if_defined(self): kwargs = { "release_uri": None, "issue_uri": None, "github_path": "foo/bar", } for type_, expected in ( ("issue", "https://github.com/foo/bar/issues/15"), ("release", "https://github.com/foo/bar/tree/1.0.2"), ): self._test_link(kwargs, type_, expected) def issue_links_prefer_explicit_setting_over_github_setting(self): kwargs = { "release_uri": None, "issue_uri": "explicit_issue_%s", "github_path": "foo/bar", } self._test_link(kwargs, "issue", "explicit_issue_15") def release_links_prefer_explicit_setting_over_github_setting(self): kwargs = { "release_uri": "explicit_release_%s", "issue_uri": None, "github_path": "foo/bar", } self._test_link(kwargs, "release", "explicit_release_1.0.2") def links_allow_curlybrace_number_formatting_too(self): kwargs = { "release_uri": "release_{number}", "issue_uri": "issue_{number}", "github_path": None, } self._test_link(kwargs, "release", "release_1.0.2") self._test_link(kwargs, "issue", "issue_15") def completely_blank_uri_settings_does_not_asplode(self): kwargs = {"release_uri": None, "issue_uri": None, "github_path": None} # Get nodes for direct inspection nodes = self._test_link(kwargs, "release", None) # Ensure release entry still displays release version. # (These are curently constructed as raw text nodes so no other great # way to test this. Meh.) text = nodes[0][0][0].astext() assert ">1.0.2 Bug] #15:" in text def _assert_prefix(self, entries, expectation): assert expectation in self._generate(*entries)[0][0][0] def bugs_marked_as_bugs(self): self._assert_prefix(["1.0.2", self.b], "Bug") def features_marked_as_features(self): self._assert_prefix(["1.1.0", self.f], "Feature") def support_marked_as_support(self): self._assert_prefix(["1.1.0", self.s], "Support") def dashed_issues_appear_as_unlinked_issues(self): node = self._generate("1.0.2", b("-")) assert not isinstance(node[0][2], reference) def zeroed_issues_appear_as_unlinked_issues(self): node = self._generate("1.0.2", b(0)) assert not isinstance(node[0][2], reference) def un_prefixed_list_items_appear_as_unlinked_bugs(self): fake = list_item( "", paragraph( "", "", Text("fixes an issue in "), literal("", "methodname") ), ) node = self._generate("1.0.2", fake) # [, , , , ] assert len(node[0]) == 5 assert "Bug" in str(node[0][0]) assert "fixes an issue" in str(node[0][3]) assert "methodname" in str(node[0][4]) def un_prefixed_list_items_get_no_prefix_under_unstable_prehistory(self): app = make_app(unstable_prehistory=True) fake = list_item("", paragraph("", "", raw("", "whatever"))) node = self._generate("0.1.0", fake, app=app, skip_initial=True) # [] assert len(node[0]) == 1 assert "Bug" not in str(node[0][0]) assert "whatever" in str(node[0][0]) def issues_remain_wrapped_in_unordered_list_nodes(self): node = self._generate("1.0.2", self.b, raw=True)[0][1] _expect_type(node, bullet_list) _expect_type(node[0], list_item) def release_headers_have_local_style_tweaks(self): node = self._generate("1.0.2", self.b, raw=True)[0][0] _expect_type(node, raw) # Header w/ bottom margin assert '

{name.capitalize()}]' # noqa signifier = [nodes.raw(text=which, format="html")] id_nodelist = [nodes.inline(text=" "), identifier] if identifier else [] trail = [] if identifier else [nodes.inline(text=" ")] return signifier + id_nodelist + [nodes.inline(text=":")] + trail release_line_re = re.compile(r"^(\d+\.\d+)\+$") # e.g. '1.2+' def scan_for_spec(keyword): """ Attempt to return some sort of Spec from given keyword value. Returns None if one could not be derived. """ # Both 'spec' formats are wrapped in parens, discard keyword = keyword.lstrip("(").rstrip(")") # First, test for intermediate '1.2+' style matches = release_line_re.findall(keyword) if matches: return Spec(f">={matches[0]}") # Failing that, see if Spec can make sense of it try: return Spec(keyword) # I've only ever seen Spec fail with ValueError. except ValueError: return None # TODO 3.0: make this less dumb, do away with %s, just have callers do # f-strings or un-opinionated .format def interpolate(text, number): if "%s" in text: return text % number return text.format(number=number) def issues_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ Use: :issue|bug|feature|support:`ticket_number` When invoked as :issue:, turns into just a "#NN" hyperlink to `releases_issue_uri`. When invoked otherwise, turns into "[Type] <#NN hyperlink>: ". Spaces present in the "ticket number" are used as fields for keywords (major, backported) and/or specs (e.g. '>=1.0'). This data is removed & used when constructing the object. May give a 'ticket number' of ``-`` or ``0`` to generate no hyperlink. """ parts = utils.unescape(text).split() issue_no = parts.pop(0) # Lol @ access back to Sphinx config = inliner.document.settings.env.app.config if issue_no not in ("-", "0"): ref = None if config.releases_issue_uri: ref = interpolate(text=config.releases_issue_uri, number=issue_no) elif config.releases_github_path: ref = f"https://github.com/{config.releases_github_path}/issues/{issue_no}" # noqa # Only generate a reference/link if we were able to make a URI if ref: identifier = nodes.reference( rawtext, "#" + issue_no, refuri=ref, **options ) # Otherwise, just make it regular text else: identifier = nodes.raw( rawtext=rawtext, text="#" + issue_no, format="html", **options ) else: identifier = None issue_no = None # So it doesn't gum up dupe detection later # Additional 'new-style changelog' stuff if name in ISSUE_TYPES: nodelist = issue_nodelist(name, identifier) spec = None keyword = None # TODO: sanity checks re: e.g. >2 parts, >1 instance of keywords, >1 # instance of specs, etc. for part in parts: maybe_spec = scan_for_spec(part) if maybe_spec: spec = maybe_spec else: if part in ("backported", "major"): keyword = part else: raise ValueError( f"Gave unknown keyword {keyword!r} for issue no. {issue_no}" # noqa ) # Create temporary node w/ data & final nodes to publish node = Issue( number=issue_no, type_=name, nodelist=nodelist, backported=(keyword == "backported"), major=(keyword == "major"), spec=spec, ) return [node], [] # Return old style info for 'issue' for older changelog entries else: return [identifier], [] def release_nodes(text, slug, date, config): # Doesn't seem possible to do this "cleanly" (i.e. just say "make me a # title and give it these HTML attributes during render time) so...fuckit. # We were already doing fully raw elements elsewhere anyway. And who cares # about a PDF of a changelog? :x uri = None if config.releases_release_uri: uri = interpolate(text=config.releases_release_uri, number=slug) elif config.releases_github_path: uri = f"https://github.com/{config.releases_github_path}/tree/{slug}" # Only construct link tag if user actually configured release URIs somehow if uri: link = f'{text}' else: link = text datespan = "" if date: datespan = f' {date}' header = f'

{link}{datespan}

' return nodes.section( "", nodes.raw(rawtext="", text=header, format="html"), ids=[text] ) year_arg_re = re.compile(r"^(.+?)\s*(?$", re.DOTALL) def release_role(name, rawtext, text, lineno, inliner, options={}, content=[]): """ Invoked as :release:`N.N.N `. Turns into useful release header + link to GH tree for the tag. """ # Make sure year has been specified match = year_arg_re.match(text) if not match: msg = inliner.reporter.error("Must specify release date!") return [inliner.problematic(rawtext, rawtext, msg)], [msg] number, date = match.group(1), match.group(2) # Lol @ access back to Sphinx config = inliner.document.settings.env.app.config nodelist = [release_nodes(number, number, date, config)] # Return intermediate node node = Release(number=number, date=date, nodelist=nodelist) return [node], [] def generate_unreleased_entry(header, line, issues, manager, app): log = partial(_log, config=app.config) nodelist = [ release_nodes( header, app.config.releases_development_branch, None, app.config, ) ] log(f"Creating {line!r} faux-release with {issues!r}") return { "obj": Release(number=line, date=None, nodelist=nodelist), "entries": issues, } def append_unreleased_entries(app, manager, releases): """ Generate new abstract 'releases' for unreleased issues. There's one for each combination of bug-vs-feature & major release line. When only one major release line exists, that dimension is ignored. """ for family, lines in manager.items(): # Skip over any unsupported lines supported = app.config.releases_supported_versions if supported is not None and family not in supported: continue for type_ in ("bugfix", "feature"): bucket = f"unreleased_{type_}" if bucket not in lines: # Implies unstable prehistory + 0.x fam continue issues = lines[bucket] fam_prefix = f"{family}.x " if len(manager) > 1 else "" header = f"Next {fam_prefix}{type_} release" line = f"unreleased_{family}.x_{type_}" releases.append( generate_unreleased_entry(header, line, issues, manager, app) ) def reorder_release_entries(releases): """ Mutate ``releases`` so the entrylist in each is ordered by feature/bug/etc. """ order = {"feature": 0, "bug": 1, "support": 2} for release in releases: entries = release["entries"].copy() release["entries"] = sorted(entries, key=lambda x: order[x.type]) def construct_entry_with_release(focus, issues, manager, log, releases, rest): """ Releases 'eat' the entries in their line's list and get added to the final data structure. They also inform new release-line 'buffers'. Release lines, once the release obj is removed, should be empty or a comma-separated list of issue numbers. """ log(f"release for line {focus.minor!r}") # Check for explicitly listed issues first explicit = None if rest[0].children: explicit = [x.strip() for x in rest[0][0].split(",")] # Do those by themselves since they override all other logic if explicit: log(f"Explicit issues requested: {explicit!r}") # First scan global issue dict, dying if not found missing = [i for i in explicit if i not in issues] if missing: raise ValueError( f"Couldn't find issue(s) #{', '.join(missing)} in the changelog!" # noqa ) # Obtain the explicitly named issues from global list entries = [] for i in explicit: for flattened_issue_item in itertools.chain(issues[i]): entries.append(flattened_issue_item) # Create release log(f"entries in this release: {entries!r}") releases.append({"obj": focus, "entries": entries}) # Introspect these entries to determine which buckets they should get # removed from (it's not "all of them"!) for obj in entries: if obj.type == "bug": # Major bugfix: remove from unreleased_feature if obj.major: log(f"Removing #{obj.number} from unreleased") # TODO: consider making a LineManager method somehow manager[focus.family]["unreleased_feature"].remove(obj) # Regular bugfix: remove from bucket for this release's # line + unreleased_bugfix else: if obj in manager[focus.family]["unreleased_bugfix"]: log(f"Removing #{obj.number} from unreleased") manager[focus.family]["unreleased_bugfix"].remove(obj) if obj in manager[focus.family][focus.minor]: log(f"Removing #{obj.number} from {focus.minor}") manager[focus.family][focus.minor].remove(obj) # Regular feature/support: remove from unreleased_feature # Backported feature/support: remove from bucket for this # release's line (if applicable) + unreleased_feature else: log(f"Removing #{obj.number} from unreleased") manager[focus.family]["unreleased_feature"].remove(obj) if obj in manager[focus.family].get(focus.minor, []): manager[focus.family][focus.minor].remove(obj) # Implicit behavior otherwise else: # Unstable prehistory -> just dump 'unreleased' and continue if manager.unstable_prehistory: # TODO: need to continue making LineManager actually OO, i.e. do # away with the subdicts + keys, move to sub-objects with methods # answering questions like "what should I give you for a release" # or whatever log("in unstable prehistory, dumping 'unreleased'") releases.append( { "obj": focus, # NOTE: explicitly dumping 0, not focus.family, since this # might be the last pre-historical release and thus not 0.x "entries": manager[0]["unreleased"].copy(), } ) manager[0]["unreleased"] = [] # If this isn't a 0.x release, it signals end of prehistory, make a # new release bucket (as is also done below in regular behavior). # Also acts like a sentinel that prehistory is over. if focus.family != 0: manager[focus.family][focus.minor] = [] # Regular behavior from here else: # New release line/branch detected. Create it & dump unreleased # features. if focus.minor not in manager[focus.family]: log("not seen prior, making feature release & bugfix bucket") manager[focus.family][focus.minor] = [] # TODO: this used to explicitly say "go over everything in # unreleased_feature and dump if it's feature, support or major # bug". But what the hell else would BE in unreleased_feature? # Why not just dump the whole thing?? # # Dump only the items in the bucket whose family this release # object belongs to, i.e. 1.5.0 should only nab the 1.0 # family's unreleased feature items. releases.append( { "obj": focus, "entries": manager[focus.family]["unreleased_feature"][ : ], } ) manager[focus.family]["unreleased_feature"] = [] # Existing line -> empty out its bucket into new release. # Skip 'major' bugs as those "belong" to the next release (and will # also be in 'unreleased_feature' - so safe to nuke the entire # line) else: log("pre-existing, making bugfix release") # TODO: as in other branch, I don't get why this wasn't just # dumping the whole thing - why would major bugs be in the # regular bugfix buckets? entries = manager[focus.family][focus.minor].copy() releases.append({"obj": focus, "entries": entries}) manager[focus.family][focus.minor] = [] # Clean out the items we just released from # 'unreleased_bugfix'. (Can't nuke it because there might # be some unreleased bugs for other release lines.) for x in entries: if x in manager[focus.family]["unreleased_bugfix"]: manager[focus.family]["unreleased_bugfix"].remove(x) def construct_entry_without_release(focus, issues, manager, log, rest): # Handle rare-but-valid non-issue-attached line items, which are # always bugs. (They are their own description.) if not isinstance(focus, Issue): # First, sanity check for potential mistakes resulting in an issue node # being buried within something else. buried = focus.traverse(Issue) if buried: msg = f""" Found issue node ({buried[0]!r}) buried inside another node: {buried[0].parent} Please double-check your ReST syntax! There is probably text in the above output that will show you which part of your changelog to look at. For example, indentation problems can accidentally generate nested definition lists. """ raise ValueError(msg) # OK, it looks legit - make it a bug. log("Found line item w/ no real issue object, creating bug") nodelist = issue_nodelist("bug") # Skip nodelist entirely if we're in unstable prehistory - # classification doesn't matter there. if manager.unstable_prehistory: nodelist = [] # Undo the 'pop' from outer scope. TODO: rework things so we don't have # to do this dumb shit uggggh rest[0].insert(0, focus) focus = Issue(type_="bug", nodelist=nodelist, description=rest) else: focus.attributes["description"] = rest # Add to global list (for use by explicit releases) or die trying issues[focus.number] = issues.get(focus.number, []) + [focus] # Add to per-release bugfix lines and/or unreleased bug/feature buckets, as # necessary. # TODO: suspect all of add_to_manager can now live in the manager; most of # Release's methods should probably go that way if manager.unstable_prehistory: log("Unstable prehistory -> adding to 0.x unreleased bucket") manager[0]["unreleased"].append(focus) else: log("Adding to release line manager") focus.add_to_manager(manager) def handle_upcoming_major_release(entries, manager): # Short-circuit if the future holds nothing for us if not entries: return # Short-circuit if we're in the middle of a block of releases, only the # last release before a bunch of issues, should be taking any action. if isinstance(entries[0], Release): return # Iterate through entries til we find the next Release or set of Releases next_releases = [] for index, obj in enumerate(entries): if isinstance(obj, Release): next_releases.append(obj) # Non-empty next_releases + encountered a non-release = done w/ release # block. elif next_releases: break # Examine result: is a major release present? If so, add its major number # to the line manager! for obj in next_releases: # TODO: update when Release gets tied closer w/ Version version = Version(obj.number) if version.minor == 0 and version.patch == 0: manager.add_family(obj.family) def handle_first_release_line(entries, manager): """ Set up initial line-manager entry for first encountered release line. To be called at start of overall process; afterwards, subsequent major lines are generated by `handle_upcoming_major_release`. """ # It's remotely possible the changelog is totally empty... if not entries: return # Obtain (short-circuiting) first Release obj. first_release = None for obj in entries: if isinstance(obj, Release): first_release = obj break # It's also possible it's non-empty but has no releases yet. if first_release: manager.add_family(obj.family) # If God did not exist, man would be forced to invent him. else: manager.add_family(0) def construct_releases(entries, app): log = partial(_log, config=app.config) # Walk from back to front, consuming entries & copying them into # per-release buckets as releases are encountered. Store releases in order. releases = [] # Release lines, to be organized by major releases, then by major+minor, # alongside per-major-release 'unreleased' bugfix/feature buckets. # NOTE: With exception of unstable_prehistory=True, which triggers use of a # separate, undifferentiated 'unreleased' bucket (albeit still within the # '0' major line family). manager = LineManager(app) # Also keep a master hash of issues by number to detect duplicates & assist # in explicitly defined release lists. issues = {} reversed_entries = list(reversed(entries)) # For the lookahead, so we're not doing this stripping O(n) times. # TODO: probs just merge the two into e.g. a list of 2-tuples of "actual # entry obj + rest"? stripped_entries = [x[0][0] for x in reversed_entries] # Perform an initial lookahead to prime manager with the 1st major release handle_first_release_line(stripped_entries, manager) # Start crawling... for index, obj in enumerate(reversed_entries): # Issue object is always found in obj (LI) index 0 (first, often only # P) and is the 1st item within that (index 0 again). # Preserve all other contents of 'obj'. focus = obj[0].pop(0) rest = obj log(repr(focus)) # Releases 'eat' the entries in their line's list and get added to the # final data structure. They also inform new release-line 'buffers'. # Release lines, once the release obj is removed, should be empty or a # comma-separated list of issue numbers. if isinstance(focus, Release): construct_entry_with_release( focus, issues, manager, log, releases, rest ) # After each release is handled, look ahead to see if we're # entering "last stretch before a major release". If so, # pre-emptively update the line-manager so upcoming features are # correctly sorted into that major release by default (re: logic in # Release.add_to_manager) handle_upcoming_major_release( stripped_entries[index + 1 :], manager ) # Entries get copied into release line buckets as follows: # * Features and support go into 'unreleased_feature' for use in new # feature releases. # * Bugfixes go into all release lines (so they can be printed in >1 # bugfix release as appropriate) as well as 'unreleased_bugfix' (so # they can be displayed prior to release'). Caveats include bugs marked # 'major' (they go into unreleased_feature instead) or with 'N.N+' # (meaning they only go into release line buckets for that release and # up.) # * Support/feature entries marked as 'backported' go into all # release lines as well, on the assumption that they were released to # all active branches. # * The 'rest' variable (which here is the bug description, vitally # important!) is preserved by stuffing it into the focus (issue) # object - it will get unpacked by construct_nodes() later. else: construct_entry_without_release(focus, issues, manager, log, rest) if manager.unstable_prehistory: releases.append( generate_unreleased_entry( header="Next release", line="unreleased", issues=manager[0]["unreleased"], manager=manager, app=app, ) ) else: append_unreleased_entries(app, manager, releases) reorder_release_entries(releases) return releases, manager def construct_nodes(releases): result = [] # Reverse the list again so the final display is newest on top for d in reversed(releases): if not d["entries"]: continue obj = d["obj"] entries = [] for entry in d["entries"]: # Use nodes.Node.deepcopy to deepcopy the description # node. If this is not done, multiple references to the same # object (e.g. a reference object in the description of #649, which # is then copied into 2 different release lists) will end up in the # doctree, which makes subsequent parse steps very angry (index() # errors). desc = entry["description"].deepcopy() # Additionally, expand any other issue roles found in the # description - sometimes we refer to related issues inline. (They # can't be left as issue() objects at render time since that's # undefined.) # Use [:] slicing (even under modern Python; the objects here are # docutils Nodes whose .copy() is weird) to avoid mutation during # the loops. for index, node in enumerate(desc[:]): for subindex, subnode in enumerate(node[:]): if isinstance(subnode, Issue): lst = subnode["nodelist"] desc[index][subindex : subindex + 1] = lst # Rework this entry to insert the now-rendered issue nodes in front # of the 1st paragraph of the 'description' nodes (which should be # the preserved LI + nested paragraph-or-more from original # markup.) # FIXME: why is there no "prepend a list" method? for node in reversed(entry["nodelist"]): desc[0].insert(0, node) entries.append(desc) # Entry list list_ = nodes.bullet_list("", *entries) # Insert list into release nodelist (as it's a section) obj["nodelist"][0].append(list_) # Release header header = nodes.paragraph("", "", *obj["nodelist"]) result.extend(header) return result class BulletListVisitor(nodes.NodeVisitor): def __init__(self, document, app, docnames, is_singlepage): nodes.NodeVisitor.__init__(self, document) self.found_changelog = False self.app = app # document names to seek out (eg "changelog") self.docnames = docnames self.is_singlepage = is_singlepage def visit_bullet_list(self, node): # Short circuit if already mutated a changelog bullet list or if the # one being visited doesn't appear to apply. if self.found_changelog: return # Also short circuit if we're in singlepage mode and the node's parent # doesn't seem to be named after an expected changelog docname. In this # mode, this is the earliest we can actually tell whether a given # bullet list is or is not "the changelog". if ( self.is_singlepage and node.parent.attributes.get("docname", None) not in self.docnames ): return # At this point, we can safely assume the node we're visiting is the # right one to mutate. self.found_changelog = True # Walk + parse into release mapping releases, _ = construct_releases(node.children, self.app) # Construct new set of nodes to replace the old, and we're done node.replace_self(construct_nodes(releases)) def unknown_visit(self, node): pass def generate_changelog(app, doctree, docname): desired_docnames = app.config.releases_document_name # Ensure we still work mostly-correctly in singlehtml builder situations # (must use name substring test as RTD's singlehtml builder doesn't # actually inherit from Sphinx's own!) is_singlepage = "singlehtml" in app.builder.name changelog_names = ["index"] if is_singlepage else desired_docnames if docname not in changelog_names: return # Find an appropriate bullet-list node & replace it with our # organized/parsed elements. changelog_visitor = BulletListVisitor( doctree, app, desired_docnames, is_singlepage ) doctree.walk(changelog_visitor) def setup(app): for key, default in ( # Issue base URI setting: releases_issue_uri # E.g. 'https://github.com/fabric/fabric/issues/' ("issue_uri", None), # Release-tag base URI setting: releases_release_uri # E.g. 'https://github.com/fabric/fabric/tree/' ("release_uri", None), # Convenience Github version of above ("github_path", None), # Which branch to use for unreleased feature items # TODO 3.0: s/master/main/ ("development_branch", "master"), # Which versions to show unreleased buckets for ("supported_versions", None), # Which document to use as the changelog ("document_name", ["changelog"]), # Debug output ("debug", False), # Whether to enable linear history during 0.x release timeline # TODO 3.0: flip this to True by default? ("unstable_prehistory", False), ): app.add_config_value( name=f"releases_{key}", default=default, rebuild="html" ) if isinstance(app.config.releases_document_name, str): app.config.releases_document_name = [app.config.releases_document_name] # Register intermediate roles for x in list(ISSUE_TYPES) + ["issue"]: add_role(app, x, issues_role) add_role(app, "release", release_role) # Hook in our changelog transmutation at appropriate step app.connect("doctree-resolved", generate_changelog) # identifies the version of our extension return {"version": __version__} def add_role(app, name, role_obj): # This (introspecting docutils.parser.rst.roles._roles) is the same trick # Sphinx uses to emit warnings about double-registering; it's a PITA to try # and configure the app early on so it doesn't emit those warnings, so we # instead just...don't double-register. Meh. if name not in roles._roles: app.add_role(name, role_obj) releases-2.1.1/releases/util.py0000644000175000001440000002334414423013135017434 0ustar jforcierusers00000000000000""" Utility functions, such as helpers for standalone changelog parsing. """ import logging import os from pathlib import Path from tempfile import mkdtemp from docutils.nodes import bullet_list from sphinx.application import Sphinx # not exposed at top level from . import construct_releases, setup def parse_changelog(path, **kwargs): """ Load and parse changelog file from ``path``, returning data structures. This function does not alter any files on disk; it is solely for introspecting a Releases ``changelog.rst`` and programmatically answering questions like "are there any unreleased bugfixes for the 2.3 line?" or "what was included in release 1.2.1?". For example, answering the above questions is as simple as:: changelog = parse_changelog("/path/to/changelog") print("Unreleased issues for 2.3.x: {}".format(changelog['2.3'])) print("Contents of v1.2.1: {}".format(changelog['1.2.1'])) Aside from the documented arguments, any additional keyword arguments are passed unmodified into an internal `get_doctree` call (which then passes them to `make_app`). :param str path: A relative or absolute file path string. :returns: A dict whose keys map to lists of ``releases.models.Issue`` objects, as follows: - Actual releases are full version number keys, such as ``"1.2.1"`` or ``"2.0.0"``. - Unreleased bugs (or bug-like issues; see the Releases docs) are stored in minor-release buckets, e.g. ``"1.2"`` or ``"2.0"``. - Unreleased features (or feature-like issues) are found in ``"unreleased_N_feature"``, where ``N`` is one of the major release families (so, a changelog spanning only 1.x will only have ``unreleased_1_feature``, whereas one with 1.x and 2.x releases will have ``unreleased_1_feature`` and ``unreleased_2_feature``, etc). .. versionchanged:: 1.6 Added support for passing kwargs to `get_doctree`/`make_app`. """ app, doctree = get_doctree(path, **kwargs) # Have to semi-reproduce the 'find first bullet list' bit from main code, # which is unfortunately side-effect-heavy (thanks to Sphinx plugin # design). first_list = None for node in doctree[0]: if isinstance(node, bullet_list): first_list = node break # Initial parse into the structures Releases finds useful internally releases, manager = construct_releases(first_list.children, app) ret = changelog2dict(releases) # Stitch them together into something an end-user would find better: # - nuke unreleased_N.N_Y as their contents will be represented in the # per-line buckets for key in ret.copy(): if key.startswith("unreleased"): del ret[key] for family in manager: # - remove unreleased_bugfix, as they are accounted for in the per-line # buckets too. No need to store anywhere. manager[family].pop("unreleased_bugfix", None) # - bring over each major family's unreleased_feature as # unreleased_N_feature unreleased = manager[family].pop("unreleased_feature", None) if unreleased is not None: ret["unreleased_{}_feature".format(family)] = unreleased # - bring over all per-line buckets from manager (flattening) # Here, all that's left in the per-family bucket should be lines, not # unreleased_* ret.update(manager[family]) return ret def _faux_write_doctree(self, docname, doctree, *args, **kwargs): self._read_doctree = doctree def get_doctree(path, **kwargs): """ Obtain a mostly-rendered Sphinx doctree from the RST file at ``path``. The returned doctree is parsed to the point where Releases' own objects (such as Release and Issue nodes) have been injected, but not yet turned into their final representation (such as HTML tags). .. note:: This is primarily useful for the use case of `parse_changelog` in this module and is not intended as a generic-use in-memory Sphinx build function! Any additional kwargs are passed unmodified into an internal `make_app` call. :param str path: A relative or absolute Sphinx sourcedir path. :returns: A two-tuple of the generated ``sphinx.application.Sphinx`` app and the doctree (a ``docutils.document`` object). .. versionchanged:: 1.6 Added support for passing kwargs to `make_app`. """ path = Path(path) # TODO: this only works for top level changelog files (i.e. ones where # their dirname is the project/doc root) # NOTE: using absolute to avoid docutils bugs app = make_app(srcdir=path.parent.absolute(), **kwargs) app.env.temp_data["docname"] = path.stem # NOTE: prior to v7, sphinx.io.read_doc was used and just returned the # generated document. its alternative tries literally writing to disk, so # we neuter that part via a nasty monkeypatch in order to obtain the value app.builder.__class__.write_doctree = _faux_write_doctree app.builder.read_doc(str(path.absolute().with_suffix(""))) return app, app.builder._read_doctree def load_conf(srcdir): """ Load ``conf.py`` from given ``srcdir``. :returns: Dictionary derived from the conf module. """ path = os.path.join(srcdir, "conf.py") mylocals = {"__file__": path} with open(path) as fd: exec(fd.read(), mylocals) return mylocals def make_app(**kwargs): """ Create a dummy Sphinx app, filling in various hardcoded assumptions. For example, Sphinx assumes the existence of various source/dest directories, even if you're only calling internals that never generate (or sometimes, even read!) on-disk files. This function creates safe temp directories for these instances. It also neuters Sphinx's internal logging, which otherwise causes verbosity in one's own test output and/or debug logs. Finally, it does load the given srcdir's ``conf.py``, but only to read specific bits like ``extensions`` (if requested); most of it is ignored. All args are stored in a single ``**kwargs``. Aside from the params listed below (all of which are optional), all kwargs given are turned into 'releases_xxx' config settings; e.g. ``make_app(foo='bar')`` is like setting ``releases_foo = 'bar'`` in ``conf.py``. :param str docname: Override the document name used (mostly for internal testing). :param str srcdir: Sphinx source directory path. :param str dstdir: Sphinx dest directory path. :param str doctreedir: Sphinx doctree directory path. :param bool load_extensions: Whether to load the real ``conf.py`` and setup any extensions it configures. Default: ``False``. :returns: A Sphinx ``Application`` instance. .. versionchanged:: 1.6 Added the ``load_extensions`` kwarg. """ srcdir = kwargs.pop("srcdir", None) if srcdir is None: srcdir = mkdtemp() dstdir = kwargs.pop("dstdir", None) if dstdir is None: dstdir = mkdtemp() doctreedir = kwargs.pop("doctreedir", None) if doctreedir is None: doctreedir = mkdtemp() load_extensions = kwargs.pop("load_extensions", False) real_conf = None try: # Turn off most logging, which is rarely useful and usually just gums # up the output of whatever tool is calling us. # NOTE: used to just do 'sphinx' but that stopped working. Unsure why # hierarchy not functioning. for name in ("sphinx", "sphinx.sphinx.application"): logging.getLogger(name).setLevel(logging.ERROR) # App API seems to work on all versions so far. app = Sphinx( srcdir=srcdir, confdir=None, outdir=dstdir, doctreedir=doctreedir, buildername="html", ) # Might as well load the conf file here too. if load_extensions: real_conf = load_conf(srcdir) finally: for d in (srcdir, dstdir, doctreedir): # Only remove empty dirs; non-empty dirs are implicitly something # that existed before we ran, and should not be touched. try: os.rmdir(d) except OSError: pass setup(app) # Mock out the config within. More assumptions by Sphinx :( # TODO: just use real config and overlay what truly needs changing? is that # feasible given the rest of the weird ordering we have to do? If it is, # maybe just literally slap this over the return value of load_conf()... config = { "releases_release_uri": "foo_%s", "releases_issue_uri": "bar_%s", "releases_debug": False, "master_doc": "index", } # Allow tinkering with document filename if "docname" in kwargs: app.env.temp_data["docname"] = kwargs.pop("docname") # Allow config overrides via kwargs for name in kwargs: config["releases_{}".format(name)] = kwargs[name] # Stitch together as the sphinx app init() usually does w/ real conf files app.config._raw_config = config app.config.init_values() # Initialize extensions (the internal call to this happens at init time, # which of course had no valid config yet here...) if load_extensions: for extension in real_conf.get("extensions", []): # But don't set up ourselves again, that causes errors if extension == "releases": continue app.setup_extension(extension) return app def changelog2dict(changelog): """ Helper turning internal list-o-releases structure into a dict. See `parse_changelog` docstring for return value details. """ return {r["obj"].number: r["entries"] for r in changelog} releases-2.1.1/releases/line_manager.py0000644000175000001440000000477414373504602021116 0ustar jforcierusers00000000000000# TODO: un-subclass dict in favor of something more explicit, once all regular # dict-like access has been factored out into methods class LineManager(dict): """ Manages multiple release lines/families as well as related config state. """ def __init__(self, app): """ Initialize new line manager dict. :param app: The core Sphinx app object. Mostly used for config. """ super().__init__() self.app = app @property def config(self): """ Return Sphinx config object. """ return self.app.config def add_family(self, major_number): """ Expand to a new release line with given ``major_number``. This will flesh out mandatory buckets like ``unreleased_bugfix`` and do other necessary bookkeeping. """ # Normally, we have separate buckets for bugfixes vs features keys = ["unreleased_bugfix", "unreleased_feature"] # But unstable prehistorical releases roll all up into just # 'unreleased' if major_number == 0 and self.config.releases_unstable_prehistory: keys = ["unreleased"] # Either way, the buckets default to an empty list self[major_number] = {key: [] for key in keys} @property def unstable_prehistory(self): """ Returns True if 'unstable prehistory' behavior should be applied. Specifically, checks config & whether any non-0.x releases exist. """ return ( self.config.releases_unstable_prehistory and not self.has_stable_releases ) @property def stable_families(self): """ Returns release family numbers which aren't 0 (i.e. prehistory). """ return [x for x in self if x != 0] @property def has_stable_releases(self): """ Returns whether stable (post-0.x) releases seem to exist. """ nonzeroes = self.stable_families # Nothing but 0.x releases -> yup we're prehistory if not nonzeroes: return False # Presumably, if there's >1 major family besides 0.x, we're at least # one release into the 1.0 (or w/e) line. if len(nonzeroes) > 1: return True # If there's only one, we may still be in the space before its N.0.0 as # well; we can check by testing for existence of bugfix buckets return any( x for x in self[nonzeroes[0]] if not x.startswith("unreleased") ) releases-2.1.1/releases/models.py0000644000175000001440000001633414373504602017753 0ustar jforcierusers00000000000000from functools import reduce from operator import xor from docutils import nodes from semantic_version import Version as StrictVersion, Spec class Version(StrictVersion): """ Version subclass toggling ``partial=True`` by default. """ def __init__(self, version_string, partial=True): super().__init__(version_string, partial) # Issue type list (keys) + color values ISSUE_TYPES = {"bug": "A04040", "feature": "40A056", "support": "4070A0"} class Issue(nodes.Element): # Technically, we just need number, but heck, you never know... _cmp_keys = ("type", "number", "backported", "major") @property def type(self): return self["type_"] @property def is_featurelike(self): if self.type == "bug": return self.major else: return not self.backported @property def is_buglike(self): return not self.is_featurelike @property def backported(self): return self.get("backported", False) @property def major(self): return self.get("major", False) @property def number(self): return self.get("number", None) @property def spec(self): return self.get("spec", None) def __eq__(self, other): for attr in self._cmp_keys: if getattr(self, attr, None) != getattr(other, attr, None): return False return True def __hash__(self): return reduce(xor, [hash(getattr(self, x)) for x in self._cmp_keys]) def minor_releases(self, manager): """ Return all minor release line labels found in ``manager``. """ # TODO: yea deffo need a real object for 'manager', heh. E.g. we do a # very similar test for "do you have any actual releases yet?" # elsewhere. (This may be fodder for changing how we roll up # pre-major-release features though...?) return [ key for key, value in manager.items() if any(x for x in value if not x.startswith("unreleased")) ] def default_spec(self, manager): """ Given the current release-lines structure, return a default Spec. Specifics: * For feature-like issues, only the highest major release is used, so given a ``manager`` with top level keys of ``[1, 2]``, this would return ``Spec(">=2")``. * When ``releases_always_forwardport_features`` is ``True``, that behavior is nullified, and this function always returns the empty ``Spec`` (which matches any and all versions/lines). * For bugfix-like issues, we only consider major release families which have actual releases already. * Thus the core difference here is that features are 'consumed' by upcoming major releases, and bugfixes are not. * When the ``unstable_prehistory`` setting is ``True``, the default spec starts at the oldest non-zero release line. (Otherwise, issues posted after prehistory ends would try being added to the 0.x part of the tree, which makes no sense in unstable-prehistory mode.) """ # TODO: I feel like this + the surrounding bits in add_to_manager() # could be consolidated & simplified... specstr = "" # Make sure truly-default spec skips 0.x if prehistory was unstable. stable_families = manager.stable_families if manager.config.releases_unstable_prehistory and stable_families: specstr = ">={}".format(min(stable_families)) if self.is_featurelike: # TODO: if app->config->=2, this means we don't even bother # looking in the 1.x family. families = [Version(str(x)) for x in manager] versions = list(spec.filter(families)) for version in versions: family = version.major # Within each family, we further limit which bugfix lines match up # to what self cares about (ignoring 'unreleased' until later) candidates = [ Version(x) for x in manager[family] if not x.startswith("unreleased") ] # Select matching release lines (& stringify) buckets = [] bugfix_buckets = [str(x) for x in spec.filter(candidates)] # Add back in unreleased_* as appropriate # TODO: probably leverage Issue subclasses for this eventually? if self.is_buglike: buckets.extend(bugfix_buckets) # Don't put into JUST unreleased_bugfix; it implies that this # major release/family hasn't actually seen any releases yet # and only exists for features to go into. if bugfix_buckets: buckets.append("unreleased_bugfix") # Obtain list of minor releases to check for "haven't had ANY # releases yet" corner case, in which case ALL issues get thrown in # unreleased_feature for the first release to consume. # NOTE: assumes first release is a minor or major one, # but...really? why would your first release be a bugfix one?? no_releases = not self.minor_releases(manager) if self.is_featurelike or self.backported or no_releases: buckets.append("unreleased_feature") # Now that we know which buckets are appropriate, add ourself to # all of them. TODO: or just...do it above...instead... for bucket in buckets: manager[family][bucket].append(self) def __repr__(self): flag = "" if self.backported: flag = "backported" elif self.major: flag = "major" elif self.spec: flag = self.spec if flag: flag = " ({})".format(flag) return "<{issue.type} #{issue.number}{flag}>".format( issue=self, flag=flag ) class Release(nodes.Element): @property def number(self): return self["number"] @property def minor(self): # TODO: use Version return ".".join(self.number.split(".")[:-1]) @property def family(self): # TODO: use Version.major # TODO: and probs just rename to .major, 'family' is dumb tbh return int(self.number.split(".")[0]) def __repr__(self): return "".format(self.number)