Skip to content

Commit 5e58499

Browse files
authored
Add clean target to Makefile that runs what is in invoke clean (#1549)
* Add clean target to Makefile that runs what is in invoke clean Also: - Remove invoke dependency - Delete tasks.py file used by invoke - Add scripts/validate_tag.py for validating git tags match semantic versioning * Refactor Makefile so cleanup code is fully cross-platform
1 parent 0cb14a8 commit 5e58499

File tree

8 files changed

+138
-290
lines changed

8 files changed

+138
-290
lines changed

.github/CODEOWNERS

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,3 @@ package.json @tleonhardt
7575
pyproject.toml @tleonhardt @kmvanbrunt
7676
ruff.toml @tleonhardt
7777
README.md @kmvanbrunt @tleonhardt
78-
tasks.py @tleonhardt

.github/CONTRIBUTING.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ for a list of dependencies needed for building `cmd2`.
9696
| Prerequisite | Minimum Version | Purpose |
9797
| -------------------------------------------------------------------- | --------------- | -------------------------------- |
9898
| [codecov](http://doc.pytest.org/en/latest/) | `2.1.13` | Cover coverage reporting |
99-
| [invoke](https://www.pyinvoke.org/) | `2.2.0` | Command automation |
10099
| [mypy](https://mypy-lang.org/) | `1.13.0` | Static type checker |
101100
| [pytest](https://docs.pytest.org/en/stable/) | `3.0.6` | Unit and integration tests |
102101
| [pytest-cov](http://doc.pytest.org/en/latest/) | `6.0.0` | Pytest code coverage |
@@ -776,14 +775,14 @@ Since 0.9.2, the process of publishing a new release of `cmd2` to [PyPi](https:/
776775
mostly automated. The manual steps are all git operations. Here's the checklist:
777776

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

788787
## Acknowledgement
789788

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
include LICENSE README.md CHANGELOG.md mkdocs.yml pyproject.toml ruff.toml tasks.py
1+
include LICENSE README.md CHANGELOG.md Makefile mkdocs.yml pyproject.toml ruff.toml
22
recursive-include examples *
33
recursive-include tests *
44
recursive-include docs *

Makefile

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Simple Makefile for use with a uv-based development environment
2+
# The at (@) prefix tells make to suppress output from the command
3+
# The hyphen (-) prefix tells make to ignore errors (e.g., if a directory doesn't exist)
4+
25
.PHONY: install
36
install: ## Install the virtual environment with dependencies
47
@echo "🚀 Creating uv Python virtual environment"
@@ -48,11 +51,6 @@ build: clean-build ## Build wheel file
4851
@echo "🚀 Creating wheel file"
4952
@uv build
5053

51-
.PHONY: clean-build
52-
clean-build: ## Clean build artifacts
53-
@echo "🚀 Removing build artifacts"
54-
@uv run python -c "import shutil; import os; shutil.rmtree('dist') if os.path.exists('dist') else None"
55-
5654
.PHONY: tag
5755
tag: ## Add a Git tag and push it to origin with syntax: make tag TAG=tag_name
5856
@echo "🚀 Creating git tag: ${TAG}"
@@ -63,7 +61,7 @@ tag: ## Add a Git tag and push it to origin with syntax: make tag TAG=tag_name
6361
.PHONY: validate-tag
6462
validate-tag: ## Check to make sure that a tag exists for the current HEAD and it looks like a valid version number
6563
@echo "🚀 Validating version tag"
66-
@uv run inv validatetag
64+
@uv run scripts/validate_tag.py
6765

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

76+
# Define variables for files/directories to clean
77+
BUILD_DIRS = build dist *.egg-info
78+
DOC_DIRS = build
79+
MYPY_DIRS = .mypy_cache dmypy.json dmypy.sock
80+
TEST_DIRS = .cache .coverage .pytest_cache htmlcov
81+
82+
.PHONY: clean-build
83+
clean-build: ## Clean build artifacts
84+
@echo "🚀 Removing build artifacts"
85+
@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)]"
86+
87+
.PHONY: clean-docs
88+
clean-docs: ## Clean documentation artifacts
89+
@echo "🚀 Removing documentation artifacts"
90+
@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)]"
91+
92+
.PHONY: clean-mypy
93+
clean-mypy: ## Clean mypy artifacts
94+
@echo "🚀 Removing mypy artifacts"
95+
@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)]"
96+
97+
.PHONY: clean-pycache
98+
clean-pycache: ## Clean pycache artifacts
99+
@echo "🚀 Removing pycache artifacts"
100+
@-find . -type d -name "__pycache__" -exec rm -r {} +
101+
102+
.PHONY: clean-ruff
103+
clean-ruff: ## Clean ruff artifacts
104+
@echo "🚀 Removing ruff artifacts"
105+
@uv run ruff clean
106+
107+
.PHONY: clean-test
108+
clean-test: ## Clean test artifacts
109+
@echo "🚀 Removing test artifacts"
110+
@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)]"
111+
112+
.PHONY: clean
113+
clean: clean-build clean-docs clean-mypy clean-pycache clean-ruff clean-test ## Clean all artifacts
114+
@echo "🚀 Cleaned all artifacts"
115+
78116
.PHONY: help
79117
help:
80118
@uv run python -c "import re; \

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ build = ["build>=1.2.2", "setuptools>=80.7.1", "setuptools-scm>=9.2"]
4242
dev = [
4343
"black>=25",
4444
"codecov>=2.1",
45-
"invoke>=2.2.1",
4645
"ipython>=8.23",
4746
"mkdocs-git-revision-date-localized-plugin>=1.5",
4847
"mkdocs-material>=9.7.1",
@@ -87,7 +86,6 @@ exclude = [
8786
"^noxfile\\.py$", # nox config file
8887
"setup\\.py$", # any files named setup.py
8988
"^site/",
90-
"^tasks\\.py$", # tasks.py invoke config file
9189
"^tests/", # tests directory
9290
]
9391
files = ['.']

ruff.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ mccabe.max-complexity = 49
151151
]
152152
"examples/scripts/*.py" = ["F821"] # Undefined name `app`
153153

