├── .coveragerc ├── .dockerignore ├── .flake8 ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── CREDITS.rst ├── Dockerfile ├── LICENSE.GPL ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── doc └── source │ ├── assumptions.rst │ ├── changelog.rst │ ├── conf.py │ ├── credits.rst │ ├── developing.rst │ ├── entrypoints.rst │ ├── further_reading.rst │ ├── index.rst │ ├── options.rst │ ├── overview.rst │ ├── project.rst │ ├── uploading.rst │ └── versions.rst ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tox.ini └── zest └── releaser ├── __init__.py ├── addchangelogentry.py ├── baserelease.py ├── bumpversion.py ├── choose.py ├── fullrelease.py ├── git.py ├── lasttagdiff.py ├── lasttaglog.py ├── longtest.py ├── postrelease.py ├── preparedocs.py ├── prerelease.py ├── pypi.py ├── release.py ├── tests ├── __init__.py ├── addchangelogentry.txt ├── baserelease.txt ├── bumpversion.txt ├── choose.txt ├── cmd_error.py ├── example.tar ├── fullrelease.txt ├── functional-git.txt ├── functional-with-hooks.txt ├── functional.py ├── git.txt ├── postrelease.txt ├── preparedocs.txt ├── prerelease.txt ├── pypi.txt ├── pypirc.txt ├── pypirc_both.txt ├── pypirc_new.txt ├── pypirc_no_input.txt ├── pypirc_no_release.txt ├── pypirc_old.txt ├── pypirc_simple.txt ├── pypirc_universal_nocreate.txt ├── pypirc_yes_release.txt ├── pyproject-toml.txt ├── pyproject.toml ├── release.txt ├── test_setup.py ├── utils.txt └── vcs.txt ├── utils.py └── vcs.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | /Library/* 4 | *buildout/eggs/* 5 | /usr/* 6 | /var/lib/hudson/.buildout/* 7 | /var/lib/jenkins/.buildout/* 8 | eggs/* 9 | local_checkouts/* 10 | parts/* 11 | 12 | ignore_errors = true 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .Python 4 | .installed.cfg 5 | bin 6 | coverage 7 | develop-eggs 8 | dist 9 | eggs 10 | include 11 | lib 12 | /local 13 | parts 14 | zest.releaser.egg-info 15 | .coverage 16 | htmlcov 17 | doc/build/ 18 | 19 | .tox/ 20 | 21 | lib-python/ 22 | lib_pypy/ 23 | site-packages/ 24 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E203, E501, W503 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | CHANGES.rst merge=union 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every week 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.11" 19 | 20 | - name: Run black, flake8, isort 21 | uses: pre-commit/action@v3.0.1 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | schedule: 7 | - cron: '13 7 * * 0' # run once a week on Sunday 8 | # Allow to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | config: 15 | # [Python version, tox env] 16 | - ["3.9", "py39"] 17 | - ["3.10", "py310"] 18 | - ["3.11", "py311"] 19 | - ["3.12", "py312"] 20 | - ["3.13", "py313"] 21 | - ["pypy3.11", "pypy311"] 22 | 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.config[1] }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.config[0] }} 31 | cache: 'pip' 32 | cache-dependency-path: | 33 | setup.* 34 | tox.ini 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install tox 40 | 41 | - name: Test 42 | run: tox -e ${{ matrix.config[1] }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .Python 4 | .installed.cfg 5 | bin 6 | coverage 7 | develop-eggs 8 | dist 9 | eggs 10 | include 11 | lib 12 | /local 13 | parts 14 | /pyvenv.cfg 15 | zest.releaser.egg-info 16 | .coverage 17 | htmlcov 18 | doc/build/ 19 | 20 | .tox/ 21 | 22 | lib-python/ 23 | lib_pypy/ 24 | site-packages/ 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.4.0 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-toml 11 | - repo: https://github.com/pycqa/isort 12 | rev: '5.12.0' 13 | hooks: 14 | - id: isort 15 | - repo: https://github.com/psf/black 16 | rev: '23.7.0' 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/pycqa/flake8 20 | rev: '6.0.0' 21 | hooks: 22 | - id: flake8 23 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-24.04 11 | tools: 12 | python: "3.12" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: doc/source/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - method: pip 23 | path: . 24 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog for zest.releaser 2 | =========================== 3 | 4 | 9.6.3 (unreleased) 5 | ------------------ 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 9.6.2 (2025-04-11) 11 | ------------------ 12 | 13 | - Administrative change: our ``pyproject.toml`` now has the license `in the new SPDX format 14 | `_. 15 | [reinout] 16 | 17 | 18 | 9.6.1 (2025-04-10) 19 | ------------------ 20 | 21 | - Fixed readthedocs documentation generation. 22 | [reinout] 23 | 24 | 25 | 9.6.0 (2025-04-08) 26 | ------------------ 27 | 28 | - Require ``build`` 1.2.0 or higher. 29 | This added the ``installer`` argument to ``DefaultIsolatedEnv``. 30 | [maurits] 31 | 32 | 33 | 9.5.0 (2025-03-08) 34 | ------------------ 35 | 36 | - Worked around python 3.9 version-reporting issue that could prevent startup. 37 | [maurits] 38 | 39 | - Really build source dist and wheel in isolation. 40 | Previously we passed along our own ``PYTHONPATH``, but that could lead to an unintended ``setuptools`` version being used. 41 | [maurits] 42 | 43 | 44 | 9.4.0 (2025-03-05) 45 | ------------------ 46 | 47 | - Requiring the ``wheel`` package now as everybody (rightfully so) uses wheels 48 | nowadays. It used to be an optional dependency beforehand, though often automatically 49 | included through setuptools' vendored libraries. 50 | You can switch off creation of wheels by setting the option ``create-wheel = false``. 51 | See our `options documentation `_. 52 | [reinout] 53 | 54 | 55 | 9.3.1 (2025-03-04) 56 | ------------------ 57 | 58 | - Add ``packaging`` to our dependencies. 59 | We were already pulling this in via another dependency. 60 | [maurits] 61 | 62 | - Removed remaining ``pkg_resources`` usage. 63 | [reinout, maurits] 64 | 65 | 66 | 9.3.0 (2025-03-03) 67 | ------------------ 68 | 69 | - Added python 3.13 compatibility (=pkg_resources deprecation). 70 | [stevepiercy] 71 | 72 | - Added support for python 3.12 en 3.13 (=we're testing on those two now). 3.12 already 73 | worked fine, 3.13 needed the pkg_resources fix mentioned above. 74 | [reinout] 75 | 76 | - Dropping support for python 3.8 as it is end of life. 77 | Also, the ``importlib`` we now use is still provisional in 3.8 and results in some errors. 78 | [reinout] 79 | 80 | 81 | 9.2.0 (2024-06-16) 82 | ------------------ 83 | 84 | - Fixed version handling documentation to use ``importlib`` instead of 85 | ``pkg_resources``. 86 | [reinout] 87 | 88 | - Build distributions in an isolated environment. 89 | Otherwise `build` cannot install packages needed for the build system, for example `hatchling`. 90 | Fixes `issue 448 `_. 91 | [maurits] 92 | 93 | 94 | 9.1.3 (2024-02-07) 95 | ------------------ 96 | 97 | - Fix to the project setup. ``tox.ini`` uses ``extras =`` instead of ``deps =`` to 98 | install the test extras. 99 | [mtelka] 100 | 101 | 102 | 9.1.2 (2024-02-05) 103 | ------------------ 104 | 105 | - If you want to build a release package (release=true, the default), but don't want to 106 | actually upload it, you can now set the ``upload-pypi`` option to false (default is 107 | true). 108 | [leplatrem] 109 | 110 | 111 | 9.1.1 (2023-10-11) 112 | ------------------ 113 | 114 | - When reading ``~/.pypirc`` config, read ``setup.cfg`` as well, as it might 115 | override some of these values, like ``[distutils] index-servers``. 116 | Fixes issue #436. [maurits] 117 | 118 | 119 | 9.1.0 (2023-10-03) 120 | ------------------ 121 | 122 | - Using newer 'build' (``>=1.0.0``) including a slight API change, fixes 123 | #433. [reinout] 124 | 125 | - Typo fix in the readme: we look at ``__version__`` instead of 126 | the previously-documented ``__versions__``... [reinout] 127 | 128 | 129 | 9.0.0 (2023-09-11) 130 | ------------------ 131 | 132 | - Make final release. Nothing changed since the last beta. [maurits] 133 | 134 | 135 | 9.0.0b1 (2023-07-31) 136 | -------------------- 137 | 138 | - When a command we call exits with a non-zero exit code, clearly state this in the output. 139 | Ask the user if she wants to continue or not. 140 | Note that this is tricky to do right. Some commands like ``git`` seem to print everything to stderr, 141 | leading us to think there are errors, but the exit code is zero, so it should be fine. 142 | We do not want to ask too many questions, but we do not want to silently swallow important errors either. 143 | [maurits] 144 | 145 | 146 | 9.0.0a3 (2023-07-25) 147 | -------------------- 148 | 149 | - Updated contributors list. 150 | 151 | - Documenting ``hook_package_dir`` setting for entry points (which isn't 152 | needed for most entry points, btw). 153 | Fixes `issue 370 `_. 154 | 155 | - Allowing for retry for ``git push``, which might fail because of a protected 156 | branch. Also displaying that possible cause when it occurs. Fixes `issue 385 157 | `_. 158 | 159 | 160 | 9.0.0a2 (2023-07-19) 161 | -------------------- 162 | 163 | - Ignore error output when calling `build`. 164 | We only need to look at the exit code to see if it worked. 165 | You can call zest.releaser with ``--verbose`` if you want 166 | to see the possible warnings. 167 | 168 | - Removed ``encoding`` config option as nobody is using it anymore (using the 169 | option would result in a crash). Apparently it isn't needed anymore now that 170 | we don't use python 2 anymore. Fixes `issue 391 171 | `_. 172 | 173 | - The ``longtest`` is now simpler. It runs readme_renderer and just displays 174 | the result in the browser, without error handling. ``twine check`` should be 175 | used if you want a real hard check (``longtest --headless`` is 176 | deprecated). The advantage is that longtest now also renders markdown 177 | correctly. This adds `readme_renderer[md]` as dependency. 178 | Fixes `issue 363 `_. 179 | 180 | 181 | 9.0.0a1 (2023-07-13) 182 | -------------------- 183 | 184 | - Changed build system to pypa/build instead of explicitly using 185 | setuptools. 186 | 187 | - Zest.releaser's settings can now also be placed in ``pyproject.toml``. 188 | 189 | - Use native namespace packages for ``zest.releaser``, instead of 190 | deprecated ``pkg_resources`` based ones. 191 | 192 | - Added pre-commit config for neater code (black, flake8, isort). 193 | 194 | - Dropped support for python 3.7. Together with switching to ``build`` and 195 | ``pyproject.toml``, this warrants a major version bump. 196 | 197 | 198 | 8.0.0 (2023-05-05) 199 | ------------------ 200 | 201 | - Make final release, no changes since latest alpha. [maurits] 202 | 203 | 204 | 8.0.0a2 (2023-04-06) 205 | -------------------- 206 | 207 | - Always create wheels, except when you explicitly switch this off in the config: 208 | ``[zest.releaser] create-wheel = no``. 209 | If the ``wheel`` package is not available, we still do not create wheels. 210 | Fixes `issue 406 `_. 211 | [maurits] 212 | 213 | - Do not fail when tag versions cannot be parsed. 214 | This can happen in ``lasttaglog``, ``lasttagdiff``, and ``bumpversion``, with ``setuptools`` 66 or higher. 215 | Fixes `issue 408 `_. 216 | [maurits] 217 | 218 | 219 | 8.0.0a1 (2023-02-08) 220 | -------------------- 221 | 222 | - Drop support for Python 3.6. [maurits] 223 | 224 | - Support reading and writing the version in ``pyproject.toml``. 225 | See `issue 295 `_, 226 | `issue 373 `_, 227 | and `PEP-621 `_, 228 | [maurits] 229 | 230 | 231 | 7.3.0 (2023-02-07) 232 | ------------------ 233 | 234 | - Add option ``run-pre-commit = yes / no``. 235 | Default: no. 236 | When set to true, pre commit hooks are run. 237 | This may interfere with releasing when they fail. 238 | [maurits] 239 | 240 | 241 | 7.2.0 (2022-12-09) 242 | ------------------ 243 | 244 | - Auto-detect ``history_format`` based on history filename. 245 | [ericof] 246 | 247 | - Add ``history_format`` option, to explicitly set changelogs 248 | entries in Markdown. 249 | [ericof] 250 | 251 | 252 | 7.1.0 (2022-11-23) 253 | ------------------ 254 | 255 | - Add the ``bumpversion`` options to the ``postrelease`` command. 256 | This means ``feature``, ``breaking``, and ``final``. 257 | [rnc, maurits] 258 | 259 | - Add ``--final`` option to ``bumpversion`` command. 260 | This removes alpha / beta / rc markers from the version. 261 | [maurits] 262 | 263 | - Add support for Python 3.11, remove ``z3c.testsetup`` from test dependencies. [maurits] 264 | 265 | 266 | 7.0.0 (2022-09-09) 267 | ------------------ 268 | 269 | - Optionally add prefix text to commit messages. This can be used ensure your messages follow some regular expression. 270 | To activate this, add ``prefix-message = [TAG]`` to a ``[zest.releaser]`` 271 | section in the ``setup.cfg`` of your package, or your global 272 | ``~/.pypirc``. Or add your favorite geeky quotes there. 273 | [LvffY] 274 | 275 | 276 | 7.0.0a3 (2022-04-04) 277 | -------------------- 278 | 279 | - Bug 381: In ``prerelease``, check with ``pep440`` if the version is canonical. 280 | Added ``pep440`` to the ``recommended`` extra, not to the core dependencies: 281 | ``zest.releaser`` can also be used for non-Python projects. 282 | [maurits] 283 | 284 | 285 | 7.0.0a2 (2022-02-10) 286 | -------------------- 287 | 288 | - Add ``--headless`` option to ``longtest``. 289 | 290 | 291 | 7.0.0a1 (2021-12-01) 292 | -------------------- 293 | 294 | Big cleanup to ease future development: 295 | 296 | - Removed support for Subversion (``svn``), Bazaar (``bzr``), Mercurial (``hg``). 297 | 298 | - Removed support for Python 2 and 3.5. 299 | 300 | - Added support for Python 3.9 and 3.10. 301 | 302 | - Tested with Python 3.6-3.10 plus PyPy3. 303 | 304 | - Switched from Travis to GitHub Actions. 305 | 306 | - Simplified running commands by using ``subprocess.run``. 307 | 308 | 309 | .. # Note: for older changes see ``doc/sources/changelog.rst``. 310 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Hi! 2 | === 3 | 4 | You're using `zest.releaser `_ and want 5 | to report a bug or problem or you want to fix something yourself? Feel free! 6 | Github is the place to do so. 7 | 8 | - Report bugs or problems at 9 | https://github.com/zestsoftware/zest.releaser/issues 10 | 11 | - Or make a fork, fix the bug or add something and open a pull request. 12 | 13 | - You can mail us if you want to ask a question, too. Or if you want to tell 14 | us about an idea you have. Mail both `reinout@vanrees.org 15 | `_ and `maurits@vanrees.org 16 | `_. 17 | 18 | Normally you ought to get a reply from Reinout or Maurits (we're the main 19 | developers) within a few days, but often within a couple of hours. If it takes 20 | longer, feel free to email us as a reminder. 21 | 22 | 23 | If you want to work on zest.releaser, read the developer info in 24 | ``doc/source/developing.rst``. Or read it online at 25 | https://zestreleaser.readthedocs.io/en/latest/developing.html . 26 | 27 | Note that there's a way to extend zest.releaser without modifying 28 | zest.releaser itself: look at the `entrypoints documentation 29 | `_ to see if 30 | that can help you. Several people have used that to tweak zest.releaser to fit 31 | in perfectly with their specific needs. 32 | -------------------------------------------------------------------------------- /CREDITS.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | * `Reinout van Rees `_ (Nelen & Schuurmans) is the 5 | original author. He's still maintaining it, together with Maurits. 6 | 7 | * `Maurits van Rees `_ (Zest Software) added 8 | a heapload of improvements and is the maintainer, together with Reinout. 9 | 10 | * `Kevin Teague `_ (Canada's Michael Smith Genome Sciences 11 | Center) added support for multiple version control systems, most notable 12 | Mercurial. 13 | 14 | * `Wouter vanden Hove `_ (University of Gent) added 15 | support for uploading to multiple servers, using collective.dist. 16 | 17 | * `Godefroid Chapelle `_ (BubbleNet) added /tag besides 18 | /tags for subversion. 19 | 20 | * `Richard Mitchell `_ 21 | (`Isotoma `_) added Python 3 support. 22 | 23 | * `Mateusz Legięcki `_ added a dockerfile for 24 | much easier testing. 25 | 26 | * `Eli Sallé `_ added pyproject.toml support 27 | for zest.releaser's own options. We're modern now! 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13 2 | WORKDIR /zest.releaser/ 3 | ENV PYTHONPATH=/zest.releaser/ 4 | ENV USER=root 5 | RUN pip install -U pip setuptools tox && \ 6 | apt-get update && \ 7 | apt-get -y install git 8 | COPY . /zest.releaser/ 9 | CMD tox -e py313 10 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | zest.releaser is copyright (C) 2008-2012 Zest Software 2 | 3 | This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program; if not, write to the Free Software 15 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, 16 | MA 02111-1307 USA. 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include zest * 2 | recursive-include doc * 3 | include *rst LICENSE.GPL tox.ini 4 | exclude .installed.cfg 5 | exclude .gitattributes 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Package releasing made easy: zest.releaser overview and installation 2 | ==================================================================== 3 | 4 | zest.releaser is collection of command-line programs to help you automate the 5 | task of releasing a Python project. 6 | 7 | It does away with all the boring bits. This is what zest.releaser automates 8 | for you: 9 | 10 | * It updates the version number. The version number can either be in 11 | ``setup.py`` or ``version.txt`` or in a ``__version__`` attribute in a 12 | Python file or in ``setup.cfg``. For example, it switches you from 13 | ``0.3.dev0`` (current development version) to ``0.3`` (release) to 14 | ``0.4.dev0`` (new development version). 15 | 16 | * It updates the history/changes file. It logs the release date on release and 17 | adds a new heading for the upcoming changes (new development version). 18 | 19 | * It tags the release. It creates a tag in your version control system named 20 | after the released version number. 21 | 22 | * It optionally uploads a source release to PyPI. It will only do this if the 23 | package is already registered there (else it will ask, defaulting to 'no'); 24 | zest releaser is careful not to publish your private projects! 25 | 26 | 27 | Most important URLs 28 | ------------------- 29 | 30 | First the three most important links: 31 | 32 | - The full documentation is at `zestreleaser.readthedocs.io 33 | `_. 34 | 35 | - We're `on PyPI `_, so we're only 36 | an ``pip install zest.releaser`` away from installation on your computer. 37 | 38 | - The code is at `github.com/zestsoftware/zest.releaser 39 | `_. 40 | 41 | 42 | Compatibility / Dependencies 43 | ---------------------------- 44 | 45 | .. image:: https://img.shields.io/pypi/pyversions/zest.releaser? :alt: PyPI - Python Version 46 | .. image:: https://img.shields.io/pypi/implementation/zest.releaser? :alt: PyPI - Implementation 47 | 48 | ``zest.releaser`` works on Python 3.9+, including PyPy3. Tested on python 49 | 3.9/3.10/11/12/13 and pypy 3.11, but see ``tox.ini`` for the canonical place for that. 50 | 51 | To be sure: the packages that you release with ``zest.releaser`` may 52 | very well work on other Python versions: that totally depends on your 53 | package. 54 | 55 | We depend on: 56 | 57 | - ``setuptools`` for the entrypoint hooks that we offer. 58 | 59 | - ``colorama`` for colorized output (some errors printed in red). 60 | 61 | - ``twine`` for secure uploading via https to pypi. Plain setuptools doesn't 62 | support this. 63 | 64 | Since version 4.0 there is a ``recommended`` extra that you can get by 65 | installing ``zest.releaser[recommended]`` instead of ``zest.releaser``. It 66 | contains a few trusted add-ons that we feel are useful for the great majority 67 | of ``zest.releaser`` users: 68 | 69 | - wheel_ for creating a Python wheel that we upload to PyPI next to the standard source 70 | distribution. Wheels are the official distribution format for Python. Since version 71 | 8.0.0a2 we always create wheels when the wheel package is installed, except when you 72 | explicitly switch this off in the config: ``create-wheel = false``. Since 9.4.0 we 73 | actually require the wheel package. If you are sure you want "universal" wheels, 74 | follow the directions from the `wheel documentation 75 | `_. 76 | 77 | - `check-manifest`_ checks your ``MANIFEST.in`` file for completeness, 78 | or tells you that you need such a file. It basically checks if all 79 | version controlled files are ending up the the distribution that we 80 | will upload. This may avoid 'brown bag' releases that are missing 81 | files. 82 | 83 | - pyroma_ checks if the package follows best practices of Python 84 | packaging. Mostly it performs checks on the ``setup.py`` file, like 85 | checking for Python version classifiers. 86 | 87 | - readme_renderer_ to check your long description in the same way as pypi does. No more 88 | unformatted restructured text on your pypi page just because there was a 89 | small error somewhere. Handy. 90 | 91 | .. _wheel: https://pypi.org/project/wheel 92 | .. _`check-manifest`: https://pypi.org/project/check-manifest 93 | .. _pyroma: https://pypi.org/project/pyroma 94 | .. _readme_renderer: https://pypi.org/project/readme_renderer 95 | 96 | 97 | Installation 98 | ------------ 99 | 100 | Just a simple ``pip install zest.releaser`` or ``easy_install zest.releaser`` is 101 | enough. If you want the recommended extra utilities, do a ``pip install 102 | zest.releaser[recommended]``. 103 | 104 | Alternatively, buildout users can install zest.releaser as part of a specific 105 | project's buildout, by having a buildout configuration such as:: 106 | 107 | [buildout] 108 | parts = 109 | scripts 110 | 111 | [scripts] 112 | recipe = zc.recipe.egg 113 | eggs = zest.releaser[recommended] 114 | 115 | 116 | Version control systems: git 117 | ---------------------------- 118 | 119 | Of course you must have a version control system installed. 120 | Since version 7, zest.releaser only supports git. 121 | 122 | If you use Subversion (svn), Mercurial (hg), Git-svn, or Bazaar (bzr), please use version 6. 123 | If you really want, you can probably copy the relevant parts from the old code to a new package, 124 | and release this as an add-on package for zest.releaser. 125 | I suspect that currently it would only work with a monkey patch. 126 | If you are planning something, please open an issue, and we can see about making this part pluggable. 127 | 128 | 129 | Available commands 130 | ------------------ 131 | 132 | Zest.releaser gives you four commands to help in releasing python 133 | packages. They must be run in a version controlled checkout. The commands 134 | are: 135 | 136 | - **prerelease**: asks you for a version number (defaults to the current 137 | version minus a 'dev' or so), updates the setup.py or version.txt and the 138 | CHANGES/HISTORY/CHANGELOG file (with either .rst/.txt/.md/.markdown or no 139 | extension) with this new version number and offers to commit those changes 140 | to subversion (or bzr or hg or git). 141 | 142 | - **release**: copies the the trunk or branch of the current checkout and 143 | creates a version control tag of it. Makes a checkout of the tag in a 144 | temporary directory. Offers to register and upload a source dist 145 | of this package to PyPI (Python Package Index). Note: if the package has 146 | not been registered yet, it will not do that for you. You must register the 147 | package manually (``python setup.py register``) so this remains a conscious 148 | decision. The main reason is that you want to avoid having to say: "Oops, I 149 | uploaded our client code to the internet; and this is the initial version 150 | with the plaintext root passwords." 151 | 152 | - **postrelease**: asks you for a version number (gives a sane default), adds 153 | a development marker to it, updates the setup.py or version.txt and the 154 | CHANGES/HISTORY/CHANGELOG file with this and offers to commit those changes 155 | to version control. Note that with git and hg, you'd also be asked to push 156 | your changes (since 3.27). Otherwise the release and tag only live in your 157 | local hg/git repository and not on the server. 158 | 159 | - **fullrelease**: all of the above in order. 160 | 161 | Note: markdown files should use the "underline" style of headings, not the 162 | "atx" style where you prefix the headers with ``#`` signs. 163 | 164 | There are some additional tools: 165 | 166 | - **longtest**: small tool that renders the long description and opens it in a 167 | web browser. Handy for debugging formatting issues locally before uploading 168 | it to pypi. 169 | 170 | - **lasttagdiff**: small tool that shows the *diff* of the current 171 | branch with the last released tag. Handy for checking whether all 172 | the changes are adequately described in the changes file. 173 | 174 | - **lasttaglog**: small tool that shows the *log* of the current 175 | branch since the last released tag. Handy for checking whether all 176 | the changes are adequately described in the changes file. 177 | 178 | - **addchangelogentry**: pass this a text on the command line and it 179 | will add this as an entry in the changelog. This is probably mostly 180 | useful when you are making the same change in a batch of packages. 181 | The same text is used as commit message. In the changelog, the text 182 | is indented and the first line is started with a dash. The command 183 | detects it if you use for example a star as first character of an 184 | entry. 185 | 186 | - **bumpversion**: do not release, only bump the version. A 187 | development marker is kept when it is there. With ``--feature`` we 188 | update the minor version. With option ``--breaking`` we update the 189 | major version. 190 | -------------------------------------------------------------------------------- /doc/source/assumptions.rst: -------------------------------------------------------------------------------- 1 | Assumptions 2 | =========== 3 | 4 | Zest.releaser originated at `Zest software `_ so there 5 | are some assumptions built-in that might or might not fit you. Lots of people 6 | are using it in various companies and open source projects, so it'll probably 7 | fit :-) 8 | 9 | - We absolutely need a version. There's a ``version.txt``, ``setup.py``, or 10 | ``pyproject.toml`` in your project. The ``version.txt`` has a single line 11 | with the version number (newline optional). The ``setup.py`` should have a 12 | single ``version = '0.3'`` line somewhere. You can also have it in the 13 | actual ``setup()`` call, on its own line still, as `` version = '0.3',``. 14 | Indentation and comma are preserved. If your ``setup.py`` actually reads 15 | the version from your ``setup.cfg`` (as `it does automatically 16 | `_ using ``setuptools`` since version 30.3.0), 18 | then the version will be modified there too. If you need something special, 19 | you can always do a ``version=version`` and put the actual version statement 20 | in a zest.releaser- friendly format near the top of the file. Reading (in 21 | Plone products) a version.txt into setup.py works great, too. If you use 22 | ``pyproject.toml``, you should put the version under the ``project`` 23 | metadata section, which also contains the package name, as a single line like 24 | ``version = "0.3"``. 25 | 26 | - The history/changes file restriction is probably the most severe at the 27 | moment. zest.releaser searches for a restructuredtext header with 28 | parenthesis. So something like:: 29 | 30 | Changelog for xyz 31 | ================= 32 | 33 | 0.3 (unreleased) 34 | ---------------- 35 | 36 | - Did something 37 | 38 | 0.2 (1972-12-25) 39 | ---------------- 40 | 41 | - Reinout was born. 42 | 43 | That's just the style we started with. Pretty clear and useful. 44 | 45 | If you use markdown for the changelog, you should use the "underline" style 46 | of headings, not the "atx" style where you prefix the headers with ``#`` 47 | signs. 48 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Note that not all possible configuration values are present in this 3 | # autogenerated file. 4 | # All configuration values have a default; values that are commented out 5 | # serve to show the default. 6 | 7 | import datetime 8 | import importlib 9 | import os 10 | 11 | 12 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 13 | 14 | project = "zest.releaser" 15 | author = "Reinout and Maurits van Rees" 16 | version = importlib.metadata.version("zest.releaser") 17 | release = version 18 | this_year = datetime.date.today().year 19 | copyright = "%s, %s" % (this_year, author) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | # sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ----------------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be extensions 32 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.intersphinx", 36 | "sphinx.ext.todo", 37 | "sphinx.ext.viewcode", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = {".rst": "restructuredtext"} 45 | 46 | # The encoding of source files. 47 | # source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = "index" 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | # language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | # today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | # today_fmt = '%B %d, %Y' 61 | 62 | # List of patterns, relative to source directory, that match files and 63 | # directories to ignore when looking for source files. 64 | exclude_patterns = [] 65 | 66 | # The reST default role (used for this markup: `text`) to use for all documents. 67 | # default_role = None 68 | 69 | # If true, '()' will be appended to :func: etc. cross-reference text. 70 | # add_function_parentheses = True 71 | 72 | # If true, the current module name will be prepended to all description 73 | # unit titles (such as .. function::). 74 | # add_module_names = True 75 | 76 | # If true, sectionauthor and moduleauthor directives will be shown in the 77 | # output. They are ignored by default. 78 | # show_authors = False 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = "sphinx" 82 | 83 | # A list of ignored prefixes for module index sorting. 84 | # modindex_common_prefix = [] 85 | 86 | 87 | # -- Options for HTML output --------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | if not on_rtd: # only import and set the theme if we're building docs locally 92 | import sphinx_rtd_theme 93 | 94 | html_theme = "sphinx_rtd_theme" 95 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 96 | else: 97 | html_theme = "default" 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | # html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | # html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | # html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | # html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | # html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ["_static"] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | html_last_updated_fmt = "%Y-%m-%d" 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | # html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | # html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | # html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | # html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | # html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | # html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | # html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | # html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | # html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | # html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | # html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = "%sdoc" % project 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | latex_paper_size = "a4" 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | latex_font_size = "11pt" 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ("index", "%s.tex" % project, "%s Documentation" % project, author, "manual"), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | # latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | # latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | # latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | # latex_show_urls = False 200 | 201 | # Additional stuff for the LaTeX preamble. 202 | # latex_preamble = '' 203 | 204 | # Documents to append as an appendix to all manuals. 205 | # latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | # latex_domain_indices = True 209 | 210 | 211 | # Example configuration for intersphinx: refer to the Python standard library. 212 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 213 | -------------------------------------------------------------------------------- /doc/source/credits.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../../CREDITS.rst 3 | -------------------------------------------------------------------------------- /doc/source/developing.rst: -------------------------------------------------------------------------------- 1 | Information for developers of zest.releaser itself 2 | =================================================== 3 | 4 | **Note:** the whole test setup used to be quite elaborate and hard to get right. 5 | Since version 7, this is much less so. 6 | You just need ``git`` and ``tox``. 7 | 8 | If you still run into problems, there is a solution however: docker. 9 | See the next section on this page. 10 | It is still work-in-progress, we'll probably add a docker-compose file, for 11 | example. And support for testing different python versions with docker. (3.13 12 | is used now). 13 | 14 | So: for easy testing, use the docker commands, described next. 15 | The rest of the document explains the original test setup requirements. 16 | 17 | 18 | Testing with docker 19 | ------------------- 20 | 21 | If you have docker installed, all you need to do to run the tests is:: 22 | 23 | $ docker build . -t zest:dev 24 | $ docker run --rm zest:dev 25 | 26 | The "run" command runs the tests. It uses the code copied into the dockerfile 27 | in the build step, but you probably want to test your current version. For 28 | that, mount the code directory into the docker:: 29 | 30 | $ docker run --rm -v $(pwd):/zest.releaser/ zest:dev 31 | 32 | 33 | Running tests 34 | ------------- 35 | 36 | Actually, this should be easy now, because we use tox. 37 | So ``pip install tox`` somewhere, probably in a virtualenv, maybe the current directory, 38 | and call it:: 39 | 40 | $ tox 41 | 42 | You probably want to run the tests for all environments in parallel:: 43 | 44 | $ tox -p auto 45 | 46 | To run a specific environment and a specific test file:: 47 | 48 | $ tox -e py38 -- utils.txt 49 | 50 | 51 | Code formatting 52 | --------------- 53 | 54 | We use black/flake8/isort. To make it easy to configure and run, there's a 55 | pre-commit config. Enable it with:: 56 | 57 | $ pre-commit install 58 | 59 | That will run it before every commit. You can also run it periodically when 60 | developing:: 61 | 62 | $ pre-commit run --all 63 | 64 | 65 | Python versions 66 | --------------- 67 | 68 | The tests currently pass on python 3.10-3.13 and PyPy 3.11. 69 | 70 | 71 | Necessary programs 72 | ------------------ 73 | 74 | To run the tests, you need to have the supported versioning systems installed. 75 | Since version 7, we only support ``git``, which you already have installed 76 | probably :-) 77 | 78 | There may be test failures when you have different versions of these programs. 79 | In that case, please investigate as these *may* be genuine errors. In the 80 | past, ``git`` commands would give slightly different output. If the output of 81 | a command changes again, we may need extra compatibility code in 82 | ``test_setup.py``. 83 | 84 | 85 | Building the documentation locally 86 | ------------------------------------- 87 | 88 | If you worked on the documentation, we suggest you verify the markup 89 | and the result by building the documentation locally and view your 90 | results. 91 | 92 | For building the documentation:: 93 | 94 | $ python3.9 -m venv . 95 | $ bin/pip install sphinx sphinx_rtd_theme 96 | $ bin/pip install -e . 97 | $ bin/sphinx-build doc/source/ doc/build/ 98 | 99 | For viewing the documentation open :file:`doc/build/html/index.html` 100 | in your browser, e.g. by running:: 101 | 102 | $ xdg-open doc/build/html/index.html 103 | -------------------------------------------------------------------------------- /doc/source/entrypoints.rst: -------------------------------------------------------------------------------- 1 | Entrypoints: extending/changing zest.releaser 2 | ============================================= 3 | 4 | A zest.releaser entrypoint gets passed a data dictionary and that's about it. 5 | You can do tasks like generating documentation. Or downloading external files 6 | you don't want to store in your repository but that you do want to have 7 | included in your egg. 8 | 9 | Every release step (prerelease, release and postrelease) has three points 10 | where you can hook in an entry point: 11 | 12 | before 13 | Only the ``workingdir`` and ``name`` are available in the data 14 | dictionary, nothing has happened yet. 15 | 16 | middle 17 | All data dictionary items are available and some questions (like new 18 | version number) have been asked. No filesystem changes have been made 19 | yet. 20 | 21 | after 22 | The action has happened, everything has been written to disk or uploaded 23 | to pypi or whatever. 24 | 25 | 26 | For the release step it made sense to create extra entry points: 27 | 28 | after_checkout 29 | The middle entry point has been handled, the tag has been made, a 30 | checkout of that tag has been made and we are now in that checkout 31 | directory. Of course, when the user chooses not to do a checkout, 32 | this entry point never triggers. 33 | 34 | before_upload 35 | The source distribution and maybe the wheel have been made. We 36 | are about to upload to PyPI with ``python setup.py`` or ``twine 37 | upload dist/*``. You may want to use this hook to to sign a 38 | distribution before twine uploads it. 39 | 40 | Note that an entry point can be specific for one package (usually the 41 | package that you are now releasing) or generic for all packages. An 42 | example of a generic one is `gocept.zestreleaser.customupload`_, which 43 | offers to upload the generated distribution to a chosen destination 44 | (like a server for internal company use). If your entry point is 45 | specific for the current package only, you should add an extra check 46 | to make sure it is not run while releasing other packages; something 47 | like this should do the trick:: 48 | 49 | def my_entry_point(data): 50 | if data['name'] != 'my.package': 51 | return 52 | ... 53 | 54 | .. _`gocept.zestreleaser.customupload`: https://pypi.org/project/gocept.zestreleaser.customupload 55 | 56 | 57 | Entry point specification 58 | ------------------------- 59 | 60 | An entry point is configured like this in your setup.py:: 61 | 62 | entry_points={ 63 | #'console_scripts': [ 64 | # 'myscript = my.package.scripts:main'], 65 | 'zest.releaser.prereleaser.middle': [ 66 | 'dosomething = my.package.some:some_entrypoint', 67 | ]}, 68 | 69 | or like this in your pyproject.toml:: 70 | 71 | [project.entry-points."zest.releaser.prereleaser.middle"] 72 | dosomething = "my.package.some:some_entrypoint" 73 | 74 | Entry-points can also be specified in the setup.cfg file like this 75 | (The options will be split by white-space and processed in the given 76 | order.):: 77 | 78 | [zest.releaser] 79 | prereleaser.middle = 80 | my.package.some.some_entrypoint 81 | our.package.other_module.other_function 82 | 83 | 84 | In ``zest.releaser.prereleaser.middle`` resp. ``prereleaser.middle`` 85 | replace ``prereleaser`` 86 | with the respective command name (`prerelease`, `release`, `postrelease`, 87 | etc.) 88 | and ``middle`` with the respective hook name (`before`, `middle`, `after`, 89 | `after_checkout`, etc.) 90 | as needed. 91 | 92 | Similarly, they can also be placed in the ``tool.zest-releaser`` config section of 93 | pyproject.toml like this:: 94 | 95 | [tool.zest-releaser] 96 | prereleaser.middle = [ 97 | "my.package.some.some_entrypoint", 98 | "our.package.other_module.other_function" 99 | ] 100 | 101 | See the ``setup.py`` of ``zest.releaser`` itself for some real world examples. 102 | 103 | You'll have to make sure that the zest.releaser scripts know about your entry 104 | points, for instance by placing your egg (with entry point) in the same 105 | zc.recipe.egg section in your buildout as where you placed zest.releaser. Or, 106 | if you installed zest.releaser globally, your egg-with-entrypoint has to be 107 | globally installed, too. 108 | 109 | Notes: 110 | 111 | * Entry-points given in ``setup.cfg`` will be processed before 112 | entry-point defined via installed packages. 113 | 114 | * The order in which entry-point defined via installed packages are 115 | processed is undefined. 116 | 117 | * *If* you use an entry point defined in the package you're releasing *and* 118 | your package has the code inside a ``src/`` dir, you might have to add 119 | ``hook_package_dir = src`` to the ``[tool.zest-releaser]`` section. 120 | 121 | 122 | Comments about data dict items 123 | ------------------------------ 124 | 125 | Your entry point gets a data dictionary: the items you get in that dictionary 126 | are documented below. Some comments about them: 127 | 128 | - Not all items are available. If no history/changelog file is found, there 129 | won't be any ``data['history_lines']`` either. 130 | 131 | - Items that are templates are normal python string templates. They use 132 | dictionary replacement: they're actually passed the same data dict. For 133 | instance, prerelease's ``data['commit_message']`` is by default ``Preparing 134 | release %(new_version)s``. A "middle" entry point could modify this 135 | template to get a different commit message. 136 | 137 | - Entry-points can change the the ``data`` dictionary, thus one hook 138 | can prepare data another one is using. But be aware that changing 139 | entries in the dict may lead to malfunction. 140 | 141 | 142 | .. ### AUTOGENERATED FROM HERE ### 143 | 144 | Common data dict items 145 | ---------------------- 146 | 147 | These items are shared among all commands. 148 | 149 | commit_msg 150 | Message template used when committing 151 | 152 | has_released_header 153 | Latest header is for a released version with a date 154 | 155 | headings 156 | Extracted headings from the history file 157 | 158 | history_encoding 159 | The detected encoding of the history file 160 | 161 | history_file 162 | Filename of history/changelog file (when found) 163 | 164 | history_header 165 | Header template used for 1st history header 166 | 167 | history_insert_line_here 168 | Line number where an extra changelog entry can be inserted. 169 | 170 | history_last_release 171 | Full text of all history entries of the current release 172 | 173 | history_lines 174 | List with all history file lines (when found) 175 | 176 | name 177 | Name of the project being released 178 | 179 | new_version 180 | New version to write, possibly with development marker 181 | 182 | nothing_changed_yet 183 | First line in new changelog section, warn when this is still in there before releasing 184 | 185 | original_version 186 | Original package version before any changes 187 | 188 | reporoot 189 | Root of the version control repository 190 | 191 | required_changelog_text 192 | Text that must be present in the changelog. Can be a string or a list, for example ["New:", "Fixes:"]. For a list, only one of them needs to be present. 193 | 194 | update_history 195 | Should zest.releaser update the history file? 196 | 197 | workingdir 198 | Original working directory 199 | 200 | ``prerelease`` data dict items 201 | ------------------------------ 202 | 203 | today 204 | Date string used in history header 205 | 206 | ``release`` data dict items 207 | --------------------------- 208 | 209 | tag 210 | Tag we're releasing 211 | 212 | tag-message 213 | Commit message for the tag 214 | 215 | tag-signing 216 | Sign tag using gpg or pgp 217 | 218 | tag_already_exists 219 | Internal detail, don't touch this :-) 220 | 221 | tagdir 222 | Directory where the tag checkout is placed (*if* a tag 223 | checkout has been made) 224 | 225 | tagworkingdir 226 | Working directory inside the tag checkout. This is 227 | the same, except when you make a release from within a sub directory. 228 | We then make sure you end up in the same relative directory after a 229 | checkout is done. 230 | 231 | version 232 | Version we're releasing 233 | 234 | ``postrelease`` data dict items 235 | ------------------------------- 236 | 237 | breaking 238 | True if we handle a breaking (major) change 239 | 240 | dev_version 241 | New version with development marker (so 1.1.dev0) 242 | 243 | dev_version_template 244 | Template for development version number 245 | 246 | development_marker 247 | String to be appended to version after postrelease 248 | 249 | feature 250 | True if we handle a feature (minor) change 251 | 252 | final 253 | True if we handle a final release 254 | 255 | new_version 256 | New version, without development marker (so 1.1) 257 | 258 | ``addchangelogentry`` data dict items 259 | ------------------------------------- 260 | 261 | commit_msg 262 | Message template used when committing. Default: same as the message passed on the command line. 263 | 264 | message 265 | The message we want to add 266 | 267 | ``bumpversion`` data dict items 268 | ------------------------------- 269 | 270 | breaking 271 | True if we handle a breaking (major) change 272 | 273 | clean_new_version 274 | Clean new version (say 1.1) 275 | 276 | feature 277 | True if we handle a feature (minor) change 278 | 279 | final 280 | True if we handle a final release 281 | 282 | release 283 | Type of release: breaking, feature, normal, final 284 | -------------------------------------------------------------------------------- /doc/source/further_reading.rst: -------------------------------------------------------------------------------- 1 | Further reading 2 | =============== 3 | 4 | Mighty fine documentation, the stuff you're reading now. But some other 5 | suggestions, ideas and a different tone might help you improve your package 6 | releasing. So here are some pointers to other material. 7 | 8 | - Big article by Mikko Ohtamaa about `high quality automated package releases 9 | for Python with zest.releaser 10 | `_. Don't 11 | forget to look at the comments. 12 | 13 | - Reinout's `blog posts tagged 'zestreleaser' 14 | `_. 15 | 16 | .. raw:: html 17 | 18 | Reinout van Rees introducing zest.releaser 22 | 23 | Photo (`by Aidas Bendoraitis 24 | `_) of Reinout 25 | introducing zest.releaser at the 2010 Djangocon.eu in Berlin. 26 | 27 | 28 | .. note:: 29 | 30 | If you have something nice to add here, mail `Reinout 31 | `_ and/or `Maurits 32 | `_. 33 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | Zest.releaser: easy releasing and tagging for Python packages 2 | ############################################################# 3 | 4 | Make easy, quick and neat releases of your Python packages. You need to change 5 | the version number, add a new heading in your changelog, record the release 6 | date, git tag your project, perhaps upload it to 7 | pypi... *zest.releaser* takes care of the boring bits for you. 8 | 9 | Here's an overview of the documentation we have for you. First the 10 | documentation on *using* zest.releaser: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | overview 16 | options 17 | versions 18 | uploading 19 | assumptions 20 | entrypoints 21 | further_reading 22 | 23 | And documentation on zest.releaser as a project; for instance for reporting 24 | bugs and fixing the code: 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | Improving zest.releaser 30 | developing 31 | changelog 32 | credits 33 | -------------------------------------------------------------------------------- /doc/source/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ======= 3 | 4 | Zest.releaser tries not to burden you with lots of command line 5 | options. Instead, it asks questions while doing its job. But in some 6 | cases, a command line option makes sense. 7 | 8 | Related: you can change some settings globally in your ``~/.pypirc`` 9 | file or per project in a ``setup.cfg`` file. This is only for Python 10 | packages. 11 | 12 | 13 | Command line options 14 | -------------------- 15 | 16 | These command line options are supported by the release commands 17 | (``fullrelease``, ``prerelease``, ``release``, ``postrelease``) 18 | and by the ``addchangelogentry`` command. 19 | 20 | -v, --verbose 21 | Run in verbose mode, printing a bit more, mostly only interesting 22 | for debugging. 23 | 24 | -h, --help 25 | Display help text 26 | 27 | --no-input 28 | Don't ask questions, just use the default values. If you are very 29 | sure that all will be fine when you answer all questions with the 30 | default answer, and you do not want to press Enter several times, 31 | you can use this option. The default answers (sometimes yes, 32 | sometimes no, sometimes a version number) are probably sane 33 | and safe. But do not blame us if this does something you do not 34 | want. :-) 35 | 36 | The ``addchangelogentry`` command requires the text you want to add as 37 | argument. For example:: 38 | 39 | $ addchangelogentry "Fixed bug." 40 | 41 | Or on multiple lines:: 42 | 43 | $ addchangelogentry "Fixed bug. 44 | 45 | This was difficult." 46 | 47 | The ``bumpversion`` and ``postrelease`` commands accept some mutually exclusive options: 48 | 49 | - With ``--feature`` we update the minor version. 50 | 51 | - With ``--breaking`` we update the major version. 52 | 53 | - With ``--final`` we remove alpha / beta / rc markers from the version. 54 | 55 | 56 | Global options 57 | -------------- 58 | 59 | You can configure zest.releaser for all projects by editing the 60 | ``.pypirc`` file in your home directory. This is the same file that 61 | needs to contain your PyPI credentials if you want to release to the 62 | Python Packaging Index. See the topic on Uploading. This also has 63 | more info on most options. 64 | 65 | Lots of things may be in this file, but zest.releaser looks for a 66 | ``zest.releaser`` section, like this:: 67 | 68 | [zest.releaser] 69 | some-option = some value 70 | 71 | For true/false options, you can use no/false/off/0 or yes/true/on/1 as 72 | answers; upper, lower or mixed case are all fine. 73 | 74 | Various options change the default answer of a question. 75 | So if you want to use the ``--no-input`` command line option 76 | or want to press Enter a couple of times without thinking too much, 77 | see if you can tweak the default answers by setting one of these options 78 | 79 | We have these options: 80 | 81 | release = true / false 82 | Default: true. When this is false, zest.releaser sets ``false`` as 83 | default answer for the question if you want to create a checkout 84 | of the tag. 85 | 86 | upload-pypi = true / false 87 | Default: true. Normally you won't use this setting. Only if you want to make a 88 | release without actually uploading it, set it to false. (Note that you still need 89 | release=true). 90 | 91 | create-wheel = true / false 92 | Default: true. Set to false if you do not want zest.releaser to create Python wheels. 93 | 94 | extra-message = [ci skip] 95 | Extra message to add to each commit (prerelease, postrelease). 96 | 97 | prefix-message = [TAG] 98 | Prefix message to add at the beginning of each commit (prerelease, postrelease). 99 | 100 | no-input = true / false 101 | Default: false. Set this to true to accept default answers for all 102 | questions. 103 | 104 | register = true / false 105 | Default: false. Set this to true to register a package before uploading. 106 | On the official Python Package Index registering a package is no longer needed, 107 | and may even fail. 108 | 109 | push-changes = true / false 110 | Default: true. When this is false, zest.releaser sets ``false`` as 111 | default answer for the question if you want to push the changes to 112 | the remote. 113 | 114 | less-zeroes = true / false 115 | Default: false. 116 | This influences the version suggested by the bumpversion command. 117 | When set to true: 118 | 119 | - Instead of 1.3.0 we will suggest 1.3. 120 | - Instead of 2.0.0 we will suggest 2.0. 121 | 122 | version-levels = a number 123 | Default: 0. 124 | This influences the version suggested by the postrelease and bumpversion commands. 125 | The default of zero means: no preference, so use the length of the current number. 126 | 127 | This means when suggesting a next version after 1.2: 128 | 129 | - with 0 we will suggest 1.3: no change in length 130 | - with 1 we will still suggest 1.3, as we will not 131 | use this to remove numbers, only to add them 132 | - with 2 we will suggest 1.3 133 | - with 3 we will suggest 1.2.1 134 | 135 | If the current version number has more levels, we keep them. 136 | So with ``version-levels=1`` the next version for 1.2.3.4 will be 1.2.3.5. 137 | 138 | development-marker = a string 139 | Default: ``.dev0`` 140 | This is the development marker. 141 | This is what gets appended to the version in postrelease. 142 | 143 | tag-format = a string 144 | Default: ``{version}`` 145 | This is a formatter that changes the name of the tag. 146 | It needs to contain ``{version}``. 147 | For backward compatibility, it can contain ``%(version)s`` instead. 148 | 149 | tag-message = a string 150 | Default: ``Tagging {version}`` 151 | This formatter defines the commit message passed to the ``tag`` 152 | command of the VCS. 153 | It must contain ``{version}``. 154 | 155 | tag-signing = true / false 156 | Default: false. 157 | When set to true, tags are signed using the signing feature of the 158 | respective vcs. Currently tag-signing is only supported for git. 159 | Note: When you enable it, everyone releasing the project is 160 | required to have git tag signing set up correctly. 161 | 162 | date-format = a string 163 | Default: ``%%Y-%%m-%%d`` 164 | This is the format string for the release date to be mentioned in the 165 | changelog. 166 | 167 | Note: the % signs should be doubled for compatibility with other tools 168 | (i.e. pip) that parse setup.cfg using the interpolating ConfigParser. 169 | 170 | history-file = a string 171 | Default: empty 172 | Usually zest.releaser can find the correct history or changelog file on its own. 173 | But sometimes it may not find anything, or it finds multiple files and selects the wrong one. 174 | Then you can set a path here. 175 | 176 | history_format = a string 177 | Default: empty. 178 | Set this to ``md`` to handle changelog entries in Markdown. 179 | 180 | run-pre-commit = true / false 181 | Default: false. 182 | New in version 7.3.0. 183 | When set to true, pre commit hooks are run. 184 | This may interfere with releasing when they fail. 185 | 186 | 187 | Per project options 188 | ------------------- 189 | 190 | You can change some settings per project by adding instructions for 191 | zest.releaser in a ``setup.cfg`` file. This will only work for a 192 | Python package. 193 | 194 | These are the same options as the global ones. If you set an option 195 | locally in a project, this will override the global option. 196 | 197 | You can also set these options in a ``pyproject.toml`` file. If you do 198 | so, instead of having a ``[zest.releaser]`` section, you should use a 199 | ``[tool.zest-releaser]`` section. For true/false options in a 200 | ``pyproject.toml``, you must use lowercase true or false; for string 201 | options like ``extra-message`` or ``prefix-message``, you should put 202 | the value between double quotes, like this:: 203 | 204 | [tool.zest-releaser] 205 | create-wheel = false 206 | extra-message = "[ci skip]" 207 | -------------------------------------------------------------------------------- /doc/source/overview.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /doc/source/project.rst: -------------------------------------------------------------------------------- 1 | Improving zest.releaser: report bugs, fork on github or email us 2 | ================================================================ 3 | 4 | Did you find a bug? Do you have an improvement? Do you have questions? We run 5 | zest.releaser as a proper open source project on github at 6 | https://github.com/zestsoftware/zest.releaser, so you have three basic 7 | options: 8 | 9 | - Feel free to report bugs on `our github issue tracker 10 | `_. And feature 11 | requests, too. Normally you'll get a quick reply within a day or so, 12 | depending on our relative timezones. If you don't get an answer within a few 13 | days, please send off a quick email to remind us. 14 | 15 | - Hey, we're on github, so fork away to your heart's delight. We got a couple 16 | of nice fixes and additions in this way. 17 | 18 | *If* you are going to fork zest.releaser, take a look at :doc:`developing` for 19 | setup and test running information. 20 | 21 | 22 | - Email Reinout and Maurits at `reinout@vanrees.org 23 | `_ and `maurits@vanrees.org 24 | `_. Please email us both at the same time, this 25 | way at least one of us can give you a quick reply. 26 | -------------------------------------------------------------------------------- /doc/source/uploading.rst: -------------------------------------------------------------------------------- 1 | Uploading to pypi (or custom servers) 2 | ======================================= 3 | 4 | When the (full)release command tries to upload your package to a pypi server, 5 | zest.releaser basically executes the command ``python setup.py sdist`` and does a 6 | ``twine upload``. The twine command replaces the less safe 7 | ``python setup.py sdist upload``. 8 | 9 | For safety reasons zest.releaser will *only* offer to upload your package to 10 | https://pypi.org when the package is already registered there. If this 11 | is not the case yet, you get a confirmation question whether you want to 12 | register a new package with ``twine register``. 13 | 14 | If the upload or register command fails, you probably need to configure 15 | your PyPI configuration file. And of course you need to have 16 | ``setuptools`` and ``twine`` installed, but that is done automatically 17 | when installing ``zest.releaser``. 18 | 19 | 20 | PyPI configuration file (``~/.pypirc``) 21 | --------------------------------------- 22 | 23 | For uploads to PyPI to work you will need a ``.pypirc`` file in your home directory that 24 | has your pypi login credentials. This may contain alternative servers too:: 25 | 26 | [distutils] 27 | index-servers = 28 | pypi 29 | local 30 | 31 | [pypi] 32 | # default repository is https://upload.pypi.org/legacy/ 33 | username:maurits 34 | password:secret 35 | 36 | [local] 37 | repository:http://localhost:8080/test/products/ 38 | username:maurits 39 | password:secret 40 | # You may need to specify the realm, which is the domain the 41 | # server sends back when you do a challenge: 42 | #realm:Zope 43 | 44 | See the `Python Packaging User Guide`_ for more info. 45 | 46 | .. _`Python Packaging User Guide`: https://packaging.python.org/en/latest/distributing.html#uploading-your-project-to-pypi for more info. 47 | 48 | When all this is configured correctly, zest.releaser will first upload 49 | to the official PyPI (if the package is registered there already). 50 | Then it will offer to upload to the other index servers that you have 51 | specified in ``.pypirc``. 52 | 53 | Note that since version 3.15, zest.releaser also looks for this information in 54 | the ``setup.cfg`` if your package has that file. One way to use this, is to 55 | restrict the servers that zest.releaser will ask you to upload to. If you have 56 | defined 40 index-servers in your pypirc but you have the following in your 57 | setup.cfg, you will not be asked to upload to any server:: 58 | 59 | [distutils] 60 | index-servers = 61 | 62 | Or restrict the index servers, for example:: 63 | 64 | [distutils] 65 | index-servers = 66 | internal-pypi-customer1 67 | 68 | Note that after creating the tag we still ask you if you want to checkout that 69 | tag for tweaks or pypi/distutils server upload. We could add some extra 70 | checks to see if that is really needed, but someone who does not have 71 | index-servers listed, may still want to use an entry point like 72 | `gocept.zestreleaser.customupload 73 | `_ to do 74 | uploading, or do some manual steps first before uploading. 75 | 76 | Since version 6.8, zest.releaser by default no longer *registers* a new package, but only uploads it. 77 | This is usually good. 78 | See `Registering a package`_ for an explanation. 79 | 80 | Some people will hardly ever want to do a release on PyPI but in 99 out of 100 81 | cases only want to create a tag. They won't like the default answer of 'yes' 82 | to that question of whether to create a checkout of the tag. So since version 83 | 3.16 you can influence this default answer. You can add some lines to the 84 | ``.pypirc`` file in your home directory to change the default answer for all 85 | packages, or change it for individual packages in their ``setup.cfg`` file. 86 | The lines are this:: 87 | 88 | [zest.releaser] 89 | release = no 90 | 91 | You can use no/false/off/0 or yes/true/on/1 as answers; upper, lower or mixed 92 | case are all fine. 93 | 94 | 95 | Uploading with twine 96 | -------------------- 97 | 98 | Since version 6.0, we always use twine_ for uploading to the Python 99 | Package Index, because it is safer: it uses ``https`` for uploading. 100 | Since version 4.0 we already preferred it if it was available, but it 101 | is now a core dependency, installed automatically. 102 | 103 | .. _twine: https://pypi.org/project/twine 104 | 105 | Since version 6.6.6 we use it in a way that should work with ``twine`` 106 | 1.6.0 and higher, including future versions. 107 | 108 | 109 | Uploading wheels 110 | ---------------- 111 | 112 | The ``wheel`` library is a dependency of zest.releaser (since 9.4.0), so it is always 113 | installed. In the (rare) case where you do not want wheels, you can switch it off in our 114 | regular places (``pyproject.toml``, ``~/.pypirc``):: 115 | 116 | [zest.releaser] 117 | create-wheel = no 118 | 119 | 120 | Registering a package 121 | --------------------- 122 | 123 | Registering a package does two things: 124 | 125 | - It claims a package name on your behalf, so that you can upload a file to it. 126 | - If you already registered the package previously, it updates the general package information. 127 | So every time you make a new release, you should register the package. 128 | 129 | Well, that used to be the case, but things have changed. 130 | 131 | Since version 6.8, zest.releaser by default no longer *registers* a package, but only uploads it. 132 | This is because for the standard Python Package Index (PyPI), 133 | registering a package is no longer needed: this is done automatically 134 | when uploading a distribution for a package. In fact, trying to 135 | register may *fail*. See this `issue `_. 136 | 137 | But you may be using your own package server, and registering 138 | may be wanted or even required there. In this case 139 | you will need to turn on the register function. 140 | In your ``setup.cfg`` or ``~/.pypirc``, use the following to ensure that 141 | register is called on the package server:: 142 | 143 | [zest.releaser] 144 | register = yes 145 | 146 | If you have specified multiple package servers, this option is used 147 | for all of them. There is no way to register and upload to server A, 148 | and only upload to server B. 149 | 150 | 151 | Adding extra text to a commit message 152 | ------------------------------------- 153 | 154 | ``zest.releaser`` makes commits in the prerelease and postrelease 155 | phase. Something like ``Preparing release 1.0`` and ``Back to 156 | development: 1.1``. You can add extra text to these messages by 157 | configuration in your ``setup.cfg`` or global ``~/.pypirc``. One use 158 | case for this is telling Travis to skip Continuous Integration builds:: 159 | 160 | [zest.releaser] 161 | extra-message = [ci skip] 162 | 163 | Internal policies might mandate some sort of tag at the start of the 164 | commit message. You can prepend this with:: 165 | 166 | [zest.releaser] 167 | prefix-message = [tools] 168 | 169 | 170 | 171 | Signing your commits or tags with git 172 | ------------------------------------- 173 | 174 | If you are using git, maybe you want to sign your commits, or more likely your tags, with your gpg key. 175 | ``zest.releaser`` does not do anything special for this: it just calls the normal ``git commit`` or ``git tag``. 176 | So if you want to sign anything, you should set this up in your ``git`` configuration, so it works outside of ``zest.releaser`` as well. 177 | Run these commands to configure gpg signing for git:: 178 | 179 | git config commit.gpgsign true 180 | git config tag.gpgsign true 181 | 182 | 183 | Including all files in your release 184 | ----------------------------------- 185 | 186 | By default, only the Python files and a ``README.txt`` are included (by 187 | setuptools) when you make a release. So you miss out on your changelog, json 188 | files, stylesheets and so on. There are two strategies to include those other 189 | files: 190 | 191 | - Add a ``MANIFEST.in`` file in the same directory as your ``setup.py`` that 192 | lists the files you want to include. Don't worry, wildcards are 193 | allowed. Actually, zest.releaser will suggest a sample ``MANIFEST.in`` for 194 | you if you don't already have it. The default is often good enough. 195 | 196 | - Setuptools *can* detect which files are included in your version control 197 | system (git) which it'll then automatically include. 198 | 199 | The last approach has a problem: not every version control system is supported 200 | out of the box. So you might need to install extra packages to get it to 201 | work. So: use a ``MANIFEST.in`` file to spare you the trouble. If not, here 202 | is an extra package: 203 | 204 | - setuptools-git (Setuptools plugin for finding files under Git 205 | version control) 206 | 207 | In general, if you are missing files in the uploaded package, the best 208 | is to put a proper ``MANIFEST.in`` file next to your ``setup.py``. 209 | See `zest.pocompile`_ for an example. 210 | 211 | .. _`zest.pocompile`: https://pypi.org/project/zest.pocompile 212 | 213 | 214 | Running automatically without input 215 | ----------------------------------- 216 | 217 | Sometimes you want to run zest.releaser without hitting ```` all the 218 | time. You might want to run zest.releaser from your automatic test 219 | environment, for instance. For that, there's the ``--no-input`` commandline 220 | option. Pass that and all defaults will be accepted automatically. 221 | 222 | This means your version number and so must be OK. If you want to have a 223 | different version number from the one in your ``setup.py``, you'll need to 224 | change it yourself by hand. And the next version number will be chosen 225 | automatically, too. So ``1.2`` will become ``1.3``. This won't detect that you 226 | might want to do a ``1.3`` after a ``1.2.1`` bugfix release, but we cannot 227 | perform feats of magic in zest.releaser :-) 228 | 229 | In case you always want to accept the defaults, a setting in your 230 | ``setup.cfg`` is available:: 231 | 232 | [zest.releaser] 233 | no-input = yes 234 | 235 | An important reminder: if you want to make sure you never upload anything 236 | automatically to the python package index, include the ``release = no`` 237 | setting in ``setup.cfg``:: 238 | 239 | [zest.releaser] 240 | no-input = yes 241 | release = no 242 | -------------------------------------------------------------------------------- /doc/source/versions.rst: -------------------------------------------------------------------------------- 1 | Version handling 2 | ================ 3 | 4 | Where does the version come from? 5 | --------------------------------- 6 | 7 | A version number is essentially what zest.releaser cannot do without. 8 | A version number can come from various different locations: 9 | 10 | - The ``setup.py`` file. Two styles are supported:: 11 | 12 | version = '1.0' 13 | 14 | def setup( 15 | version=version, 16 | name='... 17 | 18 | and also:: 19 | 20 | def setup( 21 | version='1.0', 22 | name='... 23 | 24 | - The ``pyproject.toml`` file. zest.releaser will look for something like:: 25 | 26 | [project] 27 | name = "..." 28 | version = "1.0" 29 | 30 | - The ``setup.cfg`` file. zest.releaser will look for something like:: 31 | 32 | [metadata] 33 | name = ... 34 | version = 1.0 35 | 36 | - If no ``setup.py`` is found, zest.releaser looks for a ``version.txt`` 37 | file. It should contain just a version number (a newline at the end is OK). 38 | 39 | Originally the ``version.txt`` was only meant to support really old and 40 | ancient `Plone `_ packages, but it turned out to be quite 41 | useful for non-Python packages, too. A completely static website, for 42 | instance, that you *do* want to release and that you *do* want a changelog 43 | for. 44 | 45 | - A ``__version__`` attribute in a Python file. You need to tell zest.releaser 46 | *which* Python file by adding (or updating) the ``setup.cfg`` file next to 47 | the ``setup.py``. You need a ``[zest.releaser]`` header and a 48 | ``python-file-with-version`` option:: 49 | 50 | [zest.releaser] 51 | python-file-with-version = mypackage/__init__.py 52 | 53 | Alternatively, in ``pyproject.toml``, you can use the following:: 54 | 55 | [tool.zest-releaser] 56 | python-file-with-version = "mypackage/__init__.py" 57 | 58 | Because you need to configure this explicitly, this option takes precedence 59 | over any ``setup.py``, ``setup.cfg`` or ``pyproject.toml`` package version, 60 | or ``version.txt`` file. 61 | 62 | 63 | Where is the version number being set? 64 | -------------------------------------- 65 | 66 | Of those four locations where the version can come from, only the first one 67 | found is also set to the new value again. Zest.releaser assumes that there's 68 | only *one* location. 69 | 70 | `According to PEP 396 71 | `_, the version should 72 | have **one** source and all the others should be derived from it. 73 | 74 | 75 | Using the version number in ``setup.py`` or ``setup.cfg`` as ``__version__`` 76 | ---------------------------------------------------------------------------- 77 | 78 | Here are opinionated suggestions from the zest.releaser main authors about how 79 | to use the version information. For some other ideas, see the `zest.releaser 80 | issue 37 `_ 81 | discussion. 82 | 83 | - The version in the ``setup.py`` is the real version. 84 | 85 | - Add a ``__version__`` attribute in your main module. Often this will be an 86 | ``__init__.py``. Set this version attribute with ``importlib.metadata``. Here's `the 87 | code `_ 88 | from ``zest/releaser/__init__.py``:: 89 | 90 | from importlib.metadata import version 91 | 92 | __version__ = version("zest.releaser") 93 | 94 | This way you can do:: 95 | 96 | >>> import zest.releaser 97 | >>> zest.releaser.__version__ 98 | '3.44' 99 | 100 | - If you use `Sphinx `_ for generating your 101 | documentation, use the same ``importlib.metadata`` trick to set the version and 102 | release in your Sphinx's ``conf.py``. See `zest.releaser's conf.py 103 | `_. 104 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # See https://snarky.ca/what-the-heck-is-pyproject-toml/ 3 | requires = ["setuptools>=77", "wheel"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | [project] 7 | name = "zest.releaser" 8 | version = "9.6.3.dev0" 9 | description = "Software releasing made easy and repeatable" 10 | license = "GPL-2.0-or-later" 11 | authors = [ 12 | {name = "Reinout van Rees", email = "reinout@vanrees.org"}, 13 | {name = "Maurits van Rees", email = "maurits@vanrees.org"}, 14 | ] 15 | dependencies = [ 16 | "build >= 1.2.0", # 1.2.0 added the 'installer' argument to DefaultIsolatedEnv 17 | "colorama", 18 | "importlib-metadata; python_version<'3.10'", 19 | "packaging", 20 | "readme_renderer[md] >= 40", 21 | "requests", 22 | "setuptools >= 61.0.0", # older versions can't read pyproject.toml configurations 23 | "tomli; python_version<'3.11'", 24 | "twine >= 1.6.0", 25 | "wheel", 26 | ] 27 | requires-python = ">=3.9" 28 | classifiers = [ 29 | "Development Status :: 6 - Mature", 30 | "Intended Audience :: Developers", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | "Programming Language :: Python :: Implementation :: CPython", 39 | "Programming Language :: Python :: Implementation :: PyPy", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | ] 42 | keywords = ["releasing", "packaging", "pypi"] 43 | # I thought there was a way to combine two files, but I don't see it. 44 | # So define the readme as dynamic: still defined in setup.py. 45 | dynamic = ["readme"] 46 | 47 | [project.optional-dependencies] 48 | recommended = [ 49 | "check-manifest", 50 | "pep440", 51 | "pyroma", 52 | ] 53 | test = [ 54 | "zope.testing", 55 | "zope.testrunner", 56 | ] 57 | 58 | [project.urls] 59 | documentation = "https://zestreleaser.readthedocs.io" 60 | repository = "https://github.com/zestsoftware/zest.releaser/" 61 | changelog = "https://github.com/zestsoftware/zest.releaser/blob/master/CHANGES.rst" 62 | 63 | [project.scripts] 64 | release = "zest.releaser.release:main" 65 | prerelease = "zest.releaser.prerelease:main" 66 | postrelease = "zest.releaser.postrelease:main" 67 | fullrelease = "zest.releaser.fullrelease:main" 68 | longtest = "zest.releaser.longtest:main" 69 | lasttagdiff = "zest.releaser.lasttagdiff:main" 70 | lasttaglog = "zest.releaser.lasttaglog:main" 71 | addchangelogentry = "zest.releaser.addchangelogentry:main" 72 | bumpversion = "zest.releaser.bumpversion:main" 73 | 74 | # The datachecks are implemented as entry points to be able to check 75 | # our entry point implementation. 76 | [project.entry-points."zest.releaser.prereleaser.middle"] 77 | datacheck = "zest.releaser.prerelease:datacheck" 78 | 79 | [project.entry-points."zest.releaser.releaser.middle"] 80 | datacheck = "zest.releaser.release:datacheck" 81 | 82 | [project.entry-points."zest.releaser.postreleaser.middle"] 83 | datacheck = "zest.releaser.postrelease:datacheck" 84 | 85 | [project.entry-points."zest.releaser.addchangelogentry.middle"] 86 | datacheck = "zest.releaser.addchangelogentry:datacheck" 87 | 88 | [project.entry-points."zest.releaser.bumpversion.middle"] 89 | datacheck = "zest.releaser.bumpversion:datacheck" 90 | 91 | # Documentation generation 92 | [project.entry-points."zest.releaser.prereleaser.before"] 93 | preparedocs = "zest.releaser.preparedocs:prepare_entrypoint_documentation" 94 | 95 | [tool.isort] 96 | profile = "plone" 97 | 98 | [tool.zest-releaser] 99 | extra-message = "[ci skip]" 100 | tag-signing = true 101 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long_description = file: README.rst, CREDITS.rst, CHANGES.rst 3 | long_description_content_type = text/rst 4 | 5 | [check-manifest] 6 | ignore = 7 | *cfg 8 | .*yml 9 | .coveragerc 10 | .dockerignore 11 | .flake8 12 | .pre-commit-config.yaml 13 | bootstrap.py 14 | doc/build 15 | doc/build/* 16 | Dockerfile 17 | 18 | [bdist_wheel] 19 | # Python 3 only 20 | universal = 0 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | packages=["zest.releaser", "zest.releaser.tests"], 6 | include_package_data=True, 7 | zip_safe=False, 8 | ) 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py39,py310,py311,py312,py313,pypy311 4 | 5 | [testenv] 6 | usedevelop = true 7 | extras = 8 | test 9 | recommended 10 | commands = 11 | zope-testrunner --test-path={toxinidir} -s zest.releaser --tests-pattern=^tests$ {posargs:-v -c} 12 | -------------------------------------------------------------------------------- /zest/releaser/__init__.py: -------------------------------------------------------------------------------- 1 | from colorama import init 2 | from importlib.metadata import PackageNotFoundError 3 | from importlib.metadata import version 4 | 5 | 6 | # Initialize colorized output. Set it to reset after each print, so 7 | # for example a foreground color does not linger into the next print. 8 | # Note that the colorama docs say you should call `deinit` at exit, 9 | # but it looks like it already does that itself. 10 | init(autoreset=True) 11 | 12 | # Depending on which Python and setuptools version you use, and whether you use 13 | # an editable install or install from sdist or wheel, or with pip or Buildout, 14 | # there may be problems getting our own version. Let's not break on that. 15 | try: 16 | __version__ = version("zest.releaser") 17 | except PackageNotFoundError: 18 | try: 19 | __version__ = version("zest-releaser") 20 | except PackageNotFoundError: 21 | __version__ = "unknown" 22 | -------------------------------------------------------------------------------- /zest/releaser/addchangelogentry.py: -------------------------------------------------------------------------------- 1 | """Add a changelog entry. 2 | """ 3 | 4 | from zest.releaser import baserelease 5 | from zest.releaser import utils 6 | 7 | import logging 8 | import sys 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | COMMIT_MSG = "" 14 | 15 | # Documentation for self.data. You get runtime warnings when something is in 16 | # self.data that is not in this list. Embarrasment-driven documentation! 17 | DATA = baserelease.DATA.copy() 18 | DATA.update( 19 | { 20 | "commit_msg": ( 21 | "Message template used when committing. " 22 | "Default: same as the message passed on the command line." 23 | ), 24 | "message": "The message we want to add", 25 | } 26 | ) 27 | 28 | 29 | class AddChangelogEntry(baserelease.Basereleaser): 30 | """Add a changelog entry. 31 | 32 | self.data holds data that can optionally be changed by plugins. 33 | 34 | """ 35 | 36 | def __init__(self, vcs=None, message=""): 37 | baserelease.Basereleaser.__init__(self, vcs=vcs) 38 | # Prepare some defaults for potential overriding. 39 | self.data.update( 40 | dict( 41 | commit_msg=COMMIT_MSG, 42 | message=message.strip(), 43 | ) 44 | ) 45 | 46 | def prepare(self): 47 | """Prepare self.data by asking about new dev version""" 48 | if not utils.sanity_check(self.vcs): 49 | logger.critical("Sanity check failed.") 50 | sys.exit(1) 51 | self._grab_history() 52 | self._get_message() 53 | 54 | def execute(self): 55 | """Make the changes and offer a commit""" 56 | self._remove_nothing_changed() 57 | self._insert_changelog_entry(self.data["message"]) 58 | self._write_history() 59 | self._diff_and_commit() 60 | 61 | def _remove_nothing_changed(self): 62 | """Remove nothing_changed_yet line from history lines""" 63 | nothing_changed = self.data["nothing_changed_yet"] 64 | if nothing_changed in self.data["history_last_release"]: 65 | nc_pos = self.data["history_lines"].index(nothing_changed) 66 | if nc_pos == self.data["history_insert_line_here"]: 67 | self.data["history_lines"] = ( 68 | self.data["history_lines"][:nc_pos] 69 | + self.data["history_lines"][nc_pos + 2 :] 70 | ) 71 | 72 | def _get_message(self): 73 | """Get changelog message and commit message.""" 74 | message = self.data["message"] 75 | while not message: 76 | q = "What is the changelog message? " 77 | message = utils.get_input(q) 78 | self.data["message"] = message 79 | if not self.data["commit_msg"]: 80 | # The commit message does %-replacement, so escape any %'s. 81 | message = message.replace("%", "%%") 82 | self.data["commit_msg"] = message 83 | 84 | 85 | def datacheck(data): 86 | """Entrypoint: ensure that the data dict is fully documented""" 87 | utils.is_data_documented(data, documentation=DATA) 88 | 89 | 90 | def main(): 91 | parser = utils.base_option_parser() 92 | parser.add_argument("message", help="Text of changelog entry") 93 | options = utils.parse_options(parser) 94 | utils.configure_logging() 95 | addchangelogentry = AddChangelogEntry(message=utils.fs_to_text(options.message)) 96 | addchangelogentry.run() 97 | -------------------------------------------------------------------------------- /zest/releaser/bumpversion.py: -------------------------------------------------------------------------------- 1 | """Do the checks and tasks that have to happen after doing a release. 2 | """ 3 | 4 | from packaging.version import parse as parse_version 5 | from zest.releaser import baserelease 6 | from zest.releaser import utils 7 | 8 | import logging 9 | import sys 10 | 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | HISTORY_HEADER = "%(clean_new_version)s (unreleased)" 15 | COMMIT_MSG = "Bumped version for %(release)s release." 16 | 17 | # Documentation for self.data. You get runtime warnings when something is in 18 | # self.data that is not in this list. Embarrasment-driven documentation! 19 | DATA = baserelease.DATA.copy() 20 | DATA.update( 21 | { 22 | "breaking": "True if we handle a breaking (major) change", 23 | "clean_new_version": "Clean new version (say 1.1)", 24 | "feature": "True if we handle a feature (minor) change", 25 | "final": "True if we handle a final release", 26 | "release": "Type of release: breaking, feature, normal, final", 27 | } 28 | ) 29 | 30 | 31 | class BumpVersion(baserelease.Basereleaser): 32 | """Add a changelog entry. 33 | 34 | self.data holds data that can optionally be changed by plugins. 35 | 36 | """ 37 | 38 | def __init__(self, vcs=None, breaking=False, feature=False, final=False): 39 | baserelease.Basereleaser.__init__(self, vcs=vcs) 40 | # Prepare some defaults for potential overriding. 41 | if breaking: 42 | release = "breaking" 43 | elif feature: 44 | release = "feature" 45 | elif final: 46 | release = "final" 47 | else: 48 | release = "normal" 49 | self.data.update( 50 | dict( 51 | breaking=breaking, 52 | commit_msg=COMMIT_MSG, 53 | feature=feature, 54 | final=final, 55 | history_header=HISTORY_HEADER, 56 | release=release, 57 | update_history=True, 58 | ) 59 | ) 60 | 61 | def prepare(self): 62 | """Prepare self.data by asking about new dev version""" 63 | print("Checking version bump for {} release.".format(self.data["release"])) 64 | if not utils.sanity_check(self.vcs): 65 | logger.critical("Sanity check failed.") 66 | sys.exit(1) 67 | self._grab_version(initial=True) 68 | self._grab_history() 69 | # Grab and set new version. 70 | self._grab_version() 71 | 72 | def execute(self): 73 | """Make the changes and offer a commit""" 74 | if self.data["update_history"]: 75 | self._change_header() 76 | self._write_version() 77 | if self.data["update_history"]: 78 | self._write_history() 79 | self._diff_and_commit() 80 | 81 | def _grab_version(self, initial=False): 82 | """Grab the version. 83 | 84 | When initial is False, ask the user for a non-development 85 | version. When initial is True, grab the current suggestion. 86 | 87 | """ 88 | original_version = self.vcs.version 89 | logger.debug("Extracted version: %s", original_version) 90 | if not original_version: 91 | logger.critical("No version found.") 92 | sys.exit(1) 93 | suggestion = new_version = self.data.get("new_version") 94 | if not new_version: 95 | # Get a suggestion. 96 | breaking = self.data["breaking"] 97 | feature = self.data["feature"] 98 | final = self.data["final"] 99 | # Compare the suggestion for the last tag with the current version. 100 | # The wanted version bump may already have been done. 101 | last_tag_version = utils.get_last_tag(self.vcs, allow_missing=True) 102 | if last_tag_version is None: 103 | print("No tag found. No version bump needed.") 104 | sys.exit(0) 105 | else: 106 | print(f"Last tag: {last_tag_version}") 107 | print(f"Current version: {original_version}") 108 | params = dict( 109 | feature=feature, 110 | breaking=breaking, 111 | final=final, 112 | less_zeroes=self.zest_releaser_config.less_zeroes(), 113 | levels=self.zest_releaser_config.version_levels(), 114 | dev_marker=self.zest_releaser_config.development_marker(), 115 | ) 116 | if final: 117 | minimum_version = utils.suggest_version(original_version, **params) 118 | if minimum_version is None: 119 | print("No version bump needed.") 120 | sys.exit(0) 121 | else: 122 | minimum_version = utils.suggest_version(last_tag_version, **params) 123 | if parse_version(minimum_version) <= parse_version( 124 | utils.cleanup_version(original_version) 125 | ): 126 | print("No version bump needed.") 127 | sys.exit(0) 128 | # A bump is needed. Get suggestion for next version. 129 | suggestion = utils.suggest_version(original_version, **params) 130 | if not initial: 131 | new_version = utils.ask_version("Enter version", default=suggestion) 132 | if not new_version: 133 | new_version = suggestion 134 | self.data["original_version"] = original_version 135 | self.data["new_version"] = new_version 136 | self.data["clean_new_version"] = utils.cleanup_version(new_version) 137 | 138 | 139 | def datacheck(data): 140 | """Entrypoint: ensure that the data dict is fully documented""" 141 | utils.is_data_documented(data, documentation=DATA) 142 | 143 | 144 | def main(): 145 | parser = utils.base_option_parser() 146 | parser.add_argument( 147 | "--feature", 148 | action="store_true", 149 | dest="feature", 150 | default=False, 151 | help="Bump for feature release (increase minor version)", 152 | ) 153 | parser.add_argument( 154 | "--breaking", 155 | action="store_true", 156 | dest="breaking", 157 | default=False, 158 | help="Bump for breaking release (increase major version)", 159 | ) 160 | parser.add_argument( 161 | "--final", 162 | action="store_true", 163 | dest="final", 164 | default=False, 165 | help="Bump for final release (remove alpha/beta/rc from version)", 166 | ) 167 | options = utils.parse_options(parser) 168 | # How many options are enabled? 169 | if len(list(filter(None, [options.breaking, options.feature, options.final]))) > 1: 170 | print("ERROR: Only enable one option of breaking/feature/final.") 171 | sys.exit(1) 172 | utils.configure_logging() 173 | bumpversion = BumpVersion( 174 | breaking=options.breaking, 175 | feature=options.feature, 176 | final=options.final, 177 | ) 178 | bumpversion.run() 179 | -------------------------------------------------------------------------------- /zest/releaser/choose.py: -------------------------------------------------------------------------------- 1 | from zest.releaser import git 2 | from zest.releaser import utils 3 | 4 | import logging 5 | import os 6 | import sys 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def version_control(): 13 | """Return an object that provides the version control interface. 14 | 15 | Base this on the detected version control system. 16 | 17 | We look for .git, .svn, etcetera in the current directory. We 18 | might be in a directory a few levels down from the repository 19 | root. So if we find nothing here, we go up a few directories. 20 | 21 | As safety valve we use a maximum to avoid an endless loop in case 22 | there is a logic error. 23 | """ 24 | path = os.path.abspath(os.curdir) 25 | q = "You are NOT in the root of the repository. Do you want to go there?" 26 | for level in range(8): 27 | curdir_contents = os.listdir(path) 28 | if ".git" in curdir_contents: 29 | if level != 0 and utils.ask(q, default=False): 30 | os.chdir(path) 31 | return git.Git(path) 32 | # Get parent. 33 | newpath = os.path.abspath(os.path.join(path, os.pardir)) 34 | if newpath == path: 35 | # We are at the system root. We cannot go up anymore. 36 | break 37 | path = newpath 38 | 39 | logger.critical("No version control system detected.") 40 | sys.exit(1) 41 | -------------------------------------------------------------------------------- /zest/releaser/fullrelease.py: -------------------------------------------------------------------------------- 1 | """Do the prerelease, actual release and post release in one fell swoop! 2 | """ 3 | 4 | from zest.releaser import postrelease 5 | from zest.releaser import prerelease 6 | from zest.releaser import release 7 | from zest.releaser import utils 8 | 9 | import logging 10 | import os 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def main(): 17 | utils.parse_options() 18 | utils.configure_logging() 19 | logger.info("Starting prerelease.") 20 | original_dir = os.getcwd() 21 | # prerelease 22 | prereleaser = prerelease.Prereleaser() 23 | prereleaser.run() 24 | logger.info("Starting release.") 25 | # release 26 | releaser = release.Releaser(vcs=prereleaser.vcs) 27 | releaser.run() 28 | tagdir = releaser.data.get("tagdir") 29 | logger.info("Starting postrelease.") 30 | # postrelease 31 | postreleaser = postrelease.Postreleaser(vcs=releaser.vcs) 32 | postreleaser.run() 33 | os.chdir(original_dir) 34 | logger.info("Finished full release.") 35 | if tagdir: 36 | logger.info("Reminder: tag checkout is in %s", tagdir) 37 | -------------------------------------------------------------------------------- /zest/releaser/git.py: -------------------------------------------------------------------------------- 1 | from zest.releaser.utils import execute_command 2 | from zest.releaser.utils import fs_to_text 3 | from zest.releaser.vcs import BaseVersionControl 4 | 5 | import logging 6 | import os.path 7 | import sys 8 | import tempfile 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Git(BaseVersionControl): 15 | """Command proxy for Git""" 16 | 17 | internal_filename = ".git" 18 | setuptools_helper_package = "setuptools-git" 19 | 20 | def is_setuptools_helper_package_installed(self): 21 | # The package is setuptools-git with a dash, the module is 22 | # setuptools_git with an underscore. Thanks. 23 | try: 24 | __import__("setuptools_git") 25 | except ImportError: 26 | return False 27 | return True 28 | 29 | @property 30 | def name(self): 31 | package_name = self._extract_name() 32 | if package_name: 33 | return package_name 34 | # No python package name? With git we can probably only fall back to the directory 35 | # name as there's no svn-url with a usable name in it. 36 | dir_name = os.path.basename(os.getcwd()) 37 | dir_name = fs_to_text(dir_name) 38 | return dir_name 39 | 40 | def available_tags(self): 41 | tag_info = execute_command(["git", "tag"]) 42 | tags = [line for line in tag_info.split("\n") if line] 43 | logger.debug("Available tags: '%s'", ", ".join(tags)) 44 | return tags 45 | 46 | def prepare_checkout_dir(self, prefix): 47 | # Watch out: some git versions can't clone into an existing 48 | # directory, even when it is empty. 49 | temp = tempfile.mkdtemp(prefix=prefix) 50 | cwd = os.getcwd() 51 | os.chdir(temp) 52 | cmd = ["git", "clone", "--depth", "1", self.reporoot, "gitclone"] 53 | logger.debug(execute_command(cmd)) 54 | clonedir = os.path.join(temp, "gitclone") 55 | os.chdir(clonedir) 56 | cmd = ["git", "submodule", "update", "--init", "--recursive"] 57 | logger.debug(execute_command(cmd)) 58 | os.chdir(cwd) 59 | return clonedir 60 | 61 | def tag_url(self, version): 62 | # this doesn't apply to Git, so we just return the 63 | # version name given ... 64 | return version 65 | 66 | def cmd_diff(self): 67 | return ["git", "diff"] 68 | 69 | def cmd_commit(self, message): 70 | parts = ["git", "commit", "-a", "-m", message] 71 | if not self.zest_releaser_config.run_pre_commit(): 72 | parts.append("-n") 73 | return parts 74 | 75 | def cmd_diff_last_commit_against_tag(self, version): 76 | return ["git", "diff", version] 77 | 78 | def cmd_log_since_tag(self, version): 79 | """Return log since a tagged version till the last commit of 80 | the working copy. 81 | """ 82 | return ["git", "log", "%s..HEAD" % version] 83 | 84 | def cmd_create_tag(self, version, message, sign=False): 85 | cmd = ["git", "tag", version, "-m", message] 86 | if sign: 87 | cmd.append("--sign") 88 | return cmd 89 | 90 | def cmd_checkout_from_tag(self, version, checkout_dir): 91 | if not (os.path.realpath(os.getcwd()) == os.path.realpath(checkout_dir)): 92 | # Specific to git: we need to be in that directory for the command 93 | # to work. 94 | logger.warning("We haven't been chdir'ed to %s", checkout_dir) 95 | sys.exit(1) 96 | return [ 97 | ["git", "checkout", version], 98 | ["git", "submodule", "update", "--init", "--recursive"], 99 | ] 100 | 101 | def is_clean_checkout(self): 102 | """Is this a clean checkout?""" 103 | head = execute_command(["git", "symbolic-ref", "--quiet", "HEAD"]) 104 | # This returns something like 'refs/heads/maurits-warn-on-tag' 105 | # or nothing. Nothing would be bad as that indicates a 106 | # detached head: likely a tag checkout 107 | if not head: 108 | # Greetings from Nearly Headless Nick. 109 | return False 110 | if execute_command(["git", "status", "--short", "--untracked-files=no"]): 111 | # Uncommitted changes in files that are tracked. 112 | return False 113 | return True 114 | 115 | def push_commands(self): 116 | """Push changes to the server.""" 117 | return [["git", "push"], ["git", "push", "--tags"]] 118 | 119 | def list_files(self): 120 | """List files in version control.""" 121 | return execute_command(["git", "ls-files"]).splitlines() 122 | -------------------------------------------------------------------------------- /zest/releaser/lasttagdiff.py: -------------------------------------------------------------------------------- 1 | # GPL, (c) Reinout van Rees 2 | # 3 | # Script to show the diff with the last relevant tag. 4 | 5 | from zest.releaser import utils 6 | from zest.releaser.utils import execute_command 7 | 8 | import logging 9 | import sys 10 | import zest.releaser.choose 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def main(): 17 | utils.configure_logging() 18 | vcs = zest.releaser.choose.version_control() 19 | if len(sys.argv) > 1: 20 | found = sys.argv[-1] 21 | else: 22 | found = utils.get_last_tag(vcs) 23 | name = vcs.name 24 | full_tag = vcs.tag_url(found) 25 | logger.debug( 26 | "Picked tag '%s' for %s (currently at '%s').", full_tag, name, vcs.version 27 | ) 28 | logger.info("Showing differences from the last commit against tag %s", full_tag) 29 | diff_command = vcs.cmd_diff_last_commit_against_tag(found) 30 | print(diff_command) 31 | print(execute_command(diff_command)) 32 | -------------------------------------------------------------------------------- /zest/releaser/lasttaglog.py: -------------------------------------------------------------------------------- 1 | # GPL, (c) Reinout van Rees 2 | # 3 | # Script to show the log from the last relevant tag till now. 4 | 5 | from zest.releaser import utils 6 | from zest.releaser.utils import execute_command 7 | 8 | import logging 9 | import sys 10 | import zest.releaser.choose 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def main(): 17 | utils.configure_logging() 18 | vcs = zest.releaser.choose.version_control() 19 | if len(sys.argv) > 1: 20 | found = sys.argv[-1] 21 | else: 22 | found = utils.get_last_tag(vcs) 23 | name = vcs.name 24 | full_tag = vcs.tag_url(found) 25 | logger.debug( 26 | "Picked tag '%s' for %s (currently at '%s').", full_tag, name, vcs.version 27 | ) 28 | logger.info("Showing log since tag %s and the last commit.", full_tag) 29 | log_command = vcs.cmd_log_since_tag(found) 30 | print(utils.format_command(log_command)) 31 | print(execute_command(log_command)) 32 | -------------------------------------------------------------------------------- /zest/releaser/longtest.py: -------------------------------------------------------------------------------- 1 | """Do the checks and tasks that have to happen before doing a release. 2 | """ 3 | 4 | from zest.releaser import choose 5 | from zest.releaser import utils 6 | from zest.releaser.utils import _execute_command 7 | 8 | import logging 9 | import os 10 | import readme_renderer 11 | import sys 12 | import tempfile 13 | import webbrowser 14 | 15 | 16 | HTML_PREFIX = """ 17 | 18 | 19 | 20 | """ 21 | HTML_POSTFIX = """ 22 | 23 | """ 24 | 25 | 26 | readme_renderer # noqa, indicating it as a dependency 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def show_longdesc(): 31 | vcs = choose.version_control() 32 | name = vcs.name 33 | 34 | # Corner case. importlib.metadata wants to be next to the egg-info. Which 35 | # doesn't play nice if the egg-info is in a src/ dir, which is a 36 | # relatively common occurrence. 37 | if os.path.exists(f"src/{name}.egg-info"): 38 | logger.info("egg-info dir found in src/ dir, chdir'ing to src first") 39 | os.chdir("src") 40 | 41 | filename = tempfile.mktemp(".html") 42 | html = _execute_command( 43 | [ 44 | sys.executable, 45 | "-m", 46 | "readme_renderer", 47 | "--package", 48 | name, 49 | ] 50 | ) 51 | 52 | if " 1: 143 | print("ERROR: Only enable one option of breaking/feature/final.") 144 | sys.exit(1) 145 | utils.configure_logging() 146 | postreleaser = Postreleaser( 147 | breaking=options.breaking, 148 | feature=options.feature, 149 | final=options.final, 150 | ) 151 | postreleaser.run() 152 | -------------------------------------------------------------------------------- /zest/releaser/preparedocs.py: -------------------------------------------------------------------------------- 1 | from zest.releaser import addchangelogentry 2 | from zest.releaser import baserelease 3 | from zest.releaser import bumpversion 4 | from zest.releaser import postrelease 5 | from zest.releaser import prerelease 6 | from zest.releaser import release 7 | from zest.releaser.utils import read_text_file 8 | from zest.releaser.utils import write_text_file 9 | 10 | import os 11 | 12 | 13 | def prepare_entrypoint_documentation(data): 14 | """Place the generated entrypoint doc in the source structure.""" 15 | if data["name"] != "zest.releaser": 16 | # We're available everywhere, but we're only intended for 17 | # zest.releaser internal usage. 18 | return 19 | target = os.path.join(data["reporoot"], "doc", "source", "entrypoints.rst") 20 | marker = ".. ### AUTOGENERATED FROM HERE ###" 21 | result = [] 22 | lines, encoding = read_text_file(target) 23 | for line in lines: 24 | line = line.rstrip() 25 | if line == marker: 26 | break 27 | result.append(line) 28 | result.append(marker) 29 | result.append("") 30 | 31 | for name, datadict in ( 32 | ("common", baserelease.DATA), 33 | ("prerelease", prerelease.DATA), 34 | ("release", release.DATA), 35 | ("postrelease", postrelease.DATA), 36 | ("addchangelogentry", addchangelogentry.DATA), 37 | ("bumpversion", bumpversion.DATA), 38 | ): 39 | if name == "common": 40 | heading = "%s data dict items" % name.capitalize() 41 | else: 42 | # quote the command name 43 | heading = "``%s`` data dict items" % name 44 | result.append(heading) 45 | result.append("-" * len(heading)) 46 | result.append("") 47 | if name == "common": 48 | result.append("These items are shared among all commands.") 49 | result.append("") 50 | for key in sorted(datadict.keys()): 51 | if name != "common" and datadict[key] == baserelease.DATA.get(key): 52 | # The key is already in common data, with the same value. 53 | continue 54 | result.append(key) 55 | result.append(" " + datadict[key]) 56 | result.append("") 57 | 58 | write_text_file(target, "\n".join(result), encoding) 59 | print("Wrote entry point documentation to %s" % target) 60 | -------------------------------------------------------------------------------- /zest/releaser/prerelease.py: -------------------------------------------------------------------------------- 1 | """Do the checks and tasks that have to happen before doing a release. 2 | """ 3 | 4 | from zest.releaser import baserelease 5 | from zest.releaser import utils 6 | 7 | import datetime 8 | import logging 9 | import sys 10 | 11 | 12 | try: 13 | # This is a recommended dependency. 14 | # Not a core dependency for now, as zest.releaser can also be used for 15 | # non-Python projects. 16 | from pep440 import is_canonical 17 | except ImportError: 18 | 19 | def is_canonical(version): 20 | logger.debug("Using dummy is_canonical that always returns True.") 21 | return True 22 | 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | HISTORY_HEADER = "%(new_version)s (%(today)s)" 27 | PRERELEASE_COMMIT_MSG = "Preparing release %(new_version)s" 28 | 29 | # Documentation for self.data. You get runtime warnings when something is in 30 | # self.data that is not in this list. Embarrassment-driven documentation! 31 | DATA = baserelease.DATA.copy() 32 | DATA.update( 33 | { 34 | "today": "Date string used in history header", 35 | } 36 | ) 37 | 38 | 39 | class Prereleaser(baserelease.Basereleaser): 40 | """Prepare release, ready for making a tag and an sdist. 41 | 42 | self.data holds data that can optionally be changed by plugins. 43 | 44 | """ 45 | 46 | def __init__(self, vcs=None): 47 | baserelease.Basereleaser.__init__(self, vcs=vcs) 48 | # Prepare some defaults for potential overriding. 49 | date_format = self.zest_releaser_config.date_format() 50 | self.data.update( 51 | dict( 52 | commit_msg=PRERELEASE_COMMIT_MSG, 53 | history_header=HISTORY_HEADER, 54 | today=datetime.datetime.today().strftime(date_format), 55 | update_history=True, 56 | ) 57 | ) 58 | 59 | def prepare(self): 60 | """Prepare self.data by asking about new version etc.""" 61 | if not utils.sanity_check(self.vcs): 62 | logger.critical("Sanity check failed.") 63 | sys.exit(1) 64 | if not utils.check_recommended_files(self.data, self.vcs): 65 | logger.debug("Recommended files check failed.") 66 | sys.exit(1) 67 | # Grab current version. 68 | self._grab_version(initial=True) 69 | # Grab current history. 70 | # It seems useful to do this even when we will not update the history. 71 | self._grab_history() 72 | if self.data["update_history"]: 73 | # Print changelog for this release. 74 | print( 75 | "Changelog entries for version {}:\n".format(self.data["new_version"]) 76 | ) 77 | print(self.data.get("history_last_release")) 78 | # Grab and set new version. 79 | self._grab_version() 80 | if self.data["update_history"]: 81 | # Look for unwanted 'Nothing changed yet' in latest header. 82 | self._check_nothing_changed() 83 | # Look for required text under the latest header. 84 | self._check_required() 85 | 86 | def execute(self): 87 | """Make the changes and offer a commit""" 88 | if self.data["update_history"]: 89 | self._change_header() 90 | self._write_version() 91 | if self.data["update_history"]: 92 | self._write_history() 93 | self._diff_and_commit() 94 | 95 | def _grab_version(self, initial=False): 96 | """Grab the version. 97 | 98 | When initial is False, ask the user for a non-development 99 | version. When initial is True, grab the current suggestion. 100 | 101 | """ 102 | original_version = self.vcs.version 103 | logger.debug("Extracted version: %s", original_version) 104 | if not original_version: 105 | logger.critical("No version found.") 106 | sys.exit(1) 107 | suggestion = utils.cleanup_version(original_version) 108 | new_version = None 109 | if not initial: 110 | while new_version is None: 111 | new_version = utils.ask_version("Enter version", default=suggestion) 112 | if not is_canonical(new_version): 113 | logger.warning( 114 | f"'{new_version}' is not a canonical Python package version." 115 | ) 116 | question = "Do you want to use this version anyway?" 117 | if not utils.ask(question): 118 | # Set to None: we will ask to enter a new version. 119 | new_version = None 120 | if not new_version: 121 | new_version = suggestion 122 | self.data["original_version"] = original_version 123 | self.data["new_version"] = new_version 124 | 125 | 126 | def datacheck(data): 127 | """Entrypoint: ensure that the data dict is fully documented""" 128 | utils.is_data_documented(data, documentation=DATA) 129 | 130 | 131 | def main(): 132 | utils.parse_options() 133 | utils.configure_logging() 134 | prereleaser = Prereleaser() 135 | prereleaser.run() 136 | -------------------------------------------------------------------------------- /zest/releaser/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /zest/releaser/tests/addchangelogentry.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of addchangelogentry.py 2 | ====================================== 3 | 4 | Several items are prepared for us. 5 | 6 | A git checkout of a project: 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> import sys 12 | >>> os.chdir(gitsourcedir) 13 | 14 | Asking input on the prompt is not unittestable unless we use the prepared 15 | testing hack in utils.py: 16 | 17 | >>> from zest.releaser import utils 18 | >>> utils.TESTMODE = True 19 | 20 | The message argument is required. In the tests the error is ugly, but 21 | in practice it looks fine:: 22 | 23 | >>> from zest.releaser import addchangelogentry 24 | >>> addchangelogentry.main() 25 | Traceback (most recent call last): 26 | ...too few arguments 27 | RuntimeError: SYSTEM EXIT (code=2) 28 | 29 | Run the addchangelogentry script with a message and signalling okay to 30 | commit:: 31 | 32 | >>> utils.test_answer_book.set_answers(['', '', '', '', '']) 33 | >>> sys.argv[1:] = ['My message.'] 34 | >>> addchangelogentry.main() 35 | Checking data dict 36 | Question: OK to commit this (Y/n)? 37 | Our reply: 38 | 39 | The changelog and setup.py are at 0.1 and have the message:: 40 | 41 | >>> with open('CHANGES.txt') as f: 42 | ... contents = f.read() 43 | >>> print(contents) 44 | Changelog of tha.example 45 | ======================== 46 | 47 | 0.1 (unreleased) 48 | ---------------- 49 | 50 | - My message. 51 | 52 | - Initial library skeleton created by thaskel. [your name] 53 | 54 | 55 | Add some white space in front of the list items, to show that this is 56 | kept when adding a message. Make the list items stars instead of 57 | dashes. Commit the change so we have a clean checkout. 58 | 59 | >>> utils.write_text_file('CHANGES.txt', contents.replace('- ', ' * ')) 60 | >>> commit_all_changes() 61 | 62 | Add multiple lines in one go. 63 | 64 | >>> sys.argv[1:] = ['My longer message.\nOn two lines.'] 65 | >>> addchangelogentry.main() 66 | Checking data dict 67 | Question: OK to commit this (Y/n)? 68 | Our reply: 69 | >>> with open('CHANGES.txt') as f: 70 | ... print(f.read()) 71 | Changelog of tha.example 72 | ======================== 73 | 74 | 0.1 (unreleased) 75 | ---------------- 76 | 77 | * My longer message. 78 | On two lines. 79 | 80 | * My message. 81 | 82 | * Initial library skeleton created by thaskel. [your name] 83 | 84 | 85 | Try non ascii for loads of fun::: 86 | 87 | >>> sys.argv[1:] = ['F\xc3\xbcr Elise'] 88 | >>> addchangelogentry.main() 89 | Checking data dict 90 | Question: OK to commit this (Y/n)? 91 | Our reply: 92 | >>> with open('CHANGES.txt') as f: 93 | ... contents = f.read() 94 | >>> print(contents) 95 | Changelog of tha.example 96 | ======================== 97 | 98 | 0.1 (unreleased) 99 | ---------------- 100 | 101 | * F...r Elise 102 | 103 | * My longer message. 104 | On two lines. 105 | 106 | * My message. 107 | 108 | * Initial library skeleton created by thaskel. [your name] 109 | 110 | 111 | Corner case: a percent sign also works 112 | 113 | >>> sys.argv[1:] = ['100% perfect.'] 114 | >>> addchangelogentry.main() 115 | Checking data dict 116 | Question: OK to commit this (Y/n)? 117 | Our reply: 118 | >>> with open('CHANGES.txt') as f: 119 | ... contents = f.read() 120 | >>> print(contents) 121 | Changelog of tha.example 122 | ======================== 123 | 124 | 0.1 (unreleased) 125 | ---------------- 126 | 127 | * 100% perfect. 128 | 129 | * F...r Elise 130 | 131 | * My longer message. 132 | On two lines. 133 | 134 | * My message. 135 | 136 | * Initial library skeleton created by thaskel. [your name] 137 | 138 | -------------------------------------------------------------------------------- /zest/releaser/tests/baserelease.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of baserelease.py 2 | ================================ 3 | 4 | Change to a git dir: 5 | 6 | >>> gitsourcedir 7 | 'TESTTEMP/tha.example-git' 8 | >>> import os 9 | >>> os.chdir(gitsourcedir) 10 | 11 | We need to set test mode, to avoid reading ~/.pypirc:: 12 | 13 | >>> from zest.releaser import utils 14 | >>> utils.TESTMODE = True 15 | 16 | Init the Basereleaser, which is otherwise only used as a base class. 17 | 18 | >>> from zest.releaser import baserelease 19 | >>> base = baserelease.Basereleaser() 20 | 21 | The data dict is initialized. And a vcs is chosen: 22 | 23 | >>> base.data['workingdir'] 24 | 'TESTTEMP/tha.example-git' 25 | >>> base.data['name'] 26 | 'tha.example' 27 | >>> base.vcs 28 | 29 | 30 | Two methods are unimplemented: 31 | 32 | >>> base.prepare() 33 | Traceback (most recent call last): 34 | ... 35 | NotImplementedError 36 | >>> base.execute() 37 | Traceback (most recent call last): 38 | ... 39 | NotImplementedError 40 | 41 | We can suffix commit messages based on what we find in ``setup.cfg``:: 42 | 43 | >>> lines = [ 44 | ... "[zest.releaser]", 45 | ... "extra-message = Aargh!"] 46 | >>> with open('setup.cfg', 'w') as f: 47 | ... _ = f.write('\n'.join(lines)) 48 | >>> base = baserelease.Basereleaser() 49 | >>> print(base.update_commit_message('Ni!')) 50 | Ni! 51 | 52 | Aargh! 53 | 54 | Check that this works with non-ascii too. 55 | 56 | >>> lines = [ 57 | ... "[zest.releaser]".encode('utf-8'), 58 | ... "extra-message = \u2603".encode('utf-8')] 59 | >>> with open('setup.cfg', 'wb') as f: 60 | ... _ = f.write(b'\n'.join(lines)) 61 | >>> base = baserelease.Basereleaser() 62 | >>> base.update_commit_message('Ni!') 63 | 'Ni!\n\n\u2603' 64 | 65 | And check with multiple lines. 66 | 67 | >>> lines = [ 68 | ... "[zest.releaser]", 69 | ... "extra-message =", 70 | ... " Where is my towel?", 71 | ... " Not again."] 72 | >>> with open('setup.cfg', 'w') as f: 73 | ... _ = f.write('\n'.join(lines)) 74 | >>> base = baserelease.Basereleaser() 75 | >>> print(base.update_commit_message('Ni!')) 76 | Ni! 77 | 78 | Where is my towel? 79 | Not again. 80 | 81 | We can prefix commit messages based on what we find in ``setup.cfg``:: 82 | 83 | >>> lines = [ 84 | ... "[zest.releaser]", 85 | ... "prefix-message = Aargh!"] 86 | >>> with open('setup.cfg', 'w') as f: 87 | ... _ = f.write('\n'.join(lines)) 88 | >>> base = baserelease.Basereleaser() 89 | >>> print(base.update_commit_message('Ni!')) 90 | Aargh! Ni! 91 | 92 | 93 | Check that this works with non-ascii too. 94 | 95 | >>> lines = [ 96 | ... "[zest.releaser]".encode('utf-8'), 97 | ... "prefix-message = \u2603".encode('utf-8')] 98 | >>> with open('setup.cfg', 'wb') as f: 99 | ... _ = f.write(b'\n'.join(lines)) 100 | >>> base = baserelease.Basereleaser() 101 | >>> base.update_commit_message('Ni!') 102 | '\u2603 Ni!' 103 | 104 | And check with multiple lines. 105 | 106 | >>> lines = [ 107 | ... "[zest.releaser]", 108 | ... "prefix-message =", 109 | ... " Where is my towel?", 110 | ... " Not again."] 111 | >>> with open('setup.cfg', 'w') as f: 112 | ... _ = f.write('\n'.join(lines)) 113 | >>> base = baserelease.Basereleaser() 114 | >>> print(base.update_commit_message('Ni!')) 115 | Where is my towel? 116 | Not again. Ni! 117 | 118 | We can prefix and suffix commit messages based on what we find in ``setup.cfg``:: 119 | 120 | >>> lines = [ 121 | ... "[zest.releaser]", 122 | ... "extra-message = after", 123 | ... "prefix-message = before"] 124 | >>> with open('setup.cfg', 'w') as f: 125 | ... _ = f.write('\n'.join(lines)) 126 | >>> base = baserelease.Basereleaser() 127 | >>> print(base.update_commit_message('Ni!')) 128 | before Ni! 129 | 130 | after 131 | 132 | We can use Markdown based on what we find in ``setup.cfg``:: 133 | 134 | >>> lines = [ 135 | ... "[zest.releaser]", 136 | ... "history_format = md"] 137 | >>> with open('setup.cfg', 'w') as f: 138 | ... _ = f.write('\n'.join(lines)) 139 | 140 | Now, Basereleaser will recognize the format as Markdown:: 141 | 142 | >>> base = baserelease.Basereleaser() 143 | >>> base.history_format 144 | 'md' 145 | 146 | And there won't be any underline in headers:: 147 | 148 | >>> base.underline_char == "" 149 | True 150 | 151 | Also, if we have no ``history_format`` setting, but the 152 | history file ends with ``.md``, we consider it Markdown:: 153 | 154 | >>> lines = ["[zest.releaser]", ""] 155 | >>> with open('setup.cfg', 'w') as f: 156 | ... _ = f.write('\n'.join(lines)) 157 | >>> rename_changelog("CHANGES.txt", "CHANGES.md") 158 | >>> with open('setup.py') as f: 159 | ... setup_py_contents = f.read() 160 | >>> with open('setup.py', 'w') as f: 161 | ... _ = f.write(setup_py_contents.replace("CHANGES.txt", "CHANGES.md")) 162 | >>> base = baserelease.Basereleaser() 163 | >>> base._grab_history() 164 | >>> base.history_format 165 | 'md' 166 | -------------------------------------------------------------------------------- /zest/releaser/tests/bumpversion.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of bumpversion.py 2 | ================================ 3 | 4 | Several items are prepared for us. 5 | 6 | A git checkout of a project: 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> import sys 12 | >>> os.chdir(gitsourcedir) 13 | 14 | Asking input on the prompt is not unittestable unless we use the prepared 15 | testing hack in utils.py: 16 | 17 | >>> from zest.releaser import utils 18 | >>> utils.TESTMODE = True 19 | 20 | Initially there are no tags, and we require them. In the tests the 21 | error is ugly, but in practice it looks fine, saying no bump is needed. 22 | 23 | >>> from zest.releaser import bumpversion 24 | >>> bumpversion.main() 25 | Traceback (most recent call last): 26 | ... 27 | RuntimeError: SYSTEM EXIT (code=0) 28 | 29 | So first run the fullrelease: 30 | 31 | >>> from zest.releaser import fullrelease 32 | >>> utils.test_answer_book.set_answers(['', '', '2.9.4', '', '', '', '', '', '', '', 'n']) 33 | >>> fullrelease.main() 34 | Question... 35 | Question: Enter version [0.1]: 36 | Our reply: 2.9.4 37 | ... 38 | >>> githead('CHANGES.txt') 39 | Changelog of tha.example 40 | ======================== 41 | 42 | 2.9.5 (unreleased) 43 | ------------------ 44 | >>> githead('setup.py') 45 | from setuptools import setup, find_packages 46 | version = '2.9.5.dev0' 47 | 48 | Try bumpversion again. The first time we again get an error because 49 | no version bump is needed: our current version is already higher than 50 | the latest tag, and we have no feature or breaking change. In the 51 | tests it is again ugly, but the exit code is zero, which is good. 52 | 53 | >>> utils.test_answer_book.set_answers(['', '', '', '', '', '']) 54 | >>> bumpversion.main() 55 | Traceback (most recent call last): 56 | ... 57 | RuntimeError: SYSTEM EXIT (code=0) 58 | 59 | Now a feature bump:: 60 | 61 | >>> sys.argv[1:] = ['--feature'] 62 | >>> bumpversion.main() 63 | Checking version bump for feature release. 64 | Last tag: 2.9.4 65 | Current version: 2.9.5.dev0 66 | Question: Enter version [2.10.0.dev0]: 67 | Our reply: 68 | Checking data dict 69 | Question: OK to commit this (Y/n)? 70 | Our reply: 71 | >>> githead('setup.py') 72 | from setuptools import setup, find_packages 73 | version = '2.10.0.dev0' 74 | >>> githead('CHANGES.txt') 75 | Changelog of tha.example 76 | ======================== 77 | 78 | 2.10.0 (unreleased) 79 | ------------------- 80 | 81 | Now a breaking bump, and for this test we explicitly say to create a release candidate:: 82 | 83 | >>> utils.test_answer_book.set_answers(['3.0.0rc1.dev0', '']) 84 | >>> sys.argv[1:] = ['--breaking'] 85 | >>> bumpversion.main() 86 | Checking version bump for breaking release. 87 | Last tag: 2.9.4 88 | Current version: 2.10.0.dev0 89 | Question: Enter version [3.0.0.dev0]: 90 | Our reply: 3.0.0rc1.dev0 91 | Checking data dict 92 | Question: OK to commit this (Y/n)? 93 | Our reply: 94 | >>> githead('setup.py') 95 | from setuptools import setup, find_packages 96 | version = '3.0.0rc1.dev0' 97 | >>> githead('CHANGES.txt') 98 | Changelog of tha.example 99 | ======================== 100 | 101 | 3.0.0rc1 (unreleased) 102 | --------------------- 103 | 104 | Now a final release, where we keep the dev marker:: 105 | 106 | >>> utils.test_answer_book.set_answers(['', '']) 107 | >>> sys.argv[1:] = ['--final'] 108 | >>> bumpversion.main() 109 | Checking version bump for final release. 110 | Last tag: 2.9.4 111 | Current version: 3.0.0rc1.dev0 112 | Question: Enter version [3.0.0.dev0]: 113 | Our reply: 114 | Checking data dict 115 | Question: OK to commit this (Y/n)? 116 | Our reply: 117 | >>> githead('setup.py') 118 | from setuptools import setup, find_packages 119 | version = '3.0.0.dev0' 120 | >>> githead('CHANGES.txt') 121 | Changelog of tha.example 122 | ======================== 123 | 124 | 3.0.0 (unreleased) 125 | ------------------ 126 | 127 | It will also work if we use Markdown:: 128 | 129 | >>> lines = [ 130 | ... "[zest.releaser]", 131 | ... "history_format = md"] 132 | >>> with open('setup.cfg', 'w') as f: 133 | ... _ = f.write('\n'.join(lines)) 134 | >>> lines = [ 135 | ... "# Changelog", 136 | ... "", 137 | ... "## 3.0.0 (unreleased)"] 138 | >>> with open('CHANGES.txt', 'w') as f: 139 | ... _ = f.write('\n'.join(lines)) 140 | >>> commit_all_changes() 141 | >>> utils.test_answer_book.set_answers(['', '']) 142 | >>> sys.argv[1:] = ['--final'] 143 | >>> bumpversion.main() 144 | Checking version bump for final release. 145 | Last tag: 2.9.4 146 | Current version: 3.0.0.dev0 147 | Question: Enter version [3.0.1.dev0]: 148 | Our reply: 149 | Checking data dict 150 | Question: OK to commit this (Y/n)? 151 | Our reply: 152 | >>> githead('CHANGES.txt') 153 | # Changelog 154 | 155 | ## 3.0.1 (unreleased) 156 | -------------------------------------------------------------------------------- /zest/releaser/tests/choose.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of choose.py 2 | =========================== 3 | 4 | Some initial imports: 5 | 6 | >>> from zest.releaser import choose 7 | >>> import os 8 | >>> from zest.releaser import utils 9 | >>> utils.TESTMODE = True 10 | 11 | Choose makes the choice between version control systems. 12 | 13 | Git: 14 | 15 | >>> os.chdir(gitsourcedir) 16 | >>> choose.version_control() 17 | 18 | 19 | It works when we are in a sub directory too: 20 | 21 | >>> os.chdir(gitsourcedir) 22 | >>> os.chdir('src') 23 | >>> utils.test_answer_book.set_answers(['n']) 24 | >>> choose.version_control() 25 | Question: You are NOT in the root of the repository. Do you want to go there? (y/N)? 26 | Our reply: n 27 | 28 | >>> utils.test_answer_book.set_answers(['y']) 29 | >>> choose.version_control() 30 | Question: You are NOT in the root of the repository. Do you want to go there? (y/N)? 31 | Our reply: y 32 | 33 | 34 | When no version control system is found, zest.releaser exits (with a log 35 | message, but we don't test those yet): 36 | 37 | >>> os.chdir(tempdir) 38 | >>> choose.version_control() 39 | Traceback (most recent call last): 40 | ... 41 | RuntimeError: SYSTEM EXIT (code=1) 42 | -------------------------------------------------------------------------------- /zest/releaser/tests/cmd_error.py: -------------------------------------------------------------------------------- 1 | # Python script to test some corner cases that print warnings to stderr. 2 | import sys 3 | 4 | 5 | print(sys.argv[1], file=sys.stderr) 6 | -------------------------------------------------------------------------------- /zest/releaser/tests/fullrelease.txt: -------------------------------------------------------------------------------- 1 | Fullrelease process with --no-input 2 | =================================== 3 | 4 | Several items are prepared for us. 5 | 6 | A git checkout of a project: 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> os.chdir(gitsourcedir) 12 | 13 | The version is at 0.1.dev0: 14 | 15 | >>> githead('setup.py') 16 | from setuptools import setup, find_packages 17 | version = '0.1.dev0' 18 | 19 | Asking input on the prompt is not unittestable unless we use the prepared 20 | testing hack in utils.py: 21 | 22 | >>> from zest.releaser import utils 23 | >>> utils.TESTMODE = True 24 | 25 | Run the whole process without asking for input. For that we pass the 26 | ``--no-input`` option: 27 | 28 | >>> import sys 29 | >>> sys.argv[1:] = ['--no-input'] 30 | >>> utils.parse_options() 31 | Namespace(auto_response=True, verbose=False) 32 | >>> utils.AUTO_RESPONSE 33 | True 34 | 35 | Now run the fullrelease: 36 | 37 | >>> from zest.releaser import fullrelease 38 | >>> fullrelease.main() 39 | lists of files in version control and sdist match 40 | Changelog entries for version 0.1: 41 | 42 | 0.1 (unreleased) 43 | ---------------- 44 | 45 | - Initial library skeleton created by thaskel. [your name] 46 | 47 | Checking data dict 48 | Checking data dict 49 | Tag needed ... 50 | 51 | The changelog and setup.py are at 0.2.dev0: 52 | 53 | >>> githead('CHANGES.txt') 54 | Changelog of tha.example 55 | ======================== 56 | 57 | 0.2 (unreleased) 58 | ---------------- 59 | >>> githead('setup.py') 60 | from setuptools import setup, find_packages 61 | version = '0.2.dev0' 62 | 63 | An alternative is to set a ``no-input`` option in the ``.pypirc`` or 64 | ``setup.cfg`` file: 65 | 66 | >>> sys.argv[1:] = [] 67 | >>> from zest.releaser import utils 68 | >>> utils.parse_options() 69 | Namespace(auto_response=False, verbose=False) 70 | >>> utils.AUTO_RESPONSE 71 | False 72 | >>> cfg = """ 73 | ... [zest.releaser] 74 | ... no-input = true 75 | ... """ 76 | >>> with open('setup.cfg', 'w') as f: 77 | ... _ = f.write(cfg) 78 | 79 | The prerelease part would complain when the changelog still contains 80 | '- Nothing changed yet.' So change it. 81 | 82 | >>> add_changelog_entry() 83 | 84 | Now run the fullrelease: 85 | 86 | >>> from zest.releaser import fullrelease 87 | >>> fullrelease.main() 88 | lists of files in version control and sdist match 89 | Changelog entries for version 0.2: 90 | 91 | 0.2 (unreleased) 92 | ---------------- 93 | 94 | - Brown bag release. 95 | 96 | 97 | 0.1 (1972-12-25) 98 | ---------------- 99 | Checking data dict 100 | Checking data dict 101 | Tag needed ... 102 | 103 | Yes, the no-input was detected: 104 | 105 | >>> utils.AUTO_RESPONSE 106 | True 107 | 108 | The changelog and setup.py are at 0.3.dev0 now: 109 | 110 | >>> githead('CHANGES.txt') 111 | Changelog of tha.example 112 | ======================== 113 | 114 | 0.3 (unreleased) 115 | ---------------- 116 | >>> githead('setup.py') 117 | from setuptools import setup, find_packages 118 | version = '0.3.dev0' 119 | 120 | When you do some commits, but forget to update the changelog, 121 | prerelease (or fullrelease) will warn you, as indicated earlier: 122 | 123 | >>> from zest.releaser import prerelease 124 | >>> prerelease.main() 125 | Traceback (most recent call last): 126 | ... 127 | RuntimeError: SYSTEM EXIT (code=1) 128 | -------------------------------------------------------------------------------- /zest/releaser/tests/functional-git.txt: -------------------------------------------------------------------------------- 1 | Integration test 2 | ================ 3 | 4 | Several items are prepared for us. 5 | 6 | A git directory (repository and checkout in one): 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> os.chdir(gitsourcedir) 12 | 13 | There are no tags yet: 14 | 15 | >>> from zest.releaser.utils import execute_command 16 | >>> print(execute_command(["git", "tag"])) 17 | 18 | 19 | The changelog is unreleased: 20 | 21 | >>> githead('CHANGES.txt') 22 | Changelog of tha.example 23 | ======================== 24 | 25 | 0.1 (unreleased) 26 | ---------------- 27 | 28 | The version is at 0.1.dev0: 29 | 30 | >>> githead('setup.py') 31 | from setuptools import setup, find_packages 32 | version = '0.1.dev0' 33 | 34 | Asking input on the prompt is not unittestable unless we use the prepared 35 | testing hack in utils.py: 36 | 37 | >>> from zest.releaser import utils 38 | >>> utils.TESTMODE = True 39 | 40 | Run the prerelease script: 41 | 42 | >>> from zest.releaser import prerelease 43 | >>> utils.test_answer_book.set_answers(['', '', '', '', '']) 44 | >>> prerelease.main() 45 | Question... 46 | Question: Enter version [0.1]: 47 | Our reply: 48 | Checking data dict 49 | Question: OK to commit this (Y/n)? 50 | Our reply: 51 | 52 | The changelog now has a release date instead of ``(unreleased)``: 53 | 54 | >>> githead('CHANGES.txt') 55 | Changelog of tha.example 56 | ======================== 57 | 58 | 0.1 (2008-12-20) 59 | ---------------- 60 | 61 | And the version number is just 0.1 and has lost its dev marker: 62 | 63 | >>> githead('setup.py') 64 | from setuptools import setup, find_packages 65 | version = '0.1' 66 | 67 | The release script tags the release and uploads it. Note that in the 68 | other function tests we call 69 | ``mock_pypi_available.append('tha.example')``, but we do not do this 70 | here. This way we can check what happens when a package is not yet 71 | known on PyPI: 72 | 73 | >>> utils.test_answer_book.set_answers(['y', 'y', 'y', 'yes']) 74 | >>> from zest.releaser import release 75 | >>> release.main() 76 | Checking data dict 77 | Tag needed to proceed, you can use the following command: 78 | git tag 0.1 -m 'Tagging 0.1' 79 | Question: Run this command (Y/n)? 80 | Our reply: y 81 | 82 | Question: Check out the tag 83 | (for tweaks or pypi/distutils server upload) (Y/n)? 84 | Our reply: y 85 | RED Note: ...0.1... 86 | ... 87 | RED HEAD is now at ... 88 | Preparing release 0.1 89 | 90 | Question: Fix setup.cfg (and commit to tag if possible) (Y/n)? 91 | Our reply: y 92 | [egg_info] 93 | tag_build = 94 | tag_svn_revision = false 95 | 96 | 97 | ... 98 | running sdist 99 | ... 100 | running bdist_wheel 101 | ... 102 | Question: Upload to pypi (y/N)? 103 | Our reply: yes 104 | MOCK twine dispatch upload ...whl ...example-0.1.tar.gz 105 | 106 | (Note: depending in the Python/setuptools version, the filename may contain ``tha.example`` or ``tha_example``.) 107 | 108 | There is now a tag: 109 | 110 | >>> print(execute_command(["git", "tag"])) 111 | 0.1 112 | 113 | And the postrelease script ups the version: 114 | 115 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 116 | >>> from zest.releaser import postrelease 117 | >>> postrelease.main() 118 | Current version is 0.1 119 | Question: Enter new development version ('.dev0' will be appended) [0.2]: 120 | Our reply: 121 | Checking data dict 122 | Question: OK to commit this (Y/n)? 123 | Our reply: 124 | Question: OK to push commits to the server? (Y/n)? 125 | Our reply: n 126 | >>> from zest.releaser import lasttaglog 127 | >>> lasttaglog.main() 128 | git log... 129 | Back to development: 0.2 130 | 131 | 132 | The changelog and setup.py are at 0.2 and indicate dev mode: 133 | 134 | >>> githead('CHANGES.txt') 135 | Changelog of tha.example 136 | ======================== 137 | 138 | 0.2 (unreleased) 139 | ---------------- 140 | >>> githead('setup.py') 141 | from setuptools import setup, find_packages 142 | version = '0.2.dev0' 143 | 144 | And there are no uncommitted changes: 145 | 146 | >>> print(execute_command(["git", "status"])) 147 | On branch main 148 | nothing to commit, working directory clean 149 | -------------------------------------------------------------------------------- /zest/releaser/tests/functional-with-hooks.txt: -------------------------------------------------------------------------------- 1 | Integration test 2 | ================ 3 | 4 | This test is based on the functional tests using a git repository, but enables 5 | releaser hooks on the test package and ensures that they run. 6 | 7 | Several items are prepared for us. 8 | 9 | A git directory (repository and checkout in one): 10 | 11 | >>> gitsourcedir 12 | 'TESTTEMP/tha.example-git' 13 | >>> import os 14 | >>> os.chdir(gitsourcedir) 15 | 16 | Asking input on the prompt is not unittestable unless we use the prepared 17 | testing hack in utils.py: 18 | 19 | >>> from zest.releaser import utils 20 | >>> utils.TESTMODE = True 21 | 22 | Append the [zest.releaser] section to the setup.cfg file, enabling the 23 | releaser hooks: 24 | 25 | >>> with open('setup.cfg', 'a') as f: 26 | ... _ = f.write(""" 27 | ... [zest.releaser] 28 | ... hook_package_dir = src 29 | ... prereleaser.before = tha.example.hooks.prereleaser_before 30 | ... prereleaser.middle = tha.example.hooks.prereleaser_middle 31 | ... prereleaser.after = tha.example.hooks.prereleaser_after 32 | ... releaser.before = tha.example.hooks.releaser_before 33 | ... releaser.middle = tha.example.hooks.releaser_middle 34 | ... releaser.after_checkout = tha.example.hooks.releaser_after_checkout 35 | ... releaser.before_upload = tha.example.hooks.releaser_before_upload 36 | ... releaser.after = tha.example.hooks.releaser_after 37 | ... postreleaser.before = tha.example.hooks.postreleaser_before 38 | ... postreleaser.middle = tha.example.hooks.postreleaser_middle 39 | ... postreleaser.after = tha.example.hooks.postreleaser_after""") 40 | 41 | Commit the change to setup.cfg so that we have a clean checkout: 42 | 43 | >>> from zest.releaser import git 44 | >>> checkout = git.Git() 45 | >>> cmd = checkout.cmd_commit("tweak setup.cfg to enable hooks") 46 | >>> print('dummy %s' % utils.execute_command(cmd)) 47 | dummy...tweak setup.cfg to enable hooks 48 | 1 file changed, 14 insertions(+) 49 | 50 | Run the prerelease script. Note that pyroma and check-manifest have 51 | hooks here so they are run too, but the order may differ. With the 52 | bin/test script first pyroma is run, then check-manifest. With tox it 53 | is the other way around. So we use ellipsis for that part. 54 | sys.dont_write_bytecode is set to True to avoid writing .pyc files so 55 | that check-manifest doesn't complain about differences between the sdist 56 | directory and the vcs. 57 | 58 | >>> import sys 59 | >>> sys.dont_write_bytecode = True 60 | >>> from zest.releaser import prerelease 61 | >>> utils.test_answer_book.set_answers(['', '', '', '', '']) 62 | >>> prerelease.main() 63 | prereleaser_before 64 | ... 65 | Question: Enter version [0.1]: 66 | Our reply: 67 | prereleaser_middle 68 | Checking data dict 69 | Question: OK to commit this (Y/n)? 70 | Our reply: 71 | prereleaser_after 72 | 73 | The release script tags the release and uploads it: 74 | 75 | >>> utils.test_answer_book.set_answers(['y', 'y', 'y', 'y', 'y', 'y', 'y', 'y']) 76 | >>> mock_pypi_available.append('tha.example') 77 | >>> from zest.releaser import release 78 | >>> release.main() 79 | releaser_before 80 | releaser_middle 81 | Checking data dict 82 | Tag needed to proceed, you can use the following command: 83 | git tag 0.1 -m 'Tagging 0.1' 84 | Question: Run this command (Y/n)? 85 | Our reply: y 86 | 87 | Question: Check out the tag 88 | (for tweaks or pypi/distutils server upload) (Y/n)? 89 | Our reply: y 90 | RED Note: ...0.1... 91 | ... 92 | RED HEAD is now at ... 93 | Preparing release 0.1 94 | 95 | Question: Fix setup.cfg (and commit to tag if possible) (Y/n)? 96 | Our reply: y 97 | [egg_info] 98 | tag_build = 99 | tag_svn_revision = false 100 | 101 | [zest.releaser] 102 | ... 103 | releaser_after_checkout 104 | ... 105 | running sdist 106 | ... 107 | running bdist_wheel 108 | ... 109 | releaser_before_upload 110 | ... 111 | These files are ready for upload: 112 | ... 113 | Question: Upload to pypi (Y/n)? 114 | Our reply: y 115 | MOCK twine dispatch upload ...whl ...example-0.1.tar.gz 116 | releaser_after 117 | 118 | (Note: depending in the Python/setuptools version, the filename may contain ``tha.example`` or ``tha_example``.) 119 | 120 | And the postrelease script ups the version: 121 | 122 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 123 | >>> from zest.releaser import postrelease 124 | >>> postrelease.main() 125 | postreleaser_before 126 | Current version is 0.1 127 | Question: Enter new development version ('.dev0' will be appended) [0.2]: 128 | Our reply: 129 | postreleaser_middle 130 | Checking data dict 131 | Question: OK to commit this (Y/n)? 132 | Our reply: 133 | Question: OK to push commits to the server? (Y/n)? 134 | Our reply: n 135 | postreleaser_after 136 | -------------------------------------------------------------------------------- /zest/releaser/tests/functional.py: -------------------------------------------------------------------------------- 1 | """Set up functional test fixtures""" 2 | 3 | from io import StringIO 4 | from urllib import request 5 | from urllib.error import HTTPError 6 | from zest.releaser import choose 7 | from zest.releaser import utils 8 | from zest.releaser.baserelease import NOTHING_CHANGED_YET 9 | from zest.releaser.utils import execute_command 10 | from zest.releaser.utils import filename_from_test_dir 11 | 12 | import os 13 | import shutil 14 | import sys 15 | import tarfile 16 | import tempfile 17 | 18 | 19 | def setup(test): 20 | # Reset constants to original settings: 21 | utils.AUTO_RESPONSE = False 22 | utils.TESTMODE = False 23 | 24 | partstestdir = os.getcwd() # Buildout's test run in parts/test 25 | test.orig_dir = partstestdir 26 | test.tempdir = tempfile.mkdtemp(prefix="testtemp") 27 | test.orig_argv = sys.argv[1:] 28 | sys.argv[1:] = [] 29 | # Monkey patch sys.exit 30 | test.orig_exit = sys.exit 31 | 32 | def _exit(code=None): 33 | msg = "SYSTEM EXIT (code=%s)" % code 34 | raise RuntimeError(msg) 35 | 36 | sys.exit = _exit 37 | 38 | # Monkey patch urllib for pypi access mocking. 39 | test.orig_urlopen = request.urlopen 40 | test.mock_pypi_available = [] 41 | 42 | def _make_mock_urlopen(mock_pypi_available): 43 | def _mock_urlopen(url): 44 | # print "Mock opening", url 45 | package = url.replace("https://pypi.org/simple/", "") 46 | if package not in mock_pypi_available: 47 | raise HTTPError( 48 | url, 49 | 404, 50 | "HTTP Error 404: Not Found (%s does not have any releases)" 51 | % package, 52 | None, 53 | None, 54 | ) 55 | else: 56 | answer = " ".join(mock_pypi_available) 57 | return StringIO(answer) 58 | 59 | return _mock_urlopen 60 | 61 | request.urlopen = _make_mock_urlopen(test.mock_pypi_available) 62 | 63 | # Extract example project 64 | example_tar = filename_from_test_dir("example.tar") 65 | with tarfile.TarFile(example_tar) as tf: 66 | tf.extractall(path=test.tempdir) 67 | sourcedir = os.path.join(test.tempdir, "tha.example") 68 | 69 | # Git initialization 70 | gitsourcedir = os.path.join(test.tempdir, "tha.example-git") 71 | shutil.copytree(sourcedir, gitsourcedir) 72 | os.chdir(gitsourcedir) 73 | execute_command(["git", "init", "-b", "main"]) 74 | # Configure local git. 75 | execute_command(["git", "config", "--local", "user.name", "Temp user"]) 76 | execute_command(["git", "config", "--local", "user.email", "temp@example.com"]) 77 | execute_command(["git", "config", "--local", "commit.gpgsign", "false"]) 78 | execute_command(["git", "config", "--local", "tag.gpgsign", "false"]) 79 | execute_command(["git", "add", "."]) 80 | execute_command(["git", "commit", "-a", "-m", "init" "-n"]) 81 | os.chdir(test.orig_dir) 82 | 83 | def githead(*filename_parts): 84 | filename = os.path.join(gitsourcedir, *filename_parts) 85 | with open(filename) as f: 86 | lines = f.readlines() 87 | for line in lines[:5]: 88 | line = line.strip() 89 | if line: 90 | print(line) 91 | 92 | def commit_all_changes(message="Committing all changes"): 93 | # Get a clean checkout. 94 | vcs = choose.version_control() 95 | execute_command(vcs.cmd_commit(message)) 96 | 97 | def add_changelog_entry(): 98 | # Replace '- Nothing changed yet.' by a different entry. 99 | with open("CHANGES.txt") as f: 100 | orig_changes = f.read() 101 | new_changes = orig_changes.replace(NOTHING_CHANGED_YET, "- Brown bag release.") 102 | with open("CHANGES.txt", "w") as f: 103 | f.write(new_changes) 104 | commit_all_changes() 105 | 106 | def rename_changelog(src: str, dst: str): 107 | execute_command(["git", "mv", src, dst]) 108 | commit_all_changes() 109 | 110 | test.globs.update( 111 | { 112 | "tempdir": test.tempdir, 113 | "gitsourcedir": gitsourcedir, 114 | "githead": githead, 115 | "mock_pypi_available": test.mock_pypi_available, 116 | "add_changelog_entry": add_changelog_entry, 117 | "commit_all_changes": commit_all_changes, 118 | "rename_changelog": rename_changelog, 119 | } 120 | ) 121 | 122 | 123 | def teardown(test): 124 | sys.exit = test.orig_exit 125 | request.urlopen = test.orig_urlopen 126 | os.chdir(test.orig_dir) 127 | sys.argv[1:] = test.orig_argv 128 | shutil.rmtree(test.tempdir) 129 | # Reset constants to original settings: 130 | utils.AUTO_RESPONSE = False 131 | utils.TESTMODE = False 132 | -------------------------------------------------------------------------------- /zest/releaser/tests/git.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of git.py 2 | ======================== 3 | 4 | Some initial imports: 5 | 6 | >>> from zest.releaser import git 7 | >>> from zest.releaser.utils import execute_command, execute_commands 8 | >>> import os 9 | 10 | Project name 11 | ------------ 12 | 13 | The prepared git project has a setup.py, so the name in there is used: 14 | 15 | >>> os.chdir(gitsourcedir) 16 | >>> checkout = git.Git() 17 | >>> checkout.name 18 | 'tha.example' 19 | 20 | When the setup.py doesn't exist or doesn't return a proper name, we fall back 21 | to the directory name. 22 | 23 | >>> orig = checkout.get_setup_py_name 24 | >>> checkout.get_setup_py_name= lambda: None # Hack 25 | >>> checkout.name 26 | 'tha.example-git' 27 | >>> checkout.get_setup_py_name = orig # Restore hack 28 | 29 | 30 | Diff and commit 31 | --------------- 32 | 33 | Make a change: 34 | 35 | >>> setup_py = os.path.join(gitsourcedir, 'setup.py') 36 | >>> with open(setup_py, 'a') as f: 37 | ... _ = f.write('\na = 2\n') 38 | >>> cmd = checkout.cmd_diff() 39 | >>> cmd 40 | ['git', 'diff'] 41 | >>> print(execute_command(cmd)) 42 | diff --git a/setup.py b/setup.py 43 | index 9c14143..54fa3b9 100644 44 | --- a/setup.py 45 | +++ b/setup.py 46 | @@ -41,3 +41,5 @@ setup(name='tha.example', 47 | 'console_scripts': [ 48 | ]}, 49 | ) 50 | + 51 | +a = 2 52 | 53 | Commit it: 54 | 55 | >>> cmd = checkout.cmd_commit('small tweak') 56 | >>> cmd 57 | ['git', 'commit', '-a', '-m', 'small tweak', '-n'] 58 | 59 | In some cases we get this output: 60 | ``[main ...] small tweak`` 61 | and in other this: 62 | ``Created commit ...: small tweak`` 63 | 64 | >>> print('dummy %s' % execute_command(cmd)) 65 | dummy...small tweak 66 | 1 file changed, 2 insertions(+) 67 | 68 | 69 | 70 | Tags 71 | ---- 72 | 73 | Originally there are no tags: 74 | 75 | >>> checkout.available_tags() 76 | [] 77 | 78 | Create a tag and it will show up: 79 | 80 | >>> cmd = checkout.cmd_create_tag('0.1', 'Creating tag 0.1') 81 | >>> cmd 82 | ['git', 'tag', '0.1', '-m', 'Creating tag 0.1'] 83 | >>> dont_care = execute_command(cmd) 84 | >>> checkout.available_tags() 85 | ['0.1'] 86 | 87 | We could have signed the tag, too (though we won't execute the actual command 88 | as it would need gpg setup on the test machine): 89 | 90 | >>> cmd = checkout.cmd_create_tag('0.1', 'Creating tag 0.1', sign=True) 91 | >>> cmd 92 | ['git', 'tag', '0.1', '-m', 'Creating tag 0.1', '--sign'] 93 | 94 | 95 | A specific tag url is important for subversion, but nonsensical for 96 | git. We just return the version as-is: 97 | 98 | >>> checkout.tag_url('holadijee') 99 | 'holadijee' 100 | 101 | Make and commit a small change: 102 | 103 | >>> with open(setup_py, 'a') as f: 104 | ... _ = f.write('\nb = 3\n') 105 | >>> cmd = checkout.cmd_commit('small second tweak') 106 | >>> print('dummy %s' % execute_command(cmd)) 107 | dummy...small second tweak 108 | 1 file changed, 2 insertions(+) 109 | 110 | Now we can request the changes since a specific tag: 111 | 112 | >>> cmd = checkout.cmd_diff_last_commit_against_tag('0.1') 113 | >>> cmd 114 | ['git', 'diff', '0.1'] 115 | >>> print(execute_command(cmd)) 116 | diff --git a/setup.py b/setup.py 117 | index 9c14143..54fa3b9 100644 118 | --- a/setup.py 119 | +++ b/setup.py 120 | @@ -43,3 +43,5 @@ setup(name='tha.example', 121 | ) 122 | 123 | a = 2 124 | + 125 | +b = 3 126 | 127 | 128 | Making a tag checkout 129 | --------------------- 130 | 131 | With git, we make a clone of the repository in a tempdir and 132 | afterwards switch ("checkout") that directory to the tag. 133 | 134 | Since version 6.6.3 we create a shallow clone, which only contains the 135 | last commit. And since 6.6.4 this works properly. :-) For that to 136 | work, we switch back to the tag first, otherwise the shallow copy will 137 | not contain the tag checkout that we need. In other words: you need 138 | to be on the tag when you create a release, and that is true for the 139 | other version control systems too. 140 | 141 | So we first switch back to the tag. 142 | 143 | >>> cmd = checkout.cmd_checkout_from_tag('0.1', gitsourcedir) 144 | >>> print(execute_commands(cmd)) 145 | RED Note: ...0.1... 146 | RED HEAD is now at ... small tweak 147 | 148 | Prepare the checkout directory with the clone of the local repository. 149 | 150 | >>> temp = checkout.prepare_checkout_dir('somename') 151 | >>> temp 152 | 'TMPDIR/somename...' 153 | >>> os.path.isdir(temp) 154 | True 155 | 156 | The checked out clone is really a clone and not an empty directory. 157 | 158 | >>> sorted(os.listdir(temp)) 159 | ['.git', '.gitignore', 'CHANGES.txt', ...] 160 | >>> with open(os.path.join(temp, 'setup.py')) as f: 161 | ... print(f.read()) 162 | from setuptools import setup, find_packages 163 | ... 164 | a = 2 165 | 166 | For git, we have to change to that directory! Git doesn't work with paths. 167 | 168 | >>> cmd = checkout.cmd_checkout_from_tag('0.1', temp) 169 | Traceback (most recent call last): 170 | ... 171 | RuntimeError: SYSTEM EXIT (code=1) 172 | 173 | Change to the directory. Verify that we can checkout the tag, even 174 | though we are already at the correct tag. 175 | 176 | >>> os.chdir(temp) 177 | >>> cmd = checkout.cmd_checkout_from_tag('0.1', temp) 178 | >>> cmd 179 | [['git', 'checkout', '0.1'], 180 | ['git', 'submodule', 'update', '--init', '--recursive']] 181 | >>> print(execute_commands(cmd)) 182 | RED HEAD is now at ... small tweak 183 | 184 | The tempdir should be at tag 0.1. The last line ought to be "a = 2" 185 | 186 | >>> with open(os.path.join(temp, 'setup.py')) as f: 187 | ... print(f.read()) 188 | from setuptools import setup, find_packages 189 | ... 190 | a = 2 191 | 192 | Change back to the source directory and return to the main branch. 193 | 194 | >>> os.chdir(gitsourcedir) 195 | >>> cmd = [['git', 'checkout', 'main'], 196 | ... ['git', 'submodule', 'update', '--init', '--recursive']] 197 | >>> print(execute_commands(cmd)) 198 | RED Previous HEAD position was ... small tweak 199 | RED Switched to branch 'main' 200 | 201 | Pushing changes 202 | --------------- 203 | 204 | For git, committing isn't enough. We need to push changes to the server: 205 | 206 | >>> checkout.push_commands() 207 | [['git', 'push'], ['git', 'push', '--tags']] 208 | -------------------------------------------------------------------------------- /zest/releaser/tests/postrelease.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of postrelease.py 2 | ================================ 3 | 4 | Several items are prepared for us. 5 | 6 | A git checkout of a project: 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> os.chdir(gitsourcedir) 12 | 13 | The version is at 0.1.dev0: 14 | 15 | >>> githead('setup.py') 16 | from setuptools import setup, find_packages 17 | version = '0.1.dev0' 18 | 19 | Asking input on the prompt is not unittestable unless we use the prepared 20 | testing hack in utils.py: 21 | 22 | >>> from zest.releaser import utils 23 | >>> utils.TESTMODE = True 24 | 25 | Run the postrelease script: 26 | 27 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 28 | >>> from zest.releaser import postrelease 29 | >>> postrelease.main() 30 | Current version is 0.1 31 | Question: Enter new development version ('.dev0' will be appended) [0.2]: 32 | Our reply: 33 | Checking data dict 34 | Question: OK to commit this (Y/n)? 35 | Our reply: 36 | Question: OK to push commits to the server? (Y/n)? 37 | Our reply: n 38 | 39 | The changelog and setup.py are at 0.2 and indicate dev mode: 40 | 41 | >>> githead('CHANGES.txt') 42 | Changelog of tha.example 43 | ======================== 44 | 45 | 0.2 (unreleased) 46 | ---------------- 47 | >>> githead('setup.py') 48 | from setuptools import setup, find_packages 49 | version = '0.2.dev0' 50 | 51 | Now we set the version to something that does not end in a number and 52 | is not recognized as development version (with setuptools 8 or higher 53 | the version is actually reported with a zero at the end): 54 | 55 | >>> from zest.releaser.git import Git 56 | >>> vcs = Git() 57 | >>> vcs.version 58 | '0.2.dev0' 59 | >>> vcs.version = '0.1b' 60 | >>> vcs.version 61 | '0.1b0' 62 | >>> commit_all_changes() 63 | 64 | Run the postrelease script. Since the version number does not end 65 | with a number, the script cannot make a suggestion, except when the 66 | number is normalized by setuptools already: 67 | 68 | >>> utils.test_answer_book.set_answers(['0.2', '', 'n']) 69 | >>> from zest.releaser import postrelease 70 | >>> postrelease.main() 71 | Current version is 0.1b0 72 | Question: Enter new development version ('.dev0' will be appended) [0.1b1]: 73 | Our reply: 0.2 74 | Checking data dict 75 | Question: OK to commit this (Y/n)? 76 | Our reply: 77 | Question: OK to push commits to the server? (Y/n)? 78 | Our reply: n 79 | 80 | The changelog and setup.py are at 0.2 and indicate dev mode: 81 | 82 | >>> githead('CHANGES.txt') 83 | Changelog of tha.example 84 | ======================== 85 | 86 | 0.2 (unreleased) 87 | ---------------- 88 | >>> githead('setup.py') 89 | from setuptools import setup, find_packages 90 | version = '0.2.dev0' 91 | 92 | The prerelease part would complain when the changelog still contains 93 | '- Nothing changed yet.' So change it. 94 | 95 | >>> add_changelog_entry() 96 | 97 | To check some corner cases we switch back and forth between prerelease 98 | and postrelease. The next version after 0.2.19 should not be 0.2.110 99 | but 0.2.20: 100 | 101 | >>> from zest.releaser import prerelease 102 | >>> utils.test_answer_book.set_answers(['', '', '0.2.19', '']) 103 | >>> prerelease.main() 104 | Question... 105 | Question: Enter version [0.2]: 106 | Our reply: 0.2.19 107 | Checking data dict 108 | Question: OK to commit this (Y/n)? 109 | Our reply: 110 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 111 | >>> postrelease.main() 112 | Current version is 0.2.19 113 | Question: Enter new development version ('.dev0' will be appended) [0.2.20]: 114 | Our reply: 115 | Checking data dict 116 | Question: OK to commit this (Y/n)? 117 | Our reply: 118 | Question: OK to push commits to the server? (Y/n)? 119 | Our reply: n 120 | 121 | Releases without numbers at the end should not fluster us even when we 122 | cannot suggest a reasonable number. We'll ask for a version until we get one. 123 | This this case it is not a canonical version so we have an extra question about this: 124 | 125 | >>> add_changelog_entry() 126 | >>> utils.test_answer_book.set_answers(['', '', '0.3beta', '']) 127 | >>> prerelease.main() 128 | Question... 129 | Question: Enter version [0.2.20]: 130 | Our reply: 0.3beta 131 | Question: Do you want to use this version anyway? (Y/n)? 132 | Our reply: 133 | Checking data dict 134 | Question: OK to commit this (Y/n)? 135 | Our reply: 136 | >>> utils.test_answer_book.set_answers(['0.3rc0', '', 'n']) 137 | >>> postrelease.main() 138 | Current version is 0.3b0 139 | Question: Enter new development version ('.dev0' will be appended) [0.3b1]: 140 | Our reply: 0.3rc0 141 | Checking data dict 142 | Question: OK to commit this (Y/n)? 143 | Our reply: 144 | Question: OK to push commits to the server? (Y/n)? 145 | Our reply: n 146 | 147 | Numbers and characters can be combined: 148 | 149 | >>> add_changelog_entry() 150 | >>> utils.test_answer_book.set_answers(['', '', '1.0a1', '']) 151 | >>> prerelease.main() 152 | Question... 153 | Question: Enter version [0.3rc0]: 154 | Our reply: 1.0a1 155 | Checking data dict 156 | Question: OK to commit this (Y/n)? 157 | Our reply: 158 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 159 | >>> postrelease.main() 160 | Current version is 1.0a1 161 | Question: Enter new development version ('.dev0' will be appended) [1.0a2]: 162 | Our reply: 163 | Checking data dict 164 | Question: OK to commit this (Y/n)? 165 | Our reply: 166 | Question: OK to push commits to the server? (Y/n)? 167 | Our reply: n 168 | 169 | If there's an empty history file, it gets a fresh header. 170 | 171 | >>> add_changelog_entry() 172 | >>> utils.test_answer_book.set_answers(['', '', '1.0', '']) 173 | >>> prerelease.main() 174 | Question: ... 175 | >>> with open('CHANGES.txt', 'w') as f: 176 | ... _ = f.write('') 177 | >>> commit_all_changes() 178 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 179 | >>> postrelease.main() 180 | Current ... 181 | >>> with open('CHANGES.txt') as f: 182 | ... print(f.read()) 183 | 1.1 (unreleased) 184 | ---------------- 185 | 186 | - Nothing changed yet. 187 | 188 | If there is no history file, we get no errors and a new history file is not 189 | created: 190 | 191 | >>> add_changelog_entry() 192 | >>> utils.test_answer_book.set_answers(['', '', '', '']) 193 | >>> prerelease.main() 194 | Question: ... 195 | >>> os.remove('CHANGES.txt') 196 | >>> utils.test_answer_book.set_answers(['8.2', '']) 197 | >>> postrelease.main() # The setup.py complains and quits. Our test setup catches this. 198 | Traceback (most recent call last): 199 | ... 200 | RuntimeError: SYSTEM EXIT (code=1) 201 | >>> with open('CHANGES.txt') as f: # Nope, doesn't exist. 202 | ... print(f.read()) 203 | Traceback (most recent call last): 204 | ... 205 | FileNotFoundError: [Errno 2] No such file or directory: 'CHANGES.txt' 206 | 207 | Re-instate the history file again, but omit the restructuredtext header line: 208 | 209 | >>> with open('CHANGES.txt', 'w') as f: 210 | ... _ = f.write('1.0 (1972-12-25)\n\n- hello\n') 211 | >>> commit_all_changes() 212 | >>> utils.test_answer_book.set_answers(['', '', '1.3', '']) 213 | >>> prerelease.main() 214 | Question: ... 215 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 216 | >>> postrelease.main() 217 | Current ... 218 | 219 | No errors are raised and an ``----`` underline is assumed for the new header. 220 | The old one is left untouched: 221 | 222 | >>> with open('CHANGES.txt') as f: 223 | ... print(f.read()) 224 | 1.4 (unreleased) 225 | ---------------- 226 | 227 | - Nothing changed yet. 228 | 229 | 230 | 1.0 (1972-12-25) 231 | 232 | - hello 233 | >>> githead('setup.py') 234 | from setuptools import setup, find_packages 235 | version = '1.4.dev0' 236 | 237 | Let's try some options by simply calling postrelease a few times without calling prerelease or release. 238 | First prepare a bugfix release. 239 | 240 | >>> utils.test_answer_book.set_answers(['1.5.1', '', 'n']) 241 | >>> postrelease.main() 242 | Current version is 1.4 243 | Question: Enter new development version ('.dev0' will be appended) [1.5]: 244 | Our reply: 1.5.1 245 | Checking data dict 246 | Question: OK to commit this (Y/n)? 247 | Our reply: 248 | Question: OK to push commits to the server? (Y/n)? 249 | Our reply: n 250 | >>> githead('setup.py') 251 | from setuptools import setup, find_packages 252 | version = '1.5.1.dev0' 253 | 254 | Now say that the next version should be a feature release: 255 | 256 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 257 | >>> import sys 258 | >>> sys.argv[1:] = ['--feature'] 259 | >>> postrelease.main() 260 | Current version is 1.5.1 261 | Question: Enter new development version ('.dev0' will be appended) [1.6.0]: 262 | Our reply: 263 | Checking data dict 264 | Question: OK to commit this (Y/n)? 265 | Our reply: 266 | Question: OK to push commits to the server? (Y/n)? 267 | Our reply: n 268 | >>> githead('setup.py') 269 | from setuptools import setup, find_packages 270 | version = '1.6.0.dev0' 271 | 272 | Now say that the next version should be a breaking release, but make it an alpha: 273 | 274 | >>> utils.test_answer_book.set_answers(['2.0.0a1', '', 'n']) 275 | >>> sys.argv[1:] = ['--breaking'] 276 | >>> postrelease.main() 277 | Current version is 1.6.0 278 | Question: Enter new development version ('.dev0' will be appended) [2.0.0]: 279 | Our reply: 2.0.0a1 280 | Checking data dict 281 | Question: OK to commit this (Y/n)? 282 | Our reply: 283 | Question: OK to push commits to the server? (Y/n)? 284 | Our reply: n 285 | >>> githead('setup.py') 286 | from setuptools import setup, find_packages 287 | version = '2.0.0a1.dev0' 288 | 289 | Now say that the next version is a final release. 290 | 291 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 292 | >>> sys.argv[1:] = ['--final'] 293 | >>> postrelease.main() 294 | Current version is 2.0.0a1 295 | Question: Enter new development version ('.dev0' will be appended) [2.0.0]: 296 | Our reply: 297 | Checking data dict 298 | Question: OK to commit this (Y/n)? 299 | Our reply: 300 | Question: OK to push commits to the server? (Y/n)? 301 | Our reply: n 302 | >>> githead('setup.py') 303 | from setuptools import setup, find_packages 304 | version = '2.0.0.dev0' 305 | -------------------------------------------------------------------------------- /zest/releaser/tests/preparedocs.txt: -------------------------------------------------------------------------------- 1 | Documentation utility functions 2 | =============================== 3 | 4 | We're testing ``preparedocs.py`` here: 5 | 6 | >>> from zest.releaser import preparedocs 7 | 8 | 9 | Entry-point-documentation-generation entry point 10 | ------------------------------------------------ 11 | 12 | The entry point for generating documentation does not run when the project 13 | name isn't zest.releaser. Otherwise we would generate our documentation every 14 | time we used zest.releaser... 15 | 16 | >>> data = {'name': 'vanrees.worlddomination'} 17 | >>> preparedocs.prepare_entrypoint_documentation(data) is None 18 | True 19 | 20 | Prepare a mock documentation file: 21 | 22 | >>> import os 23 | >>> import shutil 24 | >>> import tempfile 25 | >>> tempdir = tempfile.mkdtemp() 26 | >>> mock_docdir = os.path.join(tempdir, 'doc', 'source') 27 | >>> os.makedirs(mock_docdir) 28 | >>> docfile = os.path.join(mock_docdir, 'entrypoints.rst') 29 | >>> with open(docfile, 'w') as f: 30 | ... _ = f.write(""" 31 | ... line1 32 | ... line2 33 | ... .. ### AUTOGENERATED FROM HERE ### 34 | ... line3 35 | ... """) 36 | 37 | When the name *is* zest.releaser, we generate documentation. 38 | 39 | >>> data = {'name': 'zest.releaser', 40 | ... 'reporoot': tempdir} 41 | >>> preparedocs.prepare_entrypoint_documentation(data) 42 | Wrote entry point documentation to ...doc/source/entrypoints.rst 43 | 44 | The lines above the marker interface are still intact, the line below it has 45 | been replaced by the generated documentation: 46 | 47 | >>> with open(docfile) as f: 48 | ... print(f.read()) 49 | 50 | line1 51 | line2 52 | .. ### AUTOGENERATED FROM HERE ### 53 | 54 | Common data dict items 55 | ---------------------- 56 | 57 | These items are shared among all commands. 58 | 59 | commit_msg 60 | Message template used when committing 61 | ... 62 | ``prerelease`` data dict items 63 | ------------------------------ 64 | ... 65 | 66 | Clean up 67 | 68 | >>> shutil.rmtree(tempdir) 69 | -------------------------------------------------------------------------------- /zest/releaser/tests/prerelease.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of prerelease.py 2 | =============================== 3 | 4 | Several items are prepared for us. 5 | 6 | A git checkout of a project: 7 | 8 | >>> gitsourcedir 9 | 'TESTTEMP/tha.example-git' 10 | >>> import os 11 | >>> os.chdir(gitsourcedir) 12 | 13 | The version is at 0.1.dev0: 14 | 15 | >>> githead('setup.py') 16 | from setuptools import setup, find_packages 17 | version = '0.1.dev0' 18 | 19 | Asking input on the prompt is not unittestable unless we use the prepared 20 | testing hack in utils.py: 21 | 22 | >>> from zest.releaser import utils 23 | >>> utils.TESTMODE = True 24 | 25 | Run the prerelease script: 26 | 27 | >>> utils.test_answer_book.set_answers(['', '', '', '', '']) 28 | >>> from zest.releaser import prerelease 29 | >>> prerelease.main() 30 | Question... 31 | Question: Enter version [0.1]: 32 | Our reply: 33 | Checking data dict 34 | Question: OK to commit this (Y/n)? 35 | Our reply: 36 | 37 | The changelog and setup.py are at 0.1 and indicate a release date: 38 | 39 | >>> githead('CHANGES.txt') 40 | Changelog of tha.example 41 | ======================== 42 | 43 | 0.1 (...-...-...) 44 | ---------------- 45 | >>> githead('setup.py') 46 | from setuptools import setup, find_packages 47 | version = '0.1' 48 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypi.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of pypi.py 2 | ========================= 3 | 4 | Note on test setup: we don't use the "big" setup/teardown methods here. 5 | 6 | >>> from zest.releaser import pypi 7 | >>> from zest.releaser.utils import filename_from_test_dir 8 | 9 | 10 | Parsing the configuration file 11 | ------------------------------ 12 | 13 | For pypi uploads and uploads to multiple servers, a configuration file 14 | needs to be available: 15 | 16 | >>> pypi.DIST_CONFIG_FILE 17 | '.pypirc' 18 | 19 | This is the default. For testing purposes, you *can* pass in a config file's 20 | name yourself. We'll do that in the rest of these tests. 21 | 22 | A missing file doesn't lead to an error: 23 | 24 | >>> pypiconfig = pypi.PypiConfig(config_filename='non/existing') 25 | >>> pypiconfig.config is None 26 | True 27 | 28 | There are two styles of ``.pypirc`` files. The old one just for pypi: 29 | 30 | >>> pypirc_old = filename_from_test_dir('pypirc_old.txt') 31 | >>> with open(pypirc_old) as pypifile: 32 | ... print(pypifile.read()) 33 | [server-login] 34 | username:pipo_de_clown 35 | password:asjemenou 36 | >>> pypiconfig = pypi.PypiConfig(config_filename=pypirc_old) 37 | >>> pypiconfig.distutils_servers() 38 | ['pypi'] 39 | 40 | And the new format that allows multiple uploads: 41 | 42 | >>> pypirc_new = filename_from_test_dir('pypirc_new.txt') 43 | >>> with open(pypirc_new) as pypifile: 44 | ... print(pypifile.read()) 45 | [distutils] 46 | index-servers = 47 | pypi 48 | plone.org 49 | mycompany 50 | 51 | [pypi] 52 | username:user 53 | password:password 54 | 55 | [plone.org] 56 | repository:http://plone.org/products 57 | username:ploneuser 58 | password:password 59 | 60 | [mycompany] 61 | repository:http://my.company/products 62 | username:user 63 | password:password 64 | >>> pypiconfig = pypi.PypiConfig(config_filename=pypirc_new) 65 | >>> from pprint import pprint 66 | >>> pprint(sorted(pypiconfig.distutils_servers())) 67 | ['mycompany', 'plone.org', 'pypi'] 68 | 69 | A file with both is also possible. The old server-login section is 70 | used to contain the username and password that are shared among 71 | servers. Any servers that have no corresponding section are ignored: 72 | 73 | >>> pypirc_both = filename_from_test_dir('pypirc_both.txt') 74 | >>> with open(pypirc_both) as pypifile: 75 | ... print(pypifile.read()) 76 | [server-login] 77 | username:bdfl 78 | password:secret 79 | 80 | [distutils] 81 | index-servers = 82 | pypi 83 | local 84 | unknown 85 | 86 | [pypi] 87 | password:verysecret 88 | 89 | [local] 90 | repository = http://localhost:8080/test/products 91 | username = knight 92 | >>> pypiconfig = pypi.PypiConfig(config_filename=pypirc_both) 93 | >>> pprint(sorted(pypiconfig.distutils_servers())) 94 | ['local', 'pypi'] 95 | 96 | A simple file with just a pypi section is also possible: 97 | 98 | >>> pypirc_simple = filename_from_test_dir('pypirc_simple.txt') 99 | >>> with open(pypirc_simple) as pypifile: 100 | ... print(pypifile.read()) 101 | [pypi] 102 | username:bdfl 103 | password:secret 104 | >>> pypiconfig = pypi.PypiConfig(config_filename=pypirc_simple) 105 | >>> pprint(sorted(pypiconfig.distutils_servers())) 106 | ['pypi'] 107 | 108 | 109 | Asking for making a release or not 110 | ---------------------------------- 111 | 112 | Some people hardly ever want to make a full release of a package to 113 | pypi; a git tag may be enough. They can tell zest.releaser to 114 | use a different default answer when it asks to make a checkout for a 115 | release. This means you can usually just press Enter on all questions 116 | that zest.releaser asks. 117 | 118 | We try to read a [zest.releaser] section and look for a ``release`` 119 | option. We can ask for the result like this, which by default is True: 120 | 121 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_both) 122 | >>> zestreleaserconfig.want_release() 123 | True 124 | 125 | We have a pypirc for this: 126 | 127 | >>> pypirc_no_release = filename_from_test_dir('pypirc_no_release.txt') 128 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_no_release) 129 | >>> zestreleaserconfig.want_release() 130 | False 131 | 132 | We can also specify to always do that checkout during a release: 133 | 134 | >>> pypirc_yes_release = filename_from_test_dir('pypirc_yes_release.txt') 135 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_yes_release) 136 | >>> zestreleaserconfig.want_release() 137 | True 138 | 139 | 140 | Creating a wheel 141 | ---------------- 142 | 143 | When the ``wheel`` package is installed, we could create shiny new Python wheels, next 144 | to the standard old-style source distributions. In 2023 this seems good for most 145 | packages, so since version 8.0.0a2 this is by default true. Early 2025 (9.4.0) we 146 | started depending on the "wheel" package as everyone uses it. 147 | 148 | We try to read a [zest.releaser] section, in pypirc, setup.cfg or 149 | pyproject.toml and check for a ``create-wheel`` option. In this case we 150 | explicitly disable checking for the files in the package, because when running the 151 | tests with ``tox`` the current directory is the base directory of 152 | zest.releaser, which now contains a setup.cfg. 153 | 154 | We can ask for the result like this, which by default is True: 155 | 156 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_both, omit_package_config_in_test=True) 157 | >>> zestreleaserconfig.create_wheel() 158 | True 159 | 160 | We can also specify to not create the wheel, even when (universal) wheels could be created: 161 | 162 | >>> pypirc_no_create = filename_from_test_dir('pypirc_universal_nocreate.txt') 163 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_no_create, omit_package_config_in_test=True) 164 | >>> zestreleaserconfig.create_wheel() 165 | False 166 | 167 | Fixing setup.cfg 168 | ---------------- 169 | 170 | A setup.cfg file can be used to influence the release process. This 171 | may contain options that are not advisable in the released package but 172 | should only be used during development. We can clean that up. First 173 | we prepare a directory. 174 | 175 | >>> pypi.SETUP_CONFIG_FILE 176 | 'setup.cfg' 177 | >>> import os 178 | >>> import shutil 179 | >>> import tempfile 180 | >>> tempdir = tempfile.mkdtemp() 181 | >>> os.chdir(tempdir) 182 | 183 | Without a setup.cfg there is no config: 184 | 185 | >>> setup_config = pypi.SetupConfig() 186 | >>> setup_config.config is None 187 | True 188 | >>> setup_config.has_bad_commands() 189 | False 190 | >>> setup_config.fix_config() 191 | >>> os.path.exists(pypi.SETUP_CONFIG_FILE) 192 | False 193 | 194 | Now we add a setup.cfg with some good and some bad commands: 195 | 196 | >>> SETUP_CFG = """ 197 | ... [zest.releaser] 198 | ... release = no 199 | ... 200 | ... [egg_info] 201 | ... tag_build = dev 202 | ... tag_svn_revision = true""" 203 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 204 | ... _ = f.write(SETUP_CFG) 205 | >>> setup_config = pypi.SetupConfig() 206 | >>> setup_config.has_bad_commands() 207 | True 208 | 209 | Fixing the config also prints the new config: 210 | 211 | >>> setup_config.fix_config() 212 | [zest.releaser] 213 | release = no 214 | 215 | [egg_info] 216 | tag_build = 217 | tag_svn_revision = false 218 | >>> os.path.exists(pypi.SETUP_CONFIG_FILE) 219 | True 220 | >>> with open(pypi.SETUP_CONFIG_FILE) as pypifile: 221 | ... print(''.join(pypifile.readlines())) 222 | [zest.releaser] 223 | release = no 224 | 225 | [egg_info] 226 | tag_build = 227 | tag_svn_revision = false 228 | 229 | We try that again with this fixed up config file as input: 230 | 231 | >>> setup_config = pypi.SetupConfig() 232 | >>> setup_config.has_bad_commands() 233 | False 234 | >>> setup_config.fix_config() 235 | >>> os.path.exists(pypi.SETUP_CONFIG_FILE) 236 | True 237 | >>> with open(pypi.SETUP_CONFIG_FILE) as pypifile: 238 | ... print(''.join(pypifile.readlines())) 239 | [zest.releaser] 240 | release = no 241 | 242 | [egg_info] 243 | tag_build = 244 | tag_svn_revision = false 245 | 246 | Formatting release tags 247 | ---------------------- 248 | 249 | ``zest.releaser`` by default tags releases in the project version control. The 250 | format of the tag name can be customized by the ``tag-format`` setting. 251 | 252 | By default the tag format is just the version itself: 253 | 254 | >>> version = '1.2.3' 255 | >>> os.remove(pypi.SETUP_CONFIG_FILE) 256 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 257 | >>> zestreleaserconfig.tag_format(version) 258 | '1.2.3' 259 | 260 | But it can be customized with a format string, as long as it contains 261 | the string ``{version}``: 262 | 263 | >>> SETUP_CFG = """ 264 | ... [zest.releaser] 265 | ... tag-format = mytag-{version} 266 | ... """ 267 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 268 | ... _ = f.write(SETUP_CFG) 269 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 270 | >>> zestreleaserconfig.tag_format(version) 271 | 'mytag-1.2.3' 272 | 273 | Or, for backward compatibility, a Python % interpolation format: 274 | 275 | >>> SETUP_CFG = """ 276 | ... [zest.releaser] 277 | ... tag-format = yourtag-%(version)s 278 | ... """ 279 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 280 | ... _ = f.write(SETUP_CFG) 281 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 282 | >>> zestreleaserconfig.tag_format(version) 283 | `tag-format` contains deprecated `%(version)s` format. Please change to: 284 | 285 | [zest.releaser] 286 | tag-format = yourtag-{version} 287 | 'yourtag-1.2.3' 288 | 289 | 290 | Notice, however, that a ``setup.cfg`` like the one above doesn't work for 291 | distutils, as ``ConfigParser`` tries to recursively interpolate all 292 | ``%(values)s`` until there are none left. Which is why we emit a warning. 293 | 294 | 295 | Formatting release tag messages 296 | ----------------------------------- 297 | 298 | The commit message to be used when tagging can be customized by the 299 | ``tag-message`` setting. 300 | 301 | By default the tag message is ``Tagging`` plus the version: 302 | 303 | >>> version = '1.2.3' 304 | >>> os.remove(pypi.SETUP_CONFIG_FILE) 305 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 306 | >>> zestreleaserconfig.tag_message(version) 307 | 'Tagging 1.2.3' 308 | 309 | But it can be customized with a message string, as long as it contains 310 | the string ``{version}``: 311 | 312 | >>> SETUP_CFG = """ 313 | ... [zest.releaser] 314 | ... tag-message = Creating v{version} tag. 315 | ... """ 316 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 317 | ... _ = f.write(SETUP_CFG) 318 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 319 | >>> zestreleaserconfig.tag_message(version) 320 | 'Creating v1.2.3 tag.' 321 | 322 | 323 | No-input mode 324 | ------------- 325 | 326 | In some cases you want no questions asked. Zest.releaser should just do its 327 | job without asking for versions or confirmations. You can enable this 328 | behaviour with a ``--no-input`` commandline option, but also by adding 329 | ``no-input = yes`` to the ``[zest.releaser]`` section in ``.pypirc`` or 330 | ``setup.cfg``. 331 | 332 | The default is False: 333 | 334 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_yes_release) 335 | >>> zestreleaserconfig.no_input() 336 | False 337 | 338 | Enable the option in ``.pypirc``: 339 | 340 | >>> pypirc_no_input = filename_from_test_dir('pypirc_no_input.txt') 341 | >>> zestreleaserconfig = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_no_input) 342 | 343 | Now the option should be set to True: 344 | 345 | >>> zestreleaserconfig.no_input() 346 | True 347 | 348 | Let's enable the option also in setup.cfg: 349 | 350 | >>> SETUP_CFG = """ 351 | ... [zest.releaser] 352 | ... no-input = yes 353 | ... """ 354 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 355 | ... _ = f.write(SETUP_CFG) 356 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 357 | 358 | The option should be set to True here as well: 359 | 360 | >>> zestreleaserconfig.no_input() 361 | True 362 | 363 | 364 | Python version file pointer 365 | --------------------------- 366 | 367 | In some cases you want to point at a Python file with a ``__version__`` marker 368 | in it. For that, there's the ``python-file-with-version`` option. 369 | 370 | The default is None: 371 | 372 | >>> zestreleaserconfig.python_file_with_version() 373 | 374 | Enable the option: 375 | 376 | >>> SETUP_CFG = """ 377 | ... [zest.releaser] 378 | ... python-file-with-version = reinout/maurits.py 379 | ... """ 380 | >>> with open(pypi.SETUP_CONFIG_FILE, 'w') as f: 381 | ... _ = f.write(SETUP_CFG) 382 | >>> zestreleaserconfig = pypi.ZestReleaserConfig() 383 | >>> zestreleaserconfig.python_file_with_version() 384 | 'reinout/maurits.py' 385 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | username:user 7 | password:password 8 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_both.txt: -------------------------------------------------------------------------------- 1 | [server-login] 2 | username:bdfl 3 | password:secret 4 | 5 | [distutils] 6 | index-servers = 7 | pypi 8 | local 9 | unknown 10 | 11 | [pypi] 12 | password:verysecret 13 | 14 | [local] 15 | repository = http://localhost:8080/test/products 16 | username = knight 17 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_new.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | plone.org 5 | mycompany 6 | 7 | [pypi] 8 | username:user 9 | password:password 10 | 11 | [plone.org] 12 | repository:http://plone.org/products 13 | username:ploneuser 14 | password:password 15 | 16 | [mycompany] 17 | repository:http://my.company/products 18 | username:user 19 | password:password 20 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_no_input.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | username:user 7 | password:password 8 | 9 | [zest.releaser] 10 | no-input = yes 11 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_no_release.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | username:user 7 | password:password 8 | 9 | [zest.releaser] 10 | release = no 11 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_old.txt: -------------------------------------------------------------------------------- 1 | [server-login] 2 | username:pipo_de_clown 3 | password:asjemenou 4 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_simple.txt: -------------------------------------------------------------------------------- 1 | [pypi] 2 | username:bdfl 3 | password:secret 4 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_universal_nocreate.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | username:user 7 | password:password 8 | 9 | [zest.releaser] 10 | release = yes 11 | create-wheel = no 12 | 13 | [bdist_wheel] 14 | universal = 1 15 | -------------------------------------------------------------------------------- /zest/releaser/tests/pypirc_yes_release.txt: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypi 4 | 5 | [pypi] 6 | username:user 7 | password:password 8 | 9 | [zest.releaser] 10 | release = yes 11 | create-wheel = yes 12 | -------------------------------------------------------------------------------- /zest/releaser/tests/pyproject-toml.txt: -------------------------------------------------------------------------------- 1 | Integration test 2 | ================ 3 | 4 | Now try a project with only a ``pyproject.toml`` and no ``setup.cfg`` or ``setup.py``. 5 | 6 | Several items are prepared for us. 7 | 8 | A git directory (repository and checkout in one): 9 | 10 | >>> gitsourcedir 11 | 'TESTTEMP/tha.example-git' 12 | >>> import os 13 | >>> os.chdir(gitsourcedir) 14 | 15 | We remove and add files. 16 | 17 | >>> from zest.releaser import tests 18 | >>> from zest.releaser.utils import execute_command 19 | >>> import shutil 20 | >>> _ = execute_command(["git", "rm", "setup.cfg", "setup.py"]) 21 | >>> pyproject_file = os.path.join(os.path.dirname(tests.__file__), "pyproject.toml") 22 | >>> shutil.copy(pyproject_file, os.path.curdir) 23 | './pyproject.toml' 24 | >>> _ = execute_command(["git", "add", "pyproject.toml"]) 25 | >>> _ = execute_command(["git", "commit", "-m", "Move to pyproject.toml"]) 26 | >>> print(execute_command(["git", "status"])) 27 | On branch main 28 | nothing to commit, working directory clean 29 | 30 | The version is at 0.1.dev0: 31 | 32 | >>> githead('pyproject.toml') 33 | [project] 34 | name = "tha.example" 35 | version = "0.1.dev0" 36 | description = "Example package" 37 | keywords = ["example"] 38 | 39 | Asking input on the prompt is not unittestable unless we use the prepared 40 | testing hack in utils.py: 41 | 42 | >>> from zest.releaser import utils 43 | >>> utils.TESTMODE = True 44 | 45 | Run the prerelease script: 46 | 47 | >>> from zest.releaser import prerelease 48 | >>> utils.test_answer_book.set_answers(['', '', '', '', '']) 49 | >>> prerelease.main() 50 | Question... 51 | Question: Enter version [0.1]: 52 | Our reply: 53 | Checking data dict 54 | Question: OK to commit this (Y/n)? 55 | Our reply: 56 | 57 | The changelog now has a release date instead of ``(unreleased)``: 58 | 59 | >>> githead('CHANGES.txt') 60 | Changelog of tha.example 61 | ======================== 62 | 63 | 0.1 (2008-12-20) 64 | ---------------- 65 | 66 | And the version number is just 0.1 and has lost its dev marker: 67 | 68 | >>> githead('pyproject.toml') 69 | [project] 70 | name = "tha.example" 71 | version = "0.1" 72 | description = "Example package" 73 | keywords = ["example"] 74 | 75 | The release script tags the release and uploads it. Note that in the 76 | other function tests we call 77 | ``mock_pypi_available.append('tha.example')``, but we do not do this 78 | here. This way we can check what happens when a package is not yet 79 | known on PyPI: 80 | 81 | >>> utils.test_answer_book.set_answers(['y', 'y', 'y', 'yes']) 82 | >>> from zest.releaser import release 83 | >>> release.main() 84 | Checking data dict 85 | Tag needed to proceed, you can use the following command: 86 | git tag 0.1 -m 'Tagging 0.1' 87 | Question: Run this command (Y/n)? 88 | Our reply: y 89 | 90 | Question: Check out the tag 91 | (for tweaks or pypi/distutils server upload) (Y/n)? 92 | Our reply: y 93 | RED Note: ...0.1... 94 | ... 95 | Preparing release 0.1 96 | ... 97 | running sdist 98 | ... 99 | running bdist_wheel 100 | ... 101 | Question: Upload to pypi (y/N)? 102 | Our reply: yes 103 | MOCK twine dispatch upload ...whl ...example-0.1.tar.gz 104 | 105 | (Note: depending in the Python/setuptools version, the filename may contain ``tha.example`` or ``tha_example``.) 106 | 107 | There is now a tag: 108 | 109 | >>> print(execute_command(["git", "tag"])) 110 | 0.1 111 | 112 | And the postrelease script ups the version: 113 | 114 | >>> utils.test_answer_book.set_answers(['', '', 'n']) 115 | >>> from zest.releaser import postrelease 116 | >>> postrelease.main() 117 | Current version is 0.1 118 | Question: Enter new development version ('.dev0' will be appended) [0.2]: 119 | Our reply: 120 | Checking data dict 121 | Question: OK to commit this (Y/n)? 122 | Our reply: 123 | Question: OK to push commits to the server? (Y/n)? 124 | Our reply: n 125 | 126 | The changelog and pyproject.toml are at 0.2 and indicate dev mode: 127 | 128 | >>> githead('CHANGES.txt') 129 | Changelog of tha.example 130 | ======================== 131 | 132 | 0.2 (unreleased) 133 | ---------------- 134 | >>> githead('pyproject.toml') 135 | [project] 136 | name = "tha.example" 137 | version = "0.2.dev0" 138 | description = "Example package" 139 | keywords = ["example"] 140 | 141 | And there are no uncommitted changes: 142 | 143 | >>> print(execute_command(["git", "status"])) 144 | On branch main 145 | nothing to commit, working directory clean 146 | -------------------------------------------------------------------------------- /zest/releaser/tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tha.example" 3 | version = "0.1.dev0" 4 | description = "Example package" 5 | keywords = ["example"] 6 | classifiers = [ 7 | "Development Status :: 1 - Planning", 8 | "License :: OSI Approved :: GNU General Public License (GPL)", 9 | "Programming Language :: Python :: 3.9", 10 | ] 11 | requires-python = ">=3.6" 12 | 13 | [build-system] 14 | requires = ["setuptools>=61", "wheel"] 15 | build-backend = "setuptools.build_meta" 16 | -------------------------------------------------------------------------------- /zest/releaser/tests/release.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of release.py 2 | ============================ 3 | 4 | Some initial imports: 5 | 6 | >>> from zest.releaser import release 7 | >>> from zest.releaser import utils 8 | >>> import os 9 | >>> utils.TESTMODE = True 10 | 11 | 12 | Check availability on pypi 13 | -------------------------- 14 | 15 | "Testing" means "don't really poll pypi", so the test setup does some 16 | monkeypatching for us: 17 | 18 | >>> from urllib import request 19 | >>> request.urlopen 20 | 21 | 22 | There's a mock list of packages that our mock pypi provides: 23 | 24 | >>> mock_pypi_available 25 | [] 26 | 27 | Search a non-existing package: 28 | 29 | >>> release.package_in_pypi('zest.releaser') 30 | False 31 | 32 | Now search for an "existing" package: 33 | 34 | >>> mock_pypi_available.append('zest.releaser') 35 | >>> release.package_in_pypi('zest.releaser') 36 | True 37 | 38 | 39 | Version grabbing 40 | ---------------- 41 | 42 | >>> os.chdir(gitsourcedir) 43 | >>> releaser = release.Releaser() 44 | 45 | Grab the version: 46 | 47 | >>> releaser._grab_version() 48 | >>> releaser.data['version'] 49 | '0.1.dev0' 50 | 51 | If, by some weird twist of fate, there's no release: we exit. 52 | 53 | >>> releaser.vcs.get_setup_py_version = lambda: None 54 | >>> releaser._grab_version() 55 | Traceback (most recent call last): 56 | ... 57 | RuntimeError: SYSTEM EXIT (code=1) 58 | 59 | 60 | Check tag existence 61 | ------------------- 62 | 63 | We automatically check if a tag already exists. First set the version to 0.1: 64 | 65 | >>> releaser = release.Releaser() 66 | >>> releaser.vcs.version = '0.1' 67 | >>> releaser.prepare() 68 | >>> releaser.data['tag_already_exists'] 69 | False 70 | 71 | If the tag doesn't exist yet, no safety question is asked: 72 | 73 | >>> releaser._info_if_tag_already_exists() 74 | 75 | Mock that the tag exists and we get a question: 76 | 77 | >>> releaser.data['tag_already_exists'] = True 78 | >>> utils.test_answer_book.set_answers(['n']) 79 | >>> utils.AUTO_RESPONSE 80 | False 81 | >>> releaser._info_if_tag_already_exists() 82 | Question: There is already a tag 0.1, show if there are differences? (Y/n)? 83 | Our reply: n 84 | >>> utils.test_answer_book.set_answers(['y']) 85 | >>> releaser._info_if_tag_already_exists() 86 | Traceback (most recent call last): 87 | ...diff_command... 88 | RuntimeError: SYSTEM EXIT (code=1) 89 | 90 | Note: the diff itself fails as we mocked its existence. 91 | 92 | 93 | Making tags 94 | ----------- 95 | 96 | If the tag doesn't exist yet, we can make one. The actual tag creation is 97 | tested already, here we test that you get a sys.exit if you refuse to run the 98 | tag command: 99 | 100 | >>> releaser = release.Releaser() 101 | >>> releaser.data['tag_already_exists'] = False 102 | >>> releaser.data['version'] = '0.1' 103 | >>> releaser.data['tag'] = '0.1' 104 | >>> releaser.data['tag-message'] = 'Tagging {version}' 105 | >>> releaser.data['tag-signing'] = False 106 | >>> utils.test_answer_book.set_answers(['n']) 107 | >>> releaser._make_tag() 108 | Traceback (most recent call last): 109 | ... 110 | RuntimeError: SYSTEM EXIT (code=1) 111 | 112 | If the the tag already exists, we just return without doing anything. 113 | 114 | >>> releaser.data['tag_already_exists'] = True 115 | >>> releaser._make_tag() 116 | -------------------------------------------------------------------------------- /zest/releaser/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | from .functional import setup 2 | from .functional import teardown 3 | from colorama import Fore 4 | from zope.testing import renormalizing 5 | 6 | import doctest 7 | import os 8 | import re 9 | import tempfile 10 | import twine.cli 11 | import unittest 12 | 13 | 14 | def mock_dispatch(*args): 15 | print("MOCK twine dispatch {}".format(" ".join(*args))) 16 | return True 17 | 18 | 19 | print("Mocking twine.cli.dispatch...") 20 | twine.cli.dispatch = mock_dispatch 21 | 22 | checker = renormalizing.RENormalizing( 23 | [ 24 | # Date formatting 25 | (re.compile(r"\d{4}-\d{2}-\d{2}"), "1972-12-25"), 26 | # Git diff hash formatting 27 | ( 28 | re.compile(r"[0-9a-f]{7}\.\.[0-9a-f]{7} [0-9a-f]{6}"), 29 | "1234567..890abcd ef0123", 30 | ), 31 | # .pypirc seems to be case insensitive 32 | (re.compile(r"[Pp][Yy][Pp][Ii]"), "pypi"), 33 | # Normalize tempdirs. For this to work reliably, we need to use a prefix 34 | # in all tempfile.mkdtemp() calls. 35 | ( 36 | re.compile(r"/private%s/testtemp[^/]+" % re.escape(tempfile.gettempdir())), 37 | "TESTTEMP", 38 | ), # OSX madness 39 | ( 40 | re.compile(r"%s/testtemp[^/]+" % re.escape(tempfile.gettempdir())), 41 | "TESTTEMP", 42 | ), 43 | (re.compile(re.escape(tempfile.gettempdir())), "TMPDIR"), 44 | # Change in git 2.9.1: 45 | ( 46 | re.compile(r"nothing to commit, working directory clean"), 47 | "nothing to commit, working tree clean", 48 | ), 49 | # We should ignore coloring by colorama. Or actually print it 50 | # clearly. This catches Fore.RED, Fore.MAGENTA and Fore.RESET. 51 | (re.compile(re.escape(Fore.RED)), "RED "), 52 | (re.compile(re.escape(Fore.MAGENTA)), "MAGENTA "), 53 | (re.compile(re.escape(Fore.RESET)), "RESET "), 54 | ] 55 | ) 56 | 57 | 58 | def test_suite(): 59 | """Find .txt files and test code examples in them.""" 60 | suite = unittest.TestSuite() 61 | 62 | # These are simple tests without setup. 63 | simple = [ 64 | "preparedocs.txt", 65 | "pypi.txt", 66 | "utils.txt", 67 | ] 68 | suite.addTests( 69 | doctest.DocFileSuite( 70 | *simple, 71 | checker=checker, 72 | optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, 73 | ) 74 | ) 75 | 76 | # Now for more involved tests with setup and teardown 77 | doctests = [] 78 | tests_path = os.path.dirname(__file__) 79 | for filename in sorted(os.listdir(tests_path)): 80 | if not filename.endswith(".txt"): 81 | continue 82 | if filename in simple: 83 | continue 84 | if filename.startswith("pypirc_"): 85 | # Sample pypirc file 86 | continue 87 | doctests.append(filename) 88 | 89 | suite.addTests( 90 | doctest.DocFileSuite( 91 | *doctests, 92 | setUp=setup, 93 | tearDown=teardown, 94 | checker=checker, 95 | optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, 96 | ) 97 | ) 98 | 99 | return suite 100 | -------------------------------------------------------------------------------- /zest/releaser/tests/vcs.txt: -------------------------------------------------------------------------------- 1 | Detailed tests of vcs.py 2 | ======================== 3 | 4 | Some initial imports and utility functions: 5 | 6 | >>> from zest.releaser import vcs 7 | >>> import os 8 | >>> def writeto(filename, contents): 9 | ... with open(filename, 'w') as f: 10 | ... _ = f.write(contents) 11 | 12 | BaseVersionControl is the base class for subversion/mercurial/git support. It 13 | handles some base cases and has a bunch of NotImplementedError methods. 14 | 15 | When started, it stores the current working directory: 16 | 17 | >>> gitsourcedir 18 | 'TESTTEMP/tha.example-git' 19 | >>> os.chdir(gitsourcedir) 20 | >>> checkout = vcs.BaseVersionControl() 21 | >>> checkout.workingdir 22 | 'TESTTEMP/tha.example-git' 23 | 24 | 25 | Methods that must be implemented in subclasses 26 | ---------------------------------------------- 27 | 28 | >>> checkout.name() 29 | Traceback (most recent call last): 30 | ... 31 | NotImplementedError 32 | >>> checkout.available_tags() 33 | Traceback (most recent call last): 34 | ... 35 | NotImplementedError 36 | >>> checkout.prepare_checkout_dir('prefix') 37 | Traceback (most recent call last): 38 | ... 39 | NotImplementedError 40 | >>> checkout.tag_url('arg') 41 | Traceback (most recent call last): 42 | ... 43 | NotImplementedError 44 | >>> checkout.cmd_diff() 45 | Traceback (most recent call last): 46 | ... 47 | NotImplementedError 48 | >>> checkout.cmd_commit('arg') 49 | Traceback (most recent call last): 50 | ... 51 | NotImplementedError 52 | >>> checkout.cmd_diff_last_commit_against_tag('arg') 53 | Traceback (most recent call last): 54 | ... 55 | NotImplementedError 56 | >>> checkout.cmd_create_tag('arg', 'message') 57 | Traceback (most recent call last): 58 | ... 59 | NotImplementedError 60 | 61 | 62 | Tag handling 63 | ------------ 64 | 65 | Extraction of tags is handled by the subclasses. The one thing that the 66 | baseclass does is to check if a tag has already been made earlier: 67 | 68 | >>> def mock_available_tags(): 69 | ... return ['0.1', '0.2'] 70 | >>> checkout.available_tags = mock_available_tags 71 | >>> checkout.tag_exists('1.0') 72 | False 73 | >>> checkout.tag_exists('0.2') 74 | True 75 | 76 | 77 | Version handling 78 | ---------------- 79 | 80 | Create a project with a ``version.txt`` in it like often found in old zope 81 | products: 82 | 83 | >>> versiontxtproject = os.path.join(tempdir, 'vp') 84 | >>> os.mkdir(versiontxtproject) 85 | >>> os.chdir(versiontxtproject) 86 | >>> writeto('version.txt', '1.0 dev') 87 | 88 | We need some version control marker (like an ``.svn`` dir): 89 | 90 | >>> os.mkdir('.marker') 91 | >>> checkout = vcs.BaseVersionControl() 92 | >>> checkout.internal_filename = '.marker' 93 | 94 | Open the project with the BaseVersionControl and it finds the version file and 95 | returns the cleaned-up version: 96 | 97 | >>> checkout.get_version_txt_version() 98 | '1.0dev' 99 | 100 | Setting the version works also with version.txt files (the setup.py scenario 101 | is tested in the other tests). 102 | 103 | >>> checkout.version = '1.1' 104 | >>> with open('version.txt') as f: 105 | ... print(f.read()) 106 | 1.1 107 | 108 | In old Plone products, the setup.py often reads the version.txt and uses that 109 | as the version value. In those cases, the version must be written to the 110 | version.txt and the setup.py must be left unmolested. 111 | 112 | >>> lines = [ 113 | ... "from setuptools import setup", 114 | ... "with open('version.txt') as f:", 115 | ... " version = f.read().strip()", 116 | ... "setup(name='urgh', version=version)"] 117 | >>> writeto('setup.py', '\n'.join(lines)) 118 | >>> checkout.version = '1.2' 119 | >>> with open('version.txt') as f: 120 | ... print(f.read()) 121 | 1.2 122 | >>> with open('setup.py') as f: 123 | ... print(f.read()) 124 | from setuptools import setup 125 | with open('version.txt') as f: 126 | version = f.read().strip() 127 | setup(name='urgh', version=version) 128 | 129 | If the setup.py version is different, this takes precedence and the 130 | version.txt is the one left as-is. 131 | 132 | >>> lines = [ 133 | ... "from setuptools import setup", 134 | ... "setup(name='urgh',", 135 | ... " version='1.3',", 136 | ... " )"] 137 | >>> writeto('setup.py', '\n'.join(lines)) 138 | >>> checkout.version = '1.4' 139 | >>> with open('version.txt') as f: 140 | ... print(f.read()) # Still at 1.2 141 | 1.2 142 | >>> with open('setup.py') as f: 143 | ... print(f.read()) # Modified into 1.4 144 | from setuptools import setup 145 | setup(name='urgh', 146 | version='1.4', 147 | ) 148 | 149 | The version writing breaks down if there's more than just a "version=" on that 150 | line. The 99.9% case works, though. 151 | 152 | Another option is a ``__version__`` marker in a Python file. We cannot 153 | reliably figure out the right Python file to look in, so this file needs to be 154 | specified explicitly. As `PEP 396 `_ 155 | specifies, the ``__version__`` attribute and the ``setup.py`` version need to 156 | be derived one from the other, so zest.releaser only looks at one version 157 | source. If a ``__version__`` marker file is specified, that's where we'll look 158 | and otherwise not. 159 | 160 | Create a Python file with ``__version__`` without configuring it. This won't 161 | change a thing. We test with a fresh checkout: 162 | 163 | >>> checkout = vcs.BaseVersionControl() 164 | >>> checkout.internal_filename = '.marker' 165 | >>> lines = [ 166 | ... "import something", 167 | ... "__version__ = '2.0'", 168 | ... "print('something.else')"] 169 | >>> writeto('some_file.py', '\n'.join(lines)) 170 | >>> checkout = vcs.BaseVersionControl() 171 | >>> checkout.internal_filename = '.marker' 172 | >>> checkout.version 173 | '1.4' 174 | 175 | Add a ``setup.cfg`` with a pointer at the Python file and its version gets 176 | picked up. We need a fresh checkout to pick up the change: 177 | 178 | >>> lines = [ 179 | ... "[zest.releaser]", 180 | ... "python-file-with-version = some_file.py"] 181 | >>> writeto('setup.cfg', '\n'.join(lines)) 182 | >>> checkout = vcs.BaseVersionControl() 183 | >>> checkout.internal_filename = '.marker' 184 | >>> checkout.get_python_file_version() 185 | '2.0' 186 | >>> checkout.version 187 | '2.0' 188 | 189 | Setting it sets it in the correct Python file: 190 | 191 | >>> checkout.version = '2.1' 192 | >>> with open('some_file.py') as f: 193 | ... print(f.read()) 194 | import something 195 | __version__ = '2.1' 196 | print('something.else') 197 | 198 | the Python file with double quotes: 199 | 200 | >>> lines = [ 201 | ... "import something", 202 | ... '__version__ = "2.0"', 203 | ... "print('something.else')"] 204 | >>> writeto('some_file.py', '\n'.join(lines)) 205 | 206 | The version line should have retained its double quotes, otherwise black would 207 | have to change it back again: 208 | 209 | >>> checkout.version = '2.1' 210 | >>> with open('some_file.py') as f: 211 | ... print(f.read()) 212 | import something 213 | __version__ = "2.1" 214 | print('something.else') 215 | 216 | 217 | Version corner cases 218 | -------------------- 219 | 220 | Version files can also be called ``VERSION`` instead of ``version.txt`` (or 221 | ``VERSION.TXT``): 222 | 223 | >>> versiontxtproject = os.path.join(tempdir, 'vp2') 224 | >>> os.mkdir(versiontxtproject) 225 | >>> os.chdir(versiontxtproject) 226 | >>> writeto('VERSION', '1.0 dev') 227 | 228 | We need some version control marker (like an ``.svn`` dir): 229 | 230 | >>> os.mkdir('.marker') 231 | >>> checkout = vcs.BaseVersionControl() 232 | >>> checkout.internal_filename = '.marker' 233 | 234 | Open the project with the BaseVersionControl and it finds the version file and 235 | returns the cleaned-up version: 236 | 237 | >>> checkout.get_version_txt_version() 238 | '1.0dev' 239 | 240 | Setting the version works also with ``VERSION`` files: 241 | 242 | >>> checkout.version = '1.1' 243 | >>> with open('VERSION') as f: 244 | ... print(f.read()) 245 | 1.1 246 | 247 | Same with a ``VERSION.txt``: 248 | 249 | >>> os.remove('VERSION') 250 | >>> writeto('VERSION.txt', '22.3') 251 | >>> checkout.get_version_txt_version() 252 | '22.3' 253 | >>> checkout.version = '22.4' 254 | >>> with open('VERSION.txt') as f: 255 | ... print(f.read()) 256 | 22.4 257 | 258 | Version files can also be called ``version.rst`` (or .md or so) instead of ``version.txt``: 259 | 260 | >>> os.remove('VERSION.txt') 261 | >>> writeto('version.rst', '25.0') 262 | >>> checkout.get_version_txt_version() 263 | '25.0' 264 | 265 | Setup.py with an uppercase VERSION: 266 | 267 | >>> uppercaseversionproject = os.path.join(tempdir, 'uvp') 268 | >>> os.mkdir(uppercaseversionproject) 269 | >>> os.chdir(uppercaseversionproject) 270 | >>> lines = [ 271 | ... "from setuptools import setup", 272 | ... "VERSION = '1.0'", 273 | ... "setup(name='urgh', version=VERSION)"] 274 | >>> writeto('setup.py', '\n'.join(lines)) 275 | >>> checkout.version = '1.2' 276 | >>> with open('setup.py') as f: 277 | ... print(f.read()) 278 | from setuptools import setup 279 | VERSION = '1.2' 280 | setup(name='urgh', version=VERSION) 281 | 282 | version in setup.cfg: 283 | 284 | >>> setupcfgproject = os.path.join(tempdir, 'setupcfg') 285 | >>> os.mkdir(setupcfgproject) 286 | >>> os.chdir(setupcfgproject) 287 | >>> lines = [ 288 | ... "[metadata]", 289 | ... "version = 1.0"] 290 | >>> writeto('setup.cfg', '\n'.join(lines)) 291 | >>> writeto('setup.py', '') 292 | >>> checkout.version = '1.2' 293 | >>> with open('setup.cfg') as f: 294 | ... print(f.read()) 295 | [metadata] 296 | version = 1.2 297 | 298 | Setup.py with a double-quoted version, as prefered by the 'black' code 299 | formatter: 300 | 301 | >>> doublequotedversionproject = os.path.join(tempdir, 'dqv') 302 | >>> os.mkdir(doublequotedversionproject) 303 | >>> os.chdir(doublequotedversionproject) 304 | >>> lines = [ 305 | ... "from setuptools import setup", 306 | ... "setup(name='urgh',", 307 | ... " version=\"1.0\",", 308 | ... ")"] 309 | >>> writeto('setup.py', '\n'.join(lines)) 310 | >>> checkout.version = '1.2' 311 | 312 | The version line should have retained its double quotes, otherwise black would 313 | have to change it back again: 314 | 315 | >>> with open('setup.py') as f: 316 | ... print(f.read()) 317 | from setuptools import setup 318 | setup(name='urgh', 319 | version="1.2", 320 | ) 321 | --------------------------------------------------------------------------------