pax_global_header00006660000000000000000000000064134314032120014503gustar00rootroot0000000000000052 comment=bca0525b9b35bbc4017d6681c2aaa385cd70766a towncrier-19.2.0/000077500000000000000000000000001343140321200136105ustar00rootroot00000000000000towncrier-19.2.0/.coveragerc000066400000000000000000000002131343140321200157250ustar00rootroot00000000000000[run] source = towncrier branch = True [paths] source = src/ .tox/*/lib/python*/site-packages/ .tox/pypy*/site-packages/ towncrier-19.2.0/.gitignore000066400000000000000000000002641343140321200156020ustar00rootroot00000000000000*.egg-info/ *.o *.py[co] *.so _trial_temp*/ build/ dropin.cache doc/ docs/_build/ dist/ venv/ htmlcov/ .coverage *~ *.lock apidocs/ .vs/ *.pyproj .DS_Store .eggs .tox/ .coverage.* towncrier-19.2.0/.travis.yml000066400000000000000000000010621343140321200157200ustar00rootroot00000000000000language: python sudo: false install: - pip install tox codecov before_script: - git remote set-branches --add origin master - git fetch origin master script: - tox -c tox.ini -e $TOX_ENV after_script: - codecov matrix: fast_finish: true include: - python: 3.6 env: TOX_ENV=flake8 - python: 3.6 env: TOX_ENV=check-manifest - python: 3.6 env: TOX_ENV=check-newsfragment - python: 2.7 env: TOX_ENV=py27-tests - python: 3.5 env: TOX_ENV=py35-tests - python: 3.6 env: TOX_ENV=py36-tests towncrier-19.2.0/CODE_OF_CONDUCT.md000066400000000000000000000062261343140321200164150ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hawkowl@atleastfornow.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ towncrier-19.2.0/LICENSE000066400000000000000000000020551343140321200146170ustar00rootroot00000000000000Copyright (c) 2015-2016, Amber Brown, meejah Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. towncrier-19.2.0/MANIFEST.in000066400000000000000000000004121343140321200153430ustar00rootroot00000000000000include *.rst include .coveragerc include LICENSE include CODE_OF_CONDUCT.md include pyproject.toml include tox.ini recursive-include src *.rst exclude bin exclude src/towncrier/newsfragments recursive-exclude bin * recursive-exclude src/towncrier/newsfragments * towncrier-19.2.0/NEWS.rst000066400000000000000000000143751343140321200151300ustar00rootroot00000000000000``towncrier`` issues are filed on `GitHub `_, and each ticket number here corresponds to a closed GitHub issue. .. towncrier release notes start towncrier 19.2.0 (2019-02-15) ============================= Features -------- - Add support for multiple fragements per issue/type pair. This extends the naming pattern of the fragments to `issuenumber.type(.counter)` where counter is an optional integer. (`#119 `_) - Python 2.7 is now supported. (`#121 `_) - `python -m towncrier.check` now accepts an option to give the configuration file location. (`#123 `_) - towncrier.check now reports git output when it encounters a git failure. (`#124 `_) towncrier 18.6.0 (2018-07-05) ============================= Features -------- - ``python -m towncrier.check``, which will check a Git branch for the presence of added newsfiles, to be used in a CI system. (`#75 `_) - wrap is now an optional configuration option (which is False by default) which controls line wrapping of news files. Towncrier will now also not attempt to normalise (wiping newlines) from the input, but will strip leading and ending whitespace. (`#80 `_) - Towncrier can now be invoked by ``python -m towncrier``. (`#115 `_) Deprecations and Removals ------------------------- - Towncrier now supports Python 3.5+ as a script runtime. Python 2.7 will not function. (`#80 `_) towncrier 18.5.0 (2018-05-16) ============================= Features -------- - Python 3.3 is no longer supported. (`#103 `_) - Made ``package`` optional. When the version is passed on the command line, and the ``title_format`` does not use the package name, and it is not used for the path to the news fragments, then no package name is needed, so we should not enforce it. (`#111 `_) Bugfixes -------- - When cleaning up old newsfragments, if a newsfragment is named "123.feature.rst", then remove that file instead of trying to remove the non-existent "123.feature". (`#99 `_) - If there are two newsfragments with the same name (example: "123.bugfix.rst" and "123.bugfix.rst~"), then raise an error instead of silently picking one at random. (`#101 `_) towncrier 17.8.0 (2017-08-19) ============================= Features -------- - Added new option ``issue_format``. For example, this can be used to make issue text in the NEWS file be formatted as ReST links to the issue tracker. (`#52 `_) - Add ``--yes`` option to run non-interactively. (`#56 `_) - You can now name newsfragments like 123.feature.rst, or 123.feature.txt, or 123.feature.whatever.you.want, and towncrier will ignore the extension. (`#62 `_) - New option in ``pyproject.toml``: ``underlines = ["=", "-", "~"]`` to specify the ReST underline hierarchy in towncrier's generated text. (`#63 `_) - Instead of sorting sections/types alphabetically (e.g. "bugfix" before "feature" because "b" < "f"), sections/types will now have the same order in the output as they have in your config file. (`#70 `_) Bugfixes -------- - When rewrapping text, don't break words or at hyphens -- they might be inside a URL (`#68 `_) Deprecations and Removals ------------------------- - `towncrier.ini` config file support has been removed in preference to `pyproject.toml` configuration. (`#71 `_) towncrier 17.4.0 (2017-04-15) ============================= Misc ---- - #46 towncrier 17.1.0 ========== Bugfixes -------- - fix --date being ignored (#43) towncrier 16.12.0 ========== Bugfixes -------- - Towncrier will now import the local version of the package and not the global one. (#38) Features -------- - Allow configration of the template file, title text and "magic comment" (#35) - Towncrier now uses pyproject.toml, as defined in PEP-518. (#40) towncrier 16.1.0 (2016-03-25) ============================= Features -------- - Ported to Python 2.7. (#27) - towncrier now supports non-numerical news fragment names. (#32) Bugfixes -------- - towncrier would spew an unhelpful exception if it failed importing your project when autodiscovering, now it does not. (#22) - incremental is now added as a runtime dependency for towncrier. (#25) Misc ---- - #33 towncrier 16.0.0 (2016-01-06) ============================= Features -------- - towncrier now automatically puts a date beside the version as it is generated, using today's date. For repeatable builds, use the ``--date`` switch and provide a date. For no date, use ``--date=``. (#11) - towncrier will now add the version logs after ``.. towncrier release notes start``, if it is in the file, allowing you to preserve text at the top of the file. (#15) Improved Documentation ---------------------- - The README now mentions how to manually provide the version number, for non-Py3 compatible projects. (#19) towncrier 15.1.0 ================ Features -------- - towncrier now supports reading ``__version__`` attributes that are tuples of numbers (e.g. (15, 4, 0)). (#3) - towncrier now has support for testing via Tox and each commit is now ran on Travis CI. (#6) Bugfixes -------- - towncrier now defaults to the current working directory for the package_dir settings variable. (#2) towncrier 15.0.0 ================ Features -------- - Basic functionality has been implemented. This includes configuring towncrier to find your project, having a set of preconfigured news fragment categories, and assembling a newsfile from them. (#1) towncrier-19.2.0/README.rst000066400000000000000000000100241343140321200152740ustar00rootroot00000000000000Hear ye, hear ye, says the ``towncrier`` ======================================== .. image:: https://travis-ci.org/hawkowl/towncrier.svg?branch=master :target: https://travis-ci.org/hawkowl/towncrier .. image:: https://codecov.io/github/hawkowl/towncrier/coverage.svg?branch=master :target: https://codecov.io/github/hawkowl/towncrier?branch=master ``towncrier`` is a utility to produce useful, summarised news files for your project. Rather than reading the Git history as some newer tools to produce it, or having one single file which developers all write to, ``towncrier`` reads "news fragments" which contain information `useful to end users`. Philosophy ---------- ``towncrier`` delivers the news which is convenient to those that hear it, not those that write it. That is, by duplicating what has changed from the "developer log" (which may contain complex information about the original issue, how it was fixed, who authored the fix, and who reviewed the fix) into a "news fragment" (a small file containing just enough information to be useful to end users), ``towncrier`` can produce a digest of the changes which is valuable to those who may wish to use the software. These fragments are also commonly called "topfiles" or "newsfiles" in Twisted parlance. ``towncrier`` works best in a development system where all merges involve closing a ticket. Quick Start ----------- Install from PyPI:: python3 -m pip install towncrier .. note:: ``towncrier``, as a command line tool, works on Python 3.5+ only. It is usable by projects written in other languages, provided you give it the version of the project when invoking it. For Python 2/3 compatible projects, the version can be discovered automatically. In your project root, add a ``pyproject.toml`` file, with the contents:: [tool.towncrier] package = "mypackage" package_dir = "src" filename = "NEWS.rst" Then put news fragments (see "News Fragments" below) into a "newsfragments" directory under your package (so, if your project is named "myproject", and it's kept under ``src``, your newsfragments dir would be ``src/myproject/newsfragments/``). To prevent git from removing the newsfragments directory, make a ``.gitignore`` file in it with:: !.gitignore This will keep the folder around, but otherwise "empty". ``towncrier`` needs to know what version your project is, and there are two ways you can give it: - For Python 2/3 compatible projects, a ``__version__`` in the top level package. This can be either a string literal, a tuple, or an `Incremental `_ version. - Manually passing ``--version=`` when interacting with ``towncrier``. To produce a draft of the news file, run:: towncrier --draft To produce the news file for real, run:: towncrier This command will remove the news files (with ``git rm``) and append the built news to the filename specified in ``towncrier.ini``, and then stage the news file changes (with ``git add``). It leaves committing the changes up to the user. If you wish to have content at the top of the news file (for example, to say where you can find the tickets), put your text above a rST comment that says:: .. towncrier release notes start ``towncrier`` will then put the version notes after this comment, and leave your existing content that was above it where it is. News Fragments -------------- ``towncrier`` has a few standard types of news fragments, signified by the file extension. These are: - ``.feature``: Signifying a new feature. - ``.bugfix``: Signifying a bug fix. - ``.doc``: Signifying a documentation improvement. - ``.removal``: Signifying a deprecation or removal of public API. - ``.misc``: A ticket has been closed, but it is not of interest to users. The start of the filename is the ticket number, and the content is what will end up in the news file. For example, if ticket #850 is about adding a new widget, the filename would be ``myproject/newsfragments/850.feature`` and the content would be ``myproject.widget has been added``. towncrier-19.2.0/bin/000077500000000000000000000000001343140321200143605ustar00rootroot00000000000000towncrier-19.2.0/bin/towncrier000077500000000000000000000002611343140321200163210ustar00rootroot00000000000000#! /usr/bin/env python3 import sys import os.path srcdir = os.path.join(os.path.dirname(__file__), "..", "src") sys.path.insert(0, srcdir) import towncrier towncrier._main() towncrier-19.2.0/pyproject.toml000066400000000000000000000014521343140321200165260ustar00rootroot00000000000000[tool.towncrier] package = "towncrier" package_dir = "src" filename = "NEWS.rst" issue_format = "`#{issue} `_" [[tool.towncrier.section]] path = "" [[tool.towncrier.type]] directory = "feature" name = "Features" showcontent = true [[tool.towncrier.type]] directory = "bugfix" name = "Bugfixes" showcontent = true [[tool.towncrier.type]] directory = "doc" name = "Improved Documentation" showcontent = true [[tool.towncrier.type]] directory = "removal" name = "Deprecations and Removals" showcontent = true [[tool.towncrier.type]] directory = "misc" name = "Misc" showcontent = false towncrier-19.2.0/setup.cfg000066400000000000000000000001021343140321200154220ustar00rootroot00000000000000[bdist_wheel] # use py2.py3 tag for pure-python dist: universal=1 towncrier-19.2.0/setup.py000066400000000000000000000022161343140321200153230ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import, division, print_function from setuptools import setup, find_packages setup( name='towncrier', maintainer='Amber Brown', maintainer_email='hawkowl@twistedmatrix.com', url="https://github.com/hawkowl/towncrier", classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", ], use_incremental=True, setup_requires=['incremental'], install_requires=[ 'Click', 'incremental', 'jinja2', 'toml', ], package_dir={"": "src"}, packages=find_packages('src'), license="MIT", zip_safe=False, include_package_data=True, description='Building newsfiles for your project.', long_description=open('README.rst').read(), entry_points={ 'console_scripts': [ 'towncrier = towncrier:_main', ], } ) towncrier-19.2.0/src/000077500000000000000000000000001343140321200143775ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/000077500000000000000000000000001343140321200164135ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/__init__.py000066400000000000000000000104411343140321200205240ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. """ towncrier, a builder for your news files. """ from __future__ import absolute_import, division import os import click import pkg_resources from datetime import date from ._settings import load_config from ._builder import find_fragments, split_fragments, render_fragments from ._project import get_version, get_project_name from ._writer import append_to_newsfile from ._git import remove_files, stage_newsfile from ._version import __version__ def _get_date(): return date.today().isoformat() @click.command() @click.option( "--draft", "draft", default=False, flag_value=True, help=("Render the news fragments, don't write to files, " "don't check versions."), ) @click.option("--dir", "directory", default=".") @click.option("--name", "project_name", default=None) @click.option( "--version", "project_version", default=None, help="Render the news fragments using given version.", ) @click.option("--date", "project_date", default=None) @click.option( "--yes", "answer_yes", default=False, flag_value=True, help="Do not ask for confirmation to remove news fragments.", ) def _main(draft, directory, project_name, project_version, project_date, answer_yes): return __main( draft, directory, project_name, project_version, project_date, answer_yes ) def __main(draft, directory, project_name, project_version, project_date, answer_yes): """ The main entry point. """ directory = os.path.abspath(directory) config = load_config(directory) to_err = draft click.echo("Loading template...", err=to_err) if config["template"] is None: template = pkg_resources.resource_string( __name__, "templates/template.rst" ).decode("utf8") else: with open(config["template"], "rb") as tmpl: template = tmpl.read().decode("utf8") click.echo("Finding news fragments...", err=to_err) definitions = config["types"] if config.get("directory"): base_directory = os.path.abspath(config["directory"]) fragment_directory = None else: base_directory = os.path.abspath( os.path.join(directory, config["package_dir"], config["package"]) ) fragment_directory = "newsfragments" fragments, fragment_filenames = find_fragments( base_directory, config["sections"], fragment_directory, definitions ) click.echo("Rendering news fragments...", err=to_err) fragments = split_fragments(fragments, definitions) rendered = render_fragments( # The 0th underline is used for the top line template, config["issue_format"], fragments, definitions, config["underlines"][1:], config["wrap"], ) if project_version is None: project_version = get_version( os.path.join(directory, config["package_dir"]), config["package"] ) if project_name is None: package = config.get("package") if package: project_name = get_project_name( os.path.abspath(os.path.join(directory, config["package_dir"])), package ) else: # Can't determine a project_name, but maybe it is not needed. project_name = "" if project_date is None: project_date = _get_date() top_line = config["title_format"].format( name=project_name, version=project_version, project_date=project_date ) top_line += u"\n" + (config["underlines"][0] * len(top_line)) + u"\n" if draft: click.echo( "Draft only -- nothing has been written.\n" "What is seen below is what would be written.\n", err=to_err, ) click.echo("%s\n%s" % (top_line, rendered)) else: click.echo("Writing to newsfile...", err=to_err) start_line = config["start_line"] append_to_newsfile( directory, config["filename"], start_line, top_line, rendered ) click.echo("Staging newsfile...", err=to_err) stage_newsfile(directory, config["filename"]) click.echo("Removing news fragments...", err=to_err) remove_files(fragment_filenames, answer_yes) click.echo("Done!", err=to_err) __all__ = ["__version__"] towncrier-19.2.0/src/towncrier/__main__.py000066400000000000000000000001001343140321200204740ustar00rootroot00000000000000import towncrier __name__ == "__main__" and towncrier._main() towncrier-19.2.0/src/towncrier/_builder.py000066400000000000000000000142261343140321200205570ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. from __future__ import absolute_import, division, print_function import os import textwrap from collections import OrderedDict from jinja2 import Template # Returns a structure like: # # OrderedDict([ # ("", # { # ("142", "misc"): u"", # ("1", "feature"): u"some cool description", # }), # ("Names", {}), # ("Web", {("3", "bugfix"): u"Fixed a thing"}), # ]) # # We should really use attrs. # # Also returns a list of the paths that the fragments were taken from. def find_fragments(base_directory, sections, fragment_directory, definitions): """ Sections are a dictonary of section names to paths. """ content = OrderedDict() fragment_filenames = [] for key, val in sections.items(): if fragment_directory is not None: section_dir = os.path.join(base_directory, val, fragment_directory) else: section_dir = os.path.join(base_directory, val) files = os.listdir(section_dir) file_content = {} for basename in files: parts = basename.split(u".") counter = 0 if len(parts) == 1: continue else: ticket, category = parts[:2] # If there is a number after the category then use it as a counter, # otherwise ignore it. # This means 1.feature.1 and 1.feature do not conflict but # 1.feature.rst and 1.feature do. if len(parts) > 2: try: counter = int(parts[2]) except ValueError: pass if category not in definitions: continue full_filename = os.path.join(section_dir, basename) fragment_filenames.append(full_filename) with open(full_filename, "rb") as f: data = f.read().decode("utf8", "replace") if (ticket, category, counter) in file_content: raise ValueError( "multiple files for {}.{} in {}".format( ticket, category, section_dir ) ) file_content[ticket, category, counter] = data content[key] = file_content return content, fragment_filenames def indent(text, prefix): """ Adds `prefix` to the beginning of non-empty lines in `text`. """ # Based on Python 3's textwrap.indent def prefixed_lines(): for line in text.splitlines(True): yield (prefix + line if line.strip() else line) return u"".join(prefixed_lines()) # Takes the output from find_fragments above. Probably it would be useful to # add an example output here. Next time someone digs deep enough to figure it # out, please do so... def split_fragments(fragments, definitions): output = OrderedDict() for section_name, section_fragments in fragments.items(): section = {} for (ticket, category, counter), content in section_fragments.items(): content = indent(content.strip(), u" ")[2:] if definitions[category]["showcontent"] is False: content = u"" texts = section.get(category, OrderedDict()) if texts.get(content): texts[content] = sorted(texts[content] + [ticket]) else: texts[content] = [ticket] section[category] = texts output[section_name] = section return output def issue_key(issue): # We want integer issues to sort as integers, and we also want string # issues to sort as strings. We arbitrarily put string issues before # integer issues (hopefully no-one uses both at once). try: return (int(issue), u"") except Exception: # Maybe we should sniff strings like "gh-10" -> (10, "gh-10")? return (-1, issue) def entry_key(entry): _, issues = entry return [issue_key(issue) for issue in issues] def render_issue(issue_format, issue): if issue_format is None: try: int(issue) return u"#" + issue except Exception: return issue else: return issue_format.format(issue=issue) def render_fragments(template, issue_format, fragments, definitions, underlines, wrap): """ Render the fragments into a news file. """ jinja_template = Template(template, trim_blocks=True) data = OrderedDict() for section_name, section_value in fragments.items(): data[section_name] = OrderedDict() for category_name, category_value in section_value.items(): # Suppose we start with an ordering like this: # # - Fix the thing (#7, #123, #2) # - Fix the other thing (#1) # First we sort the issues inside each line: # # - Fix the thing (#2, #7, #123) # - Fix the other thing (#1) entries = [] for text, issues in category_value.items(): entries.append((text, sorted(issues, key=issue_key))) # Then we sort the lines: # # - Fix the other thing (#1) # - Fix the thing (#2, #7, #123) entries.sort(key=entry_key) # Then we put these nicely sorted entries back in an ordered dict # for the template, after formatting each issue number categories = OrderedDict() for text, issues in entries: rendered = [render_issue(issue_format, i) for i in issues] categories[text] = rendered data[section_name][category_name] = categories done = [] res = jinja_template.render( sections=data, definitions=definitions, underlines=underlines ) for line in res.split(u"\n"): if wrap: done.append( textwrap.fill( line, width=79, subsequent_indent=u" ", break_long_words=False, break_on_hyphens=False, ) ) else: done.append(line) return u"\n".join(done).rstrip() + u"\n" towncrier-19.2.0/src/towncrier/_git.py000066400000000000000000000012461343140321200177120ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. from subprocess import call import os import click def remove_files(fragment_filenames, answer_yes): if not fragment_filenames: return if answer_yes: click.echo("Removing the following files:") else: click.echo("I want to remove the following files:") for filename in fragment_filenames: click.echo(filename) if answer_yes or click.confirm("Is it okay if I remove those files?", default=True): call(["git", "rm", "--quiet"] + fragment_filenames) def stage_newsfile(directory, filename): call(["git", "add", os.path.join(directory, filename)]) towncrier-19.2.0/src/towncrier/_project.py000066400000000000000000000036751343140321200206050ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. """ Responsible for getting the version and name from a project. """ from __future__ import absolute_import, division import sys from importlib import import_module from incremental import Version def _get_package(package_dir, package): # Step 1: Try the dumbest and simplest thing that could possibly work. # Yes, that means importing it. Call the cops, I don't care. sys.path.insert(1, package_dir) try: module = import_module(package) except ImportError as e: print("Tried to import {}, but ran into this error: {}".format(package, e)) # wups that didn't work module = None # Don't leave trash in sys.path sys.path.pop(0) # Step 2: uhhhhhhh # TBA if not module: raise Exception("Can't find your project :(") return module def get_version(package_dir, package): module = _get_package(package_dir, package) version = getattr(module, "__version__", None) if not version: raise Exception("No __version__, I don't know how else to look") if isinstance(version, str): return version if isinstance(version, Version): return version.base() if isinstance(version, tuple): return ".".join(map(str, version)) raise Exception( ( "I only know how to look at a __version__ that is a str, " "an Increment Version, or a tuple. If you can't provide " "that, use the --version argument and specify one." ) ) def get_project_name(package_dir, package): module = _get_package(package_dir, package) version = getattr(module, "__version__", None) if not version: # welp idk return package.title() if isinstance(version, str): return package.title() if isinstance(version, Version): # Incremental has support for package names return version.package towncrier-19.2.0/src/towncrier/_settings.py000066400000000000000000000044221343140321200207660ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. import os import toml from collections import OrderedDict _start_string = u".. towncrier release notes start\n" _title_format = u"{name} {version} ({project_date})" _template_fname = None _default_types = OrderedDict( [ (u"feature", {"name": u"Features", "showcontent": True}), (u"bugfix", {"name": u"Bugfixes", "showcontent": True}), (u"doc", {"name": u"Improved Documentation", "showcontent": True}), (u"removal", {"name": u"Deprecations and Removals", "showcontent": True}), (u"misc", {"name": u"Misc", "showcontent": False}), ] ) _underlines = ["=", "-", "~"] def load_config(directory): return load_config_from_file(os.path.join(directory, "pyproject.toml")) def load_config_from_file(from_file): if not os.path.exists(from_file): return None with open(from_file, "r") as conffile: config = toml.load(conffile) return parse_toml(config) def parse_toml(config): if 'tool' not in config: raise ValueError("No [tool.towncrier] section.") config = config["tool"]["towncrier"] sections = OrderedDict() types = OrderedDict() if "section" in config: for x in config["section"]: sections[x.get("name", "")] = x["path"] else: sections[""] = "" if "type" in config: for x in config["type"]: types[x["directory"]] = {"name": x["name"], "showcontent": x["showcontent"]} else: types = _default_types wrap = config.get("wrap", False) if isinstance(wrap, str): if wrap in ["true", "True", "1"]: wrap = True else: wrap = False return { "package": config.get("package", ""), "package_dir": config.get("package_dir", "."), "filename": config.get("filename", "NEWS.rst"), "directory": config.get("directory"), "sections": sections, "types": types, "template": config.get("template", _template_fname), "start_line": config.get("start_string", _start_string), "title_format": config.get("title_format", _title_format), "issue_format": config.get("issue_format"), "underlines": config.get("underlines", _underlines), "wrap": wrap, } towncrier-19.2.0/src/towncrier/_version.py000066400000000000000000000004121343140321200206060ustar00rootroot00000000000000""" Provides towncrier version information. """ # This file is auto-generated! Do not edit! # Use `python -m incremental.update towncrier` to change this file. from incremental import Version __version__ = Version('towncrier', 19, 2, 0) __all__ = ["__version__"] towncrier-19.2.0/src/towncrier/_writer.py000066400000000000000000000022161343140321200204410ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. """ Responsible for writing the built news fragments to a news file without affecting existing content. """ from __future__ import absolute_import, division import os def append_to_newsfile(directory, filename, start_line, top_line, content): news_file = os.path.join(directory, filename) if not os.path.exists(news_file): existing_content = u"" else: with open(news_file, "rb") as f: existing_content = f.read().decode("utf8") existing_content = existing_content.split(start_line, 1) if top_line in existing_content: raise ValueError("It seems you've already produced newsfiles for this version?") with open(os.path.join(directory, filename), "wb") as f: if len(existing_content) > 1: f.write(existing_content.pop(0).rstrip().encode("utf8")) f.write((u"\n\n" + start_line + u"\n").encode("utf8")) f.write(top_line.encode("utf8")) f.write(content.encode("utf8")) if existing_content[0]: f.write(b"\n\n") f.write(existing_content[0].lstrip().encode("utf8")) towncrier-19.2.0/src/towncrier/check.py000066400000000000000000000051071343140321200200450ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2018 # See LICENSE for details. from __future__ import absolute_import, division import os import sys import click from subprocess import ( CalledProcessError, check_output, STDOUT, ) from ._settings import load_config, load_config_from_file from ._builder import find_fragments def _run(args, **kwargs): kwargs["stderr"] = STDOUT return check_output(args, **kwargs) @click.command() @click.option("--compare-with", default="origin/master") @click.option("--dir", "directory", default=".") @click.option("--pyproject", "pyproject", default=None) def _main(compare_with, directory, pyproject): return __main(compare_with, directory, pyproject) def __main(comparewith, directory, pyproject): base_directory = os.path.abspath(directory) if pyproject is None: config = load_config(directory) else: config = load_config_from_file(pyproject) try: files_changed = ( _run(["git", "diff", "--name-only", comparewith + "..."], cwd=base_directory) .decode(getattr(sys.stdout, "encoding", "utf8")) .strip() ) except CalledProcessError as e: click.echo("git produced output while failing:") click.echo(e.output) raise if not files_changed: click.echo("On trunk, or no diffs, so no newsfragment required.") sys.exit(0) files = set( map( lambda x: os.path.join(base_directory, x), files_changed.strip().split(os.linesep), ) ) click.echo("Looking at these files:") click.echo("----") for n, change in enumerate(files, start=1): click.echo("{}. {}".format(n, change)) click.echo("----") fragments = set() if config.get("directory"): base_directory = os.path.abspath(config["directory"]) fragment_directory = None else: base_directory = os.path.abspath( os.path.join(directory, config["package_dir"], config["package"]) ) fragment_directory = "newsfragments" fragments = set( find_fragments( base_directory, config["sections"], fragment_directory, config["types"] )[1] ) fragments_in_branch = fragments & files if not fragments_in_branch: click.echo("No new newsfragments found on this branch.") sys.exit(1) else: click.echo("Found:") for n, fragment in enumerate(fragments_in_branch, start=1): click.echo("{}. {}".format(n, fragment)) sys.exit(0) if __name__ == "__main__": # pragma: no cover _main() towncrier-19.2.0/src/towncrier/newsfragments/000077500000000000000000000000001343140321200212765ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/newsfragments/.gitignore000066400000000000000000000000141343140321200232610ustar00rootroot00000000000000!.gitignore towncrier-19.2.0/src/towncrier/templates/000077500000000000000000000000001343140321200204115ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/templates/template.rst000066400000000000000000000014311343140321200227550ustar00rootroot00000000000000{% for section, _ in sections.items() %} {% set underline = underlines[0] %}{% if section %}{{section}} {{ underline * section|length }}{% set underline = underlines[1] %} {% endif %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} - {{ text }} ({{ values|join(', ') }}) {% endfor %} {% else %} - {{ sections[section][category]['']|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} towncrier-19.2.0/src/towncrier/test/000077500000000000000000000000001343140321200173725ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/test/__init__.py000066400000000000000000000000001343140321200214710ustar00rootroot00000000000000towncrier-19.2.0/src/towncrier/test/test_check.py000066400000000000000000000100501343140321200220540ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2017 # See LICENSE for details. import os from twisted.trial.unittest import TestCase from click.testing import CliRunner from subprocess import call from towncrier.check import _main def create_project(pyproject_path): with open(pyproject_path, "w") as f: f.write("[tool.towncrier]\n" 'package = "foo"\n') os.mkdir("foo") with open("foo/__init__.py", "w") as f: f.write('__version__ = "1.2.3"\n') os.mkdir("foo/newsfragments") fragment_path = "foo/newsfragments/123.feature" with open(fragment_path, "w") as f: f.write("Adds levitation") call(["git", "init"]) call(["git", "config", "user.name", "user"]) call(["git", "config", "user.email", "user@example.com"]) call(["git", "add", "."]) call(["git", "commit", "-m", "Initial Commit"]) call(["git", "checkout", "-b", "otherbranch"]) class TestChecker(TestCase): maxDiff = None def test_git_fails(self): """ If git fails to report a comparison, git's output is reported to aid in debugging the situation. """ runner = CliRunner() with runner.isolated_filesystem(): create_project("pyproject.toml") result = runner.invoke(_main, ["--compare-with", "hblaugh"]) self.assertIn( "git produced output while failing", result.output, ) self.assertIn( "hblaugh", result.output, ) def test_no_changes_made(self): self._test_no_changes_made( "pyproject.toml", lambda runner, main, argv: runner.invoke(main, argv), ) def test_no_changes_made_pyproject_path(self): pyproject = "not-pyproject.toml" self._test_no_changes_made( pyproject, lambda runner, main, argv: runner.invoke( main, argv + ["--pyproject", pyproject], ), ) def _test_no_changes_made(self, pyproject_path, invoke): runner = CliRunner() with runner.isolated_filesystem(): create_project(pyproject_path) result = invoke(runner, _main, ["--compare-with", "master"]) self.assertEqual(0, result.exit_code) self.assertEqual( "On trunk, or no diffs, so no newsfragment required.\n", result.output ) def test_fragment_exists(self): runner = CliRunner() with runner.isolated_filesystem(): create_project("pyproject.toml") file_path = "foo/somefile.py" with open(file_path, "w") as f: f.write("import os") call(["git", "add", "foo/somefile.py"]) call(["git", "commit", "-m", "add a file"]) fragment_path = "foo/newsfragments/1234.feature" with open(fragment_path, "w") as f: f.write("Adds gravity back") call(["git", "add", fragment_path]) call(["git", "commit", "-m", "add a newsfragment"]) result = runner.invoke(_main, ["--compare-with", "master"]) self.assertTrue( result.output.endswith( "Found:\n1. " + os.path.abspath(fragment_path) + "\n" ), result, ) self.assertEqual( 0, result.exit_code, result, ) def test_fragment_missing(self): runner = CliRunner() with runner.isolated_filesystem(): create_project("pyproject.toml") file_path = "foo/somefile.py" with open(file_path, "w") as f: f.write("import os") call(["git", "add", "foo/somefile.py"]) call(["git", "commit", "-m", "add a file"]) result = runner.invoke(_main, ["--compare-with", "master"]) self.assertEqual(1, result.exit_code) self.assertTrue( result.output.endswith("No new newsfragments found on this branch.\n") ) towncrier-19.2.0/src/towncrier/test/test_cli.py000066400000000000000000000256331343140321200215630ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. import os from subprocess import call from textwrap import dedent from twisted.trial.unittest import TestCase from click.testing import CliRunner from .. import _main def setup_simple_project(): with open("pyproject.toml", "w") as f: f.write("[tool.towncrier]\n" 'package = "foo"\n') os.mkdir("foo") with open("foo/__init__.py", "w") as f: f.write('__version__ = "1.2.3"\n') os.mkdir("foo/newsfragments") class TestCli(TestCase): maxDiff = None def test_happy_path(self): runner = CliRunner() with runner.isolated_filesystem(): setup_simple_project() with open("foo/newsfragments/123.feature", "w") as f: f.write("Adds levitation") # Towncrier treats this as 124.feature, ignoring .rst extension with open("foo/newsfragments/124.feature.rst", "w") as f: f.write("Extends levitation") # Towncrier ignores files that don't have a dot with open("foo/newsfragments/README", "w") as f: f.write("Blah blah") # And files that don't have a valid category with open("foo/newsfragments/README.rst", "w") as f: f.write("**Blah blah**") result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) self.assertEqual(0, result.exit_code) self.assertEqual( result.output, u"Loading template...\nFinding news fragments...\nRendering news " u"fragments...\nDraft only -- nothing has been written.\nWhat is " u"seen below is what would be written.\n\nFoo 1.2.3 (01-01-2001)" u"\n======================\n" u"\n\nFeatures\n--------\n\n- Adds levitation (#123)\n" u"- Extends levitation (#124)\n\n", ) def test_collision(self): runner = CliRunner() with runner.isolated_filesystem(): setup_simple_project() # Note that both are 123.feature with open("foo/newsfragments/123.feature", "w") as f: f.write("Adds levitation") with open("foo/newsfragments/123.feature.rst", "w") as f: f.write("Extends levitation") result = runner.invoke(_main, ["--draft", "--date", "01-01-2001"]) # This should fail self.assertEqual(type(result.exception), ValueError) self.assertIn("multiple files for 123.feature", str(result.exception)) def test_section_and_type_sorting(self): """ Sections and types should be output in the same order that they're defined in the config file. """ runner = CliRunner() def run_order_scenario(sections, types): with runner.isolated_filesystem(): with open("pyproject.toml", "w") as f: f.write( dedent( """ [tool.towncrier] package = "foo" directory = "news" """ ) ) for section in sections: f.write( dedent( """ [[tool.towncrier.section]] path = "{section}" name = "{section}" """.format( section=section ) ) ) for type_ in types: f.write( dedent( """ [[tool.towncrier.type]] directory = "{type_}" name = "{type_}" showcontent = true """.format( type_=type_ ) ) ) os.mkdir("foo") with open("foo/__init__.py", "w") as f: f.write('__version__ = "1.2.3"\n') os.mkdir("news") for section in sections: sectdir = "news/" + section os.mkdir(sectdir) for type_ in types: with open("{}/1.{}".format(sectdir, type_), "w") as f: f.write("{} {}".format(section, type_)) return runner.invoke( _main, ["--draft", "--date", "01-01-2001"], catch_exceptions=False ) result = run_order_scenario(["section-a", "section-b"], ["type-1", "type-2"]) self.assertEqual(0, result.exit_code) self.assertEqual( result.output, u"Loading template...\nFinding news fragments...\nRendering news " u"fragments...\nDraft only -- nothing has been written.\nWhat is " u"seen below is what would be written.\n\nFoo 1.2.3 (01-01-2001)" u"\n======================\n" + dedent( """ section-a --------- type-1 ~~~~~~ - section-a type-1 (#1) type-2 ~~~~~~ - section-a type-2 (#1) section-b --------- type-1 ~~~~~~ - section-b type-1 (#1) type-2 ~~~~~~ - section-b type-2 (#1) """ ), ) result = run_order_scenario(["section-b", "section-a"], ["type-2", "type-1"]) self.assertEqual(0, result.exit_code) self.assertEqual( result.output, u"Loading template...\nFinding news fragments...\nRendering news " u"fragments...\nDraft only -- nothing has been written.\nWhat is " u"seen below is what would be written.\n\nFoo 1.2.3 (01-01-2001)" u"\n======================\n" + dedent( """ section-b --------- type-2 ~~~~~~ - section-b type-2 (#1) type-1 ~~~~~~ - section-b type-1 (#1) section-a --------- type-2 ~~~~~~ - section-a type-2 (#1) type-1 ~~~~~~ - section-a type-1 (#1) """ ), ) def test_no_confirmation(self): runner = CliRunner() with runner.isolated_filesystem(): setup_simple_project() fragment_path1 = "foo/newsfragments/123.feature" fragment_path2 = "foo/newsfragments/124.feature.rst" with open(fragment_path1, "w") as f: f.write("Adds levitation") with open(fragment_path2, "w") as f: f.write("Extends levitation") call(["git", "init"]) call(["git", "config", "user.name", "user"]) call(["git", "config", "user.email", "user@example.com"]) call(["git", "add", "."]) call(["git", "commit", "-m", "Initial Commit"]) result = runner.invoke(_main, ["--date", "01-01-2001", "--yes"]) self.assertEqual(0, result.exit_code) path = "NEWS.rst" self.assertTrue(os.path.isfile(path)) self.assertFalse(os.path.isfile(fragment_path1)) self.assertFalse(os.path.isfile(fragment_path2)) def test_projectless_changelog(self): """In which a directory containing news files is built into a changelog - without a Python project or version number. We override the project title from the commandline. """ runner = CliRunner() with runner.isolated_filesystem(): with open("pyproject.toml", "w") as f: f.write("[tool.towncrier]\n" 'package = "foo"\n') os.mkdir("foo") os.mkdir("foo/newsfragments") with open("foo/newsfragments/123.feature", "w") as f: f.write("Adds levitation") # Towncrier ignores .rst extension with open("foo/newsfragments/124.feature.rst", "w") as f: f.write("Extends levitation") result = runner.invoke( _main, [ "--name", "FooBarBaz", "--version", "7.8.9", "--date", "01-01-2001", "--draft", ], ) self.assertEqual(0, result.exit_code) self.assertEqual( result.output, dedent( """ Loading template... Finding news fragments... Rendering news fragments... Draft only -- nothing has been written. What is seen below is what would be written. FooBarBaz 7.8.9 (01-01-2001) ============================ Features -------- - Adds levitation (#123) - Extends levitation (#124) """ ).lstrip(), ) def test_no_package_changelog(self): """The calling towncrier with any package argument. Specifying a package in the toml file or the command line should not always be needed: - we can set the version number on the command line, so we do not need the package for that. - we don't need to include the package in the changelog header. """ runner = CliRunner() with runner.isolated_filesystem(): with open("pyproject.toml", "w") as f: f.write( "[tool.towncrier]\n" 'title_format = "{version} ({project_date})"\n' ) os.mkdir("newsfragments") with open("newsfragments/123.feature", "w") as f: f.write("Adds levitation") result = runner.invoke( _main, ["--version", "7.8.9", "--date", "01-01-2001", "--draft"] ) self.assertEqual(0, result.exit_code) self.assertEqual( result.output, dedent( """ Loading template... Finding news fragments... Rendering news fragments... Draft only -- nothing has been written. What is seen below is what would be written. 7.8.9 (01-01-2001) ================== Features -------- - Adds levitation (#123) """ ).lstrip(), ) towncrier-19.2.0/src/towncrier/test/test_format.py000066400000000000000000000203041343140321200222720ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. from __future__ import absolute_import, division import pkg_resources from twisted.trial.unittest import TestCase from collections import OrderedDict from .._builder import render_fragments, split_fragments class FormatterTests(TestCase): def test_split(self): fragments = { "": { ("1", "misc", 0): u"", ("baz", "misc", 0): u"", ("2", "feature", 0): u"Foo added.", ("5", "feature", 0): u"Foo added. \n", ("6", "bugfix", 0): u"Foo added.", }, "Web": { ("3", "bugfix", 0): u"Web fixed. ", ("4", "feature", 0): u"Foo added.", }, } expected_output = { "": { "misc": {"": ["1", "baz"]}, "feature": {u"Foo added.": ["2", "5"]}, "bugfix": {u"Foo added.": ["6"]}, }, "Web": { "bugfix": {u"Web fixed.": ["3"]}, "feature": {u"Foo added.": ["4"]}, }, } definitions = OrderedDict( [ ("feature", {"name": "Features", "showcontent": True}), ("bugfix", {"name": "Bugfixes", "showcontent": True}), ("misc", {"name": "Misc", "showcontent": False}), ] ) output = split_fragments(fragments, definitions) self.assertEqual(expected_output, output) def test_basic(self): """ Basic functionality -- getting a bunch of news fragments and formatting them into a rST file -- works. """ fragments = OrderedDict( [ ( "", { # asciibetical sorting will do 1, 142, 9 # we want 1, 9, 142 instead ("142", "misc", 0): u"", ("1", "misc", 0): u"", ("9", "misc", 0): u"", ("bar", "misc", 0): u"", ("4", "feature", 0): u"Stuff!", ("2", "feature", 0): u"Foo added.", ("72", "feature", 0): u"Foo added.", ("9", "feature", 0): u"Foo added.", ("baz", "feature", 0): u"Fun!", }, ), ("Names", {}), ("Web", {("3", "bugfix", 0): u"Web fixed."}), ] ) definitions = OrderedDict( [ ("feature", {"name": "Features", "showcontent": True}), ("bugfix", {"name": "Bugfixes", "showcontent": True}), ("misc", {"name": "Misc", "showcontent": False}), ] ) expected_output = ( u""" Features -------- - Fun! (baz) - Foo added. (#2, #9, #72) - Stuff! (#4) Misc ---- - bar, #1, #9, #142 Names ----- No significant changes. Web --- Bugfixes ~~~~~~~~ - Web fixed. (#3) """ ) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") fragments = split_fragments(fragments, definitions) output = render_fragments( template, None, fragments, definitions, ["-", "~"], wrap=True ) self.assertEqual(output, expected_output) # Check again with non-default underlines expected_output_weird_underlines = ( u""" Features ******** - Fun! (baz) - Foo added. (#2, #9, #72) - Stuff! (#4) Misc **** - bar, #1, #9, #142 Names ***** No significant changes. Web *** Bugfixes ^^^^^^^^ - Web fixed. (#3) """ ) output = render_fragments( template, None, fragments, definitions, ["*", "^"], wrap=True ) self.assertEqual(output, expected_output_weird_underlines) def test_issue_format(self): """ issue_format option can be used to format issue text. And sorting happens before formatting, so numerical issues are still ordered numerically even if that doesn't match asciibetical order on the final text. """ fragments = { "": { # asciibetical sorting will do 1, 142, 9 # we want 1, 9, 142 instead ("142", "misc", 0): u"", ("1", "misc", 0): u"", ("9", "misc", 0): u"", ("bar", "misc", 0): u"", } } definitions = OrderedDict([("misc", {"name": "Misc", "showcontent": False})]) expected_output = ( u""" Misc ---- - xxbar, xx1, xx9, xx142 """ ) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") fragments = split_fragments(fragments, definitions) output = render_fragments( template, u"xx{issue}", fragments, definitions, ["-", "~"], wrap=True ) self.assertEqual(output, expected_output) def test_line_wrapping(self): """ Output is nicely wrapped, but doesn't break up words (which can mess up URLs) """ self.maxDiff = None fragments = { "": { ( "1", "feature", 0, ): u""" asdf asdf asdf asdf looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong newsfragment. """, # NOQA ("2", "feature", 0): u"https://google.com/q=?" + u"-" * 100, ("3", "feature", 0): u"a " * 80, } } definitions = OrderedDict( [("feature", {"name": "Features", "showcontent": True})] ) expected_output = ( u""" Features -------- - asdf asdf asdf asdf looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong newsfragment. (#1) - https://google.com/q=?---------------------------------------------------------------------------------------------------- (#2) - a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a (#3) """ ) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") fragments = split_fragments(fragments, definitions) output = render_fragments( template, None, fragments, definitions, ["-", "~"], wrap=True ) self.assertEqual(output, expected_output) def test_line_wrapping_disabled(self): """ Output is not wrapped if it's disabled. """ self.maxDiff = None fragments = { "": { ( "1", "feature", 0 ): u""" asdf asdf asdf asdf looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong newsfragment. """, # NOQA ("2", "feature", 0): u"https://google.com/q=?" + u"-" * 100, ("3", "feature", 0): u"a " * 80, } } definitions = OrderedDict( [("feature", {"name": "Features", "showcontent": True})] ) expected_output = ( u""" Features -------- - asdf asdf asdf asdf looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong newsfragment. (#1) - https://google.com/q=?---------------------------------------------------------------------------------------------------- (#2) - a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a (#3) """ # NOQA ) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") fragments = split_fragments(fragments, definitions) output = render_fragments( template, None, fragments, definitions, ["-", "~"], wrap=False ) self.assertEqual(output, expected_output) towncrier-19.2.0/src/towncrier/test/test_project.py000066400000000000000000000020251343140321200224500ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. import os from twisted.trial.unittest import TestCase from .._project import get_version class VersionFetchingTests(TestCase): def test_str(self): """ A str __version__ will be picked up. """ temp = self.mktemp() os.makedirs(temp) os.makedirs(os.path.join(temp, "mytestproj")) with open(os.path.join(temp, "mytestproj", "__init__.py"), "w") as f: f.write("__version__ = '1.2.3'") version = get_version(temp, "mytestproj") self.assertEqual(version, "1.2.3") def test_tuple(self): """ A tuple __version__ will be picked up. """ temp = self.mktemp() os.makedirs(temp) os.makedirs(os.path.join(temp, "mytestproja")) with open(os.path.join(temp, "mytestproja", "__init__.py"), "w") as f: f.write("__version__ = (1, 3, 12)") version = get_version(temp, "mytestproja") self.assertEqual(version, "1.3.12") towncrier-19.2.0/src/towncrier/test/test_settings.py000066400000000000000000000013571343140321200226510ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. from twisted.trial.unittest import TestCase import os from .._settings import load_config class TomlSettingsTests(TestCase): def test_base(self): """ Test a "base config". """ temp = self.mktemp() os.makedirs(temp) with open(os.path.join(temp, "pyproject.toml"), "w") as f: f.write( """[tool.towncrier] package = "foobar" """ ) config = load_config(temp) self.assertEqual(config["package"], "foobar") self.assertEqual(config["package_dir"], ".") self.assertEqual(config["filename"], "NEWS.rst") self.assertEqual(config["underlines"], ["=", "-", "~"]) towncrier-19.2.0/src/towncrier/test/test_write.py000066400000000000000000000122111343140321200221320ustar00rootroot00000000000000# Copyright (c) Amber Brown, 2015 # See LICENSE for details. from twisted.trial.unittest import TestCase import pkg_resources import os from collections import OrderedDict from .._builder import render_fragments, split_fragments from .._writer import append_to_newsfile class WritingTests(TestCase): def test_append_at_top(self): fragments = OrderedDict( [ ( "", OrderedDict( [ (("142", "misc", 0), u""), (("1", "misc", 0), u""), (("4", "feature", 0), u"Stuff!"), (("4", "feature", 1), u"Second Stuff!"), (("2", "feature", 0), u"Foo added."), (("72", "feature", 0), u"Foo added."), ] ), ), ("Names", {}), ("Web", {("3", "bugfix", 0): u"Web fixed."}), ] ) definitions = OrderedDict( [ ("feature", {"name": "Features", "showcontent": True}), ("bugfix", {"name": "Bugfixes", "showcontent": True}), ("misc", {"name": "Misc", "showcontent": False}), ] ) expected_output = """MyProject 1.0 ============= Features -------- - Foo added. (#2, #72) - Stuff! (#4) - Second Stuff! (#4) Misc ---- - #1, #142 Names ----- No significant changes. Web --- Bugfixes ~~~~~~~~ - Web fixed. (#3) Old text. """ tempdir = self.mktemp() os.mkdir(tempdir) with open(os.path.join(tempdir, "NEWS.rst"), "w") as f: f.write("Old text.\n") fragments = split_fragments(fragments, definitions) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") append_to_newsfile( tempdir, "NEWS.rst", ".. towncrier release notes start\n", "MyProject 1.0\n=============\n", render_fragments( template, None, fragments, definitions, ["-", "~"], wrap=True ), ) with open(os.path.join(tempdir, "NEWS.rst"), "r") as f: output = f.read() self.assertEqual(expected_output, output) def test_append_at_top_with_hint(self): """ If there is a comment with C{.. towncrier release notes start}, towncrier will add the version notes after it. """ fragments = OrderedDict( [ ( "", { ("142", "misc", 0): u"", ("1", "misc", 0): u"", ("4", "feature", 0): u"Stuff!", ("2", "feature", 0): u"Foo added.", ("72", "feature", 0): u"Foo added.", ("99", "feature", 0): u"Foo! " * 100, }, ), ("Names", {}), ("Web", {("3", "bugfix", 0): u"Web fixed."}), ] ) definitions = OrderedDict( [ ("feature", {"name": "Features", "showcontent": True}), ("bugfix", {"name": "Bugfixes", "showcontent": True}), ("misc", {"name": "Misc", "showcontent": False}), ] ) expected_output = """Hello there! Here is some info. .. towncrier release notes start MyProject 1.0 ============= Features -------- - Foo added. (#2, #72) - Stuff! (#4) - Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! Foo! (#99) Misc ---- - #1, #142 Names ----- No significant changes. Web --- Bugfixes ~~~~~~~~ - Web fixed. (#3) Old text. """ tempdir = self.mktemp() os.mkdir(tempdir) with open(os.path.join(tempdir, "NEWS.rst"), "w") as f: f.write( ( "Hello there! Here is some info.\n\n" ".. towncrier release notes start\nOld text.\n" ) ) fragments = split_fragments(fragments, definitions) template = pkg_resources.resource_string( "towncrier", "templates/template.rst" ).decode("utf8") append_to_newsfile( tempdir, "NEWS.rst", ".. towncrier release notes start\n", "MyProject 1.0\n=============\n", render_fragments( template, None, fragments, definitions, ["-", "~"], wrap=True ), ) with open(os.path.join(tempdir, "NEWS.rst"), "r") as f: output = f.read() self.assertEqual(expected_output, output) towncrier-19.2.0/tox.ini000066400000000000000000000012441343140321200151240ustar00rootroot00000000000000[tox] envlist = flake8, {pypy3,py27,py34,py35,py36}-tests [testenv:flake8] skip_install = True deps = flake8 commands = flake8 src/towncrier/ basepython = python3.6 [testenv:check-manifest] skip_install = True deps = check_manifest commands = check-manifest -v basepython = python3.6 [testenv:check-newsfragment] commands = python -m towncrier.check basepython = python3.6 [testenv] deps = Twisted coverage incremental commands = python -V coverage --version {envbindir}/trial --version coverage erase coverage run -p {envbindir}/trial {posargs:towncrier} coverage combine coverage report [flake8] max-line-length = 99