diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8642b97..88eba04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,9 +80,13 @@ jobs: python3 -m build # Install the newly built .whl file to verify the package is installable. + # Install with the 'visualize' extra so matplotlib is available for the + # plotting tests (tests/test_visualize.py). This also validates that the + # optional 'visualize' extra resolves correctly. - name: Install PyGAD from Wheel run: | - pip install dist/*.whl + WHEEL=$(ls dist/*.whl) + pip install "${WHEEL}[visualize]" - name: Install PyTest run: pip install pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d9d3b05 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: release + +# On a version tag this builds the package once, publishes it to PyPI via +# trusted publishing (no API token stored in the repo), and attaches the built +# wheel and sdist to a GitHub Release for the tag. The PyPI project must list +# this repo and workflow as a trusted publisher first. + +on: + push: + # PyGAD tags releases as the bare version number, e.g. 3.6.0 (no "v"). + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Build distributions + run: | + pip install build + python -m build + - name: Check distributions + run: | + pip install twine + python -m twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Attach the built files to the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --title "$GITHUB_REF_NAME" \ + --generate-notes \ + || gh release upload "$GITHUB_REF_NAME" dist/* \ + --repo "$GITHUB_REPOSITORY" --clobber diff --git a/README.md b/README.md index 3679e8e..403cac9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ Install PyGAD with the following command: pip install pygad ``` +PyGAD's core install is intentionally lightweight (only `numpy` and `cloudpickle`). Some features need extra libraries, which are available as optional extras: + +``` +# Plotting features (e.g. plot_fitness(), plot_genes()) need matplotlib: +pip install pygad[visualize] + +# Training Keras/PyTorch models (pygad.kerasga, pygad.torchga): +pip install pygad[deep_learning] +``` + To get started with PyGAD, read the documentation at [Read the Docs](https://pygad.readthedocs.io). # PyGAD Source Code diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..37ed635 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,51 @@ +# Releasing + +Releases are automated. Pushing a version tag builds the package, publishes it to +PyPI, and attaches the built files to a GitHub Release. Nothing is uploaded by +hand. + +## Steps + +1. Bump the version in `pygad/_version.py`. This is the only place the version + lives. +2. Update the release notes in the docs if you keep them there. +3. Commit and push: + ```bash + git add pygad/_version.py + git commit -m "Release 3.6.1" + git push + ``` +4. Wait for the test workflow (`main.yml`) to pass on that commit. +5. Tag the release and push the tag: + ```bash + git tag 3.6.1 + git push origin 3.6.1 + ``` + +The `release` workflow does the rest: it builds the wheel and sdist, publishes +them to PyPI, and creates a GitHub Release with both files attached. Follow it +with `gh run watch` or the Actions tab. + +## Rules + +- The tag must match `pygad/_version.py` and is the bare version number with no + `v` prefix, for example `3.6.1`. The tag is what triggers the release. +- Every release needs a new version number. PyPI does not allow re-uploading or + overwriting a version that already exists. +- Do not run `twine upload` or upload files to the GitHub Release by hand. The + tag does both for you. + +## Manual fallback + +`publish.sh` can build and upload to PyPI from your machine if you ever need it. + +## One-time setup (maintainers) + +Done once per project. No API token is involved, because PyPI trusted publishing +is tokenless. + +- On the PyPI `pygad` project, open Settings, then Publishing, and add a GitHub + publisher: owner `ahmedfgad`, repository `GeneticAlgorithmPython`, workflow + `release.yml`, environment `pypi`. +- In the GitHub repo, open Settings, then Environments, and create an environment + named `pypi`. diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..97f4fe7 --- /dev/null +++ b/publish.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# Publish the `pygad` Python package to PyPI (or TestPyPI). +# +# This is the manual release script. Run it from anywhere, it resolves +# paths relative to its own location and uses whatever Python environment +# is currently active (installing `build` + `twine` into it if they are +# missing). +# +# Pipeline: +# 1. Check build tooling +# 2. Wipe stale dist/ artefacts (confirmation prompt) +# 3. Build sdist + wheel into dist/ +# 4. `twine check` the artefacts for README/metadata issues +# 5. Prompt to upload to TestPyPI +# 6. Pause so you can `pip install -i https://test.pypi.org/simple/ pygad` +# in a scratch venv to confirm the release works end-to-end +# 7. Prompt to upload to production PyPI (the irreversible step) +# +# All prompts default to the SAFE answer ('no' for irreversible +# actions) and require a typed 'y' to proceed. + +set -euo pipefail + +# Colour helpers — silently no-op when stdout isn't a TTY (e.g. piped to +# `tee` or run from a CI runner). +if [ -t 1 ]; then + CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' + RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m' +else + CYAN=''; GREEN=''; YELLOW=''; RED=''; BOLD=''; NC='' +fi + +heading() { echo -e "\n${CYAN}${BOLD}== $* ==${NC}"; } +info() { echo -e "${CYAN}$*${NC}"; } +warn() { echo -e "${YELLOW}$*${NC}"; } +error() { echo -e "${RED}$*${NC}" >&2; } +success() { echo -e "${GREEN}$*${NC}"; } + +# Default to "no" — caller has to type `y` (case-insensitive) to confirm. +# Used for every destructive / irreversible step. +confirm() { + local prompt="$1" + local answer + read -r -p "$(echo -e "${YELLOW}${prompt} [y/N]:${NC} ")" answer + case "${answer:-}" in + y|Y|yes|YES) return 0 ;; + *) return 1 ;; + esac +} + +# Resolve the script's own directory so it works from any cwd. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ---- 1. Check build tooling ---- +heading "Checking build tooling" +if ! command -v python >/dev/null 2>&1; then + error "No 'python' on PATH. Activate a virtual environment and rerun." + exit 1 +fi +info "Active python: $(command -v python)" +info "Active pip: $(command -v pip)" + +# Confirm `build` and `twine` are present — install if missing so a +# fresh checkout works without a separate setup step. +if ! python -c "import build" >/dev/null 2>&1 || \ + ! python -c "import twine" >/dev/null 2>&1; then + warn "Installing missing build tooling (build, twine)..." + pip install --quiet build twine +fi + +cd "$SCRIPT_DIR" + +# ---- 2. Wipe stale dist/ ---- +heading "Cleaning previous artefacts" +if [ -d "dist" ] && [ -n "$(ls -A dist 2>/dev/null)" ]; then + echo "Existing dist/ contents:" + ls -1 dist + if confirm "Delete dist/ before building?"; then + rm -rf dist + success "Removed stale dist/" + else + warn "Keeping existing dist/. Note: twine will refuse to upload duplicates." + fi +else + info "No stale artefacts found." +fi + +# ---- 3. Build ---- +heading "Building sdist + wheel" +python -m build +ls -1 dist + +# ---- 4. twine check ---- +heading "Running twine check" +python -m twine check dist/* + +# ---- 5. Upload to TestPyPI ---- +heading "Upload to TestPyPI" +warn "TestPyPI lives at https://test.pypi.org/ and is the safe place to" +warn "verify the upload before touching production." +if confirm "Upload to TestPyPI now?"; then + python -m twine upload --repository testpypi dist/* + success "Uploaded to TestPyPI." + echo + info "Verify with (in a fresh scratch venv):" + info " pip install --index-url https://test.pypi.org/simple/ \\" + info " --extra-index-url https://pypi.org/simple/ pygad" + echo + read -r -p "Press Enter once the TestPyPI install looks good..." +else + warn "Skipped TestPyPI upload." +fi + +# ---- 6. Upload to production PyPI ---- +heading "Upload to PRODUCTION PyPI" +warn "This step is IRREVERSIBLE. Once a version is published you cannot" +warn "re-upload the same filename — you'd have to bump the version" +warn "(pyproject.toml, setup.py, and pygad/__init__.py) and rebuild." +warn "Make sure the TestPyPI smoke test passed." +if confirm "Upload to production PyPI now?"; then + python -m twine upload dist/* + success "Uploaded to PyPI: https://pypi.org/project/pygad/" +else + warn "Skipped production upload. Run this script again when ready." +fi + +heading "Done" diff --git a/pygad/__init__.py b/pygad/__init__.py index d7dca9d..2f5215a 100644 --- a/pygad/__init__.py +++ b/pygad/__init__.py @@ -1,3 +1,3 @@ from .pygad import * # Relative import. -__version__ = "3.6.0" +from ._version import __version__ diff --git a/pygad/_version.py b/pygad/_version.py new file mode 100644 index 0000000..85197cb --- /dev/null +++ b/pygad/_version.py @@ -0,0 +1 @@ +__version__ = "3.6.0" diff --git a/pyproject.toml b/pyproject.toml index 2f7c604..059b331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygad" -version = "3.6.0" +dynamic = ["version"] description = "PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch)." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3" @@ -42,7 +42,6 @@ classifiers = [ keywords = ["genetic algorithm", "GA", "optimization", "evolutionary algorithm", "natural evolution", "pygad", "machine learning", "deep learning", "neural networks", "tensorflow", "keras", "pytorch"] dependencies = [ "numpy", - "matplotlib", "cloudpickle", ] @@ -58,7 +57,11 @@ dependencies = [ [project.optional-dependencies] deep_learning = ["keras", "tensorflow", "torch"] +visualize = ["matplotlib"] # PyTest Configuration. Later, PyTest will support the [tool.pytest] table. [tool.pytest.ini_options] -testpaths = ["tests"] \ No newline at end of file +testpaths = ["tests"] + +[tool.setuptools.dynamic] +version = { attr = "pygad._version.__version__" } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a344de1..24e3762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ numpy -matplotlib cloudpickle \ No newline at end of file diff --git a/setup.py b/setup.py index 9626c37..d875fae 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,11 @@ setuptools.setup( name="pygad", - version="3.6.0", author="Ahmed Fawzy Gad", - install_requires=["numpy", "matplotlib", "cloudpickle",], + install_requires=["numpy", "cloudpickle",], extras_require={ "deep_learning": ["keras", "tensorflow", "torch"], + "visualize": ["matplotlib"], }, author_email="ahmed.f.gad@gmail.com",