diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbf75ca..937f01e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,74 +7,130 @@ on: pull_request: workflow_dispatch: schedule: - - cron: "0 0 * * *" # run once a day + # run every week (for --pre release tests) + - cron: "0 0 * * 0" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + check-manifest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx run check-manifest + test: - uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2 - with: - os: ${{ matrix.os }} - python-version: ${{ matrix.python-version }} - pip-post-installs: ${{ matrix.pydantic }} - pip-install-pre-release: ${{ github.event_name == 'schedule' }} - coverage-upload: artifact + name: ${{ matrix.platform }} py${{ matrix.python-version }} + runs-on: ${{ matrix.platform }} + env: + UV_PRERELEASE: ${{ github.event_name == 'schedule' && 'allow' || 'if-necessary-or-explicit' }} + UV_NO_SYNC: 1 strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.12", "3.13"] - os: [ubuntu-latest, macos-latest, windows-latest] - pydantic: [""] + python-version: ["3.9", "3.11", "3.13"] + platform: [ubuntu-latest, macos-latest, windows-latest] include: - - python-version: "3.11" - os: "ubuntu-latest" + - python-version: "3.10" + platform: "macos-latest" - python-version: "3.12" - os: "ubuntu-latest" - pydantic: "'pydantic<2'" + platform: "macos-latest" + - python-version: "3.9" + platform: "ubuntu-latest" + resolution: "lowest-direct" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿ Set up Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + + - name: Install Dependencies + run: uv sync --no-dev --group test --resolution ${{ matrix.resolution || 'highest'}} + + - name: ๐Ÿงช Run Tests + run: uv run coverage run -p -m pytest -v + env: + PYTEST_ADDOPTS: ${{ matrix.resolution == 'lowest-direct' && '-W ignore' || '' }} + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: covreport-${{ matrix.platform }}-py${{ matrix.python-version }} + path: ./.coverage* + include-hidden-files: true test-qt: - uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2 - with: - qt: ${{ matrix.qt }} - os: ${{ matrix.os }} - python-version: ${{ matrix.python-version }} - extras: test-qt - pip-install-pre-release: ${{ github.event_name == 'schedule' }} - coverage-upload: artifact + name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{matrix.extra }} ${{ matrix.resolution }} + runs-on: ${{ matrix.platform }} + env: + UV_PRERELEASE: ${{ github.event_name == 'schedule' && 'allow' || 'if-necessary-or-explicit' }} + UV_NO_SYNC: 1 strategy: fail-fast: false matrix: - include: - - python-version: "3.10" - os: "ubuntu-latest" - qt: "PyQt5~=5.15.0" - - python-version: "3.10" - os: "ubuntu-latest" - qt: "PySide2~=5.15.0" - - python-version: "3.10" - os: "ubuntu-latest" - qt: "PySide6~=6.3.0" - - python-version: "3.10" - os: "ubuntu-latest" - qt: "PyQt6~=6.4.0" - - python-version: "3.11" - os: "ubuntu-latest" - qt: "PySide6~=6.5.0" - - python-version: "3.11" - os: "ubuntu-latest" - qt: "PySide6~=6.6.0" - - python-version: "3.13" - os: "ubuntu-latest" - qt: pyqt6 - - python-version: "3.10" - os: "windows-latest" - qt: "PySide2" - - python-version: "3.9" - os: "macos-13" - qt: "PySide2" + python-version: ["3.10", "3.12"] + platform: [macos-latest, windows-latest] + extra: [pyqt5, pyside2, pyside6, pyqt6] + resolution: [highest, lowest-direct] + exclude: + - platform: "macos-latest" + extra: "pyside2" + - python-version: "3.12" + extra: "pyside2" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿ Set up Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + + - uses: pyvista/setup-headless-display-action@v4 + with: + qt: true + + - name: Install Dependencies + run: uv sync --no-dev --group test-qt --extra ${{ matrix.extra }} --resolution ${{ matrix.resolution }} + + - name: ๐Ÿงช Run Tests + run: uv run coverage run -p -m pytest -v + env: + PYTEST_ADDOPTS: ${{ matrix.resolution == 'lowest-direct' && '-W ignore' || '' }} + + # If something goes wrong with --pre tests, we can open an issue in the repo + - name: ๐Ÿ“ Report --pre Failures + if: failure() && github.event_name == 'schedule' + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLATFORM: ${{ matrix.platform }} + PYTHON: ${{ matrix.python-version }} + RUN_ID: ${{ github.run_id }} + TITLE: "[test-bot] pip install --pre is failing" + with: + filename: .github/TEST_FAIL_TEMPLATE.md + update_existing: true + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: covreport-${{ matrix.platform }}-py${{ matrix.python-version }}-${{ matrix.extra }}-${{ matrix.resolution }} + path: ./.coverage* + include-hidden-files: true upload_coverage: if: always() @@ -86,51 +142,42 @@ jobs: uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2 with: dependency-repo: napari/napari - dependency-ref: ${{ matrix.napari-version }} dependency-extras: "testing" qt: ${{ matrix.qt }} - pytest-args: 'napari/_qt/_qapp_model napari/_app_model napari/utils/_tests/test_key_bindings.py -k "not async and not qt_dims_2"' + pytest-args: 'src/napari/_qt/_qapp_model src/napari/_app_model src/napari/utils/_tests/test_key_bindings.py --import-mode=importlib -k "not async and not qt_dims_2"' python-version: "3.10" - post-install-cmd: "pip install lxml_html_clean" # fix for napari v0.4.19 strategy: fail-fast: false matrix: - napari-version: ["", "v0.4.19.post1"] qt: ["pyqt5", "pyside2"] - check-manifest: - name: Check Manifest + build-and-inspect-package: + name: Build & inspect package. + needs: [check-manifest, test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: pipx run check-manifest + with: + fetch-depth: 0 + - uses: hynek/build-and-inspect-python-package@v2 - deploy: - name: Deploy - needs: [check-manifest, test, test_napari] - if: success() && startsWith(github.ref, 'refs/tags/') + upload-to-pypi: + name: Upload package to PyPI + needs: build-and-inspect-package + if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule' runs-on: ubuntu-latest permissions: id-token: write contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 + steps: + - name: Download built artifact to dist/ + uses: actions/download-artifact@v4 with: - python-version: "3.x" - - - name: ๐Ÿ‘ท Build - run: | - python -m pip install build - python -m build - + name: Packages + path: dist - name: ๐Ÿšข Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - - uses: softprops/action-gh-release@v2 with: generate_release_notes: true diff --git a/.gitignore b/.gitignore index 57403d9..74c4b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ ENV/ app_model/_version.py src/app_model/_version.py + +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d092473..8051bf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,26 +4,27 @@ ci: autoupdate_commit_msg: "ci: [pre-commit.ci] autoupdate" repos: + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.24.1 + hooks: + - id: validate-pyproject + - repo: https://github.com/crate-ci/typos - rev: v1.31.1 + rev: v1.33.1 hooks: - id: typos - args: [] + args: [--force-exclude] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.12.0 hooks: - - id: ruff + - id: ruff-check args: ["--fix", "--unsafe-fixes"] - id: ruff-format - - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 - hooks: - - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.16.1 hooks: - id: mypy files: "^src/" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 930f6fa..5deed32 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,18 +1,16 @@ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.10" + python: "3.12" + jobs: + post_install: + - pip install uv + - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --link-mode=copy mkdocs: configuration: mkdocs.yml fail_on_warning: true - -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/pyproject.toml b/pyproject.toml index d9c32f6..9ea6754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,16 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +# https://hatch.pypa.io/latest/config/metadata/ +[tool.hatch.version] +source = "vcs" + +# read more about configuring hatch at: +# https://hatch.pypa.io/latest/config/build/ +[tool.hatch.build.targets.wheel] +only-include = ["src"] +sources = ["src"] + # https://peps.python.org/pep-0621/ [project] name = "app-model" @@ -27,61 +37,72 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "psygnal>=0.3.4", - "pydantic>=1.8", + "psygnal>=0.10", + "pydantic>=1.10.18", "pydantic-compat>=0.1.1", "in-n-out>=0.1.5", - "typing_extensions", + "typing_extensions>=4.12", ] -# extras -# https://peps.python.org/pep-0621/#dependencies-optional-dependencies +[project.urls] +homepage = "https://github.com/pyapp-kit/app-model" +repository = "https://github.com/pyapp-kit/app-model" + [project.optional-dependencies] -qt = ["qtpy", "superqt[iconify]"] -test = ["pytest>=6.0", "pytest-cov"] +qt = ["qtpy>=2.4.0", "superqt[iconify]>=0.7.2"] +pyqt5 = [ + "app-model[qt]", + "PyQt5>=5.15.10", + "pyqt5-qt5<=5.15.2; sys_platform == 'win32'", + "pyqt5-qt5>=5.15.4; sys_platform != 'win32'", +] +pyqt6 = ["app-model[qt]", "PyQt6>=6.4.0"] +pyside2 = ["app-model[qt]", "PySide2>=5.15.2.1"] +pyside6 = ["app-model[qt]", "PySide6>=6.6.0"] + +# https://peps.python.org/pep-0735/ +# setup with `uv sync` or `pip install -e . --group dev` +[dependency-groups] +test = ["pytest>=7.0", "pytest-cov >=6.1"] test-qt = [ + { include-group = "test" }, "app-model[qt]", - "app-model[test]", - "pytest-qt", - "fonticon-fontawesome6", + "pytest-qt >=4.3.0", + "fonticon-fontawesome6 >=6.4.0", +] +dev = [ + { include-group = "test-qt" }, + "ruff>=0.8.3", + "ipython>=8.18.0", + "mypy>=1.13.0", + "pdbpp>=0.11.6; sys_platform != 'win32'", + "pre-commit-uv>=4", + "pyqt6>=6.8.0", + "rich>=13.9.4", ] -dev = ["app-model[test-qt]", "ipython", "mypy", "pdbpp", "pre-commit", "rich"] docs = [ - "griffe-fieldz", + "griffe-fieldz>=0.1.0", "griffe==0.36.9", - "mkdocs-gen-files", - "mkdocs-literate-nav", + "mkdocs-gen-files>=0.5.0", + "mkdocs-literate-nav>=0.6.2", "mkdocs-macros-plugin==1.0.5", "mkdocs-material==9.4.1", "mkdocs==1.5.3", "mkdocstrings-python==1.7.3", "mkdocstrings==0.23.0", - "typing_extensions>=4.0", + "typing_extensions>=4.11", ] -[project.urls] -homepage = "https://github.com/pyapp-kit/app-model" -repository = "https://github.com/pyapp-kit/app-model" - -[tool.hatch.version] -source = "vcs" +[tool.uv.sources] +app-model = { workspace = true } -[tool.hatch.envs.test] -features = ["test"] -[tool.hatch.envs.test.scripts] -run = "pytest -v --color=yes --cov-config=pyproject.toml -W i --cov=app_model --cov-report=xml --cov-report=term-missing" - - -# https://pycqa.github.io/isort/docs/configuration/options.html -[tool.isort] -profile = "black" -src_paths = ["src/app_model", "tests"] - -# https://github.com/charliermarsh/ruff +# https://docs.astral.sh/ruff [tool.ruff] line-length = 88 -src = ["src", "tests"] target-version = "py39" +src = ["src", "tests"] +fix = true +# unsafe-fixes = true [tool.ruff.lint] pydocstyle = { convention = "numpy" } @@ -96,12 +117,13 @@ select = [ "C4", # flake8-comprehensions "B", # flake8-bugbear "A001", # flake8-builtins - "RUF", # ruff-specific rules - "TID", # tidy imports - "TCH", # type checking + # "ANN", # flake8-annotations + "RUF", # ruff-specific rules + "TC", # flake8-type-checking + "TID", # flake8-tidy-imports ] ignore = [ - "D401", # First line should be in imperative mood + "D401", # First line should be in imperative mood (remove to opt in) ] [tool.ruff.lint.pyupgrade] @@ -120,12 +142,16 @@ keep-runtime-typing = true [tool.ruff.format] docstring-code-format = true -# https://docs.pytest.org/en/6.2.x/customize.html +# https://docs.pytest.org/ [tool.pytest.ini_options] -minversion = "6.0" +minversion = "7.0" +addopts = ["--color=yes"] +testpaths = ["tests"] filterwarnings = [ "error", "ignore:Enum value:DeprecationWarning:superqt", + "ignore:Failed to disconnect::pytestqt", + "ignore:Failing to pass a value to the 'type_params' parameter::pydantic", "ignore:`__get_validators__` is deprecated and will be removed", ] @@ -147,23 +173,27 @@ disallow_untyped_defs = false module = ["qtpy.*"] implicit_reexport = true -[tool.coverage.run] -source = ["app_model"] - -# https://coverage.readthedocs.io/en/6.4/config.html +# https://coverage.readthedocs.io/ [tool.coverage.report] +show_missing = true +skip_covered = true exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "@overload", "except ImportError", - "return NotImplemented", + "raise AssertionError", + "\\.\\.\\.", + "raise NotImplementedError()", "pass", ] -skip_covered = true -show_missing = true -# https://github.com/mgedmin/check-manifest#configuration +[tool.coverage.run] +source = ["app_model"] + +[tool.coverage.paths] +source = ["src/", "*/app-model/app-model/src", "*/site-packages/"] + [tool.check-manifest] ignore = [ ".github_changelog_generator", @@ -178,5 +208,6 @@ ignore = [ ".ruff_cache/**/*", ] -[tool.typos] -default.extend-ignore-identifiers-re = ["to_string_ser_schema"] +# https://github.com/crate-ci/typos/blob/master/docs/reference.md +[tool.typos.default] +extend-ignore-identifiers-re = ["to_string_ser_schema"] diff --git a/src/app_model/types/_keys/_standard_bindings.py b/src/app_model/types/_keys/_standard_bindings.py index 2277784..c504227 100644 --- a/src/app_model/types/_keys/_standard_bindings.py +++ b/src/app_model/types/_keys/_standard_bindings.py @@ -1,6 +1,7 @@ from collections import namedtuple from enum import Enum, auto from typing import TYPE_CHECKING, Dict +from typing_extensions import Final from ._key_codes import KeyCode, KeyMod @@ -81,7 +82,7 @@ def to_keybinding_rule(self) -> "KeyBindingRule": return KeyBindingRule(**_STANDARD_KEY_MAP[self]) -_ = None +_: Final[None] = None SK = namedtuple("SK", "sk, primary, win, mac, gnome", defaults=(_, _, _, _, _)) # fmt: off