diff --git a/README.rst b/README.rst index cc62ef6..7ce43c8 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Theory of Operation - A version number has the form YY.MM.PATCH. - If your project is named "Shrubbery", its code is found in ``shrubbery/`` or ``src/shrubbery/``. - Incremental stores your project's version number in ``{src/}shrubbery/_version.py``. -- To update the version, run ``python -m incremental.update Shrubbery``, passing ``--rc`` and/or ``--patch`` as appropriate (see `Updating`_, below). +- To update the version, run ``incremental update Shrubbery``, passing ``--rc`` and/or ``--patch`` as appropriate (see `Updating`_, below). - Changing the version also updates any `indeterminate versions`_ in your codebase, like "Shrubbery NEXT", so you can reference the upcoming release in documentation. That's how Incremental supports the future. @@ -86,7 +86,7 @@ activate Incremental's Hatchling plugin by altering your ``pyproject.toml``: Incremental can be configured as usual in an optional ``[tool.incremental]`` table. The ``hatch version`` command will report the Incremental-managed version. -Use the ``python -m incremental.update`` command to change the version (setting it with ``hatch version`` is not supported). +Use the ``incremental`` command to change the version (setting it with ``hatch version`` is not supported). Next, `initialize the project`_. @@ -112,8 +112,8 @@ Then `initialize the project`_. Initialize the project ~~~~~~~~~~~~~~~~~~~~~~ -Install Incremental to your local environment with ``pip install incremental[scripts]``. -Then run ``python -m incremental.update --create``. +Install Incremental to your local environment with ``pipx install incremental``. +Then run ``incremental update --create``. It will create a file in your package named ``_version.py`` like this: .. code:: python @@ -124,15 +124,26 @@ It will create a file in your package named ``_version.py`` like this: __all__ = ["__version__"] -Then, so users of your project can find your version, in your root package's ``__init__.py`` add: +Subsequent installations of your project will then use Incremental for versioning. + + +Runtime integration +~~~~~~~~~~~~~~~~~~~ + +You may expose the ``incremental.Version`` from ``_version.py`` in your package's API. +To do so, add to your root package's ``__init__.py``: .. code:: python from ._version import __version__ +.. note:: -Subsequent installations of your project will then use Incremental for versioning. + Providing a ``__version__`` attribute is falling out of fashion following the introduction of `importlib.metadata.version() `_ in Python 3.6, which can retrieve an installed package's version. +If you don't expose this object publicly, nor make use of it within your package, +then there is no need to depend on Incremental at runtime. +You can remove it from your project's ``dependencies`` array (or, in ``setup.py``, from ``install_requires``). Incremental Versions @@ -155,12 +166,12 @@ Calling ``repr()`` with a ``Version`` will give a Python-source-code representat Updating -------- -Incremental includes a tool to automate updating your Incremental-using project's version called ``incremental.update``. +Incremental includes a tool to automate updating your Incremental-using project's version called ``incremental``. It updates the ``_version.py`` file and automatically updates some uses of Incremental versions from an indeterminate version to the current one. It requires ``click`` from PyPI. -``python -m incremental.update `` will perform updates on that package. -The commands that can be given after that will determine what the next version is. +``incremental update `` will perform updates on that package. +The commands that can be given after that determine what the next version is. - ``--newversion=``, to set the project version to a fully-specified version (like 1.2.3, or 17.1.0dev1). - ``--rc``, to set the project version to ``..0rc1`` if the current version is not a release candidate, or bump the release candidate number by 1 if it is. @@ -178,7 +189,7 @@ Incremental supports "indeterminate" versions, as a stand-in for the next "full" - ``Version("", "NEXT", 0, 0)`` - `` NEXT`` -When you run ``python -m incremental.update --rc``, these will be updated to real versions (assuming the target final version is 17.1.0): +When you run ``incremental update --rc``, these will be updated to real versions (assuming the target final version is 17.1.0): - ``Version("", 17, 1, 0, release_candidate=1)`` - `` 17.1.0rc1`` diff --git a/pyproject.toml b/pyproject.toml index 082f085..97f3a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ [project.optional-dependencies] scripts = [ - "click>=6.0", + # This extra remains for backwards compatibility. ] [project.urls] @@ -45,6 +45,9 @@ Documentation = "https://twisted.org/incremental/docs/" Issues = "https://github.com/twisted/incremental/issues" Changelog = "https://github.com/twisted/incremental/blob/trunk/NEWS.rst" +[project.scripts] +incremental = "incremental.update:_main" + [project.entry-points."distutils.setup_keywords"] use_incremental = "incremental:_get_distutils_version" [project.entry-points."setuptools.finalize_distribution_options"] diff --git a/src/incremental/_hatch.py b/src/incremental/_hatch.py index b7e5325..5d6edac 100644 --- a/src/incremental/_hatch.py +++ b/src/incremental/_hatch.py @@ -24,10 +24,12 @@ def get_version_data(self) -> _VersionData: # type: ignore[override] return {"version": _existing_version(config.version_path).public()} def set_version(self, version: str, version_data: Dict[Any, Any]) -> None: + path = os.path.join(self.root, "./pyproject.toml") # TODO: #111 Delete this. + config = _load_pyproject_toml(path) raise NotImplementedError( - f"Run `python -m incremental.version --newversion" + f"Run `incremental update {shlex.quote(config.package)} --newversion" f" {shlex.quote(version)}` to set the version.\n\n" - f" See `python -m incremental.version --help` for more options." + f" See `incremental --help` for more options." ) diff --git a/src/incremental/newsfragments/99.feature.rst b/src/incremental/newsfragments/99.feature.rst new file mode 100644 index 0000000..6c6cb43 --- /dev/null +++ b/src/incremental/newsfragments/99.feature.rst @@ -0,0 +1,2 @@ +Incremental now provides a CLI script, ``incremental``, allowing you to run it with ``pipx run incremental``. +The ``incremental update`` subcommand offers the same functionality as ``python -m incremental.update``. diff --git a/src/incremental/newsfragments/99.removal.rst b/src/incremental/newsfragments/99.removal.rst new file mode 100644 index 0000000..662a845 --- /dev/null +++ b/src/incremental/newsfragments/99.removal.rst @@ -0,0 +1,2 @@ +Incremental's CLI no longer depends on Click, so you no longer need to install ``incremental[scripts]`` for it to function. +The ``scripts`` extra is deprecated. diff --git a/src/incremental/tests/test_update.py b/src/incremental/tests/test_update.py index 679e294..b0217f2 100644 --- a/src/incremental/tests/test_update.py +++ b/src/incremental/tests/test_update.py @@ -10,12 +10,12 @@ import sys import os import datetime +from io import StringIO from twisted.python.filepath import FilePath -from twisted.python.compat import NativeStringIO from twisted.trial.unittest import TestCase -from incremental.update import _run, run +from incremental.update import _run, run, _main class NonCreatedUpdateTests(TestCase): @@ -44,7 +44,7 @@ class Date(object): def test_create(self): """ - `incremental.update package --create` initialises the version. + `incremental update package --create` initialises the version. """ self.assertFalse(self.packagedir.child("_version.py").exists()) @@ -71,7 +71,7 @@ def test_create(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -116,7 +116,7 @@ class Date(object): def test_path(self): """ - `incremental.update package --dev` raises and quits if it can't find + `incremental update package --dev` raises and quits if it can't find the package. """ out = [] @@ -171,7 +171,7 @@ class Date(object): def test_path(self): """ - `incremental.update package --path= --dev` increments the dev + `incremental update package --path= --dev` increments the dev version of the package on the given path """ out = [] @@ -197,7 +197,7 @@ def test_path(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -228,7 +228,7 @@ def test_path(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -273,7 +273,7 @@ class Date(object): def test_path(self): """ - `incremental.update package --path= --dev` increments the dev + `incremental update package --path= --dev` increments the dev version of the package on the given path """ out = [] @@ -298,7 +298,7 @@ def test_path(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -309,7 +309,7 @@ def test_path(self): def test_dev(self): """ - `incremental.update package --dev` increments the dev version. + `incremental update package --dev` increments the dev version. """ out = [] _run( @@ -334,7 +334,7 @@ def test_dev(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -369,7 +369,7 @@ def test_patch(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -388,7 +388,7 @@ def test_patch(self): def test_patch_with_prerelease_and_dev(self): """ - `incremental.update package --patch` increments the patch version, and + `incremental update package --patch` increments the patch version, and disregards any old prerelease/dev versions. """ self.packagedir.child("_version.py").setContent( @@ -421,7 +421,7 @@ def test_patch_with_prerelease_and_dev(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -432,7 +432,7 @@ def test_patch_with_prerelease_and_dev(self): def test_rc_patch(self): """ - `incremental.update package --patch --rc` increments the patch + `incremental update package --patch --rc` increments the patch version and makes it a release candidate. """ out = [] @@ -457,7 +457,7 @@ def test_rc_patch(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -476,7 +476,7 @@ def test_rc_patch(self): def test_rc_with_existing_rc(self): """ - `incremental.update package --rc` increments the rc version if the + `incremental update package --rc` increments the rc version if the existing version is an rc, and discards any dev version. """ self.packagedir.child("_version.py").setContent( @@ -509,7 +509,7 @@ def test_rc_with_existing_rc(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -528,7 +528,7 @@ def test_rc_with_existing_rc(self): def test_rc_with_no_rc(self): """ - `incremental.update package --rc`, when the package is not a release + `incremental update package --rc`, when the package is not a release candidate, will issue a new major/minor rc, and disregards the micro and dev. """ @@ -562,7 +562,7 @@ def test_rc_with_no_rc(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -606,7 +606,7 @@ def test_full_with_rc(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -644,7 +644,7 @@ def test_full_with_rc(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -714,7 +714,7 @@ def test_post(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -758,7 +758,7 @@ def test_post_with_prerelease_and_dev(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -802,7 +802,7 @@ def test_post_with_existing_post(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -1055,7 +1055,7 @@ def test_newversion(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -1103,7 +1103,7 @@ def test_newversion_bare(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -1147,7 +1147,7 @@ def test_newversion_bare_major_minor(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -1202,28 +1202,31 @@ def today(self): self.date = DateModule() - def test_run(self): + def test_help(self): """ - Calling run() with no args will cause it to print help. + Running `python -m incremental.update --help` causes it to print help. """ - stringio = NativeStringIO() + stringio = StringIO() self.patch(sys, "stdout", stringio) with self.assertRaises(SystemExit) as e: run(["--help"]) self.assertEqual(e.exception.args[0], 0) - self.assertIn("Show this message and exit", stringio.getvalue()) + self.assertIn("show this help message and exit", stringio.getvalue()) - def test_insufficient_args(self): + def test_incrementalDotUpdate(self): """ - Calling run() with no args will cause it to print help. + Running `python -m incremental.update inctestpkg --rc` creates + a release candidate. """ - stringio = NativeStringIO() + stringio = StringIO() self.patch(sys, "stdout", stringio) self.patch(os, "getcwd", self.getcwd) self.patch(datetime, "date", self.date) + # This used to be implemented with Click, which always raises + # SystemExit. We continue to do so for compatability. with self.assertRaises(SystemExit) as e: run(["inctestpkg", "--rc"]) @@ -1237,7 +1240,45 @@ def test_insufficient_args(self): """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update inctestpkg` to change this file. +# Use `incremental` to change this file. + +from incremental import Version + +__version__ = Version("inctestpkg", 16, 8, 0, release_candidate=1) +__all__ = ["__version__"] +''', + ) + self.assertEqual( + self.packagedir.child("__init__.py").getContent(), + b""" +from incremental import Version +introduced_in = Version("inctestpkg", 16, 8, 0, release_candidate=1).short() +next_released_version = "inctestpkg 16.8.0rc1" +""", + ) + + def test_incrementalUpdate(self): + """ + Running `incremental update inctestpkg --rc` creates a release + candidate. + """ + stringio = StringIO() + self.patch(sys, "stdout", stringio) + self.patch(os, "getcwd", self.getcwd) + self.patch(datetime, "date", self.date) + + _main(["update", "inctestpkg", "--rc"]) + + self.assertIn("Updating codebase", stringio.getvalue()) + + self.assertEqual( + self.packagedir.child("_version.py").getContent(), + b'''""" +Provides inctestpkg version information. +""" + +# This file is auto-generated! Do not edit! +# Use `incremental` to change this file. from incremental import Version diff --git a/src/incremental/update.py b/src/incremental/update.py index 0c92e77..dd11c3b 100644 --- a/src/incremental/update.py +++ b/src/incremental/update.py @@ -3,10 +3,10 @@ from __future__ import absolute_import, division, print_function -import click +from argparse import ArgumentParser import os import datetime -from typing import Dict, Optional, Callable +from typing import Any, Callable, Dict, Optional, Sequence from incremental import Version, _findPath, _existing_version @@ -15,7 +15,7 @@ """ # This file is auto-generated! Do not edit! -# Use `python -m incremental.update {package}` to change this file. +# Use `incremental` to change this file. from incremental import Version @@ -216,41 +216,64 @@ def _run( _print("Updating %s" % (versionpath,)) with open(versionpath, "wb") as f: f.write( - ( - _VERSIONPY_TEMPLATE.format(package=package, version_repr=version_repr) - ).encode("utf8") + _VERSIONPY_TEMPLATE.format( + package=package, version_repr=version_repr + ).encode("utf-8") ) -@click.command() -@click.argument("package") -@click.option("--path", default=None) -@click.option("--newversion", default=None) -@click.option("--patch", is_flag=True) -@click.option("--rc", is_flag=True) -@click.option("--post", is_flag=True) -@click.option("--dev", is_flag=True) -@click.option("--create", is_flag=True) -def run( - package, # type: str - path, # type: Optional[str] - newversion, # type: Optional[str] - patch, # type: bool - rc, # type: bool - post, # type: bool - dev, # type: bool - create, # type: bool -): # type: (...) -> None - return _run( - package=package, - path=path, - newversion=newversion, - patch=patch, - rc=rc, - post=post, - dev=dev, - create=create, +def _add_update_args(p): # type: (ArgumentParser) -> None + p.add_argument("package") + p.add_argument("--path", default=None) + p.add_argument("--newversion", default=None, metavar="VERSION") + p.add_argument("--patch", default=False, action="store_true") + p.add_argument("--rc", default=False, action="store_true") + p.add_argument("--post", default=False, action="store_true") + p.add_argument("--dev", default=False, action="store_true") + p.add_argument("--create", default=False, action="store_true") + + +def _main(argv=None): # type: (Optional[Sequence[str]]) -> None + """ + Entrypoint of the `incremental` script + """ + p = ArgumentParser() + subparsers = p.add_subparsers(required=True) + + update_p = subparsers.add_parser("update") + _add_update_args(update_p) + + args = p.parse_args(argv) # type: Any + _run( + package=args.package, + path=args.path, + newversion=args.newversion, + patch=args.patch, + rc=args.rc, + post=args.post, + dev=args.dev, + create=args.create, + ) + + +def run(argv=None): # type: (Optional[Sequence[str]]) -> None + """ + Entrypoint for `python -m incremental.update` + """ + p = ArgumentParser() + _add_update_args(p) + args = p.parse_args(argv) # type: Any + _run( + package=args.package, + path=args.path, + newversion=args.newversion, + patch=args.patch, + rc=args.rc, + post=args.post, + dev=args.dev, + create=args.create, ) + raise SystemExit(0) # Behave like Click. if __name__ == "__main__": # pragma: no cover diff --git a/tests/test_examples.py b/tests/test_examples.py index 5920c9d..5e6c09f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -214,7 +214,7 @@ def test_hatch_version(self): def test_hatch_version_set(self): """ The ``hatch version`` command can't set the version so its output - tells the user to use ``incremental.update`` instead. + tells the user to use ``incremental`` instead. """ proc = run( ["hatch", "--no-color", "version", "24.8.0"], @@ -222,7 +222,7 @@ def test_hatch_version_set(self): check=False, capture_output=True, ) - suggestion = b"Run `python -m incremental.version --newversion 24.8.0` to set the version." + suggestion = b"Run `incremental update example_hatchling --newversion 24.8.0` to set the version." self.assertGreater(proc.returncode, 0) self.assertRegex(