Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,3 @@ package.json @tleonhardt
pyproject.toml @tleonhardt @kmvanbrunt
ruff.toml @tleonhardt
README.md @kmvanbrunt @tleonhardt
tasks.py @tleonhardt
9 changes: 4 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ for a list of dependencies needed for building `cmd2`.
| Prerequisite | Minimum Version | Purpose |
| -------------------------------------------------------------------- | --------------- | -------------------------------- |
| [codecov](http://doc.pytest.org/en/latest/) | `2.1.13` | Cover coverage reporting |
| [invoke](https://www.pyinvoke.org/) | `2.2.0` | Command automation |
| [mypy](https://mypy-lang.org/) | `1.13.0` | Static type checker |
| [pytest](https://docs.pytest.org/en/stable/) | `3.0.6` | Unit and integration tests |
| [pytest-cov](http://doc.pytest.org/en/latest/) | `6.0.0` | Pytest code coverage |
Expand Down Expand Up @@ -776,14 +775,14 @@ Since 0.9.2, the process of publishing a new release of `cmd2` to [PyPi](https:/
mostly automated. The manual steps are all git operations. Here's the checklist:

1. Make sure you're on the proper branch (almost always **main**)
1. Make sure all the unit tests pass with `invoke pytest` or `py.test`
1. Make sure all the unit tests pass with `make test`
1. Make sure latest year in `LICENSE` matches current year
1. Make sure `CHANGELOG.md` describes the version and has the correct release date
1. Add a git tag representing the version number using `invoke tag x.y.z`
1. Add a git tag representing the version number using `make tag TAG=x.y.z`
- Where x, y, and z are all small non-negative integers
1. (Optional) Run `invoke pypi-test` to clean, build, and upload a new release to
1. (Optional) Run `make publish-test` to clean, build, and upload a new release to
[Test PyPi](https://test.pypi.org)
1. Run `invoke pypi` to clean, build, and upload a new release to [PyPi](https://pypi.org/)
1. Run `make publish` to clean, build, and upload a new release to [PyPi](https://pypi.org/)

## Acknowledgement

Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include LICENSE README.md CHANGELOG.md mkdocs.yml pyproject.toml ruff.toml tasks.py
include LICENSE README.md CHANGELOG.md Makefile mkdocs.yml pyproject.toml ruff.toml
recursive-include examples *
recursive-include tests *
recursive-include docs *
Expand Down
50 changes: 44 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Simple Makefile for use with a uv-based development environment
# The at (@) prefix tells make to suppress output from the command
# The hyphen (-) prefix tells make to ignore errors (e.g., if a directory doesn't exist)

.PHONY: install
install: ## Install the virtual environment with dependencies
@echo "🚀 Creating uv Python virtual environment"
Expand Down Expand Up @@ -48,11 +51,6 @@ build: clean-build ## Build wheel file
@echo "🚀 Creating wheel file"
@uv build

.PHONY: clean-build
clean-build: ## Clean build artifacts
@echo "🚀 Removing build artifacts"
@uv run python -c "import shutil; import os; shutil.rmtree('dist') if os.path.exists('dist') else None"

.PHONY: tag
tag: ## Add a Git tag and push it to origin with syntax: make tag TAG=tag_name
@echo "🚀 Creating git tag: ${TAG}"
Expand All @@ -63,7 +61,7 @@ tag: ## Add a Git tag and push it to origin with syntax: make tag TAG=tag_name
.PHONY: validate-tag
validate-tag: ## Check to make sure that a tag exists for the current HEAD and it looks like a valid version number
@echo "🚀 Validating version tag"
@uv run inv validatetag
@uv run scripts/validate_tag.py

.PHONY: publish-test
publish-test: validate-tag build ## Test publishing a release to PyPI, uses token from ~/.pypirc file.
Expand All @@ -75,6 +73,46 @@ publish: validate-tag build ## Publish a release to PyPI, uses token from ~/.pyp
@echo "🚀 Publishing."
@uv run uv-publish

# Define variables for files/directories to clean
BUILD_DIRS = build dist *.egg-info
DOC_DIRS = build
MYPY_DIRS = .mypy_cache dmypy.json dmypy.sock
TEST_DIRS = .cache .coverage .pytest_cache htmlcov

.PHONY: clean-build
clean-build: ## Clean build artifacts
@echo "🚀 Removing build artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(BUILD_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean-docs
clean-docs: ## Clean documentation artifacts
@echo "🚀 Removing documentation artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(DOC_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean-mypy
clean-mypy: ## Clean mypy artifacts
@echo "🚀 Removing mypy artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(MYPY_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean-pycache
clean-pycache: ## Clean pycache artifacts
@echo "🚀 Removing pycache artifacts"
@-find . -type d -name "__pycache__" -exec rm -r {} +

.PHONY: clean-ruff
clean-ruff: ## Clean ruff artifacts
@echo "🚀 Removing ruff artifacts"
@uv run ruff clean

.PHONY: clean-test
clean-test: ## Clean test artifacts
@echo "🚀 Removing test artifacts"
@uv run python -c "import shutil; import os; [shutil.rmtree(d, ignore_errors=True) for d in '$(TEST_DIRS)'.split() if os.path.isdir(d)]"

.PHONY: clean
clean: clean-build clean-docs clean-mypy clean-pycache clean-ruff clean-test ## Clean all artifacts
@echo "🚀 Cleaned all artifacts"

.PHONY: help
help:
@uv run python -c "import re; \
Expand Down
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ build = ["build>=1.2.2", "setuptools>=80.7.1", "setuptools-scm>=9.2"]
dev = [
"black>=25",
"codecov>=2.1",
"invoke>=2.2.1",
"ipython>=8.23",
"mkdocs-git-revision-date-localized-plugin>=1.5",
"mkdocs-material>=9.7.1",
Expand Down Expand Up @@ -87,7 +86,6 @@ exclude = [
"^noxfile\\.py$", # nox config file
"setup\\.py$", # any files named setup.py
"^site/",
"^tasks\\.py$", # tasks.py invoke config file
"^tests/", # tests directory
]
files = ['.']
Expand Down
3 changes: 3 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ mccabe.max-complexity = 49
]
"examples/scripts/*.py" = ["F821"] # Undefined name `app`

# Ignore starting a process with a partial executable path (i.e. git)
"scripts/validate_tag.py" = ["S607"]

# Ingore various rulesets in test directories
"{tests}/*.py" = [
"ANN", # Ignore all type annotation rules in test folders
Expand Down
86 changes: 86 additions & 0 deletions scripts/validate_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python
"""A simple script to validate that a git tag matches a SemVer pattern."""

import re
import subprocess

SEMVER_SIMPLE = re.compile(r'(\d+)\.(\d+)\.(\d+)((a|b|rc)(\d+))?')
SEMVER_PATTERN = re.compile(
r"""
^ # Start of the string
v? # Optional 'v' prefix (common in Git tags)
(?P<major>0|[1-9]\d*)\. # Major version
(?P<minor>0|[1-9]\d*)\. # Minor version
(?P<patch>0|[1-9]\d*) # Patch version
(?:-(?P<prerelease> # Optional pre-release section
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Identifier
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
))?
(?:\+(?P<build> # Optional build metadata section
[0-9a-zA-Z-]+ # Identifier
(?:\.[0-9a-zA-Z-]+)*
))?
$ # End of the string
""",
re.VERBOSE,
)


def get_current_tag() -> str:
"""Get current git tag."""
try:
# Gets the name of the latest tag reachable from the current commit
result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'], capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
print("Could not find a reachable tag.")
return ''


def is_semantic_version(tag_name: str) -> bool:
"""Check if a given string complies with the semantic versioning 2.0.0 specification.

Args:
tag_name: The name of the Git tag to validate.

Returns:
bool: True if the tag is a valid semantic version, False otherwise.

"""
# The regex pattern for semantic versioning 2.0.0 (source: https://semver.org/)
semver_pattern = re.compile(
r"""
^ # Start of the string
v? # Optional 'v' prefix (common in Git tags)
(?P<major>0|[1-9]\d*)\. # Major version
(?P<minor>0|[1-9]\d*)\. # Minor version
(?P<patch>0|[1-9]\d*) # Patch version
(?:-(?P<prerelease> # Optional pre-release section
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Identifier
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
))?
(?:\+(?P<build> # Optional build metadata section
[0-9a-zA-Z-]+ # Identifier
(?:\.[0-9a-zA-Z-]+)*
))?
$ # End of the string
""",
re.VERBOSE,
)

return bool(semver_pattern.match(tag_name))


if __name__ == '__main__':
import sys

git_tag = get_current_tag()
if not git_tag:
print('Git tag does not exist for current commit.')
sys.exit(-1)

if not is_semantic_version(git_tag):
print(rf"Git tag '{git_tag}' is invalid according to SemVer.")
sys.exit(-1)

print(rf"Git tag '{git_tag}' is valid.")
Loading
Loading