154+
# Ignore starting a process with a partial executable path (i.e. git)
155+
"scripts/validate_tag.py" = ["S607"]
156+
154157
# Ingore various rulesets in test directories
155158
"{tests}/*.py" = [
156159
"ANN", # Ignore all type annotation rules in test folders

scripts/validate_tag.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python
2+
"""A simple script to validate that a git tag matches a SemVer pattern."""
3+
4+
import re
5+
import subprocess
6+
7+
SEMVER_SIMPLE = re.compile(r'(\d+)\.(\d+)\.(\d+)((a|b|rc)(\d+))?')
8+
SEMVER_PATTERN = re.compile(
9+
r"""
10+
^ # Start of the string
11+
v? # Optional 'v' prefix (common in Git tags)
12+
(?P<major>0|[1-9]\d*)\. # Major version
13+
(?P<minor>0|[1-9]\d*)\. # Minor version
14+
(?P<patch>0|[1-9]\d*) # Patch version
15+
(?:-(?P<prerelease> # Optional pre-release section
16+
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Identifier
17+
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
18+
))?
19+
(?:\+(?P<build> # Optional build metadata section
20+
[0-9a-zA-Z-]+ # Identifier
21+
(?:\.[0-9a-zA-Z-]+)*
22+
))?
23+
$ # End of the string
24+
""",
25+
re.VERBOSE,
26+
)
27+
28+
29+
def get_current_tag() -> str:
30+
"""Get current git tag."""
31+
try:
32+
# Gets the name of the latest tag reachable from the current commit
33+
result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'], capture_output=True, text=True, check=True)
34+
return result.stdout.strip()
35+
except subprocess.CalledProcessError:
36+
print("Could not find a reachable tag.")
37+
return ''
38+
39+
40+
def is_semantic_version(tag_name: str) -> bool:
41+
"""Check if a given string complies with the semantic versioning 2.0.0 specification.
42+
43+
Args:
44+
tag_name: The name of the Git tag to validate.
45+
46+
Returns:
47+
bool: True if the tag is a valid semantic version, False otherwise.
48+
49+
"""
50+
# The regex pattern for semantic versioning 2.0.0 (source: https://semver.org/)
51+
semver_pattern = re.compile(
52+
r"""
53+
^ # Start of the string
54+
v? # Optional 'v' prefix (common in Git tags)
55+
(?P<major>0|[1-9]\d*)\. # Major version
56+
(?P<minor>0|[1-9]\d*)\. # Minor version
57+
(?P<patch>0|[1-9]\d*) # Patch version
58+
(?:-(?P<prerelease> # Optional pre-release section
59+
(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) # Identifier
60+
(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
61+
))?
62+
(?:\+(?P<build> # Optional build metadata section
63+
[0-9a-zA-Z-]+ # Identifier
64+
(?:\.[0-9a-zA-Z-]+)*
65+
))?
66+
$ # End of the string
67+
""",
68+
re.VERBOSE,
69+
)
70+
71+
return bool(semver_pattern.match(tag_name))
72+
73+
74+
if __name__ == '__main__':
75+
import sys
76+
77+
git_tag = get_current_tag()
78+
if not git_tag:
79+
print('Git tag does not exist for current commit.')
80+
sys.exit(-1)
81+
82+
if not is_semantic_version(git_tag):
83+
print(rf"Git tag '{git_tag}' is invalid according to SemVer.")
84+
sys.exit(-1)
85+
86+
print(rf"Git tag '{git_tag}' is valid.")

0 commit comments

Comments
 (0)