-
Notifications
You must be signed in to change notification settings - Fork 4
feat(version): append +dev suffix for non-release builds #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,82 @@ | ||
| """stonks-cli package metadata. | ||
|
|
||
| ``__version__`` is the static version recorded in ``pyproject.toml`` for | ||
| released wheels. When the package is imported from a source checkout | ||
| (the ``.git`` directory sits next to the ``src/`` tree), a PEP 440 | ||
| local-version suffix is appended so that ``stonks --version`` makes it | ||
| obvious the running build isn't a tagged release: | ||
|
|
||
| * exact tag, clean tree -> ``0.6.3`` | ||
| * exact tag, dirty tree -> ``0.6.3+dirty`` | ||
| * past tag, clean tree -> ``0.6.3+dev.<N>.g<sha>`` | ||
| * past tag, dirty tree -> ``0.6.3+dev.<N>.g<sha>.dirty`` | ||
| * no reachable tag, clean / dirty -> ``0.6.3+dev.<sha>[.dirty]`` | ||
|
|
||
| PyPI / wheel installs never carry the suffix because no ``.git`` | ||
| directory is co-located with the installed package. | ||
| """ | ||
|
|
||
| import subprocess | ||
| from importlib.metadata import PackageNotFoundError, version | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def _git_dev_suffix(base: str) -> str: | ||
| """Return a PEP 440 local-version suffix for source-checkout installs. | ||
|
|
||
| Returns an empty string for wheel/PyPI installs (no ``.git`` next to the | ||
| package), when ``git`` isn't on PATH, when the probe times out, or when | ||
| HEAD is exactly on the release tag with a clean tree. | ||
| """ | ||
| repo_root = Path(__file__).resolve().parents[2] | ||
| if not (repo_root / ".git").exists(): | ||
| return "" | ||
| try: | ||
| result = subprocess.run( | ||
| [ | ||
| "git", | ||
| "-C", | ||
| str(repo_root), | ||
| "describe", | ||
| "--tags", | ||
| "--always", | ||
| "--dirty=.dirty", | ||
| ], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=2, | ||
| check=False, | ||
| ) | ||
| except (OSError, subprocess.SubprocessError): | ||
| return "" | ||
| if result.returncode != 0: | ||
| return "" | ||
| desc = result.stdout.strip() | ||
| if not desc: | ||
| return "" | ||
|
|
||
| tag_prefix = f"v{base}" | ||
| # Exactly on the release tag with a clean tree -- no suffix needed. | ||
| if desc == tag_prefix: | ||
| return "" | ||
| # On the release tag with uncommitted changes. | ||
| if desc == f"{tag_prefix}.dirty": | ||
| return "+dirty" | ||
| # Past the release tag: "v0.6.3-2-gabc1234[.dirty]" -> "+dev.2.gabc1234[.dirty]" | ||
| if desc.startswith(f"{tag_prefix}-"): | ||
| rest = desc[len(tag_prefix) + 1 :] | ||
| return f"+dev.{rest.replace('-', '.')}" | ||
| # No reachable matching tag (shallow clone, different tag, etc.) -- | ||
| # fall back to whatever git describe returned, normalized for PEP 440. | ||
| return f"+dev.{desc.replace('-', '.')}" | ||
|
|
||
|
|
||
| def _resolve_version() -> str: | ||
| try: | ||
| base = version("stonks-cli") | ||
| except PackageNotFoundError: | ||
| return "0.0.0.dev" | ||
| return base + _git_dev_suffix(base) | ||
|
|
||
|
|
||
| try: | ||
| __version__ = version("stonks-cli") | ||
| except PackageNotFoundError: | ||
| __version__ = "0.0.0.dev" | ||
| __version__ = _resolve_version() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| """Tests for ``stonks_cli.__init__`` version-suffix logic.""" | ||
|
|
||
| import subprocess | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
| from stonks_cli import _git_dev_suffix | ||
| from stonks_cli.doctor import _version_tuple | ||
|
|
||
|
|
||
| def _set_repo_root(mock_path: MagicMock, root) -> None: | ||
| """Wire ``mock_path`` so ``Path(__file__).resolve().parents[2]`` returns *root*.""" | ||
| mock_path.return_value.resolve.return_value.parents.__getitem__.return_value = root | ||
|
|
||
|
|
||
| def _fake_git_describe(stdout: str = "", returncode: int = 0) -> MagicMock: | ||
| """Build a mock ``subprocess.run`` return for a successful ``git describe``.""" | ||
| result = MagicMock() | ||
| result.stdout = stdout | ||
| result.returncode = returncode | ||
| return result | ||
|
|
||
|
|
||
| class TestGitDevSuffix: | ||
| @patch("stonks_cli.Path") | ||
| def test_no_git_dir_returns_empty(self, mock_path, tmp_path): | ||
| _set_repo_root(mock_path, tmp_path) | ||
| # No ``.git`` dir under tmp_path -- the git probe must be skipped | ||
| # entirely so PyPI/wheel installs never pay for subprocess.run. | ||
| assert _git_dev_suffix("0.6.3") == "" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_on_exact_tag_clean_returns_empty(self, mock_path, mock_run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("v0.6.3") | ||
| assert _git_dev_suffix("0.6.3") == "" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_on_exact_tag_dirty_returns_plus_dirty(self, mock_path, mock_run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("v0.6.3.dirty") | ||
| assert _git_dev_suffix("0.6.3") == "+dirty" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_past_tag_clean_returns_dev_suffix(self, mock_path, mock_run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("v0.6.3-2-gabc1234") | ||
| assert _git_dev_suffix("0.6.3") == "+dev.2.gabc1234" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_past_tag_dirty_returns_dev_dirty_suffix( | ||
| self, mock_path, mock_run, tmp_path | ||
| ): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("v0.6.3-2-gabc1234.dirty") | ||
| assert _git_dev_suffix("0.6.3") == "+dev.2.gabc1234.dirty" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_no_reachable_tag_returns_sha_only(self, mock_path, mock_run, tmp_path): | ||
| # Shallow clone or pre-first-tag history: git describe --always | ||
| # falls back to just the short SHA. | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("abc1234.dirty") | ||
| assert _git_dev_suffix("0.6.3") == "+dev.abc1234.dirty" | ||
|
|
||
| @patch("stonks_cli.subprocess.run", side_effect=FileNotFoundError("no git")) | ||
| @patch("stonks_cli.Path") | ||
| def test_missing_git_binary_returns_empty(self, mock_path, _run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| assert _git_dev_suffix("0.6.3") == "" | ||
|
|
||
| @patch( | ||
| "stonks_cli.subprocess.run", | ||
| side_effect=subprocess.TimeoutExpired(cmd="git", timeout=2), | ||
| ) | ||
| @patch("stonks_cli.Path") | ||
| def test_subprocess_timeout_returns_empty(self, mock_path, _run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| assert _git_dev_suffix("0.6.3") == "" | ||
|
|
||
| @patch("stonks_cli.subprocess.run") | ||
| @patch("stonks_cli.Path") | ||
| def test_nonzero_returncode_returns_empty(self, mock_path, mock_run, tmp_path): | ||
| (tmp_path / ".git").mkdir() | ||
| _set_repo_root(mock_path, tmp_path) | ||
| mock_run.return_value = _fake_git_describe("", returncode=128) | ||
| assert _git_dev_suffix("0.6.3") == "" | ||
|
|
||
|
|
||
| class TestVersionTupleStripsLocalSegment: | ||
| @pytest.mark.parametrize( | ||
| "raw, expected", | ||
| [ | ||
| ("0.6.3", (0, 6, 3)), | ||
| ("0.6.3+dirty", (0, 6, 3)), | ||
| ("0.6.3+dev.abc1234.dirty", (0, 6, 3)), | ||
| ("0.6.3+dev.2.gabc1234", (0, 6, 3)), | ||
| ("1.0.0", (1, 0, 0)), | ||
| ], | ||
| ) | ||
| def test_strips_local_segment(self, raw, expected): | ||
| # Without the strip, "0.6.3+dev.abc1234.dirty" would split into | ||
| # ["0","6","3+dev","abc1234","dirty"] and collapse to (0, 6), | ||
| # causing doctor to falsely report the dev build as out of date | ||
| # against PyPI's 0.6.3. | ||
| assert _version_tuple(raw) == expected |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation assumes that git tags always have a
vprefix (e.g.,v0.6.3). While common, some repositories use tags without the prefix (e.g.,0.6.3). Making this check more robust by supporting both formats ensures the version string remains clean even if the tagging convention changes or differs from the assumption.