diff --git a/.github/workflows/build-debian-multiarch.yml b/.github/workflows/build-debian-multiarch.yml index 8e60e6b160..543486b8e7 100644 --- a/.github/workflows/build-debian-multiarch.yml +++ b/.github/workflows/build-debian-multiarch.yml @@ -65,10 +65,10 @@ jobs: - { arch: armv7, base_image: 'balenalib/raspberrypi3-debian:bookworm' } steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Build sources and run tests - uses: uraimo/run-on-arch-action@v2.7.2 + uses: uraimo/run-on-arch-action@v2.8.1 id: build with: arch: ${{ matrix.base_image && 'none' || matrix.arch }} @@ -136,7 +136,7 @@ jobs: done - name: Test armv7 wheel on armv6 - uses: uraimo/run-on-arch-action@v2.7.2 + uses: uraimo/run-on-arch-action@v2.8.1 with: arch: armv6 distro: bookworm diff --git a/.github/workflows/build-emsdk.yml b/.github/workflows/build-emsdk.yml index fb9e15c963..d61e0c5420 100644 --- a/.github/workflows/build-emsdk.yml +++ b/.github/workflows/build-emsdk.yml @@ -41,7 +41,7 @@ jobs: SDKROOT: /opt/python-wasm-sdk steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Regen with latest cython (using system python3) run: | diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 3034df22d7..23e304285f 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -47,11 +47,11 @@ jobs: - { macarch: x86_64, os: macos-13 } steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Test for Mac Deps cache hit id: macdep-cache - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: ${{ github.workspace }}/pygame_mac_deps_${{ matrix.macarch }} # The hash of all files in buildconfig manylinux-build and macdependencies is @@ -116,17 +116,17 @@ jobs: CIBW_BEFORE_TEST: rm -rf ${{ github.workspace }}/pygame_mac_deps steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: pip cache - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: ~/Library/Caches/pip # This cache path is only right on mac key: pip-cache-${{ matrix.macarch }}-${{ matrix.os }} - name: Fetch Mac deps id: macdep-cache - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: ${{ github.workspace }}/pygame_mac_deps_${{ matrix.macarch }} key: macdep-${{ hashFiles('buildconfig/manylinux-build/**') }}-${{ hashFiles('buildconfig/macdependencies/*.sh') }}-${{ matrix.macarch }} diff --git a/.github/workflows/build-manylinux.yml b/.github/workflows/build-manylinux.yml index bac5362375..b3bda630ad 100644 --- a/.github/workflows/build-manylinux.yml +++ b/.github/workflows/build-manylinux.yml @@ -52,7 +52,7 @@ jobs: CIBW_ARCHS: ${{ matrix.arch }} steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Log in to the Container registry uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 diff --git a/.github/workflows/build-on-msys2.yml b/.github/workflows/build-on-msys2.yml index 315e7c41f1..6ac74c15f4 100644 --- a/.github/workflows/build-on-msys2.yml +++ b/.github/workflows/build-on-msys2.yml @@ -42,13 +42,12 @@ jobs: matrix: include: - { sys: mingw64, env: x86_64 } - - { sys: mingw32, env: i686 } - { sys: ucrt64, env: ucrt-x86_64 } - { sys: clang64, env: clang-x86_64 } # - { sys: clangarm64, env: clang-aarch64 } steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.sys }} diff --git a/.github/workflows/build-sdl3.yml b/.github/workflows/build-sdl3.yml index cb2c555f04..403141b51a 100644 --- a/.github/workflows/build-sdl3.yml +++ b/.github/workflows/build-sdl3.yml @@ -52,16 +52,26 @@ jobs: PG_DEPS_FROM_SYSTEM: 1 steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - - name: Install deps (linux) + - name: Install pygame deps (linux) if: matrix.os == 'ubuntu-24.04' run: sudo apt-get install libfreetype6-dev libportmidi-dev python3-dev - - name: Install deps (mac) + - name: Install pygame deps (mac) if: matrix.os == 'macos-14' run: brew install freetype portmidi + # taken from dependencies of the 'libsdl2-dev' package + - name: Install SDL deps (linux) + if: matrix.os == 'ubuntu-24.04' + run: > + sudo apt-get install libasound2-dev libdbus-1-dev libdecor-0-dev libdrm-dev + libegl-dev libgbm-dev libgl-dev libgles-dev libibus-1.0-dev libpulse-dev + libsamplerate0-dev libsndio-dev libudev-dev libwayland-dev libx11-dev + libxcursor-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev + libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev + # taken from https://wiki.libsdl.org/SDL3/Installation - name: Install SDL3 if: matrix.os != 'windows-latest' @@ -74,12 +84,8 @@ jobs: cmake --build . --config Release --parallel sudo cmake --install . --config Release - - name: Make sdist and install it - run: > - python3 -m pip install . -v -Csetup-args=-Dsdl_api=3 - -Csetup-args=-Dimage=disabled - -Csetup-args=-Dmixer=disabled - -Csetup-args=-Dfont=disabled + - name: Build with SDL3 + run: python3 dev.py build --sdl3 # - name: Run tests # env: diff --git a/.github/workflows/build-ubuntu-coverage.yml b/.github/workflows/build-ubuntu-coverage.yml index 5999252842..699448770e 100644 --- a/.github/workflows/build-ubuntu-coverage.yml +++ b/.github/workflows/build-ubuntu-coverage.yml @@ -58,25 +58,20 @@ jobs: PG_DEPS_FROM_SYSTEM: 1 steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Install deps - # install numpy from pip and not apt because the one from pip is newer, - # and has typestubs # https://github.com/actions/runner-images/issues/7192 # https://github.com/orgs/community/discussions/47863 run: | sudo apt-get update --fix-missing sudo apt-get install lcov -y sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev libfreetype6-dev libportmidi-dev python3-dev -y - pip3 install --upgrade pip - pip3 install meson-python ninja cython "sphinx<=7.2.6" # because we are doing --no-build-isolation - pip3 install numpy>=1.21.0 - name: Build with coverage hooks and install id: build run: | - pip3 install -e . --no-build-isolation -Cbuild-dir=./.mesonpy-rel -Csetup-args=-Dcoverage=true + python3 dev.py build --coverage - name: Run tests env: diff --git a/.github/workflows/build-ubuntu-sdist.yml b/.github/workflows/build-ubuntu-sdist.yml index d7382a34bb..c58b9d8a6c 100644 --- a/.github/workflows/build-ubuntu-sdist.yml +++ b/.github/workflows/build-ubuntu-sdist.yml @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false # if a particular matrix build fails, don't skip the rest matrix: - os: [ubuntu-24.04, ubuntu-22.04] + os: [ubuntu-22.04] env: # Pip now forces us to either make a venv or set this flag, so we will do @@ -58,7 +58,7 @@ jobs: PG_DEPS_FROM_SYSTEM: 1 steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Install deps # install numpy from pip and not apt because the one from pip is newer, @@ -89,7 +89,6 @@ jobs: # We upload the generated files under github actions assets - name: Upload sdist - if: matrix.os == 'ubuntu-24.04' # upload sdist only once uses: actions/upload-artifact@v4 with: name: pygame-wheels-sdist diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 6fc13d8203..d868bfabce 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -50,7 +50,7 @@ jobs: CIBW_ARCHS: ${{ matrix.winarch }} steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - uses: TheMrMilchmann/setup-msvc-dev@v3 # this lets us use the developer command prompt on windows with: diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml index a1746369b8..2594db25d7 100644 --- a/.github/workflows/cppcheck.yml +++ b/.github/workflows/cppcheck.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Install deps # https://github.com/actions/runner-images/issues/7192 diff --git a/.github/workflows/dev-check.yml b/.github/workflows/dev-check.yml new file mode 100644 index 0000000000..83d546df84 --- /dev/null +++ b/.github/workflows/dev-check.yml @@ -0,0 +1,42 @@ +name: python3 dev.py all + +# Run CI on changes to main branch, or any PR to main. Do not run CI on +# any other branch. +# Run on changes to all files. +on: + push: + branches: main + + pull_request: + branches: main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-dev-check + cancel-in-progress: true + +jobs: + dev-check: + runs-on: ubuntu-24.04 + + env: + # Pip now forces us to either make a venv or set this flag, so we will do + # this + PIP_BREAK_SYSTEM_PACKAGES: 1 + + # We are using dependencies installed from apt + PG_DEPS_FROM_SYSTEM: 1 + + # environment variables to set while testing + SDL_VIDEODRIVER: "dummy" + SDL_AUDIODRIVER: "disk" + + steps: + - uses: actions/checkout@v4.2.2 + + - name: Install deps + run: | + sudo apt-get update --fix-missing + sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev libfreetype6-dev libportmidi-dev python3-dev + + - name: Check dev.py all + run: python3 dev.py all diff --git a/.github/workflows/format-lint.yml b/.github/workflows/format-lint.yml deleted file mode 100644 index 99016b7dde..0000000000 --- a/.github/workflows/format-lint.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: python3 setup.py lint - -# Run lint CI on changes to main branch, or any PR to main. Do not run CI on -# any other branch. -# Run only if there are changes on files that are linted (C, Python and rst files) -on: - push: - branches: main - paths: - - '**.h' - - '**.c' - - '**.py' - - '**.rst' - - '.pre-commit-config.yaml' - - pull_request: - branches: main - paths: - - '**.h' - - '**.c' - - '**.py' - - '**.rst' - - '.pre-commit-config.yaml' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-format-lint - cancel-in-progress: true - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.1 - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - uses: pre-commit/action@v3.0.1 - - format-lint-code-check: - runs-on: ubuntu-24.04 - - env: - # Pip now forces us to either make a venv or set this flag, so we will do - # this - PIP_BREAK_SYSTEM_PACKAGES: 1 - - steps: - - uses: actions/checkout@v4.2.1 - - - name: Install deps - run: python3 -m pip install pylint sphinx"<7.2.0" - - - name: Check code linting - run: pylint src_py docs - - - name: Check docs changes are checked in - run: | - python3 buildconfig/make_docs.py - if [[ `git status --porcelain` ]]; then - echo "Generating docs caused changes. Please check them in." - echo "You may need to run: python3 buildconfig/make_docs.py full_generation" - # Run git status again, so people can see what changed. - git status --porcelain - exit 1 - fi diff --git a/.github/workflows/release-gh-draft.yml b/.github/workflows/release-gh-draft.yml index 3aebecc494..32917cf703 100644 --- a/.github/workflows/release-gh-draft.yml +++ b/.github/workflows/release-gh-draft.yml @@ -38,7 +38,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Download all artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 64aa7ac25b..97db455522 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -12,7 +12,7 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - - uses: actions/checkout@v4.2.1 + - uses: actions/checkout@v4.2.2 - name: Pull all release assets uses: robinraju/release-downloader@v1.11 diff --git a/buildconfig/download_win_prebuilt.py b/buildconfig/download_win_prebuilt.py index a03b225933..aa12faca0f 100644 --- a/buildconfig/download_win_prebuilt.py +++ b/buildconfig/download_win_prebuilt.py @@ -78,12 +78,12 @@ def get_urls(x86=True, x64=True): url_sha1 = [] url_sha1.extend([ [ - 'https://github.com/libsdl-org/SDL/releases/download/release-2.30.8/SDL2-devel-2.30.8-VC.zip', - '389a7575afaeccd3586a8105520cbec46b930dcb', + 'https://github.com/libsdl-org/SDL/releases/download/release-2.30.9/SDL2-devel-2.30.9-VC.zip', + 'd89a2ad46b98ba08db5ec5877cb2fde46e127825', ], [ - 'https://github.com/libsdl-org/SDL/releases/download/preview-3.1.3/SDL3-devel-3.1.3-VC.zip', - '8e4d7104193ba976406fe9968301de6f6b57f342' + 'https://github.com/libsdl-org/SDL/releases/download/preview-3.1.6/SDL3-devel-3.1.6-VC.zip', + '7a3b9ed85cfe735c7e106d98c4b6395a113e5d7e' ], [ 'https://github.com/pygame-community/SDL_image/releases/download/2.8.2-pgce/SDL2_image-devel-2.8.2-VCpgce.zip', @@ -238,23 +238,23 @@ def copy(src, dst): copy( os.path.join( temp_dir, - 'SDL2-devel-2.30.8-VC/SDL2-2.30.8' + 'SDL2-devel-2.30.9-VC/SDL2-2.30.9' ), os.path.join( move_to_dir, prebuilt_dir, - 'SDL2-2.30.8' + 'SDL2-2.30.9' ) ) copy( os.path.join( temp_dir, - 'SDL3-devel-3.1.3-VC/SDL3-3.1.3' + 'SDL3-devel-3.1.6-VC/SDL3-3.1.6' ), os.path.join( move_to_dir, prebuilt_dir, - 'SDL3-3.1.3' + 'SDL3-3.1.6' ) ) diff --git a/buildconfig/macdependencies/clean_usr_local.sh b/buildconfig/macdependencies/clean_usr_local.sh index 0551d67759..c058008ac2 100644 --- a/buildconfig/macdependencies/clean_usr_local.sh +++ b/buildconfig/macdependencies/clean_usr_local.sh @@ -15,6 +15,7 @@ ln -s /usr/bin/git /opt/homebrew/bin/git rm -rf /usr/local/lib/libtiff* rm -rf /usr/local/lib/libzstd* rm -rf /usr/local/lib/libwebp* +rm -rf /usr/local/lib/libdeflate* rm -rf /usr/local/lib/libsndfile* rm -rf /usr/local/lib/glib* rm -rf /usr/local/lib/libglib* @@ -27,6 +28,7 @@ rm -rf /usr/local/opt/freetype* rm -rf /usr/local/Cellar/libtiff /opt/homebrew/Cellar/libtiff rm -rf /usr/local/Cellar/libsndfile /opt/homebrew/Cellar/libsndfile +rm -rf /usr/local/Cellar/libdeflate* /opt/homebrew/Cellar/libdeflate* rm -rf /usr/local/Cellar/glib /opt/homebrew/Cellar/glib rm -rf /usr/local/Cellar/brotli /opt/homebrew/Cellar/brotli rm -rf /usr/local/Cellar/pcre* /opt/homebrew/Cellar/pcre* diff --git a/buildconfig/manylinux-build/docker_base/sdl_libs/build-sdl2-libs.sh b/buildconfig/manylinux-build/docker_base/sdl_libs/build-sdl2-libs.sh index f096177fec..d0546b18bb 100644 --- a/buildconfig/manylinux-build/docker_base/sdl_libs/build-sdl2-libs.sh +++ b/buildconfig/manylinux-build/docker_base/sdl_libs/build-sdl2-libs.sh @@ -3,7 +3,7 @@ set -e -x cd $(dirname `readlink -f "$0"`) -SDL2_VER="2.30.8" +SDL2_VER="2.30.9" SDL2="SDL2-$SDL2_VER" IMG2_VER="2.8.2" IMG2="SDL2_image-$IMG2_VER" diff --git a/buildconfig/manylinux-build/docker_base/sdl_libs/sdl2.sha512 b/buildconfig/manylinux-build/docker_base/sdl_libs/sdl2.sha512 index 699b618be4..2cae1f3830 100644 --- a/buildconfig/manylinux-build/docker_base/sdl_libs/sdl2.sha512 +++ b/buildconfig/manylinux-build/docker_base/sdl_libs/sdl2.sha512 @@ -1,4 +1,4 @@ -72e49d8a67f5ca1241a262e7e7ae7f6ff148e8774343110db652589ab2e72d3425534ca7f8c7825b2ae1afc779c09228da33a9586219ac4e53546a4930380b64 SDL2-2.30.8.tar.gz +30dfa86fcced174fef0ed78ffa53476a31765e19cdcdf8233ab92876445b4dedaa758fc42a3ec332324d13faa2daafcadcc44fc0f536a2969ef836162ec3cd36 SDL2-2.30.9.tar.gz 0ff345824f95158dfa72f83f9d4a540601c178cd759334bf849c14a2920b5330d0763413b58c08b3deba8d3a4ccb6ea2a8159f87efe4cbb0e8ea850f63d09454 SDL2_image-2.8.2.tar.gz 5ddbc4b0b5fad2e0844a503daa79564b912654192599ef8fa7698531f08323ce01801f6bb17b2b3905020a3df362a967b7566ae725eb085da991578cc0807aad SDL2_mixer-2.8.0.tar.gz 34a1d210d8f1b1e802139d65ba47e36033bb7881e75a8862c1b1c515565bef85e3d81ee42e952aa664de043debef387ba60088a9cf3ba3297413db39a13af912 SDL2_ttf-2.22.0.tar.gz diff --git a/dev.py b/dev.py new file mode 100644 index 0000000000..c3f27a00b8 --- /dev/null +++ b/dev.py @@ -0,0 +1,481 @@ +""" +This script is aimed at making development more convenient by having all useful +development commands under one place. + +For help on how to use this, do `python dev.py -h` to get a general overview +and `python dev.py [subcommand] -h` to get subcommand specific help. +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any, Union +from enum import Enum + +MOD_NAME = "pygame-ce" +DIST_DIR = "dist" + +VENV_NAME = "dev_venv" + +source_tree = Path(__file__).parent +venv_path = source_tree / VENV_NAME +pyproject_path = source_tree / "pyproject.toml" + +SDL3_ARGS = [ + "-Csetup-args=-Dsdl_api=3", + "-Csetup-args=-Dimage=disabled", + "-Csetup-args=-Dmixer=disabled", + "-Csetup-args=-Dfont=disabled", +] +COVERAGE_ARGS = ["-Csetup-args=-Dcoverage=true"] + +# We assume this script works with any pip version above this. +PIP_MIN_VERSION = "23.1" + + +class Colors(Enum): + RESET = "\033[0m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + + +# logic based on https://docs.python.org/3.13/using/cmdline.html#controlling-color +def has_color(): + # highest precedence + python_colors = os.environ.get("PYTHON_COLORS", "").strip() + if python_colors == "1": + return True + + if python_colors == "0": + return False + + # second highest precedence + if "NO_COLOR" in os.environ: + return False + + # third highest precedence + if "FORCE_COLOR" in os.environ: + return True + + # lowest precedence + return os.environ.get("TERM", "").strip().lower() != "dumb" + + +def pprint(arg: str, col: Colors = Colors.YELLOW): + do_col = has_color() + start = Colors.BLUE.value if do_col else "" + mid = col.value if do_col else "" + end = Colors.RESET.value if do_col else "" + print(f"{start}[dev.py] {mid}{arg}{end}", flush=True) + + +def cmd_run( + cmd: list[Union[str, Path]], + capture_output: bool = False, + error_on_output: bool = False, +) -> str: + if error_on_output: + capture_output = True + + norm_cmd = [str(i) for i in cmd] + pprint(f"> {' '.join(norm_cmd)}", Colors.CYAN) + try: + ret = subprocess.run( + norm_cmd, + stdout=subprocess.PIPE if capture_output else sys.stdout, + stderr=subprocess.STDOUT, + text=capture_output, + cwd=source_tree, + ) + except FileNotFoundError: + pprint(f"{norm_cmd[0]}: command not found", Colors.RED) + sys.exit(1) + + if ret.stdout: + print(ret.stdout, end="", flush=True) + + if (error_on_output and ret.stdout) and not ret.returncode: + # Convert success code to failure code if we have stdout and need to + # error + ret.returncode = 1 + + ret.check_returncode() + return ret.stdout + + +def pip_install(py: Path, args: list[str]): + return cmd_run([py, "-m", "pip", "install", "-v", *args]) + + +def get_pyproject_list_param(section: str, key: str) -> list[str]: + with open(pyproject_path, "r", encoding="utf-8") as f: + content = f.read() + if sys.version_info >= (3, 11): + import tomllib + + cur = tomllib.loads(content) + for i in section.split("."): + cur = cur[i] + + return cur[key] + + # hacky solution, because we don't have tomllib in stdlib on older + # python versions + import ast + import re + + # this regex only works to extract a list, nothing else + pattern = rf"\[{section}\].*\n\s*{key}\s*=\s*(\[.*?\])" + match = re.search(pattern, content, re.DOTALL) + if not match: + return [] + + return ast.literal_eval(match.group(1).strip()) + + +def get_build_deps(): + return set(get_pyproject_list_param("build-system", "requires")) + + +def get_cibw_setup_args(): + return [ + f"-Csetup-args={i}" + for i in get_pyproject_list_param( + "tool.cibuildwheel.config-settings", "setup-args" + ) + ] + + +def show_diff_and_suggest_fix(parent: str): + try: + cmd_run(["git", "status", "--porcelain"], error_on_output=True) + except subprocess.CalledProcessError: + try: + cmd_run(["git", "diff"]) + finally: + pprint(f"Running '{parent}' caused changes") + pprint(f"You need to run `python3 dev.py {parent}` and commit the changes") + pprint( + "Alternatively, you may run `python3 dev.py all` to catch more issues" + ) + raise + + +def check_version_atleast(version: str, min_version: str): + try: + version_tup = tuple(int(i.strip()) for i in version.split(".")) + min_version_tup = tuple(int(i.strip()) for i in min_version.split(".")) + except (AttributeError, TypeError, ValueError): + return False + + return version_tup >= min_version_tup + + +def check_module_in_constraint(mod: str, constraint: str): + constraint_mod = re.match(r"[a-z0-9._-]*", constraint.lower().strip()) + if not constraint_mod: + return False + + return mod.lower().strip() == constraint_mod[0] + + +class Dev: + def __init__(self) -> None: + self.py: Path = Path(sys.executable) + self.args: dict[str, Any] = {} + + self.deps: dict[str, set[str]] = { + "build": get_build_deps(), + "docs": get_build_deps(), + "test": {"numpy"}, + "lint": {"pylint==3.3.0", "numpy"}, + "stubs": {"mypy==1.11.2", "numpy"}, + "format": {"pre-commit==3.8.0"}, + } + self.deps["all"] = set() + for k in self.deps.values(): + self.deps["all"] |= k + + def cmd_build(self): + wheel_dir = self.args.get("wheel", DIST_DIR) + debug = self.args.get("debug", False) + lax = self.args.get("lax", False) + sdl3 = self.args.get("sdl3", False) + coverage = self.args.get("coverage", False) + if wheel_dir and coverage: + pprint("Cannot pass --wheel and --coverage together", Colors.RED) + sys.exit(1) + + build_suffix = "" + if debug: + build_suffix += "-dbg" + if lax: + build_suffix += "-lax" + if sdl3: + build_suffix += "-sdl3" + if coverage: + build_suffix += "-cov" + install_args = [ + "--no-build-isolation", + f"-Cbuild-dir=.mesonpy-build{build_suffix}", + ] + + if not wheel_dir: + # editable install + install_args.append("--editable") + + install_args.append(".") + + if debug: + install_args.append("-Csetup-args=-Dbuildtype=debug") + + if not lax: + # use the same flags as CI + install_args.extend(get_cibw_setup_args()) + + if sdl3: + install_args.extend(SDL3_ARGS) + + if coverage: + install_args.extend(COVERAGE_ARGS) + + info_str = f"with {debug=}, {lax=}, {sdl3=}, and {coverage=}" + if wheel_dir: + pprint(f"Building wheel at '{wheel_dir}' ({info_str})") + cmd_run( + [self.py, "-m", "pip", "wheel", "-v", "-w", wheel_dir, *install_args] + ) + pprint("Installing wheel") + pip_install( + self.py, ["--no-index", "--force", "--find-links", wheel_dir, MOD_NAME] + ) + else: + pprint(f"Installing in editable mode ({info_str})") + pip_install(self.py, install_args) + + def cmd_docs(self): + full = self.args.get("full", False) + + pprint(f"Generating docs (with {full=})") + extra_args = ["full_generation"] if full else [] + cmd_run([self.py, "buildconfig/make_docs.py", *extra_args]) + + if "CI" in os.environ: + show_diff_and_suggest_fix("docs") + + def cmd_lint(self): + pprint("Linting code (with pylint)") + cmd_run([self.py, "-m", "pylint", "src_py", "docs"]) + + def cmd_stubs(self): + pprint("Generating and testing type stubs (with mypy)") + cmd_run([self.py, "buildconfig/stubs/gen_stubs.py"]) + if "CI" in os.environ: + show_diff_and_suggest_fix("stubs") + + cmd_run([self.py, "buildconfig/stubs/stubcheck.py"]) + + def cmd_format(self): + pre_commit = self.py.parent / "pre-commit" + + pprint("Formatting code (with pre-commit)") + try: + cmd_run( + [ + pre_commit if pre_commit.exists() else "pre-commit", + "run", + "--all-files", + ] + ) + except subprocess.CalledProcessError: + # pre_commit may set error code when it modifies a file, ignore it + pass + + if "CI" in os.environ: + show_diff_and_suggest_fix("format") + + def cmd_test(self): + mod = self.args.get("mod", []) + + if mod: + pprint(f"Running tests (with module(s): {' '.join(mod)})") + for i in mod: + cmd_run([self.py, "-m", f"pygame.tests.{i}_test"]) + else: + pprint("Running tests (with all modules)") + cmd_run([self.py, "-m", "pygame.tests"]) + + def cmd_all(self): + self.cmd_format() + self.cmd_docs() + self.cmd_build() + self.cmd_stubs() + self.cmd_lint() + self.cmd_test() + + def parse_args(self): + parser = argparse.ArgumentParser( + description=( + "Build commands for the project. " + "For more info on any subcommand you can run -h/--help on it like: " + "dev.py build -h" + ) + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + parser.add_argument( + "--venv", + action="store_true", + help="Make and use a venv (recommended, but not default)", + ) + parser.add_argument( + "--ignore-dep", action="append", help="Dependency to ignore in pip install" + ) + + # Build command + build_parser = subparsers.add_parser("build", help="Build the project") + build_parser.add_argument( + "--wheel", + nargs="?", + const=DIST_DIR, # Used if argument is provided without a value + default="", # Used if argument is not provided at all + help=( + "Generate a wheel and do a regular install from it. By default, this " + "value is empty in which case the script does an 'editable' install. " + "A value can passed optionally, to indicate the directory to place the " + f"wheel (if not passed, '{DIST_DIR}' is used)" + ), + ) + build_parser.add_argument( + "--debug", + action="store_true", + help="Install in debug mode (optimizations disabled and debug symbols enabled)", + ) + build_parser.add_argument( + "--lax", + action="store_true", + help="Be lax about build warnings, allow the build to succeed with them", + ) + build_parser.add_argument( + "--sdl3", + action="store_true", + help="Build against SDL3 instead of the default SDL2", + ) + build_parser.add_argument( + "--coverage", + action="store_true", + help=( + "Do a coverage build. To generate a test coverage report, you need " + "to compile pygame with this flag and run tests. This flag is only " + "supported if the underlying compiler supports the --coverage argument" + ), + ) + + # Docs command + docs_parser = subparsers.add_parser("docs", help="Generate docs") + docs_parser.add_argument( + "--full", + action="store_true", + help="Force a full regeneration of docs, ignoring previous build cache", + ) + + # Test command + test_parser = subparsers.add_parser("test", help="Run tests") + test_parser.add_argument( + "mod", + nargs="*", + help=( + "Name(s) of sub-module(s) to test. If no args are given all are tested" + ), + ) + + # Lint command + subparsers.add_parser("lint", help="Lint code") + + # Stubs command + subparsers.add_parser("stubs", help="Generate and test type stubs") + + # Format command + subparsers.add_parser("format", help="Format code") + + # All command + subparsers.add_parser( + "all", + help=( + "Run all the subcommands. This is handy for checking that your work is " + "ready to be submitted" + ), + ) + + args = parser.parse_args() + self.args = vars(args) + + def prep_env(self): + if self.args["venv"]: + if venv_path.is_dir(): + pprint(f"Using existing virtual environment '{venv_path}'") + else: + cmd_run([sys.executable, "-m", "venv", VENV_NAME]) + pprint(f"Virtual environment '{venv_path}' created") + + bin = venv_path / "Scripts" if os.name == "nt" else venv_path / "bin" + self.py = bin / "python" + else: + pprint(f"Using python '{self.py}'") + + # set PATH to give high priority to executables in the python bin folder + # this is where the binaries for meson/ninja/cython/sphinx/etc are installed + os.environ["PATH"] = f"{self.py.parent}{os.pathsep}{os.environ.get('PATH', '')}" + + pprint("Checking pip version") + pip_v = cmd_run([self.py, "-m", "pip", "-V"], capture_output=True) + try: + pip_version = pip_v.split()[1] + except (AttributeError, IndexError): + pip_version = "UNKNOWN" + + pprint(f"Determined pip version: {pip_version}") + if not check_version_atleast(pip_version, PIP_MIN_VERSION): + pprint("pip version is too old or unknown, attempting pip upgrade") + pip_install(self.py, ["-U", "pip"]) + + deps = self.deps.get(self.args["command"]) + ignored_deps = self.args["ignore_dep"] + deps_filtered = deps.copy() + if ignored_deps: + for constr in deps: + for dep in ignored_deps: + if check_module_in_constraint(dep, constr): + deps_filtered.remove(constr) + break + + if deps: + pprint("Installing dependencies") + pip_install(self.py, list(deps_filtered)) + + def run(self): + self.parse_args() + self.prep_env() + try: + func = getattr(self, f"cmd_{self.args['command']}") + func() + except subprocess.CalledProcessError as e: + pprint(f"Process exited with error code {e.returncode}", Colors.RED) + sys.exit(e.returncode) + except KeyboardInterrupt: + pprint("Got KeyboardInterrupt, exiting", Colors.RED) + sys.exit(1) + + pprint("Process exited successfully", Colors.GREEN) + + +if __name__ == "__main__": + Dev().run() diff --git a/docs/README.md b/docs/README.md index 349849b658..337938d68e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,9 +9,8 @@ but the documentation can also be launched with `python -m pygame.docs` Steps: -- Install Sphinx (`pip install Sphinx`) - Fork the pygame-ce repository, download and navigate to it in the terminal -- Run `python -m buildconfig docs` +- Run `python dev.py docs` - If you are using the legacy `python setup.py docs` (which is now deprecated): - (Run `python -m pip install -U pip setuptools` first if `ModuleNotFoundError: No module named setuptools` occurs) @@ -30,7 +29,7 @@ is useful when editing the theme CSS. --- **INSTEAD USE** --- -There is also `python -m buildconfig docs full_generation` for regenerating +There is also `python dev.py docs --full` for regenerating everything regardless of whether Sphinx thinks it should be regenerated. This is useful when editing the theme CSS. diff --git a/docs/reST/ref/draw.rst b/docs/reST/ref/draw.rst index 44ac398a3c..53f5c35b44 100644 --- a/docs/reST/ref/draw.rst +++ b/docs/reST/ref/draw.rst @@ -493,7 +493,7 @@ object around the draw calls (see :func:`pygame.Surface.lock` and .. versionchangedold:: 2.0.0 Added support for keyword arguments. .. versionchanged:: 2.4.0 Removed deprecated 'blend' argument .. versionchanged:: 2.5.0 ``blend`` argument readded for backcompat, but will always raise a deprecation exception when used - .. versionchanged:: 2.5.2 Added line width + .. versionchanged:: 2.5.3 Added line width .. ## pygame.draw.aaline ## diff --git a/docs/reST/ref/window.rst b/docs/reST/ref/window.rst index dfd293f166..7c78de9dc0 100644 --- a/docs/reST/ref/window.rst +++ b/docs/reST/ref/window.rst @@ -12,7 +12,7 @@ The Window class (formerly known as _sdl2.video.Window), is a newly published feature of pygame-ce 2.5.2. This class allows for programs - to drive multiple Windows on-screen at once, something not possible with + to drive multiple windows on-screen at once, something not possible with the :func:`pygame.display.set_mode` API. Not everything possible with :mod:`pygame.display` is possible yet in the Window API, but the new window class will continue to be developed, and we're excited to share @@ -59,10 +59,10 @@ pygame.quit() raise SystemExit - Event behavior if multiple Windows are created: When the close button is + Event behavior if multiple ``Window``\ s are created: When the close button is pressed, a ``WINDOWCLOSE`` event is sent. You need to explicitly destroy - the window. Note that the event ``QUIT`` will only be sent if all Window - has been destroyed. + the window. Note that the event ``QUIT`` will only be sent if all + ``Window``\ s have been destroyed. .. code-block:: python diff --git a/examples/README.rst b/examples/README.rst index fce9b30b90..1e38461b5d 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -78,6 +78,9 @@ music_drop_fade.py several events. Uses fade_ms added in pygame2, as well as set_endevent, set_volume, drag and drop events, and the scrap module. +ninepatch.py + Demonstrate the purpose of the 9-patch scale method and a way to implement it. + pixelarray.py Process whole arrays of pixels at a time. Like numpy, but for pixels, and also built into pygame. diff --git a/examples/audiocapture.py b/examples/audiocapture.py index 3811a20567..c4026516f9 100644 --- a/examples/audiocapture.py +++ b/examples/audiocapture.py @@ -18,14 +18,6 @@ ) from pygame._sdl2.mixer import set_post_mix - -pygame.mixer.pre_init(44100, 32, 2, 512) -pygame.init() - -# init_subsystem(INIT_AUDIO) -names = get_audio_device_names(True) -print(names) - sounds = [] sound_chunks = [] @@ -49,31 +41,42 @@ def postmix_callback(postmix, audiomemoryview): print(postmix) -set_post_mix(postmix_callback) +def main(): + pygame.mixer.pre_init(44100, 32, 2, 512) + pygame.init() -audio = AudioDevice( - devicename=names[0], - iscapture=True, - frequency=44100, - audioformat=AUDIO_F32, - numchannels=2, - chunksize=512, - allowed_changes=AUDIO_ALLOW_FORMAT_CHANGE, - callback=callback, -) -# start recording. -audio.pause(0) + # init_subsystem(INIT_AUDIO) + names = get_audio_device_names(True) + print(names) + + set_post_mix(postmix_callback) + + audio = AudioDevice( + devicename=names[0], + iscapture=True, + frequency=44100, + audioformat=AUDIO_F32, + numchannels=2, + chunksize=512, + allowed_changes=AUDIO_ALLOW_FORMAT_CHANGE, + callback=callback, + ) + # start recording. + audio.pause(0) + + print(audio) -print(audio) + print(f"recording with '{names[0]}'") + time.sleep(5) -print(f"recording with '{names[0]}'") -time.sleep(5) + print("Turning data into a pygame.mixer.Sound") + sound = pygame.mixer.Sound(buffer=b"".join(sound_chunks)) + print("playing back recorded sound") + sound.play() + time.sleep(5) + pygame.quit() -print("Turning data into a pygame.mixer.Sound") -sound = pygame.mixer.Sound(buffer=b"".join(sound_chunks)) -print("playing back recorded sound") -sound.play() -time.sleep(5) -pygame.quit() +if __name__ == "__main__": + main() diff --git a/examples/font_viewer.py b/examples/font_viewer.py index 67569e4351..3acd24c034 100644 --- a/examples/font_viewer.py +++ b/examples/font_viewer.py @@ -122,7 +122,7 @@ def render_fonts(self, text="A display of font &N"): line = text.replace("&N", name) try: surf = font.render( - line, 1, color, self.back_color, self.screen_size[0] - 20 + line, True, color, self.back_color, self.screen_size[0] - 20 ) except pygame.error as e: print(e) @@ -281,5 +281,6 @@ def handle_events(self): return True -viewer = FontViewer() -pygame.quit() +if __name__ == "__main__": + viewer = FontViewer() + pygame.quit() diff --git a/examples/go_over_there.py b/examples/go_over_there.py index 10ff3742ee..7284134190 100644 --- a/examples/go_over_there.py +++ b/examples/go_over_there.py @@ -19,10 +19,6 @@ SCREEN_SIZE = pygame.Vector2(1000, 600) CIRCLE_RADIUS = 5 -pygame.init() -screen = pygame.display.set_mode(SCREEN_SIZE) -clock = pygame.Clock() - target_position = None balls = [] @@ -49,35 +45,47 @@ def reset(): balls.append(b) -reset() -delta_time = 0 -running = True -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False +def main(): + global target_position + global balls - if event.type == pygame.MOUSEBUTTONUP: - target_position = pygame.mouse.get_pos() + pygame.init() + screen = pygame.display.set_mode(SCREEN_SIZE) + clock = pygame.Clock() - if event.type == pygame.KEYUP: - if event.key == pygame.K_ESCAPE: + reset() + delta_time = 0 + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: running = False - if event.key == pygame.K_r: - reset() + if event.type == pygame.MOUSEBUTTONUP: + target_position = pygame.mouse.get_pos() + + if event.type == pygame.KEYUP: + if event.key == pygame.K_ESCAPE: + running = False - screen.fill((31, 143, 65)) + if event.key == pygame.K_r: + reset() + + screen.fill((31, 143, 65)) + + for o in balls: + if target_position is not None: + o.position.move_towards_ip(target_position, o.speed * delta_time) + pygame.draw.circle(screen, (118, 207, 145), o.position, CIRCLE_RADIUS) + + pygame.display.flip() + delta_time = clock.tick(60) + pygame.display.set_caption( + f"fps: {round(clock.get_fps(), 2)}, ball count: {len(balls)}" + ) - for o in balls: - if target_position is not None: - o.position.move_towards_ip(target_position, o.speed * delta_time) - pygame.draw.circle(screen, (118, 207, 145), o.position, CIRCLE_RADIUS) + pygame.quit() - pygame.display.flip() - delta_time = clock.tick(60) - pygame.display.set_caption( - f"fps: {round(clock.get_fps(), 2)}, ball count: {len(balls)}" - ) -pygame.quit() +if __name__ == "__main__": + main() diff --git a/examples/multiplayer_joystick.py b/examples/multiplayer_joystick.py index a95d652b6e..6c8eac7a07 100644 --- a/examples/multiplayer_joystick.py +++ b/examples/multiplayer_joystick.py @@ -1,8 +1,10 @@ import sys import pygame +WIDTH, HEIGHT = 500, 500 + -def connect_joystick(index): +def connect_joystick(index, active_players, players, colors): if len(active_players) < len(players): joy = pygame.Joystick(index) index = players.index(None) @@ -18,7 +20,7 @@ def connect_joystick(index): print(f"P{index + 1} Connected") -def disconnect_joystick(instance_id: int): +def disconnect_joystick(instance_id: int, active_players, players): index = active_players[instance_id] players[index] = None del active_players[instance_id] @@ -39,106 +41,116 @@ def create_surf(size, color): return surf -pygame.init() +def main(): + pygame.init() + + screen = pygame.display.set_mode((WIDTH, HEIGHT)) + pygame.display.set_caption("Multiplayer Joystick example") + clock = pygame.Clock() + font_b = pygame.font.SysFont(None, 25) + font_a = pygame.font.SysFont(None, 16) + + players = [None, None] # two players limit + active_players = {} + + colors = [ + create_surf((32, 32), (220, 180, 10)), + create_surf((32, 32), (60, 230, 170)), + create_surf((32, 32), (230, 20, 70)), + create_surf((32, 32), (20, 170, 230)), + ] + are_no_controllers_connected = True + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.JOYDEVICEADDED: + if len(active_players) < len(players): + # connect controller + connect_joystick( + event.device_index, active_players, players, colors + ) + are_no_controllers_connected = False + elif event.type == pygame.JOYDEVICEREMOVED: + # disconnect controller + if event.instance_id in active_players: + disconnect_joystick(event.instance_id, active_players, players) + # check if there is at least one controller connected + are_no_controllers_connected = True + for player in players: + if player: + are_no_controllers_connected = False + break + elif event.type == pygame.JOYBUTTONDOWN: + if event.instance_id in active_players: + # join player + if event.button == 0: + index = active_players[event.instance_id] + players[index]["joined"] = True + print(f"P{index + 1} joined") + # leave player + if event.button == 1: + index = active_players[event.instance_id] + if players[index]["joined"]: + players[index]["joined"] = False + players[index]["pos"] = [WIDTH * 0.25 + index * 64, 128] + print(f"P{index + 1} leave") + elif event.type == pygame.JOYAXISMOTION: + if event.instance_id in active_players: + # change the color if player still hasn't joined + if event.axis == 0: + index = active_players[event.instance_id] + player = players[index] + if not player["joined"]: + if event.value >= 1.0: + player["surf_idx"] += 1 + elif event.value <= -1.0: + player["surf_idx"] -= 1 + player["surf_idx"] = player["surf_idx"] % len(colors) + player["surf"] = colors[player["surf_idx"]] + + screen.fill((30, 30, 30)) + pygame.draw.line(screen, (230, 230, 230), (0, 96), (WIDTH, 96), 2) + + # update and draw players + for player in players: + if player: + control_player(player) + screen.blit(player["surf"], player["pos"]) + + # draw available colors + for i, surf in enumerate(colors): + screen.blit(surf, (WIDTH * 0.25 + i * 64, 32)) + + # show message for connecting a controller + if are_no_controllers_connected: + screen.blit( + font_b.render( + "Please connect a controller.", + True, + (230, 230, 230), + None, + 500 - 20, + ), + (WIDTH * 0.3, HEIGHT * 0.5), + ) -WIDTH, HEIGHT = 500, 500 -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -pygame.display.set_caption("Multiplayer Joystick example") -clock = pygame.Clock() -font_b = pygame.font.SysFont(None, 25) -font_a = pygame.font.SysFont(None, 16) - -players = [None, None] # two players limit -active_players = {} - -colors = [ - create_surf((32, 32), (220, 180, 10)), - create_surf((32, 32), (60, 230, 170)), - create_surf((32, 32), (230, 20, 70)), - create_surf((32, 32), (20, 170, 230)), -] -are_no_controllers_connected = True - -while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - sys.exit() - elif event.type == pygame.JOYDEVICEADDED: - if len(active_players) < len(players): - # connect controller - connect_joystick(event.device_index) - are_no_controllers_connected = False - elif event.type == pygame.JOYDEVICEREMOVED: - # disconnect controller - if event.instance_id in active_players: - disconnect_joystick(event.instance_id) - # check if there is at least one controller connected - are_no_controllers_connected = True - for player in players: - if player: - are_no_controllers_connected = False - break - elif event.type == pygame.JOYBUTTONDOWN: - if event.instance_id in active_players: - # join player - if event.button == 0: - index = active_players[event.instance_id] - players[index]["joined"] = True - print(f"P{index + 1} joined") - # leave player - if event.button == 1: - index = active_players[event.instance_id] - if players[index]["joined"]: - players[index]["joined"] = False - players[index]["pos"] = [WIDTH * 0.25 + index * 64, 128] - print(f"P{index + 1} leave") - elif event.type == pygame.JOYAXISMOTION: - if event.instance_id in active_players: - # change the color if player still hasn't joined - if event.axis == 0: - index = active_players[event.instance_id] - player = players[index] - if not player["joined"]: - if event.value >= 1.0: - player["surf_idx"] += 1 - elif event.value <= -1.0: - player["surf_idx"] -= 1 - player["surf_idx"] = player["surf_idx"] % len(colors) - player["surf"] = colors[player["surf_idx"]] - - screen.fill((30, 30, 30)) - pygame.draw.line(screen, (230, 230, 230), (0, 96), (WIDTH, 96), 2) - - # update and draw players - for player in players: - if player: - control_player(player) - screen.blit(player["surf"], player["pos"]) - - # draw available colors - for i, surf in enumerate(colors): - screen.blit(surf, (WIDTH * 0.25 + i * 64, 32)) - - # show message for connecting a controller - if are_no_controllers_connected: screen.blit( - font_b.render( - "Please connect a controller.", True, (230, 230, 230), None, 500 - 20 + font_a.render( + "A: join B: leave Joystick: move / change color", + True, + (230, 230, 230), + None, + WIDTH - 20, ), - (WIDTH * 0.3, HEIGHT * 0.5), + (10, HEIGHT - 20), ) - screen.blit( - font_a.render( - "A: join B: leave Joystick: move / change color", - True, - (230, 230, 230), - None, - WIDTH - 20, - ), - (10, HEIGHT - 20), - ) - - clock.tick(60) - pygame.display.update() + clock.tick(60) + pygame.display.update() + + +if __name__ == "__main__": + main() diff --git a/examples/ninepatch.py b/examples/ninepatch.py index ddcfdb9220..ccdac7dc4f 100644 --- a/examples/ninepatch.py +++ b/examples/ninepatch.py @@ -17,7 +17,6 @@ import os import pygame -import typing SCREEN_SIZE = pygame.Vector2(600, 500) SCALE_SIZE = pygame.Vector2(500, 150) @@ -28,7 +27,7 @@ def ninepatch_scale( surface: pygame.Surface, - size: typing.Sequence[int], + size: pygame.typing.Point, corner_size: int, alpha: bool = True, smooth: bool = False, diff --git a/examples/prevent_display_stretching.py b/examples/prevent_display_stretching.py index 53dafafa88..35c0ad5d1f 100644 --- a/examples/prevent_display_stretching.py +++ b/examples/prevent_display_stretching.py @@ -29,64 +29,69 @@ raise NotImplementedError("this script requires Windows Vista or newer") import pygame - import ctypes -# Determine whether or not the user would like to prevent stretching -if os.path.basename(sys.executable) == "pythonw.exe": - selection = "y" -else: - selection = None - while selection not in ("y", "n"): - selection = input("Prevent stretching? (y/n): ").strip().lower() - -if selection == "y": - msg = "Stretching is prevented." -else: - msg = "Stretching is not prevented." - -# Prevent stretching -if selection == "y": - user32 = ctypes.windll.user32 - user32.SetProcessDPIAware() - -# Show screen -pygame.display.init() -RESOLUTION = (350, 350) -screen = pygame.display.set_mode(RESOLUTION) - -# Render message onto a surface -pygame.font.init() -font = pygame.Font(None, 36) -msg_surf = font.render(msg, 1, TEXTCOLOR) -res_surf = font.render("Intended resolution: %ix%i" % RESOLUTION, 1, TEXTCOLOR) - -# Control loop -running = True -clock = pygame.Clock() -counter = 0 -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - screen.fill(BACKGROUNDCOLOR) - - # Draw lines which will be blurry if the window is stretched - # or clear if the window is not stretched. - pygame.draw.line(screen, AXISCOLOR, (0, counter), (RESOLUTION[0] - 1, counter)) - pygame.draw.line(screen, AXISCOLOR, (counter, 0), (counter, RESOLUTION[1] - 1)) - - # Blit message onto screen surface - msg_blit_rect = screen.blit(msg_surf, (0, 0)) - screen.blit(res_surf, (0, msg_blit_rect.bottom)) - - clock.tick(10) - - pygame.display.flip() - - counter += 1 - if counter == RESOLUTION[0]: - counter = 0 - -pygame.quit() + +def main(): + # Determine whether or not the user would like to prevent stretching + if os.path.basename(sys.executable) == "pythonw.exe": + selection = "y" + else: + selection = None + while selection not in ("y", "n"): + selection = input("Prevent stretching? (y/n): ").strip().lower() + + if selection == "y": + msg = "Stretching is prevented." + else: + msg = "Stretching is not prevented." + + # Prevent stretching + if selection == "y": + user32 = ctypes.windll.user32 + user32.SetProcessDPIAware() + + # Show screen + pygame.display.init() + RESOLUTION = (350, 350) + screen = pygame.display.set_mode(RESOLUTION) + + # Render message onto a surface + pygame.font.init() + font = pygame.Font(None, 36) + msg_surf = font.render(msg, True, TEXTCOLOR) + res_surf = font.render("Intended resolution: %ix%i" % RESOLUTION, 1, TEXTCOLOR) + + # Control loop + running = True + clock = pygame.Clock() + counter = 0 + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + screen.fill(BACKGROUNDCOLOR) + + # Draw lines which will be blurry if the window is stretched + # or clear if the window is not stretched. + pygame.draw.line(screen, AXISCOLOR, (0, counter), (RESOLUTION[0] - 1, counter)) + pygame.draw.line(screen, AXISCOLOR, (counter, 0), (counter, RESOLUTION[1] - 1)) + + # Blit message onto screen surface + msg_blit_rect = screen.blit(msg_surf, (0, 0)) + screen.blit(res_surf, (0, msg_blit_rect.bottom)) + + clock.tick(10) + + pygame.display.flip() + + counter += 1 + if counter == RESOLUTION[0]: + counter = 0 + + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/resizing_new.py b/examples/resizing_new.py index cccde5badd..48eeadf06a 100644 --- a/examples/resizing_new.py +++ b/examples/resizing_new.py @@ -1,41 +1,49 @@ #!/usr/bin/env python import pygame -pygame.init() RESOLUTION = (160, 120) FPS = 30 -clock = pygame.Clock() - -screen = pygame.display.set_mode(RESOLUTION, pygame.RESIZABLE) -pygame.display._set_autoresize(False) - -# MAIN LOOP - -done = False - -i = 0 -j = 0 - -while not done: - for event in pygame.event.get(): - if event.type == pygame.KEYDOWN and event.key == pygame.K_q: - done = True - elif event.type == pygame.VIDEORESIZE: - screen = pygame.display.get_surface() - elif event.type == pygame.QUIT: - done = True - i += 1 - i = i % screen.get_width() - j += i % 2 - j = j % screen.get_height() - - screen.fill((255, 0, 255)) - pygame.draw.circle(screen, (0, 0, 0), (100, 100), 20) - pygame.draw.circle(screen, (0, 0, 200), (0, 0), 10) - pygame.draw.circle(screen, (200, 0, 0), (160, 120), 30) - pygame.draw.line(screen, (250, 250, 0), (0, 120), (160, 0)) - pygame.draw.circle(screen, (255, 255, 255), (i, j), 5) - - pygame.display.flip() - clock.tick(FPS) -pygame.quit() + + +def main(): + pygame.init() + + clock = pygame.Clock() + + screen = pygame.display.set_mode(RESOLUTION, pygame.RESIZABLE) + pygame.display._set_autoresize(False) + + # MAIN LOOP + + done = False + + i = 0 + j = 0 + + while not done: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN and event.key == pygame.K_q: + done = True + elif event.type == pygame.VIDEORESIZE: + screen = pygame.display.get_surface() + elif event.type == pygame.QUIT: + done = True + i += 1 + i = i % screen.get_width() + j += i % 2 + j = j % screen.get_height() + + screen.fill((255, 0, 255)) + pygame.draw.circle(screen, (0, 0, 0), (100, 100), 20) + pygame.draw.circle(screen, (0, 0, 200), (0, 0), 10) + pygame.draw.circle(screen, (200, 0, 0), (160, 120), 30) + pygame.draw.line(screen, (250, 250, 0), (0, 120), (160, 0)) + pygame.draw.circle(screen, (255, 255, 255), (i, j), 5) + + pygame.display.flip() + clock.tick(FPS) + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/scrap_clipboard.py b/examples/scrap_clipboard.py index db37250e31..f5f957c381 100644 --- a/examples/scrap_clipboard.py +++ b/examples/scrap_clipboard.py @@ -16,54 +16,58 @@ import pygame -pygame.init() -pygame.display.set_caption("Clipboard Example") -width, height = (960, 540) -screen = pygame.display.set_mode((width, height)) -clock = pygame.Clock() -font = pygame.Font(None, 30) - - -clipboard_text = "" -running = True - -while running: - screen.fill("black") - - instruction = "Keyboard Controls:\nV - View the current clipboard data.\nC - Copy some text into the clipboard.\nEscape - Quit" - text = font.render(instruction, True, "white") - screen.blit(text, (0, 0)) - - text = font.render( - f"Text on the clipboard:\n{clipboard_text}", True, "white", None, width - 20 - ) - screen.blit(text, (0, 100)) - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_v: - # Look for any text data in the clipboard. - print("Looking for text in the clipboard.") - if pygame.scrap.has_text(): - print("Text found in the clipboard.") - clipboard_text = pygame.scrap.get_text() - else: - print("No text in the clipboard.") - - elif event.key == pygame.K_c: - # put some text into the clipboard. - print("Putting text into the clipboard.") - - pygame.scrap.put_text( - "Hello World! This is some text from the pygame scrap example.", - ) - - elif event.key == pygame.K_ESCAPE: +def main(): + pygame.init() + pygame.display.set_caption("Clipboard Example") + width, height = (960, 540) + screen = pygame.display.set_mode((width, height)) + clock = pygame.Clock() + font = pygame.Font(None, 30) + + clipboard_text = "" + running = True + + while running: + screen.fill("black") + + instruction = "Keyboard Controls:\nV - View the current clipboard data.\nC - Copy some text into the clipboard.\nEscape - Quit" + text = font.render(instruction, True, "white") + screen.blit(text, (0, 0)) + + text = font.render( + f"Text on the clipboard:\n{clipboard_text}", True, "white", None, width - 20 + ) + screen.blit(text, (0, 100)) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: running = False - pygame.display.flip() - clock.tick(60) -pygame.quit() + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_v: + # Look for any text data in the clipboard. + print("Looking for text in the clipboard.") + if pygame.scrap.has_text(): + print("Text found in the clipboard.") + clipboard_text = pygame.scrap.get_text() + else: + print("No text in the clipboard.") + + elif event.key == pygame.K_c: + # put some text into the clipboard. + print("Putting text into the clipboard.") + + pygame.scrap.put_text( + "Hello World! This is some text from the pygame scrap example.", + ) + + elif event.key == pygame.K_ESCAPE: + running = False + + pygame.display.flip() + clock.tick(60) + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/setmodescale.py b/examples/setmodescale.py index 2e58f50868..b8d119da77 100644 --- a/examples/setmodescale.py +++ b/examples/setmodescale.py @@ -13,71 +13,78 @@ import pygame import sys -pygame.init() - RES = (160, 120) FPS = 30 -clock = pygame.Clock() - -print("desktops", pygame.display.get_desktop_sizes()) - -do_vsync = bool("--vsync" in sys.argv) - -if do_vsync: - screen = pygame.display.set_mode(RES, pygame.SCALED | pygame.RESIZABLE, vsync=1) -else: - screen = pygame.display.set_mode(RES, pygame.SCALED | pygame.RESIZABLE) - -# MAIN LOOP - -done = False - -i = 0 -j = 0 - -r_name, r_flags = pygame.display._get_renderer_info() -print("renderer:", r_name, "flags:", bin(r_flags)) -for flag, name in [ - (1, "software"), - (2, "accelerated"), - (4, "VSync"), - (8, "render to texture"), -]: - if flag & r_flags: - print(name) - -while not done: - for event in pygame.event.get(): - if event.type == pygame.KEYDOWN and event.key == pygame.K_q: - done = True - if event.type == pygame.QUIT: - done = True - if event.type == pygame.KEYDOWN and event.key == pygame.K_f: - pygame.display.toggle_fullscreen() - i += 1 - i = i % screen.get_width() - j += i % 2 - j = j % screen.get_height() - - screen.fill((255, 0, 255)) - pygame.draw.circle(screen, (0, 0, 0), (100, 100), 20) - pygame.draw.circle(screen, (0, 0, 200), (0, 0), 10) - pygame.draw.circle(screen, (200, 0, 0), (160, 120), 30) - if do_vsync: - # vertical line that moves horizontally to make screen tearing obvious - pygame.draw.line(screen, (250, 250, 0), (i, 0), (i, 120)) - else: - pygame.draw.line(screen, (250, 250, 0), (0, 120), (160, 0)) - pygame.draw.circle(screen, (255, 255, 255), (i, j), 5) - pygame.display.set_caption("FPS:" + str(clock.get_fps())) + +def main(): + pygame.init() + + clock = pygame.Clock() + + print("desktops", pygame.display.get_desktop_sizes()) + + do_vsync = bool("--vsync" in sys.argv) + if do_vsync: - pygame.display.flip() - # FPS should be limited by vsync, so we tick really fast - # we only need to have the clock tick to track FPS - clock.tick() + screen = pygame.display.set_mode(RES, pygame.SCALED | pygame.RESIZABLE, vsync=1) else: - clock.tick(FPS) - pygame.display.flip() - -pygame.quit() + screen = pygame.display.set_mode(RES, pygame.SCALED | pygame.RESIZABLE) + + # MAIN LOOP + + done = False + + i = 0 + j = 0 + + r_name, r_flags = pygame.display._get_renderer_info() + print("renderer:", r_name, "flags:", bin(r_flags)) + for flag, name in [ + (1, "software"), + (2, "accelerated"), + (4, "VSync"), + (8, "render to texture"), + ]: + if flag & r_flags: + print(name) + + while not done: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN and event.key == pygame.K_q: + done = True + if event.type == pygame.QUIT: + done = True + if event.type == pygame.KEYDOWN and event.key == pygame.K_f: + pygame.display.toggle_fullscreen() + i += 1 + i = i % screen.get_width() + j += i % 2 + j = j % screen.get_height() + + screen.fill((255, 0, 255)) + pygame.draw.circle(screen, (0, 0, 0), (100, 100), 20) + pygame.draw.circle(screen, (0, 0, 200), (0, 0), 10) + pygame.draw.circle(screen, (200, 0, 0), (160, 120), 30) + if do_vsync: + # vertical line that moves horizontally to make screen tearing obvious + pygame.draw.line(screen, (250, 250, 0), (i, 0), (i, 120)) + else: + pygame.draw.line(screen, (250, 250, 0), (0, 120), (160, 0)) + pygame.draw.circle(screen, (255, 255, 255), (i, j), 5) + + pygame.display.set_caption("FPS:" + str(clock.get_fps())) + if do_vsync: + pygame.display.flip() + # FPS should be limited by vsync, so we tick really fast + # we only need to have the clock tick to track FPS + clock.tick() + else: + clock.tick(FPS) + pygame.display.flip() + + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/sprite_texture.py b/examples/sprite_texture.py index 72eb5bfd16..34539c833f 100644 --- a/examples/sprite_texture.py +++ b/examples/sprite_texture.py @@ -24,81 +24,84 @@ def load_img(file): return pygame.image.load(os.path.join(data_dir, file)) -pygame.display.init() -pygame.key.set_repeat(10, 10) +def main(): + pygame.display.init() + pygame.key.set_repeat(10, 10) -win = Window("asdf", resizable=True) -renderer = Renderer(win) -tex = Texture.from_surface(renderer, load_img("alien1.gif")) + win = Window("asdf", resizable=True) + renderer = Renderer(win) + tex = Texture.from_surface(renderer, load_img("alien1.gif")) + class Something(pygame.sprite.Sprite): + def __init__(self, img): + pygame.sprite.Sprite.__init__(self) -class Something(pygame.sprite.Sprite): - def __init__(self, img): - pygame.sprite.Sprite.__init__(self) + self.rect = img.get_rect() + self.image = img - self.rect = img.get_rect() - self.image = img + self.rect.w *= 5 + self.rect.h *= 5 - self.rect.w *= 5 - self.rect.h *= 5 + img.origin = self.rect.w / 2, self.rect.h / 2 - img.origin = self.rect.w / 2, self.rect.h / 2 + sprite = Something(Image(tex, (0, 0, tex.width / 2, tex.height / 2))) + sprite.rect.x = 250 + sprite.rect.y = 50 + # sprite2 = Something(Image(sprite.image)) + sprite2 = Something(Image(tex)) + sprite2.rect.x = 250 + sprite2.rect.y = 250 + sprite2.rect.w /= 2 + sprite2.rect.h /= 2 -sprite = Something(Image(tex, (0, 0, tex.width / 2, tex.height / 2))) -sprite.rect.x = 250 -sprite.rect.y = 50 + group = pygame.sprite.Group() + group.add(sprite2) + group.add(sprite) -# sprite2 = Something(Image(sprite.image)) -sprite2 = Something(Image(tex)) -sprite2.rect.x = 250 -sprite2.rect.y = 250 -sprite2.rect.w /= 2 -sprite2.rect.h /= 2 + import math -group = pygame.sprite.Group() -group.add(sprite2) -group.add(sprite) + t = 0 + running = True + clock = pygame.Clock() + renderer.draw_color = (255, 0, 0, 255) -import math + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + elif event.key == pygame.K_LEFT: + sprite.rect.x -= 5 + elif event.key == pygame.K_RIGHT: + sprite.rect.x += 5 + elif event.key == pygame.K_DOWN: + sprite.rect.y += 5 + elif event.key == pygame.K_UP: + sprite.rect.y -= 5 -t = 0 -running = True -clock = pygame.Clock() -renderer.draw_color = (255, 0, 0, 255) + renderer.clear() + t += 1 -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - running = False - elif event.key == pygame.K_LEFT: - sprite.rect.x -= 5 - elif event.key == pygame.K_RIGHT: - sprite.rect.x += 5 - elif event.key == pygame.K_DOWN: - sprite.rect.y += 5 - elif event.key == pygame.K_UP: - sprite.rect.y -= 5 - - renderer.clear() - t += 1 - - img = sprite.image - img.angle += 1 - img.flip_x = t % 50 < 25 - img.flip_y = t % 100 < 50 - img.color[0] = int(255.0 * (0.5 + math.sin(0.5 * t + 10.0) / 2.0)) - img.alpha = int(255.0 * (0.5 + math.sin(0.1 * t) / 2.0)) - # img.draw(dstrect=(x, y, 5 * img.srcrect['w'], 5 * img.srcrect['h'])) - - group.draw(renderer) - - renderer.present() - - clock.tick(60) - win.title = str(f"FPS: {clock.get_fps()}") - -pygame.quit() + img = sprite.image + img.angle += 1 + img.flip_x = t % 50 < 25 + img.flip_y = t % 100 < 50 + img.color[0] = int(255.0 * (0.5 + math.sin(0.5 * t + 10.0) / 2.0)) + img.alpha = int(255.0 * (0.5 + math.sin(0.1 * t) / 2.0)) + # img.draw(dstrect=(x, y, 5 * img.srcrect['w'], 5 * img.srcrect['h'])) + + group.draw(renderer) + + renderer.present() + + clock.tick(60) + win.title = str(f"FPS: {clock.get_fps()}") + + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/video.py b/examples/video.py index e4da1aded2..81b11f03dc 100644 --- a/examples/video.py +++ b/examples/video.py @@ -22,139 +22,142 @@ def load_img(file): return pygame.image.load(os.path.join(data_dir, file)) -pygame.display.init() -pygame.key.set_repeat(1000, 10) +def main(): + pygame.display.init() + pygame.key.set_repeat(1000, 10) -for driver in get_drivers(): - print(driver) + for driver in get_drivers(): + print(driver) -import random + import random -answer = pygame.display.message_box( - "I will open two windows! Continue?", - "Hello!", - message_type="info", - buttons=("Yes", "No", "Chance"), - return_button=0, - escape_button=1, -) + answer = pygame.display.message_box( + "I will open two windows! Continue?", + "Hello!", + message_type="info", + buttons=("Yes", "No", "Chance"), + return_button=0, + escape_button=1, + ) -if answer == 1 or (answer == 2 and random.random() < 0.5): - import sys + if answer == 1 or (answer == 2 and random.random() < 0.5): + import sys - sys.exit(0) + sys.exit(0) -win = Window("asdf", resizable=True) -renderer = Renderer(win) -tex = Texture.from_surface(renderer, load_img("alien1.gif")) + win = Window("asdf", resizable=True) + renderer = Renderer(win) + tex = Texture.from_surface(renderer, load_img("alien1.gif")) -running = True + running = True -x, y = 250, 50 -clock = pygame.Clock() + x, y = 250, 50 + clock = pygame.Clock() -backgrounds = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)] -bg_index = 0 + backgrounds = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)] + bg_index = 0 -renderer.draw_color = backgrounds[bg_index] - -win2 = Window("2nd window", size=(256, 256), always_on_top=True) -win2.opacity = 0.5 -win2.set_icon(load_img("bomb.gif")) -renderer2 = Renderer(win2) -tex2 = Texture.from_surface(renderer2, load_img("asprite.bmp")) -renderer2.clear() -tex2.draw() -renderer2.present() -del tex2 - -full = 0 + renderer.draw_color = backgrounds[bg_index] -tex = Image(tex) + win2 = Window("2nd window", size=(256, 256), always_on_top=True) + win2.opacity = 0.5 + win2.set_icon(load_img("bomb.gif")) + renderer2 = Renderer(win2) + tex2 = Texture.from_surface(renderer2, load_img("asprite.bmp")) + renderer2.clear() + tex2.draw() + renderer2.present() + del tex2 + full = 0 -surf = pygame.Surface((64, 64)) -streamtex = Texture(renderer, (64, 64), streaming=True) -tex_update_interval = 1000 -next_tex_update = pygame.time.get_ticks() + tex = Image(tex) + surf = pygame.Surface((64, 64)) + streamtex = Texture(renderer, (64, 64), streaming=True) + tex_update_interval = 1000 + next_tex_update = pygame.time.get_ticks() -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif getattr(event, "window", None) == win2: - if ( - event.type == pygame.KEYDOWN - and event.key == pygame.K_ESCAPE - or event.type == pygame.WINDOWCLOSE - ): - win2.destroy() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: running = False - elif event.key == pygame.K_LEFT: - x -= 5 - elif event.key == pygame.K_RIGHT: - x += 5 - elif event.key == pygame.K_DOWN: - y += 5 - elif event.key == pygame.K_UP: - y -= 5 - elif event.key == pygame.K_f: - if full == 0: - win.set_fullscreen(True) - full = 1 - else: - win.set_windowed() - full = 0 - elif event.key == pygame.K_s: - readsurf = renderer.to_surface() - pygame.image.save(readsurf, "test.png") - - elif event.key == pygame.K_SPACE: - bg_index = (bg_index + 1) % len(backgrounds) - renderer.draw_color = backgrounds[bg_index] - - renderer.clear() - - # update texture - curtime = pygame.time.get_ticks() - if curtime >= next_tex_update: - for x_ in range(streamtex.width // 4): - for y_ in range(streamtex.height // 4): - newcol = ( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), - 255, - ) - area = (4 * x_, 4 * y_, 4, 4) - surf.fill(newcol, area) - streamtex.update(surf) - next_tex_update = curtime + tex_update_interval - streamtex.draw(dstrect=pygame.Rect(64, 128, 64, 64)) - - tex.draw(dstrect=(x, y)) - - # TODO: should these be? - # - line instead of draw_line - # - point instead of draw_point - # - rect(rect, width=1)->draw 1 pixel, instead of draw_rect - # - rect(rect, width=0)->filled ? , instead of fill_rect - # - # TODO: should these work with pygame.draw.line(renderer, ...) functions? - renderer.draw_color = (255, 255, 255, 255) - renderer.draw_line((0, 0), (64, 64)) - renderer.draw_line((64, 64), (128, 0)) - renderer.draw_point((72, 32)) - renderer.draw_rect(pygame.Rect(0, 64, 64, 64)) - renderer.fill_rect(pygame.Rect(0, 128, 64, 64)) - renderer.draw_color = backgrounds[bg_index] - - renderer.present() - - clock.tick(60) - win.title = str(f"FPS: {clock.get_fps()}") - -pygame.quit() + elif getattr(event, "window", None) == win2: + if ( + event.type == pygame.KEYDOWN + and event.key == pygame.K_ESCAPE + or event.type == pygame.WINDOWCLOSE + ): + win2.destroy() + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + elif event.key == pygame.K_LEFT: + x -= 5 + elif event.key == pygame.K_RIGHT: + x += 5 + elif event.key == pygame.K_DOWN: + y += 5 + elif event.key == pygame.K_UP: + y -= 5 + elif event.key == pygame.K_f: + if full == 0: + win.set_fullscreen(True) + full = 1 + else: + win.set_windowed() + full = 0 + elif event.key == pygame.K_s: + readsurf = renderer.to_surface() + pygame.image.save(readsurf, "test.png") + + elif event.key == pygame.K_SPACE: + bg_index = (bg_index + 1) % len(backgrounds) + renderer.draw_color = backgrounds[bg_index] + + renderer.clear() + + # update texture + curtime = pygame.time.get_ticks() + if curtime >= next_tex_update: + for x_ in range(streamtex.width // 4): + for y_ in range(streamtex.height // 4): + newcol = ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + 255, + ) + area = (4 * x_, 4 * y_, 4, 4) + surf.fill(newcol, area) + streamtex.update(surf) + next_tex_update = curtime + tex_update_interval + streamtex.draw(dstrect=pygame.Rect(64, 128, 64, 64)) + + tex.draw(dstrect=(x, y)) + + # TODO: should these be? + # - line instead of draw_line + # - point instead of draw_point + # - rect(rect, width=1)->draw 1 pixel, instead of draw_rect + # - rect(rect, width=0)->filled ? , instead of fill_rect + # + # TODO: should these work with pygame.draw.line(renderer, ...) functions? + renderer.draw_color = (255, 255, 255, 255) + renderer.draw_line((0, 0), (64, 64)) + renderer.draw_line((64, 64), (128, 0)) + renderer.draw_point((72, 32)) + renderer.draw_rect(pygame.Rect(0, 64, 64, 64)) + renderer.fill_rect(pygame.Rect(0, 128, 64, 64)) + renderer.draw_color = backgrounds[bg_index] + + renderer.present() + + clock.tick(60) + win.title = str(f"FPS: {clock.get_fps()}") + + pygame.quit() + + +if __name__ == "__main__": + main() diff --git a/examples/window_opengl.py b/examples/window_opengl.py index 9f10e4b400..253e5def48 100644 --- a/examples/window_opengl.py +++ b/examples/window_opengl.py @@ -9,68 +9,74 @@ import pygame import zengl -window_size = (1280, 720) - -pygame.init() -window = pygame.Window(size=window_size, opengl=True) - -ctx = zengl.context() - -image = ctx.image(window_size, "rgba8unorm", samples=4) - -pipeline = ctx.pipeline( - vertex_shader=""" - #version 330 core - - out vec3 v_color; - - vec2 vertices[3] = vec2[]( - vec2(0.0, 0.8), - vec2(-0.6, -0.8), - vec2(0.6, -0.8) - ); - - vec3 colors[3] = vec3[]( - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0) - ); - - void main() { - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); - v_color = colors[gl_VertexID]; - } - """, - fragment_shader=""" - #version 330 core - - in vec3 v_color; - - layout (location = 0) out vec4 out_color; - - void main() { - out_color = vec4(v_color, 1.0); - out_color.rgb = pow(out_color.rgb, vec3(1.0 / 2.2)); - } - """, - framebuffer=[image], - topology="triangles", - vertex_count=3, -) - -clock = pygame.Clock() - -while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.quit() - quit() - - ctx.new_frame() - image.clear() - pipeline.render() - image.blit() - ctx.end_frame() - - window.flip() - clock.tick(60) + +def main(): + window_size = (1280, 720) + + pygame.init() + window = pygame.Window(size=window_size, opengl=True) + + ctx = zengl.context() + + image = ctx.image(window_size, "rgba8unorm", samples=4) + + pipeline = ctx.pipeline( + vertex_shader=""" + #version 330 core + + out vec3 v_color; + + vec2 vertices[3] = vec2[]( + vec2(0.0, 0.8), + vec2(-0.6, -0.8), + vec2(0.6, -0.8) + ); + + vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) + ); + + void main() { + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + v_color = colors[gl_VertexID]; + } + """, + fragment_shader=""" + #version 330 core + + in vec3 v_color; + + layout (location = 0) out vec4 out_color; + + void main() { + out_color = vec4(v_color, 1.0); + out_color.rgb = pow(out_color.rgb, vec3(1.0 / 2.2)); + } + """, + framebuffer=[image], + topology="triangles", + vertex_count=3, + ) + + clock = pygame.Clock() + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + quit() + + ctx.new_frame() + image.clear() + pipeline.render() + image.blit() + ctx.end_frame() + + window.flip() + clock.tick(60) + + +if __name__ == "__main__": + main() diff --git a/meson.build b/meson.build index 8a62fa1ba9..318de28b1f 100644 --- a/meson.build +++ b/meson.build @@ -109,7 +109,7 @@ if plat == 'win' and host_machine.cpu_family().startswith('x86') ) endif - sdl_ver = (sdl_api == 3) ? '3.1.3' : '2.30.8' + sdl_ver = (sdl_api == 3) ? '3.1.6' : '2.30.9' sdl_image_ver = '2.8.2' sdl_mixer_ver = '2.8.0' sdl_ttf_ver = '2.22.0' @@ -399,6 +399,7 @@ else '-Wno-error=unused-but-set-variable', '-Wno-error=array-bounds', ] + warnings_temp_mask += '-Wno-error=array-bounds' endif if cc.sizeof('long') != 8 diff --git a/setup.py b/setup.py index 47648be978..cbad740727 100644 --- a/setup.py +++ b/setup.py @@ -807,11 +807,9 @@ def run(self): runs Sphinx to build the docs. ''' import subprocess - command_line = [ - sys.executable, "-m", "buildconfig", "docs" - ] + command_line = [sys.executable, "dev.py", "docs"] if self.fullgeneration: - command_line.append('full_generation') + command_line.append('--full') print("WARNING: This command is deprecated and will be removed in the future.") print(f"Please use the following replacement: `{' '.join(command_line)}`\n") @@ -834,9 +832,7 @@ def run(self): runs mypy to build the docs. ''' import subprocess - command_line = [ - sys.executable, os.path.join("buildconfig", "stubs", "stubcheck.py") - ] + command_line = [sys.executable, "dev.py", "stubs"] print("WARNING: This command is deprecated and will be removed in the future.") print(f"Please use the following replacement: `{' '.join(command_line)}`\n") result = subprocess.run(command_line) diff --git a/src_c/_pygame.h b/src_c/_pygame.h index 0cbfed872f..46fe2d966d 100644 --- a/src_c/_pygame.h +++ b/src_c/_pygame.h @@ -75,7 +75,9 @@ #define PG_CreateSurface SDL_CreateSurface #define PG_CreateSurfaceFrom SDL_CreateSurfaceFrom #define PG_ConvertSurface SDL_ConvertSurface -#define PG_ConvertSurfaceFormat SDL_ConvertSurfaceFormat +#define PG_ConvertSurfaceFormat SDL_ConvertSurface + +#define PG_PixelFormatEnum SDL_PixelFormat #define PG_SurfaceHasRLE SDL_SurfaceHasRLE @@ -115,6 +117,8 @@ PG_UnlockMutex(SDL_mutex *mutex) #define PG_FIND_VNUM_MINOR(ver) SDL_VERSIONNUM_MINOR(ver) #define PG_FIND_VNUM_MICRO(ver) SDL_VERSIONNUM_MICRO(ver) +#define PG_INIT_TIMER 0 + #else /* ~SDL_VERSION_ATLEAST(3, 0, 0)*/ #define PG_ShowCursor() SDL_ShowCursor(SDL_ENABLE) #define PG_HideCursor() SDL_ShowCursor(SDL_DISABLE) @@ -144,6 +148,8 @@ PG_UnlockMutex(SDL_mutex *mutex) #define PG_ConvertSurfaceFormat(src, pixel_format) \ SDL_ConvertSurfaceFormat(src, pixel_format, 0) +#define PG_PixelFormatEnum SDL_PixelFormatEnum + #define PG_SoftStretchNearest(src, srcrect, dst, dstrect) \ SDL_SoftStretch(src, srcrect, dst, dstrect) @@ -180,6 +186,8 @@ PG_UnlockMutex(SDL_mutex *mutex) #define PG_FIND_VNUM_MINOR(ver) ver.minor #define PG_FIND_VNUM_MICRO(ver) ver.patch +#define PG_INIT_TIMER SDL_INIT_TIMER + #if SDL_VERSION_ATLEAST(2, 0, 14) #define PG_SurfaceHasRLE SDL_HasSurfaceRLE #else diff --git a/src_c/base.c b/src_c/base.c index 573bca3aa2..f42b045783 100644 --- a/src_c/base.c +++ b/src_c/base.c @@ -81,7 +81,7 @@ static int pg_is_init = 0; static int pg_sdl_was_init = 0; SDL_Window *pg_default_window = NULL; pgSurfaceObject *pg_default_screen = NULL; -static char *pg_env_blend_alpha_SDL2 = NULL; +static int pg_env_blend_alpha_SDL2 = 0; static void pg_install_parachute(void); @@ -170,7 +170,7 @@ static pgSurfaceObject * pg_GetDefaultWindowSurface(void); static void pg_SetDefaultWindowSurface(pgSurfaceObject *); -static char * +static int pg_EnvShouldBlendAlphaSDL2(void); /* compare compiled to linked, raise python error on incompatibility */ @@ -341,13 +341,13 @@ pg_init(PyObject *self, PyObject *_null) /*nice to initialize timer, so startup time will reflec pg_init() time*/ #if defined(WITH_THREAD) && !defined(MS_WIN32) && defined(SDL_INIT_EVENTTHREAD) - pg_sdl_was_init = SDL_Init(SDL_INIT_EVENTTHREAD | SDL_INIT_TIMER | + pg_sdl_was_init = SDL_Init(SDL_INIT_EVENTTHREAD | PG_INIT_TIMER | PG_INIT_NOPARACHUTE) == 0; #else - pg_sdl_was_init = SDL_Init(SDL_INIT_TIMER | PG_INIT_NOPARACHUTE) == 0; + pg_sdl_was_init = SDL_Init(PG_INIT_TIMER | PG_INIT_NOPARACHUTE) == 0; #endif - pg_env_blend_alpha_SDL2 = SDL_getenv("PYGAME_BLEND_ALPHA_SDL2"); + pg_env_blend_alpha_SDL2 = SDL_getenv("PYGAME_BLEND_ALPHA_SDL2") != NULL; /* initialize all pygame modules */ for (i = 0; modnames[i]; i++) { @@ -2075,28 +2075,28 @@ pg_SetDefaultWindowSurface(pgSurfaceObject *screen) pg_default_screen = screen; } -SDL_PixelFormat *pg_default_convert_format = NULL; +PG_PixelFormatEnum pg_default_convert_format = 0; -static SDL_PixelFormat * +static PG_PixelFormatEnum pg_GetDefaultConvertFormat(void) { if (pg_default_screen) { +#if SDL_VERSION_ATLEAST(3, 0, 0) return pg_default_screen->surf->format; +#else + return pg_default_screen->surf->format->format; +#endif } return pg_default_convert_format; } -static SDL_PixelFormat * -pg_SetDefaultConvertFormat(Uint32 format) +static void +pg_SetDefaultConvertFormat(PG_PixelFormatEnum format) { - if (pg_default_convert_format != NULL) { - SDL_FreeFormat(pg_default_convert_format); - } - pg_default_convert_format = SDL_AllocFormat(format); - return pg_default_convert_format; // returns for NULL error checking + pg_default_convert_format = format; } -static char * +static int pg_EnvShouldBlendAlphaSDL2(void) { return pg_env_blend_alpha_SDL2; diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index bcfb0fdc8c..b3548a66ee 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -181,13 +181,13 @@ typedef struct pg_bufferinfo_s { (*(void (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(base, 22)) #define pg_EnvShouldBlendAlphaSDL2 \ - (*(char *(*)(void))PYGAMEAPI_GET_SLOT(base, 23)) + (*(int (*)(void))PYGAMEAPI_GET_SLOT(base, 23)) #define pg_GetDefaultConvertFormat \ - (*(SDL_PixelFormat * (*)(void)) PYGAMEAPI_GET_SLOT(base, 27)) + (*(PG_PixelFormatEnum(*)(void))PYGAMEAPI_GET_SLOT(base, 27)) #define pg_SetDefaultConvertFormat \ - (*(SDL_PixelFormat * (*)(Uint32)) PYGAMEAPI_GET_SLOT(base, 28)) + (*(void (*)(Uint32))PYGAMEAPI_GET_SLOT(base, 28)) #define import_pygame_base() IMPORT_PYGAME_MODULE(base) #endif /* ~PYGAMEAPI_BASE_INTERNAL */ diff --git a/src_c/meson.build b/src_c/meson.build index ca10cca737..fb43a584eb 100644 --- a/src_c/meson.build +++ b/src_c/meson.build @@ -1,7 +1,5 @@ # first the "required" modules -# TODO: support SDL3 -if sdl_api != 3 base = py.extension_module( 'base', 'base.c', @@ -10,7 +8,6 @@ base = py.extension_module( install: true, subdir: pg, ) -endif color = py.extension_module( 'color', diff --git a/src_c/surface.c b/src_c/surface.c index b9e3008ae3..d4a8eae2e9 100644 --- a/src_c/surface.c +++ b/src_c/surface.c @@ -1582,27 +1582,21 @@ surf_convert(pgSurfaceObject *self, PyObject *args) static SDL_Surface * pg_DisplayFormat(SDL_Surface *surface) { - SDL_PixelFormat *default_format = pg_GetDefaultConvertFormat(); + PG_PixelFormatEnum default_format = pg_GetDefaultConvertFormat(); if (!default_format) { SDL_SetError( "No convert format has been set, try display.set_mode()" " or Window.get_surface()."); return NULL; } - return PG_ConvertSurface(surface, default_format); + return PG_ConvertSurfaceFormat(surface, default_format); } static SDL_Surface * pg_DisplayFormatAlpha(SDL_Surface *surface) { - SDL_PixelFormat *dformat; - Uint32 pfe; - Uint32 amask = 0xff000000; - Uint32 rmask = 0x00ff0000; - Uint32 gmask = 0x0000ff00; - Uint32 bmask = 0x000000ff; - - dformat = pg_GetDefaultConvertFormat(); + PG_PixelFormatEnum pfe = SDL_PIXELFORMAT_ARGB8888; + PG_PixelFormatEnum dformat = pg_GetDefaultConvertFormat(); if (!dformat) { SDL_SetError( "No convert format has been set, try display.set_mode()" @@ -1610,38 +1604,32 @@ pg_DisplayFormatAlpha(SDL_Surface *surface) return NULL; } - switch (PG_FORMAT_BytesPerPixel(dformat)) { - case 2: - /* same behavior as SDL1 */ - if ((dformat->Rmask == 0x1f) && - (dformat->Bmask == 0xf800 || dformat->Bmask == 0x7c00)) { - rmask = 0xff; - bmask = 0xff0000; - } + switch (dformat) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + case SDL_PIXELFORMAT_XBGR1555: +#else + case SDL_PIXELFORMAT_BGR555: +#endif + case SDL_PIXELFORMAT_ABGR1555: + case SDL_PIXELFORMAT_BGR565: + case PG_PIXELFORMAT_XBGR8888: + case SDL_PIXELFORMAT_ABGR8888: + pfe = SDL_PIXELFORMAT_ABGR8888; break; - case 3: - case 4: - /* keep the format if the high bits are free */ - if ((dformat->Rmask == 0xff) && (dformat->Bmask == 0xff0000)) { - rmask = 0xff; - bmask = 0xff0000; - } - else if (dformat->Rmask == 0xff00 && - (dformat->Bmask == 0xff000000)) { - amask = 0x000000ff; - rmask = 0x0000ff00; - gmask = 0x00ff0000; - bmask = 0xff000000; - } + + case SDL_PIXELFORMAT_BGRX8888: + case SDL_PIXELFORMAT_BGRA8888: +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + case SDL_PIXELFORMAT_BGR24: +#else + case SDL_PIXELFORMAT_RGB24: +#endif + pfe = SDL_PIXELFORMAT_BGRA8888; break; - default: /* ARGB8888 */ + + default: break; } - pfe = SDL_MasksToPixelFormatEnum(32, rmask, gmask, bmask, amask); - if (pfe == SDL_PIXELFORMAT_UNKNOWN) { - SDL_SetError("unknown pixel format"); - return NULL; - } return PG_ConvertSurfaceFormat(surface, pfe); } diff --git a/src_c/window.c b/src_c/window.c index f2217cd20c..63b7270d06 100644 --- a/src_c/window.c +++ b/src_c/window.c @@ -157,12 +157,8 @@ window_get_surface(pgWindowObject *self, PyObject *_null) return RAISE(pgExc_SDLError, SDL_GetError()); } - if (pg_GetDefaultConvertFormat() == NULL) { - if (pg_SetDefaultConvertFormat(_surf->format->format) == NULL) { - /* This is very unlikely, I think only would happen if SDL runs - * out of memory when allocating the format. */ - return RAISE(pgExc_SDLError, SDL_GetError()); - } + if (pg_GetDefaultConvertFormat() == 0) { + pg_SetDefaultConvertFormat(_surf->format->format); } if (self->surf == NULL) {