diff --git a/.gitignore b/.gitignore index d93a2d64..e63b1cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ coverage.xml /.tox/ /build/ /dist/ +/stone/_version.py __pycache__/ .DS_Store parser.out diff --git a/pyproject.toml b/pyproject.toml index 26ae037d..aff7608d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [build-system] # setuptools>=77 is required for the SPDX ``license`` expression used in # setup.py (see PEP 639). setuptools-scm derives the version from the latest -# git tag (e.g. ``v3.5.0`` -> ``3.5.0``), so releases only require tagging. +# git tag (e.g. ``v3.5.0`` -> ``3.5.0``), so releases only require tagging, +# and writes it to stone/_version.py for runtime access. requires = ["setuptools>=77", "setuptools-scm>=8,<9"] build-backend = "setuptools.build_meta" @@ -10,3 +11,4 @@ build-backend = "setuptools.build_meta" # by default (``v3.5.0`` -> ``3.5.0``); an untagged build gets a dev version # from the last tag plus commit distance. parentdir_prefix_version = "stone-" +version_file = "stone/_version.py" diff --git a/stone/__init__.py b/stone/__init__.py index e69de29b..9a1c6741 100644 --- a/stone/__init__.py +++ b/stone/__init__.py @@ -0,0 +1,7 @@ +try: + from ._version import __version__ +except ModuleNotFoundError: + # A source tree that has not been built or installed yet. + __version__ = '0.0.0.dev0' + +__all__ = ('__version__',) diff --git a/stone/cli.py b/stone/cli.py index ba2e7d4a..417d0c82 100644 --- a/stone/cli.py +++ b/stone/cli.py @@ -15,6 +15,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) __package__ = 'stone' # pylint: disable=redefined-builtin +from . import __version__ from .cli_helpers import parse_route_attr_filter from .compiler import ( BackendException, @@ -54,6 +55,12 @@ 'example.spec -- -h".' ) _cmdline_parser = argparse.ArgumentParser(description=_cmdline_description) +_cmdline_parser.add_argument( + '--version', + action='version', + version='%(prog)s ' + __version__, + help="Show the Stone version and exit.", +) _cmdline_parser.add_argument( '-v', '--verbose', diff --git a/test/test_cli.py b/test/test_cli.py index 8f057db8..fb426a63 100755 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,13 +1,17 @@ #!/usr/bin/env python +import contextlib +import io import json import os import tempfile import unittest +from stone import __version__ from stone.cli import ( _actual_outputs, + _cmdline_parser, _load_expected_output_manifest, _recursive_stone_specs, _validate_expected_output_manifest, @@ -180,6 +184,15 @@ def test_validate_expected_output_manifest(self): with self.assertRaises(SystemExit): _validate_expected_output_manifest(['a.py'], ['b.py']) + def test_version_flag(self): + # --version prints the version and exits 0, without requiring the + # otherwise-mandatory positional arguments. + out = io.StringIO() + with self.assertRaises(SystemExit) as cm, contextlib.redirect_stdout(out): + _cmdline_parser.parse_args(['--version']) + self.assertEqual(cm.exception.code, 0) + self.assertIn(__version__, out.getvalue()) + if __name__ == '__main__': unittest.main() diff --git a/test/test_version.py b/test/test_version.py new file mode 100644 index 00000000..157f8971 --- /dev/null +++ b/test/test_version.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + + +import importlib.util +import sys +import types +import unittest +from unittest import mock + +import stone + + +class TestVersion(unittest.TestCase): + + def test_generated_version_is_used_in_source_checkout(self): + generated_version = types.ModuleType('stone._version') + generated_version.__version__ = '9.8.7' + + spec = importlib.util.spec_from_file_location( + 'stone._version_test', stone.__file__) + module = importlib.util.module_from_spec(spec) + module.__package__ = 'stone' + with mock.patch.dict(sys.modules, {'stone._version': generated_version}): + spec.loader.exec_module(module) + + self.assertEqual(module.__version__, '9.8.7') + + +if __name__ == '__main__': + unittest.main()