├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── config-file-deps-bump.yml │ ├── dependabot-changenote.yml │ ├── pre-commit-update.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE ├── README.rst ├── changes ├── .gitignore ├── 599.misc.rst └── template.rst ├── docs ├── Makefile ├── _mocked_modules │ └── ctypes │ │ ├── __init__.py │ │ └── util.py ├── _static │ └── images │ │ └── rubicon.png ├── background │ ├── community.rst │ ├── faq.rst │ ├── index.rst │ ├── releases.rst │ ├── roadmap.rst │ └── success.rst ├── conf.py ├── how-to │ ├── async.rst │ ├── c-functions.rst │ ├── contribute │ │ ├── code.rst │ │ ├── docs.rst │ │ └── index.rst │ ├── get-started.rst │ ├── index.rst │ ├── internal │ │ ├── index.rst │ │ └── release.rst │ ├── memory-management.rst │ ├── protocols.rst │ └── type-mapping.rst ├── index.rst ├── make.bat ├── reference │ ├── index.rst │ ├── rubicon-objc-api.rst │ ├── rubicon-objc-eventloop.rst │ ├── rubicon-objc-runtime.rst │ ├── rubicon-objc-types.rst │ └── rubicon-objc.rst ├── spelling_wordlist └── tutorial │ ├── index.rst │ ├── tutorial-1.rst │ └── tutorial-2.rst ├── pyproject.toml ├── src └── rubicon │ └── objc │ ├── __init__.py │ ├── api.py │ ├── collections.py │ ├── ctypes_patch.py │ ├── eventloop.py │ ├── runtime.py │ └── types.py ├── tests ├── __init__.py ├── objc │ ├── Altered_Example.h │ ├── Altered_Example.m │ ├── BaseExample.h │ ├── BaseExample.m │ ├── Blocks.h │ ├── Blocks.m │ ├── Callback.h │ ├── DescriptionTester.h │ ├── DescriptionTester.m │ ├── Example.h │ ├── Example.m │ ├── Makefile │ ├── Protocols.h │ ├── SpecificExample.h │ ├── SpecificExample.m │ ├── Thing.h │ └── Thing.m ├── test_NSArray.py ├── test_NSDictionary.py ├── test_NSString.py ├── test_async.py ├── test_blocks.py ├── test_core.py └── test_ctypes_patch.py └── tox.ini /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Migrate code style to Black 2 | 24b86b5bc04f6a4d5eae0953d58914b11265350b 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates on Sunday, 8PM UTC 8 | interval: "weekly" 9 | day: "sunday" 10 | time: "20:00" 11 | 12 | - package-ecosystem: "pip" 13 | directory: "/" 14 | schedule: 15 | # Check for updates on Sunday, 8PM UTC 16 | interval: "weekly" 17 | day: "sunday" 18 | time: "20:00" 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | inputs: 9 | attest-package: 10 | description: "Create GitHub provenance attestation for the package." 11 | default: "false" 12 | type: string 13 | outputs: 14 | artifact-name: 15 | description: "Name of the uploaded artifact; use for artifact retrieval." 16 | value: ${{ jobs.package.outputs.artifact-name }} 17 | 18 | # Cancel active CI runs for a PR before starting another run 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | defaults: 24 | run: 25 | shell: bash 26 | 27 | env: 28 | FORCE_COLOR: "1" 29 | 30 | jobs: 31 | pre-commit: 32 | name: Pre-commit checks 33 | uses: beeware/.github/.github/workflows/pre-commit-run.yml@main 34 | with: 35 | runner-os: "macos-latest" 36 | 37 | towncrier: 38 | name: Check towncrier 39 | uses: beeware/.github/.github/workflows/towncrier-run.yml@main 40 | with: 41 | runner-os: "macos-latest" 42 | 43 | package: 44 | name: Package Rubicon-ObjC 45 | permissions: 46 | id-token: write 47 | contents: read 48 | attestations: write 49 | uses: beeware/.github/.github/workflows/python-package-create.yml@main 50 | with: 51 | attest: ${{ inputs.attest-package }} 52 | 53 | unit-tests: 54 | name: Unit tests 55 | needs: [ pre-commit, towncrier, package ] 56 | runs-on: ${{ matrix.platform }} 57 | continue-on-error: false 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | platform: [ 62 | # X86-64 runners 63 | "macos-13", 64 | # M1 runners 65 | "macos-14", "macos-15" 66 | ] 67 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] 68 | exclude: 69 | # actions/setup-python doesn't provide Python 3.9 for M1. 70 | - platform: "macos-14" 71 | python-version : "3.9" 72 | 73 | - platform: "macos-15" 74 | python-version : "3.9" 75 | 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4.2.2 79 | with: 80 | fetch-depth: 0 81 | 82 | - name: Set up Python ${{ matrix.python-version }} 83 | uses: actions/setup-python@v5.6.0 84 | with: 85 | python-version: ${{ matrix.python-version }} 86 | allow-prereleases: true 87 | 88 | - name: Get packages 89 | uses: actions/download-artifact@v4.3.0 90 | with: 91 | name: ${{ needs.package.outputs.artifact-name }} 92 | path: dist 93 | 94 | - name: Install Tox 95 | uses: beeware/.github/.github/actions/install-requirement@main 96 | with: 97 | requirements: tox 98 | extra: dev 99 | 100 | - name: Test 101 | run: tox -e py --installpkg dist/rubicon_objc-*.whl 102 | -------------------------------------------------------------------------------- /.github/workflows/config-file-deps-bump.yml: -------------------------------------------------------------------------------- 1 | name: Bump Config File Dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "0 20 * * SUN" # Sunday @ 2000 UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | dep-bump-versions: 10 | name: Bump Config File Dependencies 11 | uses: beeware/.github/.github/workflows/dep-version-bump.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-changenote.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Change Note 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'dependabot/**' 7 | 8 | jobs: 9 | changenote: 10 | name: Dependabot Change Note 11 | uses: beeware/.github/.github/workflows/dependabot-changenote.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-update.yml: -------------------------------------------------------------------------------- 1 | name: Update pre-commit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 20 * * SUN" # Sunday @ 2000 UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | pre-commit-update: 10 | name: Update pre-commit 11 | uses: beeware/.github/.github/workflows/pre-commit-update.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: published 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # This permission is required for trusted publishing. 12 | id-token: write 13 | steps: 14 | - uses: dsaltares/fetch-gh-release-asset@1.1.2 15 | with: 16 | version: tags/${{ github.event.release.tag_name }} 17 | # This next line is *not* a bash filename expansion - it's a regex. 18 | # We need to match all files that start with rubicon-objc or 19 | # rubicon_objc, but not the "Source code" zip and tarball. 20 | file: rubicon.* 21 | regex: true 22 | target: dist/ 23 | 24 | - name: Publish release to production PyPI 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | ci: 10 | name: CI 11 | uses: ./.github/workflows/ci.yml 12 | with: 13 | attest-package: "true" 14 | 15 | docs: 16 | name: Verify Docs Build 17 | uses: beeware/.github/.github/workflows/docs-build-verify.yml@main 18 | secrets: inherit 19 | with: 20 | project-name: "rubicon-objc" 21 | project-version: ${{ github.ref_name }} 22 | 23 | release: 24 | name: Create Release 25 | needs: [ ci, docs ] 26 | # This has to be run on macOS, because rubicon tries to load the Foundation library 27 | runs-on: macOS-latest 28 | permissions: 29 | contents: write 30 | steps: 31 | - name: Set build variables 32 | run: | 33 | echo "VERSION=${GITHUB_REF_NAME#v}" | tee -a $GITHUB_ENV 34 | 35 | - name: Set up Python 36 | uses: actions/setup-python@v5.6.0 37 | with: 38 | python-version: "3.X" 39 | 40 | - name: Get packages 41 | uses: actions/download-artifact@v4.3.0 42 | with: 43 | name: ${{ needs.ci.outputs.artifact-name }} 44 | path: dist 45 | 46 | - name: Install packages 47 | run: python -m pip install dist/rubicon_objc-*.whl 48 | 49 | - name: Check version number 50 | # Check that the setuptools_scm-generated version number is still the same when 51 | # installed from a wheel with setuptools_scm not present. 52 | run: | 53 | set -x 54 | test $(python -c "from rubicon.objc import __version__; print(__version__)") = $VERSION 55 | 56 | - name: Create Release 57 | uses: ncipollo/release-action@v1.16.0 58 | with: 59 | name: ${{ env.VERSION }} 60 | draft: true 61 | artifacts: dist/* 62 | artifactErrorsFailBuild: true 63 | 64 | test-publish: 65 | name: Publish test package 66 | needs: [ ci, docs, release ] 67 | runs-on: ubuntu-latest 68 | permissions: 69 | contents: write 70 | # This permission is required for trusted publishing. 71 | id-token: write 72 | steps: 73 | - name: Get packages 74 | uses: actions/download-artifact@v4.3.0 75 | with: 76 | name: ${{ needs.ci.outputs.artifact-name }} 77 | path: dist 78 | 79 | - name: Publish release to Test PyPI 80 | uses: pypa/gh-action-pypi-publish@release/v1 81 | with: 82 | repository-url: https://test.pypi.org/legacy/ 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .*.sw[op] 4 | *.egg-info 5 | .coverage 6 | dist 7 | build 8 | _build 9 | distribute-* 10 | venv*/ 11 | local 12 | .tox 13 | .envrc 14 | 15 | # OS X 16 | .DS_Store 17 | 18 | # PyCharm 19 | .idea/ 20 | *.iml 21 | 22 | # VS Code 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-toml 6 | - id: check-yaml 7 | - id: check-case-conflict 8 | - id: check-docstring-first 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - repo: https://github.com/PyCQA/isort 12 | rev: 6.0.1 13 | hooks: 14 | - id: isort 15 | additional_dependencies: [toml] 16 | - repo: https://github.com/asottile/pyupgrade 17 | rev: v3.20.0 18 | hooks: 19 | - id: pyupgrade 20 | args: [--py39-plus] 21 | - repo: https://github.com/psf/black-pre-commit-mirror 22 | rev: 25.1.0 23 | hooks: 24 | - id: black 25 | - repo: https://github.com/PyCQA/flake8 26 | rev: 7.2.0 27 | hooks: 28 | - id: flake8 29 | - repo: https://github.com/codespell-project/codespell 30 | rev: v2.4.1 31 | hooks: 32 | - id: codespell 33 | additional_dependencies: 34 | - tomli 35 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | # Docs are always built on Python 3.12. See also the tox config and contribution docs. 13 | python: "3.12" 14 | jobs: 15 | pre_build: 16 | - tox -e docs-lint 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | builder: html 21 | configuration: docs/conf.py 22 | fail_on_warning: true 23 | 24 | # Optionally build your docs in additional formats such as PDF 25 | formats: 26 | - epub 27 | - pdf 28 | 29 | # Set the version of Python and requirements required to build the docs 30 | python: 31 | install: 32 | - method: pip 33 | path: . 34 | extra_requirements: 35 | - docs 36 | - dev 37 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | A complete history of the changes to Rubicon can be found in `Rubicon's 5 | documentation <./docs/background/releases>`__. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | BeeWare <3's contributions! 4 | 5 | Please be aware that BeeWare operates under a [Code of 6 | Conduct](https://beeware.org/community/behavior/code-of-conduct/). 7 | 8 | If you'd like to contribute to Rubicon-ObjC development, our `contribution guide 9 | `__ 10 | details how to set up a development environment, and other requirements we have 11 | as part of our contribution process. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Russell Keith-Magee. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Rubicon nor the names of its contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | ---------------------------------------------------------------------------- 30 | This project uses code derived from Pyglet, which, in turn, derived its code 31 | from objective-ctypes. The license for objective-ctypes is as follows: 32 | 33 | Copyright (c) 2011, Phillip Nguyen 34 | All rights reserved. 35 | 36 | Redistribution and use in source and binary forms, with or without modification, 37 | are permitted provided that the following conditions are met: 38 | 39 | 1. Redistributions of source code must retain the above copyright notice, 40 | this list of conditions and the following disclaimer. 41 | 42 | 2. Redistributions in binary form must reproduce the above copyright 43 | notice, this list of conditions and the following disclaimer in the 44 | documentation and/or other materials provided with the distribution. 45 | 46 | 3. Neither the name of objective-ctypes nor the names of its contributors may 47 | be used to endorse or promote products derived from this software without 48 | specific prior written permission. 49 | 50 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 51 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 52 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 53 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 54 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 55 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 56 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 57 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 58 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 59 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. |logo| image:: https://beeware.org/project/utilities/rubicon/rubicon.png 2 | :width: 72px 3 | :target: https://beeware.org/rubicon 4 | 5 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/rubicon-objc.svg 6 | :target: https://pypi.python.org/pypi/rubicon-objc 7 | :alt: Python Versions 8 | 9 | .. |version| image:: https://img.shields.io/pypi/v/rubicon-objc.svg 10 | :target: https://pypi.python.org/pypi/rubicon-objc 11 | :alt: Project Version 12 | 13 | .. |maturity| image:: https://img.shields.io/pypi/status/rubicon-objc.svg 14 | :target: https://pypi.python.org/pypi/rubicon-objc 15 | :alt: Project status 16 | 17 | .. |license| image:: https://img.shields.io/pypi/l/rubicon-objc.svg 18 | :target: https://github.com/beeware/rubicon-objc/blob/main/LICENSE 19 | :alt: License 20 | 21 | .. |ci| image:: https://github.com/beeware/rubicon-objc/workflows/CI/badge.svg?branch=main 22 | :target: https://github.com/beeware/rubicon-objc/actions 23 | :alt: Build Status 24 | 25 | .. |social| image:: https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic 26 | :target: https://beeware.org/bee/chat/ 27 | :alt: Discord server 28 | 29 | |logo| 30 | 31 | Rubicon-ObjC 32 | ============ 33 | 34 | |pyversions| |version| |maturity| |license| |ci| |social| 35 | 36 | Rubicon-ObjC is a bridge between Objective-C and Python. It enables you to: 37 | 38 | * Use Python to instantiate objects defined in Objective-C, 39 | * Use Python to invoke methods on objects defined in Objective-C, and 40 | * Subclass and extend Objective-C classes in Python. 41 | 42 | It also includes wrappers of the some key data types from the Foundation 43 | framework (e.g., ``NSString``). 44 | 45 | Tutorial 46 | -------- 47 | 48 | Want to jump in and get started? We have a `hands-on tutorial for 49 | beginners `__. 50 | 51 | How-to guides 52 | ------------- 53 | 54 | Looking for guidance on how to solve a specific problems? We have `how-to 55 | guides and recipes `__ 56 | for common problems and tasks, including how to contribute. 57 | 58 | Reference 59 | --------- 60 | 61 | Just want the raw technical details? Here's our `Technical 62 | reference `__. 63 | 64 | Background 65 | ---------- 66 | 67 | Looking for explanations and discussion of key topics and concepts? 68 | Our `background `__ 69 | guides may help. 70 | 71 | 72 | Community 73 | --------- 74 | 75 | Rubicon is part of the `BeeWare suite `__. You can talk to 76 | the community through: 77 | 78 | * `@beeware@fosstodon.org on Mastodon `__ 79 | 80 | * `Discord `__ 81 | 82 | * The Rubicon-ObjC `Github Discussions forum `__ 83 | 84 | Code of Conduct 85 | --------------- 86 | 87 | The BeeWare community has a strict `Code of Conduct 88 | `__. All users and developers are 89 | expected to adhere to this code. 90 | 91 | If you have any concerns about this code of conduct, or you wish to report a 92 | violation of this code, please contact the project founder `Russell Keith- 93 | Magee `__. 94 | 95 | Contributing 96 | ------------ 97 | 98 | If you experience problems with Rubicon-ObjC, `log them on GitHub 99 | `__. 100 | 101 | If you'd like to contribute to Rubicon-ObjC development, our `contribution guide 102 | `__ 103 | details how to set up a development environment, and other requirements we have 104 | as part of our contribution process. 105 | -------------------------------------------------------------------------------- /changes/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changes/599.misc.rst: -------------------------------------------------------------------------------- 1 | Updated pytest from 8.3.5 to 8.4.0. 2 | -------------------------------------------------------------------------------- /changes/template.rst: -------------------------------------------------------------------------------- 1 | {% for section, _ in sections.items() %} 2 | {% set underline = underlines[0] %}{% if section %}{{section}} 3 | {{ underline * section|length }}{% set underline = underlines[1] %} 4 | 5 | {% endif %} 6 | 7 | {% if sections[section] %} 8 | {% for category, val in definitions.items() if category in sections[section]%} 9 | {{ definitions[category]['name'] }} 10 | {{ underline * definitions[category]['name']|length }} 11 | 12 | {% if definitions[category]['showcontent'] %} 13 | {% for text, values in sections[section][category].items() %} 14 | * {{ text }} ({{ values|join(', ') }}) 15 | {% endfor %} 16 | 17 | {% else %} 18 | * {{ sections[section][category]['']|join(', ') }} 19 | 20 | {% endif %} 21 | {% if sections[section][category]|length == 0 %} 22 | No significant changes. 23 | 24 | {% else %} 25 | {% endif %} 26 | 27 | {% endfor %} 28 | {% else %} 29 | No significant changes. 30 | 31 | {% endif %} 32 | {% endfor %} 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help livehtml Makefile 16 | 17 | livehtml: 18 | sphinx-autobuild -b html $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html" 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | -------------------------------------------------------------------------------- /docs/_mocked_modules/ctypes/__init__.py: -------------------------------------------------------------------------------- 1 | """Bare minimum mock version of ctypes. 2 | 3 | This shadows the real ctypes module when building the documentation, 4 | so that :mod:`rubicon.objc` can be imported by Sphinx autodoc even when no Objective-C runtime is available. 5 | 6 | This module only emulates enough of ctypes to make the docs build. 7 | Most parts are in no way accurately implemented, and some ctypes features are missing entirely. 8 | 9 | Parts of this file are based on the source code of the ctypes module from CPython, 10 | under the terms of the PSF License Version 2, included below. 11 | The code in question has all parts removed that we don't need, 12 | and any remaining dependencies on the native _ctypes module have been replaced with pure Python code. 13 | Specifically, the following parts are (partially) based on CPython source code: 14 | 15 | * the definitions of the "ctypes primitive types" (the :class:`_SimpleCData` subclasses and their aliases) 16 | * the implementations of :func:`CFUNCTYPE` and :func:`PYFUNCTYPE` 17 | * the implementations of :class:`CDLL`, :class:`PyDLL` and :class:`LibraryLoader` 18 | * the definitions of the :data:`pythonapi`, :data:`cdll` and :data:`pydll` globals 19 | 20 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 21 | -------------------------------------------- 22 | 23 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 24 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 25 | otherwise using this software ("Python") in source or binary form and 26 | its associated documentation. 27 | 28 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 29 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 30 | analyze, test, perform and/or display publicly, prepare derivative works, 31 | distribute, and otherwise use Python alone or in any derivative version, 32 | provided, however, that PSF's License Agreement and PSF's notice of copyright, 33 | i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 34 | 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; 35 | All Rights Reserved" are retained in Python alone or in any derivative version 36 | prepared by Licensee. 37 | 38 | 3. In the event Licensee prepares a derivative work that is based on 39 | or incorporates Python or any part thereof, and wants to make 40 | the derivative work available to others as provided herein, then 41 | Licensee hereby agrees to include in any such work a brief summary of 42 | the changes made to Python. 43 | 44 | 4. PSF is making Python available to Licensee on an "AS IS" 45 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 46 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 47 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 48 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 49 | INFRINGE ANY THIRD PARTY RIGHTS. 50 | 51 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 52 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 53 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 54 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 55 | 56 | 6. This License Agreement will automatically terminate upon a material 57 | breach of its terms and conditions. 58 | 59 | 7. Nothing in this License Agreement shall be deemed to create any 60 | relationship of agency, partnership, or joint venture between PSF and 61 | Licensee. This License Agreement does not grant permission to use PSF 62 | trademarks or trade name in a trademark sense to endorse or promote 63 | products or services of Licensee, or any third party. 64 | 65 | 8. By copying, installing or otherwise using Python, Licensee 66 | agrees to be bound by the terms and conditions of this License 67 | Agreement. 68 | """ 69 | 70 | import struct 71 | 72 | # We pretend to be a 64-bit system. 73 | _POINTER_SIZE = 8 74 | 75 | 76 | class ArgumentError(Exception): 77 | pass 78 | 79 | 80 | _array_type_cache = {} 81 | 82 | 83 | class _CDataMeta(type): 84 | def __mul__(self, count): 85 | try: 86 | return _array_type_cache[self, count] 87 | except KeyError: 88 | array_type = type( 89 | f"{self.__name__}_Array_{count}", 90 | (Array,), 91 | {"_type_": self, "_length_": count}, 92 | ) 93 | _array_type_cache[self, count] = array_type 94 | return array_type 95 | 96 | 97 | class _CData(metaclass=_CDataMeta): 98 | @classmethod 99 | def from_address(cls, address): 100 | return cls() 101 | 102 | @classmethod 103 | def in_dll(cls, dll, name): 104 | return cls() 105 | 106 | def _auto_unwrap(self): 107 | return self 108 | 109 | 110 | class _SimpleCData(_CData): 111 | @classmethod 112 | def _sizeof(cls): 113 | return struct.calcsize(cls._type_) 114 | 115 | def __new__(cls, value=None): 116 | self = super().__new__(cls) 117 | self.value = value if value is not None else cls._DEFAULT_VALUE 118 | return self 119 | 120 | def __init__(self, value=None): 121 | pass 122 | 123 | def _auto_unwrap(self): 124 | if _SimpleCData in type(self).__bases__: 125 | return self.value 126 | else: 127 | return self 128 | 129 | 130 | class py_object(_SimpleCData): 131 | _type_ = "O" 132 | _DEFAULT_VALUE = None 133 | 134 | @classmethod 135 | def _sizeof(cls): 136 | return _POINTER_SIZE 137 | 138 | 139 | class c_short(_SimpleCData): 140 | _DEFAULT_VALUE = 0 141 | _type_ = "h" 142 | 143 | 144 | class c_ushort(_SimpleCData): 145 | _DEFAULT_VALUE = 0 146 | _type_ = "H" 147 | 148 | 149 | class c_long(_SimpleCData): 150 | _DEFAULT_VALUE = 0 151 | _type_ = "l" 152 | 153 | 154 | class c_ulong(_SimpleCData): 155 | _DEFAULT_VALUE = 0 156 | _type_ = "L" 157 | 158 | 159 | class c_int(_SimpleCData): 160 | _DEFAULT_VALUE = 0 161 | _type_ = "i" 162 | 163 | 164 | class c_uint(_SimpleCData): 165 | _DEFAULT_VALUE = 0 166 | _type_ = "I" 167 | 168 | 169 | class c_float(_SimpleCData): 170 | _DEFAULT_VALUE = 0.0 171 | _type_ = "f" 172 | 173 | 174 | class c_double(_SimpleCData): 175 | _DEFAULT_VALUE = 0.0 176 | _type_ = "d" 177 | 178 | 179 | class c_longdouble(_SimpleCData): 180 | _DEFAULT_VALUE = 0.0 181 | _type_ = "g" 182 | 183 | 184 | c_longlong = c_long 185 | c_ulonglong = c_ulong 186 | 187 | 188 | class c_ubyte(_SimpleCData): 189 | _DEFAULT_VALUE = 0 190 | _type_ = "B" 191 | 192 | 193 | class c_byte(_SimpleCData): 194 | _DEFAULT_VALUE = 0 195 | _type_ = "b" 196 | 197 | 198 | class c_char(_SimpleCData): 199 | _DEFAULT_VALUE = b"\x00" 200 | _type_ = "c" 201 | 202 | 203 | class c_char_p(_SimpleCData): 204 | _DEFAULT_VALUE = None 205 | _type_ = "z" 206 | 207 | @classmethod 208 | def _sizeof(cls): 209 | return _POINTER_SIZE 210 | 211 | 212 | class c_void_p(_SimpleCData): 213 | _DEFAULT_VALUE = None 214 | _type_ = "P" 215 | 216 | @classmethod 217 | def _sizeof(cls): 218 | return _POINTER_SIZE 219 | 220 | 221 | class c_bool(_SimpleCData): 222 | _DEFAULT_VALUE = False 223 | _type_ = "?" 224 | 225 | 226 | class c_wchar_p(_SimpleCData): 227 | _DEFAULT_VALUE = None 228 | _type_ = "Z" 229 | 230 | @classmethod 231 | def _sizeof(cls): 232 | return _POINTER_SIZE 233 | 234 | 235 | class c_wchar(_SimpleCData): 236 | _DEFAULT_VALUE = "\x00" 237 | _type_ = "u" 238 | 239 | 240 | c_size_t = c_ulong 241 | c_ssize_t = c_long 242 | c_int8 = c_byte 243 | c_uint8 = c_ubyte 244 | c_int16 = c_short 245 | c_uint16 = c_ushort 246 | c_int32 = c_int 247 | c_uint32 = c_uint 248 | c_int64 = c_long 249 | c_uint64 = c_ulong 250 | 251 | 252 | class _Pointer(_CData): 253 | pass 254 | 255 | 256 | _pointer_type_cache = {None: c_void_p} 257 | 258 | 259 | def POINTER(ctype): 260 | try: 261 | return _pointer_type_cache[ctype] 262 | except KeyError: 263 | pointer_ctype = type(f"LP_{ctype.__name__}", (_Pointer,), {"_type_": ctype}) 264 | _pointer_type_cache[ctype] = pointer_ctype 265 | return pointer_ctype 266 | 267 | 268 | def pointer(cvalue): 269 | return POINTER(type(cvalue))(cvalue) 270 | 271 | 272 | class Array(_CData): 273 | pass 274 | 275 | 276 | class Structure(_CData): 277 | def __init__(self, *args): 278 | super().__init__() 279 | 280 | if args: 281 | for (name, _ctype), value in zip(type(self)._fields_, args): 282 | setattr(self, name, value) 283 | else: 284 | for name, ctype in type(self)._fields_: 285 | setattr(self, name, ctype()._auto_unwrap()) 286 | 287 | 288 | class Union(_CData): 289 | pass 290 | 291 | 292 | class CFuncPtr(_CData): 293 | _restype_ = None 294 | _argtypes_ = () 295 | 296 | def __init__(self, src): 297 | super().__init__() 298 | 299 | if isinstance(src, tuple): 300 | (name, dll) = src 301 | self._func_name = name 302 | self._dll_name = dll._name 303 | else: 304 | self._func_name = None 305 | self._dll_name = None 306 | 307 | self.restype = type(self)._restype_ 308 | self.argtypes = type(self)._argtypes_ 309 | 310 | def __call__(self, *args): 311 | if self.restype is None: 312 | return None 313 | else: 314 | if self._dll_name == "objc" and self._func_name in { 315 | "objc_getClass", 316 | "objc_getProtocol", 317 | }: 318 | res = self.restype(hash(args[0])) 319 | else: 320 | res = self.restype() 321 | return res._auto_unwrap() 322 | 323 | 324 | _c_functype_cache = {} 325 | 326 | 327 | def CFUNCTYPE(restype, *argtypes): 328 | try: 329 | return _c_functype_cache[(restype, argtypes)] 330 | except KeyError: 331 | 332 | class CFunctionType(CFuncPtr): 333 | _argtypes_ = argtypes 334 | _restype_ = restype 335 | 336 | _c_functype_cache[(restype, argtypes)] = CFunctionType 337 | return CFunctionType 338 | 339 | 340 | def PYFUNCTYPE(restype, *argtypes): 341 | class CFunctionType(CFuncPtr): 342 | _argtypes_ = argtypes 343 | _restype_ = restype 344 | 345 | return CFunctionType 346 | 347 | 348 | def sizeof(ctype): 349 | return ctype._sizeof() 350 | 351 | 352 | def addressof(cvalue): 353 | return id(cvalue) 354 | 355 | 356 | def alignment(ctype): 357 | return sizeof(ctype) 358 | 359 | 360 | def byref(ctype): 361 | return pointer(ctype) 362 | 363 | 364 | def cast(cvalue, ctype): 365 | if isinstance(cvalue, ctype): 366 | return cvalue 367 | else: 368 | return ctype(cvalue.value) 369 | 370 | 371 | def memmove(dst, src, count): 372 | raise NotImplementedError(f"memmove({dst}, {src}, {count})") 373 | 374 | 375 | def string_at(address): 376 | return c_char_p(b"") 377 | 378 | 379 | class CDLL: 380 | _func_restype_ = c_int 381 | 382 | def __init__(self, name): 383 | super().__init__() 384 | 385 | self._name = name 386 | 387 | class _FuncPtr(CFuncPtr): 388 | _restype_ = self._func_restype_ 389 | 390 | self._FuncPtr = _FuncPtr 391 | 392 | def __getattr__(self, name): 393 | if name.startswith("__") and name.endswith("__"): 394 | raise AttributeError(name) 395 | func = self.__getitem__(name) 396 | setattr(self, name, func) 397 | return func 398 | 399 | def __getitem__(self, name_or_ordinal): 400 | func = self._FuncPtr((name_or_ordinal, self)) 401 | if not isinstance(name_or_ordinal, int): 402 | func.__name__ = name_or_ordinal 403 | return func 404 | 405 | 406 | class PyDLL(CDLL): 407 | pass 408 | 409 | 410 | pythonapi = PyDLL(None) 411 | 412 | 413 | class LibraryLoader: 414 | def __init__(self, dlltype): 415 | self._dlltype = dlltype 416 | 417 | def __getattr__(self, name): 418 | if name[0] == "_": 419 | raise AttributeError(name) 420 | dll = self._dlltype(name) 421 | setattr(self, name, dll) 422 | return dll 423 | 424 | def __getitem__(self, name): 425 | return getattr(self, name) 426 | 427 | def LoadLibrary(self, name): 428 | return self._dlltype(name) 429 | 430 | 431 | cdll = LibraryLoader(CDLL) 432 | pydll = LibraryLoader(PyDLL) 433 | -------------------------------------------------------------------------------- /docs/_mocked_modules/ctypes/util.py: -------------------------------------------------------------------------------- 1 | """Bare minimum mock version of ctypes.util. 2 | 3 | For more details, see __init__.py. 4 | """ 5 | 6 | 7 | def find_library(name): 8 | return name 9 | -------------------------------------------------------------------------------- /docs/_static/images/rubicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beeware/rubicon-objc/d10026cb140e8970327677ab0eef6ce54697b162/docs/_static/images/rubicon.png -------------------------------------------------------------------------------- /docs/background/community.rst: -------------------------------------------------------------------------------- 1 | The Rubicon Objective-C Developer and User community 2 | ==================================================== 3 | 4 | Rubicon Objective-C is part of the `BeeWare suite `__. You 5 | can talk to the community through: 6 | 7 | * `@beeware@fosstodon.org `__ 8 | 9 | * `Discord `__ 10 | 11 | Code of Conduct 12 | --------------- 13 | 14 | The BeeWare community has a strict `Code of Conduct 15 | `__. All users and developers are 16 | expected to adhere to this code. 17 | 18 | If you have any concerns about this code of conduct, or you wish to report a 19 | violation of this code, please contact the project founder `Russell Keith-Magee 20 | `__. 21 | 22 | Contributing 23 | ------------ 24 | 25 | If you experience problems with Rubicon, `log them on GitHub 26 | `__. If you want to contribute 27 | code, please `fork the code `__ and 28 | `submit a pull request `__. 29 | -------------------------------------------------------------------------------- /docs/background/faq.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Why "Rubicon"? 3 | ============== 4 | 5 | So... why the name Rubicon? 6 | =========================== 7 | 8 | The Rubicon is a river in Italy. It was of importance in ancient times as the 9 | border of Rome. The Roman Army was prohibited from crossing this border, as that 10 | would be considered a hostile act against the Roman Senate. 11 | 12 | In 54 BC, Julius Caesar marched the Roman Army across the Rubicon, signaling 13 | his intention to overthrow the Roman Senate. As he did so, legend says he 14 | uttered the words "Alea Iacta Est" - The die is cast. This action led to Julius 15 | being crowned as Emperor of Rome, and the start of the Roman Empire. 16 | 17 | Of course, in order to cross any river, you need to use a bridge. 18 | 19 | This project provides a bridge between the open world of the Python 20 | ecosystem, and the walled garden of Apple's Objective-C ecosystem. 21 | -------------------------------------------------------------------------------- /docs/background/index.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Background 3 | ========== 4 | 5 | Want to know more about the Rubicon project, it's history, community, and 6 | plans for the future? That's what you'll find here! 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | faq 12 | community 13 | success 14 | releases 15 | roadmap 16 | -------------------------------------------------------------------------------- /docs/background/roadmap.rst: -------------------------------------------------------------------------------- 1 | Road map 2 | ======== 3 | -------------------------------------------------------------------------------- /docs/background/success.rst: -------------------------------------------------------------------------------- 1 | Success Stories 2 | =============== 3 | 4 | Want to see examples of Rubicon in use? Here's some: 5 | 6 | * `Travel Tips `_ is an app in the iOS App Store that uses Rubicon to access the iOS UIKit libraries. 7 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Rubicon documentation build configuration file, created by 2 | # sphinx-quickstart on Sat Jul 27 14:58:42 2013. 3 | # 4 | # This file is execfile()d with the current directory set to its containing dir. 5 | # 6 | # Note that not all possible configuration values are present in this 7 | # autogenerated file. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | import os 13 | import sys 14 | from importlib.metadata import version as metadata_version 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath("../src")) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | # needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx_tabs.tabs", 31 | "sphinx_copybutton", 32 | "sphinx.ext.intersphinx", 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = ".rst" 40 | 41 | # The encoding of source files. 42 | # source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = "index" 46 | 47 | # General information about the project. 48 | project = "Rubicon" 49 | copyright = "Russell Keith-Magee" 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The full version, including alpha/beta/rc tags. 56 | release = metadata_version("rubicon-objc") 57 | # The short X.Y version 58 | version = ".".join(release.split(".")[:2]) 59 | 60 | autoclass_content = "both" 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | # today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | # today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ["_build"] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | # default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | # add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | # add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | # show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = "sphinx" 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | # modindex_common_prefix = [] 95 | 96 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 97 | 98 | nitpick_ignore = [ 99 | # These functions do not have documentation 100 | ("py:func", "rubicon.objc.types.CGPointMake"), 101 | ("py:func", "rubicon.objc.types.CGRectMake"), 102 | ("py:func", "rubicon.objc.types.CGSizeMake"), 103 | ("py:func", "rubicon.objc.types.NSEdgeInsetsMake"), 104 | ("py:func", "rubicon.objc.types.NSMakePoint"), 105 | ("py:func", "rubicon.objc.types.NSMakeRect"), 106 | ("py:func", "rubicon.objc.types.NSMakeSize"), 107 | ("py:func", "rubicon.objc.types.UIEdgeInsetsMake"), 108 | ] 109 | 110 | # 2023-07-10: Github Line number anchors fail link checks when retrieved by robots. This 111 | # drops the line number anchor from any link checks, but still validates the base URL is 112 | # valid. 113 | linkcheck_anchors_ignore = [r"L\d+"] 114 | 115 | linkcheck_ignore = [ 116 | r"^https://github.com/beeware/rubicon-objc/issues/\d+$", 117 | r"^https://github.com/beeware/rubicon-objc/pull/\d+$", 118 | ] 119 | 120 | # -- Options for copy button --------------------------------------------------- 121 | 122 | # virtual env prefix: (venv), (beeware-venv), (testenv) 123 | venv = r"\((?:(?:beeware-)?venv|testvenv)\)" 124 | # macOS and Linux shell prompt: $ 125 | shell = r"\$" 126 | # win CMD prompt: C:\>, C:\...> 127 | cmd = r"C:\\>|C:\\\.\.\.>" 128 | # PowerShell prompt: PS C:\>, PS C:\...> 129 | ps = r"PS C:\\>|PS C:\\\.\.\.>" 130 | # zero or one whitespace char 131 | sp = r"\s?" 132 | 133 | # optional venv prefix 134 | venv_prefix = rf"(?:{venv})?" 135 | # one of the platforms' shell prompts 136 | shell_prompt = rf"(?:{shell}|{cmd}|{ps})" 137 | 138 | copybutton_prompt_text = "|".join( 139 | [ 140 | # Python REPL 141 | r">>>\s?", 142 | r"\.\.\.\s?", 143 | # IPython and Jupyter 144 | # r"In \[\d*\]:\s?", r" {5,8}:\s?", r" {2,5}\.\.\.:\s?", 145 | # Shell prompt 146 | rf"{venv_prefix}{sp}{shell_prompt}{sp}", 147 | ] 148 | ) 149 | copybutton_prompt_is_regexp = True 150 | copybutton_remove_prompts = True 151 | copybutton_only_copy_prompt_lines = True 152 | copybutton_copy_empty_lines = False 153 | 154 | # -- Options for HTML output --------------------------------------------------- 155 | 156 | # Theme options are theme-specific and customize the look and feel of a theme 157 | # further. For a list of options available for each theme, see the 158 | # documentation. 159 | # html_theme_options = {} 160 | 161 | # Add any paths that contain custom themes here, relative to this directory. 162 | # html_theme_path = [] 163 | 164 | # The name for this set of Sphinx documents. If None, it defaults to 165 | # " v documentation". 166 | html_title = f"Rubicon {release}" 167 | 168 | # A shorter title for the navigation bar. Default is the same as html_title. 169 | # html_short_title = None 170 | 171 | # The name of an image file (relative to this directory) to place at the top 172 | # of the sidebar. 173 | html_logo = "_static/images/rubicon.png" 174 | 175 | # The name of an image file (within the static path) to use as favicon of the 176 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 177 | # pixels large. 178 | # html_favicon = None 179 | 180 | # Add any paths that contain custom static files (such as style sheets) here, 181 | # relative to this directory. They are copied after the builtin static files, 182 | # so a file named "default.css" will overwrite the builtin "default.css". 183 | html_static_path = ["_static"] 184 | 185 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 186 | # using the given strftime format. 187 | # html_last_updated_fmt = '%b %d, %Y' 188 | 189 | # If true, SmartyPants will be used to convert quotes and dashes to 190 | # typographically correct entities. 191 | # html_use_smartypants = True 192 | 193 | # Custom sidebar templates, maps document names to template names. 194 | # html_sidebars = {} 195 | 196 | # Additional templates that should be rendered to pages, maps page names to 197 | # template names. 198 | # html_additional_pages = {} 199 | 200 | # If false, no module index is generated. 201 | # html_domain_indices = True 202 | 203 | # If false, no index is generated. 204 | # html_use_index = True 205 | 206 | # If true, the index is split into individual pages for each letter. 207 | # html_split_index = False 208 | 209 | # If true, links to the reST sources are added to the pages. 210 | # html_show_sourcelink = True 211 | 212 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 213 | # html_show_sphinx = True 214 | 215 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 216 | # html_show_copyright = True 217 | 218 | # If true, an OpenSearch description file will be output, and all pages will 219 | # contain a tag referring to it. The value of this option must be the 220 | # base URL from which the finished HTML is served. 221 | # html_use_opensearch = '' 222 | 223 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 224 | # html_file_suffix = None 225 | 226 | # Output file base name for HTML help builder. 227 | htmlhelp_basename = "rubicondoc" 228 | 229 | html_theme = "furo" 230 | 231 | # -- Options for LaTeX output -------------------------------------------------- 232 | 233 | latex_elements = { 234 | # The paper size ('letterpaper' or 'a4paper'). 235 | # 'papersize': 'letterpaper', 236 | # The font size ('10pt', '11pt' or '12pt'). 237 | # 'pointsize': '10pt', 238 | # Additional stuff for the LaTeX preamble. 239 | # 'preamble': '', 240 | } 241 | 242 | # Grouping the document tree into LaTeX files. List of tuples 243 | # (source start file, target name, title, author, documentclass [howto/manual]). 244 | latex_documents = [ 245 | ("index", "rubicon.tex", "Rubicon Documentation", "Russell Keith-Magee", "manual"), 246 | ] 247 | 248 | # The name of an image file (relative to this directory) to place at the top of 249 | # the title page. 250 | # latex_logo = None 251 | 252 | # For "manual" documents, if this is true, then toplevel headings are parts, 253 | # not chapters. 254 | # latex_use_parts = False 255 | 256 | # If true, show page references after internal links. 257 | # latex_show_pagerefs = False 258 | 259 | # If true, show URL addresses after external links. 260 | # latex_show_urls = False 261 | 262 | # Documents to append as an appendix to all manuals. 263 | # latex_appendices = [] 264 | 265 | # If false, no module index is generated. 266 | # latex_domain_indices = True 267 | 268 | 269 | # -- Options for manual page output -------------------------------------------- 270 | 271 | # One entry per manual page. List of tuples 272 | # (source start file, name, description, authors, manual section). 273 | man_pages = [ 274 | ("index", "rubicon", "Rubicon Documentation", ["Russell Keith-Magee"], 1), 275 | ] 276 | 277 | # If true, show URL addresses after external links. 278 | # man_show_urls = False 279 | 280 | 281 | # -- Options for Texinfo output ------------------------------------------------ 282 | 283 | # Grouping the document tree into Texinfo files. List of tuples 284 | # (source start file, target name, title, author, 285 | # dir menu entry, description, category) 286 | texinfo_documents = [ 287 | ( 288 | "index", 289 | "rubicon", 290 | "Rubicon Documentation", 291 | "Russell Keith-Magee", 292 | "Rubicon", 293 | "A bridge between an Objective-C runtime environment and Python.", 294 | "Miscellaneous", 295 | ), 296 | ] 297 | 298 | # Documents to append as an appendix to all manuals. 299 | # texinfo_appendices = [] 300 | 301 | # If false, no module index is generated. 302 | # texinfo_domain_indices = True 303 | 304 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 305 | # texinfo_show_urls = 'footnote' 306 | 307 | # -- Options for spelling ------------------------------------------- 308 | 309 | # Spelling check needs an additional module that is not installed by default. 310 | # Add it only if spelling check is requested so docs can be generated without it. 311 | if "spelling" in sys.argv: 312 | extensions.append("sphinxcontrib.spelling") 313 | # Load the enchant package here before `ctypes` is mocked out. 314 | # Otherwise, it will not be able to load its external library later. 315 | import enchant # noqa: F401, E402 316 | 317 | # Spelling language. 318 | spelling_lang = "en_US" 319 | 320 | # Location of word list. 321 | spelling_word_list_filename = "spelling_wordlist" 322 | 323 | # We mock the ctypes and ctypes.util modules during the documentation build, 324 | # so that Sphinx autodoc is able to import and inspect rubicon.objc even on systems without an Objective-C runtime. 325 | # For more details, see the docstring of _mocked_modules/ctypes/__init__.py. 326 | sys.path.insert(0, os.path.abspath("_mocked_modules")) 327 | sys.modules.pop("ctypes", None) 328 | sys.modules.pop("ctypes.util", None) 329 | import ctypes.util # noqa: F401, E402 330 | 331 | del sys.path[0] 332 | -------------------------------------------------------------------------------- /docs/how-to/async.rst: -------------------------------------------------------------------------------- 1 | ===================================== 2 | Asynchronous Programming with Rubicon 3 | ===================================== 4 | 5 | One of the banner features of Python 3 is the introduction of native 6 | asynchronous programming, implemented in :mod:`asyncio`. 7 | 8 | For an introduction to the use of asynchronous programming, see `the 9 | documentation for the asyncio module 10 | `__. 11 | 12 | Integrating asyncio with CoreFoundation 13 | ======================================= 14 | 15 | The :mod:`asyncio` module provides an event loop to coordinate asynchronous 16 | features. However, if you're running an Objective C GUI application, you 17 | probably already have an event loop - the one provided by CoreFoundation. 18 | This CoreFoundation event loop is then wrapped by ``NSApplication`` or 19 | ``UIApplication`` in end-user code. 20 | 21 | However, you can't have two event loops running at the same time, so you need 22 | a way to integrate the two. Luckily, :mod:`asyncio` provides a way to customize 23 | it's event loop so it can be integrated with other event sources. 24 | 25 | It does this using a custom event loop. Rubicon provides a ``RubiconEventLoop`` 26 | that inserts Core Foundation event handling into the asyncio event loop. 27 | 28 | To use asyncio in a pure Core Foundation application, do the following:: 29 | 30 | # Import the Event Loop 31 | from rubicon.objc.eventloop import RubiconEventLoop 32 | 33 | # Create an event loop, and run it! 34 | loop = RubiconEventLoop() 35 | loop.run_forever() 36 | 37 | The last call (``loop.run_forever()``) will, as the name suggests, run forever 38 | - or, at least, until an event handler calls ``loop.stop()`` to terminate the 39 | event loop. 40 | 41 | Integrating asyncio with AppKit and ``NSApplication`` 42 | ===================================================== 43 | 44 | If you're using AppKit and NSApplication, you don't just need to start the 45 | CoreFoundation event loop - you need to start the full ``NSApplication`` 46 | life cycle. To do this, you pass the application instance into the call to 47 | ``loop.run_forever()``:: 48 | 49 | # Import the Event Loop and lifecycle 50 | from rubicon.objc.eventloop import RubiconEventLoop, CocoaLifecycle 51 | 52 | # Get a handle to the shared NSApplication 53 | from ctypes import cdll, util 54 | from rubicon.objc import ObjCClass 55 | 56 | appkit = cdll.LoadLibrary(util.find_library('AppKit')) 57 | NSApplication = ObjCClass('NSApplication') 58 | NSApplication.declare_class_property('sharedApplication') 59 | app = NSApplication.sharedApplication 60 | 61 | # Create an event loop, and run it, using the NSApplication! 62 | loop = RubiconEventLoop() 63 | loop.run_forever(lifecycle=CocoaLifecycle(app)) 64 | 65 | Again, this will run "forever" -- until either ``loop.stop()`` is called, or 66 | ``terminate:`` is invoked on the NSApplication. 67 | 68 | Integrating asyncio with iOS and UIApplication 69 | ============================================== 70 | 71 | If you're using UIKit and UIApplication on iOS, you need to use the iOS 72 | life cycle. To do this, you pass an ``iOSLifecycle`` object into the call to 73 | ``loop.run_forever()``:: 74 | 75 | # Import the Event Loop and lifecycle 76 | from rubicon.objc.eventloop import RubiconEventLoop, iOSLifecycle 77 | 78 | # Create an event loop, and run it, using the UIApplication! 79 | loop = RubiconEventLoop() 80 | loop.run_forever(lifecycle=iOSLifecycle()) 81 | 82 | Again, this will run "forever" -- until either ``loop.stop()`` is called, or 83 | ``terminate:`` is invoked on the UIApplication. 84 | -------------------------------------------------------------------------------- /docs/how-to/contribute/code.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | How to contribute code to Rubicon 3 | ================================= 4 | 5 | If you experience problems with Rubicon, `log them on GitHub`_. If you want 6 | to contribute code, please `fork the code`_ and `submit a pull request`_. 7 | 8 | .. _log them on Github: https://github.com/beeware/rubicon-objc/issues 9 | .. _fork the code: https://github.com/beeware/rubicon-objc 10 | .. _submit a pull request: https://github.com/beeware/rubicon-objc/pulls 11 | 12 | .. _setup-dev-environment: 13 | 14 | Set up your development environment 15 | =================================== 16 | 17 | The recommended way of setting up your development environment for Rubicon is 18 | to clone the repository, create a virtual environment, and install the required 19 | dependencies: 20 | 21 | .. code-block:: console 22 | 23 | $ git clone https://github.com/beeware/rubicon-objc.git 24 | $ cd rubicon-objc 25 | $ python3 -m venv venv 26 | $ source venv/bin/activate 27 | (venv) $ python3 -m pip install -Ue ".[dev]" 28 | 29 | Rubicon uses a tool called `Pre-Commit `__ to identify 30 | simple issues and standardize code formatting. It does this by installing a git 31 | hook that automatically runs a series of code linters prior to finalizing any 32 | git commit. To enable pre-commit, run: 33 | 34 | .. code-block:: console 35 | 36 | (venv) $ pre-commit install 37 | pre-commit installed at .git/hooks/pre-commit 38 | 39 | When you commit any change, pre-commit will run automatically. If there are any 40 | issues found with the commit, this will cause your commit to fail. Where possible, 41 | pre-commit will make the changes needed to correct the problems it has found: 42 | 43 | .. code-block:: console 44 | 45 | (venv) $ git add some/interesting_file.py 46 | (venv) $ git commit -m "Minor change" 47 | black....................................................................Failed 48 | - hook id: black 49 | - files were modified by this hook 50 | 51 | reformatted some/interesting_file.py 52 | 53 | All done! ✨ 🍰 ✨ 54 | 1 file reformatted. 55 | 56 | flake8...................................................................Passed 57 | check toml...........................................(no files to check)Skipped 58 | check yaml...........................................(no files to check)Skipped 59 | check for case conflicts.................................................Passed 60 | check docstring is first.................................................Passed 61 | fix end of files.........................................................Passed 62 | trim trailing whitespace.................................................Passed 63 | isort....................................................................Passed 64 | pyupgrade................................................................Passed 65 | docformatter.............................................................Passed 66 | 67 | 68 | You can then re-add any files that were modified as a result of the pre-commit checks, 69 | and re-commit the change. 70 | 71 | .. code-block:: console 72 | 73 | (venv) $ git add some/interesting_file.py 74 | (venv) $ git commit -m "Minor change" 75 | black....................................................................Passed 76 | flake8...................................................................Passed 77 | check toml...........................................(no files to check)Skipped 78 | check yaml...........................................(no files to check)Skipped 79 | check for case conflicts.................................................Passed 80 | check docstring is first.................................................Passed 81 | fix end of files.........................................................Passed 82 | trim trailing whitespace.................................................Passed 83 | isort....................................................................Passed 84 | pyupgrade................................................................Passed 85 | docformatter.............................................................Passed 86 | [bugfix e3e0f73] Minor change 87 | 1 file changed, 4 insertions(+), 2 deletions(-) 88 | 89 | Rubicon uses `tox `__ to manage the 90 | testing process. To set up a testing environment and run the full test suite, 91 | run: 92 | 93 | .. code-block:: console 94 | 95 | (venv) $ tox 96 | 97 | By default this will run the test suite multiple times, once on each Python 98 | version supported by Rubicon, as well as running some pre-commit checks of 99 | code style and validity. This can take a while, so if you want to speed up 100 | the process while developing, you can run the tests on one Python version only: 101 | 102 | .. code-block:: console 103 | 104 | (venv) $ tox -e py 105 | 106 | Or, to run using a specific version of Python: 107 | 108 | .. code-block:: console 109 | 110 | (venv) $ tox -e py310 111 | 112 | substituting the version number that you want to target. You can also specify 113 | one of the pre-commit checks `flake8`, `docs` or `package` to check code 114 | formatting, documentation syntax and packaging metadata, respectively. 115 | 116 | Now you are ready to start hacking on Rubicon. Have fun! 117 | -------------------------------------------------------------------------------- /docs/how-to/contribute/docs.rst: -------------------------------------------------------------------------------- 1 | Contributing to the documentation 2 | ================================= 3 | 4 | Here are some tips for working on this documentation. You're welcome to add 5 | more and help us out! 6 | 7 | First of all, you should check the `reStructuredText (reST) Primer 8 | `_ to 9 | learn how to write your ``.rst`` file. 10 | 11 | Create a ``.rst`` file 12 | ---------------------- 13 | 14 | Look at the structure and choose the best category to put your ``.rst`` file. 15 | Make sure that it is referenced in the index of the corresponding category, 16 | so it will show on in the documentation. If you have no idea how to do this, 17 | study the other index files for clues. 18 | 19 | Build documentation locally 20 | --------------------------- 21 | 22 | .. Docs are always built on Python 3.12. See also the RTD and tox config. 23 | 24 | To build the documentation locally, :ref:`set up a development environment 25 | `. However, you **must** have a Python 3.12 interpreter 26 | installed and available on your path (i.e., ``python3.12`` must start a Python 27 | 3.12 interpreter). 28 | 29 | You'll also need to install the Enchant spell checking library. 30 | Enchant can be installed using `Homebrew `__: 31 | 32 | .. code-block:: console 33 | 34 | (venv) $ brew install enchant 35 | 36 | If you're on an M1 machine, you'll also need to manually set the location 37 | of the Enchant library: 38 | 39 | .. code-block:: console 40 | 41 | (venv) $ export PYENCHANT_LIBRARY_PATH=/opt/homebrew/lib/libenchant-2.2.dylib 42 | 43 | Once your development environment is set up, run: 44 | 45 | .. code-block:: console 46 | 47 | (venv) $ tox -e docs 48 | 49 | The output of the file should be in the ``docs/_build/html`` folder. If there 50 | are any markup problems, they'll raise an error. 51 | 52 | Documentation linting 53 | --------------------- 54 | 55 | Before committing and pushing documentation updates, run linting for the 56 | documentation: 57 | 58 | .. tabs:: 59 | 60 | .. group-tab:: macOS 61 | 62 | .. code-block:: console 63 | 64 | (venv) $ tox -e docs-lint 65 | 66 | .. group-tab:: Linux 67 | 68 | .. code-block:: console 69 | 70 | (venv) $ tox -e docs-lint 71 | 72 | .. group-tab:: Windows 73 | 74 | .. code-block:: doscon 75 | 76 | C:\...>tox -e docs-lint 77 | 78 | This will validate the documentation does not contain: 79 | 80 | * invalid syntax and markup 81 | * dead hyperlinks 82 | * misspelled words 83 | 84 | If a valid spelling of a word is identified as misspelled, then add the word to 85 | the list in ``docs/spelling_wordlist``. This will add the word to the 86 | spellchecker's 87 | 88 | Rebuilding all documentation 89 | ---------------------------- 90 | 91 | To force a rebuild for all of the documentation: 92 | 93 | .. tabs:: 94 | 95 | .. group-tab:: macOS 96 | 97 | .. code-block:: console 98 | 99 | (venv) $ tox -e docs-all 100 | 101 | .. group-tab:: Linux 102 | 103 | .. code-block:: console 104 | 105 | (venv) $ tox -e docs-all 106 | 107 | .. group-tab:: Windows 108 | 109 | .. code-block:: doscon 110 | 111 | C:\...>tox -e docs-all 112 | 113 | The documentation should be fully rebuilt in the ``docs/_build/html`` folder. 114 | If there are any markup problems, they'll raise an error. 115 | 116 | Live documentation preview 117 | -------------------------- 118 | 119 | To support rapid editing of documentation, Rubicon also has a "live preview" mode: 120 | 121 | .. tabs:: 122 | 123 | .. group-tab:: macOS 124 | 125 | .. code-block:: console 126 | 127 | (venv) $ tox -e docs-live 128 | 129 | .. group-tab:: Linux 130 | 131 | .. code-block:: console 132 | 133 | (venv) $ tox -e docs-live 134 | 135 | .. group-tab:: Windows 136 | 137 | .. code-block:: doscon 138 | 139 | (venv) C:\...>tox -e docs-live 140 | 141 | This will build the documentation, start a web server to serve the build documentation, 142 | and watch the file system for any changes to the documentation source. If a change is 143 | detected, the documentation will be rebuilt, and any browser viewing the modified page 144 | will be automatically refreshed. 145 | 146 | Live preview mode will only monitor the ``docs`` directory for changes. If you're 147 | updating the inline documentation associated with Toga source code, you'll need to use 148 | the ``docs-live-src`` target to build docs: 149 | 150 | .. tabs:: 151 | 152 | .. group-tab:: macOS 153 | 154 | .. code-block:: console 155 | 156 | (venv) $ tox -e docs-live-src 157 | 158 | .. group-tab:: Linux 159 | 160 | .. code-block:: console 161 | 162 | (venv) $ tox -e docs-live-src 163 | 164 | .. group-tab:: Windows 165 | 166 | .. code-block:: doscon 167 | 168 | (venv) C:\...>tox -e docs-live-src 169 | 170 | This behaves the same as ``docs-live``, but will also monitor any changes to the 171 | ``src/rubicon/objc`` folder, reflecting any changes to inline documentation. 172 | However, the rebuild process takes much longer, so you may not want to use this 173 | target unless you're actively editing inline documentation. 174 | -------------------------------------------------------------------------------- /docs/how-to/contribute/index.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Contributing to Rubicon ObjC 3 | ============================ 4 | 5 | Rubicon ObjC is an open source project, and actively encourages community 6 | contributions. The following guides will help you get started contributing. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :glob: 11 | 12 | code 13 | docs 14 | -------------------------------------------------------------------------------- /docs/how-to/get-started.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Getting Started with Rubicon 3 | ============================ 4 | 5 | To use Rubicon, create a new virtual environment, and install it: 6 | 7 | .. code-block:: console 8 | 9 | $ python3 -m venv venv 10 | $ source venv/bin/activate 11 | (venv) $ pip install rubicon-objc 12 | 13 | You're now ready to use Rubicon! Your next step is to work through the 14 | :doc:`/tutorial/index`, which will take you step-by-step through your first 15 | steps and introduce you to the important concepts you need to become familiar 16 | with. 17 | -------------------------------------------------------------------------------- /docs/how-to/index.rst: -------------------------------------------------------------------------------- 1 | .. _how-to: 2 | 3 | ============= 4 | How-to Guides 5 | ============= 6 | 7 | How-to guides are recipes that take the user through steps in key subjects. 8 | They are more advanced than tutorials and assume a lot more about what the user 9 | already knows than tutorials do, and unlike documents in the tutorial they can 10 | stand alone. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :glob: 15 | 16 | get-started 17 | type-mapping 18 | memory-management 19 | protocols 20 | async 21 | c-functions 22 | contribute/index 23 | internal/index 24 | -------------------------------------------------------------------------------- /docs/how-to/internal/index.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Internal How-to guides 3 | ====================== 4 | 5 | These guides are for the maintainers of the Rubicon-ObjC project, documenting 6 | internal project procedures. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :glob: 11 | 12 | release 13 | -------------------------------------------------------------------------------- /docs/how-to/internal/release.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | How to cut a Rubicon-ObjC release 3 | ================================= 4 | 5 | The release infrastructure for Rubicon is semi-automated, using GitHub Actions 6 | to formally publish releases. 7 | 8 | This guide assumes that you have an ``upstream`` remote configured on your 9 | local clone of the Rubicon repository, pointing at the official repository. If 10 | all you have is a checkout of a personal fork of the Rubicon-ObjC repository, 11 | you can configure that checkout by running: 12 | 13 | .. code-block:: console 14 | 15 | $ git remote add upstream https://github.com/beeware/rubicon-objc.git 16 | 17 | The procedure for cutting a new release is as follows: 18 | 19 | #. Check the contents of the upstream repository's main branch: 20 | 21 | .. code-block:: console 22 | 23 | $ git fetch upstream 24 | $ git checkout --detach upstream/main 25 | 26 | Check that the HEAD of release now matches upstream/main. 27 | 28 | #. Ensure that the release notes are up to date. Run: 29 | 30 | .. code-block:: console 31 | 32 | $ tox -e towncrier -- --draft 33 | 34 | to review the release notes that will be included, and then: 35 | 36 | .. code-block:: console 37 | 38 | $ tox -e towncrier 39 | 40 | to generate the updated release notes. 41 | 42 | #. Build the documentation to ensure that the new release notes don't include any 43 | spelling errors or markup problems: 44 | 45 | .. code-block:: console 46 | 47 | $ tox -e docs-lint,docs 48 | 49 | #. Tag the release, and push the tag upstream: 50 | 51 | .. code-block:: console 52 | 53 | $ git tag v1.2.3 54 | $ git push upstream HEAD:main 55 | $ git push upstream v1.2.3 56 | 57 | #. Pushing the tag will start a workflow to create a draft release on GitHub. 58 | You can `follow the progress of the workflow on GitHub 59 | `__; 60 | once the workflow completes, there should be a new `draft release 61 | `__, and an entry on the 62 | `Test PyPI server `__. 63 | 64 | Confirm that this action successfully completes. If it fails, there's a 65 | couple of possible causes: 66 | 67 | a. The final upload to Test PyPI failed. Test PyPI is not have the same 68 | service monitoring as PyPI-proper, so it sometimes has problems. However, 69 | it's also not critical to the release process; if this step fails, you can 70 | perform Step 6 by manually downloading the "packages" artifact from the 71 | GitHub workflow instead. 72 | b. Something else fails in the build process. If the problem can be fixed 73 | without a code change to the Rubicon-ObjC repository (e.g., a transient 74 | problem with build machines not being available), you can re-run the 75 | action that failed through the GitHub Actions GUI. If the fix requires a 76 | code change, delete the old tag, make the code change, and re-tag the 77 | release. 78 | 79 | #. Create a clean virtual environment, install the new release from Test PyPI, and 80 | perform any pre-release testing that may be appropriate: 81 | 82 | .. code-block:: console 83 | 84 | $ python3 -m venv testvenv 85 | $ . ./testvenv/bin/activate 86 | (testvenv) $ pip install --extra-index-url https://test.pypi.org/simple/ rubicon-objc==1.2.3 87 | (testvenv) $ python -c "from rubicon.objc import __version__; print(__version__)" 88 | 1.2.3 89 | (testvenv) $ #... any other manual checks you want to perform ... 90 | 91 | #. Log into ReadTheDocs, visit the `Versions tab 92 | `__, and activate the 93 | new version. Ensure that the build completes; if there's a problem, you 94 | may need to correct the build configuration, roll back and re-tag the release. 95 | 96 | #. Edit the GitHub release to add release notes. You can use the text generated 97 | by Towncrier, but you'll need to update the format to Markdown, rather than 98 | ReST. If necessary, check the pre-release checkbox. 99 | 100 | #. Double check everything, then click Publish. This will trigger a 101 | `publication workflow on GitHub 102 | `__. 103 | 104 | #. Wait for the `package to appear on PyPI 105 | `__. 106 | 107 | Congratulations, you've just published a release! 108 | 109 | If anything went wrong during steps 3 or 5, you will need to delete the draft 110 | release from GitHub, and push an updated tag. Once the release has successfully 111 | appeared on PyPI, it cannot be changed; if you spot a problem in a published 112 | package, you'll need to tag a completely new release. 113 | -------------------------------------------------------------------------------- /docs/how-to/memory-management.rst: -------------------------------------------------------------------------------- 1 | =========================================== 2 | Memory management for Objective-C instances 3 | =========================================== 4 | 5 | Reference counting in Objective-C 6 | ================================= 7 | 8 | Reference counting works differently in Objective-C compared to Python. Python 9 | will automatically track where variables are referenced and free memory when 10 | the reference count drops to zero whereas Objective-C uses explicit reference 11 | counting to manage memory. The methods ``retain``, ``release`` and 12 | ``autorelease`` are used to increase and decrease the reference counts as 13 | described in the `Apple developer documentation 14 | `__. 15 | When enabling automatic reference counting (ARC), the appropriate calls for 16 | memory management will be inserted for you at compile-time. However, since 17 | Rubicon Objective-C operates at runtime, it cannot make use of ARC. 18 | 19 | Reference management in Rubicon 20 | =============================== 21 | 22 | In most cases, you won't have to manage reference counts in Python, Rubicon 23 | Objective-C will do that work for you. It does so by calling ``retain`` on an 24 | object when Rubicon creates a ``ObjCInstance`` for it on the Python side, and calling 25 | ``autorelease`` when the ``ObjCInstance`` is garbage collected in Python. Retaining 26 | the object ensures it is not deallocated while it is still referenced from Python 27 | and releasing it again on ``__del__`` ensures that we do not leak memory. 28 | 29 | The only exception to this is when you create an object -- which is always done 30 | through methods starting with "alloc", "new", "copy", or "mutableCopy". Rubicon does 31 | not explicitly retain such objects because we own objects created by us, but Rubicon 32 | does autorelease them when the Python wrapper is garbage collected. 33 | 34 | Rubicon Objective-C will not keep track if you additionally manually ``retain`` an 35 | object. You will be responsible to insert appropriate ``release`` or ``autorelease`` 36 | calls yourself to prevent leaking memory. 37 | 38 | Weak references in Objective-C 39 | ------------------------------ 40 | 41 | You will need to pay attention to reference counting in case of **weak 42 | references**. In Objective-C, as in Python, creating a weak reference means that 43 | the reference count of the object is not incremented and the object will be 44 | deallocated when no strong references remain. Any weak references to the object 45 | are then set to ``nil``. 46 | 47 | Some Objective-C objects store references to other objects as a weak reference. 48 | Such properties will be declared in the Apple developer documentation as 49 | "@property(weak)" or "@property(assign)". This is commonly the case for 50 | delegates. For example, in the code below, the ``NSOutlineView`` only stores a 51 | weak reference to the object which is assigned to its delegate property: 52 | 53 | .. code-block:: python 54 | 55 | from rubicon.objc import NSObject, ObjCClass 56 | from rubicon.objc.runtime import load_library 57 | 58 | app_kit = load_library("AppKit") 59 | NSOutlineView = ObjCClass("NSOutlineView") 60 | 61 | outline_view = NSOutlineView.alloc().init() 62 | delegate = NSObject.alloc().init() 63 | 64 | outline_view.delegate = delegate 65 | 66 | You will need to keep a reference to the Python variable ``delegate`` so that 67 | the corresponding Objective-C instance does not get deallocated. 68 | 69 | Reference cycles in Objective-C 70 | ------------------------------- 71 | 72 | Python has a garbage collector which detects references cycles and frees 73 | objects in such cycles if no other references remain. Cyclical references can 74 | be useful in a number of cases, for instance to refer to a "parent" of an 75 | instance, and Python makes life easier by properly freeing such references. For 76 | example: 77 | 78 | .. code-block:: python 79 | 80 | class TreeNode: 81 | def __init__(self, val): 82 | self.val = val 83 | self.parent = None 84 | self.children = [] 85 | 86 | 87 | root = TreeNode("/home") 88 | 89 | child = TreeNode("/Documents") 90 | child.parent = root 91 | 92 | root.children.append(child) 93 | 94 | # This will free both root and child on 95 | # the next garbage collection cycle: 96 | del root 97 | del child 98 | 99 | 100 | Similar code in Objective-C will lead to memory leaks. This also holds for 101 | Objective-C instances created through Rubicon Objective-C since Python's 102 | garbage collector is unable to detect reference cycles on the Objective-C side. 103 | If you are writing code which would lead to reference cycles, consider storing 104 | objects as weak references instead. The above code would be written as follows 105 | when using Objective-C classes: 106 | 107 | .. code-block:: python 108 | 109 | from rubicon.objc import NSObject, NSMutableArray 110 | from rubicon.objc.api import objc_property, objc_method 111 | 112 | 113 | class TreeNode(NSObject): 114 | val = objc_property() 115 | children = objc_property() 116 | parent = objc_property(weak=True) 117 | 118 | @objc_method 119 | def initWithValue_(self, val): 120 | self.val = val 121 | self.children = NSMutableArray.new() 122 | return self 123 | 124 | 125 | root = TreeNode.alloc().initWithValue("/home") 126 | 127 | child = TreeNode.alloc().initWithValue("/Documents") 128 | child.parent = root 129 | 130 | root.children.addObject(child) 131 | 132 | # This will free both root and child: 133 | del root 134 | del child 135 | -------------------------------------------------------------------------------- /docs/how-to/protocols.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Using and creating Objective-C protocols 3 | ======================================== 4 | 5 | Protocols are used in Objective-C to declare a set of methods and properties 6 | for a class to implement. They have a similar purpose to ABCs (abstract base 7 | classes) in Python. 8 | 9 | Looking up a protocol 10 | --------------------- 11 | 12 | Protocol objects can be looked up using the ``ObjCProtocol`` constructor, 13 | similar to how classes can be looked up using ``ObjCClass``: 14 | 15 | .. code-block:: pycon 16 | 17 | >>> NSCopying = ObjCProtocol('NSCopying') 18 | >>> NSCopying 19 | 20 | 21 | The ``isinstance`` function can be used to check whether an object conforms to 22 | a protocol: 23 | 24 | .. code-block:: pycon 25 | 26 | >>> isinstance(NSObject.new(), NSCopying) 27 | False 28 | >>> isinstance(NSArray.array(), NSCopying) 29 | True 30 | 31 | Implementing a protocol 32 | ------------------------ 33 | 34 | When writing a custom Objective-C class, you might want to have it conform to 35 | one or multiple protocols. In Rubicon, this is done by using the ``protocols`` 36 | keyword argument in the base class list. For example, if you have a class 37 | ``UserAccount`` and want it to conform to ``NSCopyable``, you would write it 38 | like this: 39 | 40 | .. code-block:: python 41 | 42 | class UserAccount(NSObject, protocols=[NSCopying]): 43 | username = objc_property() 44 | emailAddress = objc_property() 45 | 46 | @objc_method 47 | def initWithUsername_emailAddress_(self, username, emailAddress): 48 | self = self.init() 49 | if self is None: 50 | return None 51 | self.username = username 52 | self.emailAddress = emailAddress 53 | return self 54 | 55 | # This method is required by NSCopying. 56 | # The "zone" parameter is obsolete and can be ignored, but must be included for backwards compatibility. 57 | # This method is not normally used directly. Usually you call the copy method instead, 58 | # which calls copyWithZone: internally. 59 | @objc_method 60 | def copyWithZone_(self, zone): 61 | return UserAccount.alloc().initWithUsername(self.username, emailAddress=self.emailAddress) 62 | 63 | We can now use our class. The ``copy`` method (which uses our implemented 64 | ``copyWithZone:`` method) can also be used: 65 | 66 | .. code-block:: pycon 67 | 68 | >>> ua = UserAccount.alloc().initWithUsername_emailAddress_(at('person'), at('person@example.com')) 69 | >>> ua 70 | > 71 | >>> ua.copy() 72 | > 73 | 74 | And we can check that the class conforms to the protocol: 75 | 76 | .. code-block:: pycon 77 | 78 | >>> isinstance(ua, NSCopying) 79 | True 80 | 81 | Writing custom protocols 82 | ------------------------ 83 | 84 | You can also create custom protocols. This works similarly to creating custom 85 | Objective-C classes: 86 | 87 | .. code-block:: python 88 | 89 | class Named(metaclass=ObjCProtocol): 90 | name = objc_property() 91 | 92 | @objc_method 93 | def sayName(self): 94 | ... 95 | 96 | There are two notable differences between creating classes and protocols: 97 | 98 | 1. Protocols do not need to extend exactly one other protocol - they can also 99 | extend multiple protocols, or none at all. When not extending other 100 | protocols, as is the case here, we need to explicitly add 101 | ``metaclass=ObjCProtocol`` to the base class list, to tell Python that this 102 | is a protocol and not a regular Python class. When extending other 103 | protocols, Python detects this automatically. 104 | 2. Protocol methods do not have a body. Python has no dedicated syntax for 105 | functions without a body, so we put ``...`` in the body instead. (You could 106 | technically put code in the body, but this would be misleading and is not 107 | recommended.) 108 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Rubicon Objective-C 3 | =================== 4 | 5 | Rubicon Objective-C is a bridge between Objective-C and Python. It enables you 6 | to: 7 | 8 | * Use Python to instantiate objects defined in Objective-C, 9 | * Use Python to invoke methods on objects defined in Objective-C, and 10 | * Subclass and extend Objective-C classes in Python. 11 | 12 | It also includes wrappers of the some key data types from the Foundation 13 | framework (e.g., ``NSString``). 14 | 15 | Table of contents 16 | ================= 17 | 18 | :doc:`Tutorial <./tutorial/index>` 19 | ---------------------------------- 20 | 21 | Get started with a hands-on introduction for beginners 22 | 23 | :doc:`How-to guides <./how-to/index>` 24 | ------------------------------------- 25 | 26 | Guides and recipes for common problems and tasks, including how to contribute 27 | 28 | :doc:`Background <./background/index>` 29 | -------------------------------------- 30 | 31 | Explanation and discussion of key topics and concepts 32 | 33 | :doc:`Reference <./reference/index>` 34 | ------------------------------------ 35 | 36 | Technical reference - commands, modules, classes, methods 37 | 38 | Community 39 | ========= 40 | 41 | Rubicon is part of the `BeeWare suite `__. You can talk to the community through: 42 | 43 | * `@beeware@fosstodon.org on Mastodon `__ 44 | 45 | * `Discord `__ 46 | 47 | * The Rubicon-ObjC `Github Discussions forum `__ 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | :hidden: 52 | :titlesonly: 53 | 54 | tutorial/index 55 | how-to/index 56 | background/index 57 | reference/index 58 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | if "%1" == "livehtml" goto livehtml 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.https://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 34 | goto end 35 | 36 | :livehtml 37 | sphinx-autobuild -b html %SPHINXOPTS% %SOURCEDIR% %BUILDDIR%/html 38 | goto end 39 | 40 | :end 41 | popd 42 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Reference 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 1 7 | 8 | rubicon-objc 9 | rubicon-objc-api 10 | rubicon-objc-eventloop 11 | rubicon-objc-runtime 12 | rubicon-objc-types 13 | 14 | This is the technical reference for public APIs provided by Rubicon. 15 | 16 | Note that the :mod:`rubicon.objc` package also contains other submodules not 17 | documented here. These are for internal use only and not part of the public 18 | API; they may change at any time without notice. 19 | -------------------------------------------------------------------------------- /docs/reference/rubicon-objc-eventloop.rst: -------------------------------------------------------------------------------- 1 | ==================================================================================== 2 | :mod:`rubicon.objc.eventloop` --- Integrating native event loops with :mod:`asyncio` 3 | ==================================================================================== 4 | 5 | .. module:: rubicon.objc.eventloop 6 | 7 | .. note:: 8 | 9 | The documentation for this module is incomplete. You can help by 10 | :doc:`contributing to the documentation <../how-to/contribute/docs>`. 11 | 12 | .. autoclass:: EventLoopPolicy 13 | 14 | .. automethod:: new_event_loop 15 | .. automethod:: get_default_loop 16 | .. automethod:: get_child_watcher 17 | .. automethod:: set_child_watcher 18 | 19 | .. autoclass:: CocoaLifecycle 20 | 21 | .. automethod:: start 22 | .. automethod:: stop 23 | 24 | .. autoclass:: iOSLifecycle 25 | 26 | .. automethod:: start 27 | .. automethod:: stop 28 | -------------------------------------------------------------------------------- /docs/reference/rubicon-objc-runtime.rst: -------------------------------------------------------------------------------- 1 | ==================================================================== 2 | :mod:`rubicon.objc.runtime` --- Low-level Objective-C runtime access 3 | ==================================================================== 4 | 5 | .. module:: rubicon.objc.runtime 6 | 7 | This module contains types, functions, and C libraries used for low-level 8 | access to the Objective-C runtime. 9 | 10 | In most cases there is no need to use this module directly --- the 11 | :mod:`rubicon.objc.api` module provides the same functionality through a 12 | high-level interface. 13 | 14 | .. _predefined-c-libraries: 15 | 16 | C libraries 17 | ----------- 18 | 19 | Some commonly used C libraries are provided as :class:`~ctypes.CDLL`\s. Other 20 | libraries can be loaded using the :func:`load_library` function. 21 | 22 | .. data:: libc 23 | :annotation: = load_library('c') 24 | 25 | The `C standard library `__. 26 | 27 | The following functions are accessible by default: 28 | 29 | .. hlist:: 30 | * ``free`` 31 | 32 | .. data:: libobjc 33 | :annotation: = load_library('objc') 34 | 35 | The `Objective-C runtime library `__. 36 | 37 | The following functions are accessible by default: 38 | 39 | .. hlist:: 40 | * ``class_addIvar`` 41 | * ``class_addMethod`` 42 | * ``class_addProperty`` 43 | * ``class_addProtocol`` 44 | * ``class_copyIvarList`` 45 | * ``class_copyMethodList`` 46 | * ``class_copyPropertyList`` 47 | * ``class_copyProtocolList`` 48 | * ``class_getClassMethod`` 49 | * ``class_getClassVariable`` 50 | * ``class_getInstanceMethod`` 51 | * ``class_getInstanceSize`` 52 | * ``class_getInstanceVariable`` 53 | * ``class_getIvarLayout`` 54 | * ``class_getMethodImplementation`` 55 | * ``class_getName`` 56 | * ``class_getProperty`` 57 | * ``class_getSuperclass`` 58 | * ``class_getVersion`` 59 | * ``class_getWeakIvarLayout`` 60 | * ``class_isMetaClass`` 61 | * ``class_replaceMethod`` 62 | * ``class_respondsToSelector`` 63 | * ``class_setIvarLayout`` 64 | * ``class_setVersion`` 65 | * ``class_setWeakIvarLayout`` 66 | * ``ivar_getName`` 67 | * ``ivar_getOffset`` 68 | * ``ivar_getTypeEncoding`` 69 | * ``method_exchangeImplementations`` 70 | * ``method_getImplementation`` 71 | * ``method_getName`` 72 | * ``method_getTypeEncoding`` 73 | * ``method_setImplementation`` 74 | * ``objc_allocateClassPair`` 75 | * ``objc_copyProtocolList`` 76 | * ``objc_getAssociatedObject`` 77 | * ``objc_getClass`` 78 | * ``objc_getMetaClass`` 79 | * ``objc_getProtocol`` 80 | * ``objc_registerClassPair`` 81 | * ``objc_removeAssociatedObjects`` 82 | * ``objc_setAssociatedObject`` 83 | * ``object_getClass`` 84 | * ``object_getClassName`` 85 | * ``object_getIvar`` 86 | * ``object_setIvar`` 87 | * ``property_getAttributes`` 88 | * ``property_getName`` 89 | * ``property_copyAttributeList`` 90 | * ``protocol_addMethodDescription`` 91 | * ``protocol_addProtocol`` 92 | * ``protocol_addProperty`` 93 | * ``objc_allocateProtocol`` 94 | * ``protocol_conformsToProtocol`` 95 | * ``protocol_copyMethodDescriptionList`` 96 | * ``protocol_copyPropertyList`` 97 | * ``protocol_copyProtocolList`` 98 | * ``protocol_getMethodDescription`` 99 | * ``protocol_getName`` 100 | * ``objc_registerProtocol`` 101 | * ``sel_getName`` 102 | * ``sel_isEqual`` 103 | * ``sel_registerName`` 104 | 105 | .. data:: Foundation 106 | :annotation: = load_library('Foundation') 107 | 108 | The `Foundation `__ 109 | framework. 110 | 111 | .. autofunction:: load_library 112 | 113 | Objective-C runtime types 114 | ------------------------- 115 | 116 | These are various types used by the Objective-C runtime functions. 117 | 118 | .. autoclass:: objc_id([value]) 119 | .. autoclass:: objc_block([value]) 120 | 121 | .. autoclass:: SEL([value]) 122 | 123 | .. autoattribute:: name 124 | 125 | .. autoclass:: Class([value]) 126 | .. autoclass:: IMP([value]) 127 | .. autoclass:: Method([value]) 128 | .. autoclass:: Ivar([value]) 129 | .. autoclass:: objc_property_t([value]) 130 | 131 | .. autoclass:: objc_property_attribute_t([name, value]) 132 | 133 | .. attribute:: 134 | name 135 | value 136 | 137 | The attribute name and value as C strings (:class:`bytes`). 138 | 139 | .. autoclass:: objc_method_description([name, value]) 140 | 141 | .. attribute:: name 142 | 143 | The method name as a :class:`SEL`. 144 | 145 | .. attribute:: types 146 | 147 | The method's signature encoding as a C string (:class:`bytes`). 148 | 149 | .. autoclass:: objc_super([receiver, super_class]) 150 | 151 | .. attribute:: receiver 152 | 153 | The receiver of the call, as an :class:`objc_id`. 154 | 155 | .. attribute:: super_class 156 | 157 | The class in which to start searching for method implementations, as a 158 | :class:`Class`. 159 | 160 | Objective-C runtime utility functions 161 | ------------------------------------- 162 | 163 | These utility functions provide easier access from Python to certain parts of 164 | the Objective-C runtime. 165 | 166 | .. function:: object_isClass(obj) 167 | 168 | Return whether the given Objective-C object is a class (or a metaclass). 169 | 170 | This is equivalent to the :data:`libobjc` function `object_isClass 171 | `__ 172 | from ````, which is only available since OS X 10.10 and iOS 173 | 8. This module-level function is provided to support older systems --- it 174 | uses the :data:`libobjc` function if available, and otherwise emulates it. 175 | 176 | .. autofunction:: get_class 177 | .. autofunction:: should_use_stret 178 | .. autofunction:: should_use_fpret 179 | .. autofunction:: send_message 180 | .. autofunction:: send_super 181 | .. autofunction:: add_method 182 | .. autofunction:: add_ivar 183 | .. autofunction:: get_ivar 184 | .. autofunction:: set_ivar 185 | -------------------------------------------------------------------------------- /docs/reference/rubicon-objc.rst: -------------------------------------------------------------------------------- 1 | =============================================== 2 | :mod:`rubicon.objc` --- The main Rubicon module 3 | =============================================== 4 | 5 | .. module:: rubicon.objc 6 | 7 | This is the main namespace of Rubicon-ObjC. Rubicon is structured into multiple 8 | submodules of :mod:`rubicon.objc`, and the most commonly used attributes from 9 | these submodules are exported via the :mod:`rubicon.objc` module. This means 10 | that most users only need to import and use the main :mod:`rubicon.objc` 11 | module; the individual submodules only need to be used for attributes that are 12 | not also available on :mod:`rubicon.objc`. 13 | 14 | Exported Attributes 15 | ------------------- 16 | 17 | This is a full list of all attributes exported on the :mod:`rubicon.objc` 18 | module. For detailed documentation on these attributes, click the links below 19 | to visit the relevant sections of the submodules' documentation. 20 | 21 | From :mod:`rubicon.objc.api` 22 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 23 | 24 | .. hlist:: 25 | 26 | * :class:`~rubicon.objc.api.Block` 27 | * :data:`~rubicon.objc.api.NSArray` 28 | * :data:`~rubicon.objc.api.NSDictionary` 29 | * :data:`~rubicon.objc.api.NSMutableArray` 30 | * :data:`~rubicon.objc.api.NSMutableDictionary` 31 | * :data:`~rubicon.objc.api.NSObject` 32 | * :data:`~rubicon.objc.api.NSObjectProtocol` 33 | * :class:`~rubicon.objc.api.ObjCBlock` 34 | * :class:`~rubicon.objc.api.ObjCClass` 35 | * :class:`~rubicon.objc.api.ObjCInstance` 36 | * :class:`~rubicon.objc.api.ObjCMetaClass` 37 | * :class:`~rubicon.objc.api.ObjCProtocol` 38 | * :func:`~rubicon.objc.api.at` 39 | * :func:`~rubicon.objc.api.ns_from_py` 40 | * :func:`~rubicon.objc.api.objc_classmethod` 41 | * :func:`~rubicon.objc.api.objc_const` 42 | * :func:`~rubicon.objc.api.objc_ivar` 43 | * :func:`~rubicon.objc.api.objc_method` 44 | * :func:`~rubicon.objc.api.objc_property` 45 | * :func:`~rubicon.objc.api.objc_rawmethod` 46 | * :func:`~rubicon.objc.api.py_from_ns` 47 | 48 | From :mod:`rubicon.objc.runtime` 49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 50 | 51 | .. hlist:: 52 | 53 | * :class:`~rubicon.objc.runtime.SEL` 54 | * :func:`~rubicon.objc.runtime.send_message` 55 | * :func:`~rubicon.objc.runtime.send_super` 56 | 57 | From :mod:`rubicon.objc.types` 58 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | 60 | .. hlist:: 61 | 62 | * :class:`~rubicon.objc.types.CFIndex` 63 | * :class:`~rubicon.objc.types.CFRange` 64 | * :class:`~rubicon.objc.types.CGFloat` 65 | * :class:`~rubicon.objc.types.CGGlyph` 66 | * :class:`~rubicon.objc.types.CGPoint` 67 | * :func:`~rubicon.objc.types.CGPointMake` 68 | * :class:`~rubicon.objc.types.CGRect` 69 | * :func:`~rubicon.objc.types.CGRectMake` 70 | * :class:`~rubicon.objc.types.CGSize` 71 | * :func:`~rubicon.objc.types.CGSizeMake` 72 | * :class:`~rubicon.objc.types.NSEdgeInsets` 73 | * :func:`~rubicon.objc.types.NSEdgeInsetsMake` 74 | * :class:`~rubicon.objc.types.NSInteger` 75 | * :func:`~rubicon.objc.types.NSMakePoint` 76 | * :func:`~rubicon.objc.types.NSMakeRect` 77 | * :func:`~rubicon.objc.types.NSMakeSize` 78 | * :class:`~rubicon.objc.types.NSPoint` 79 | * :class:`~rubicon.objc.types.NSRange` 80 | * :class:`~rubicon.objc.types.NSRect` 81 | * :class:`~rubicon.objc.types.NSSize` 82 | * :class:`~rubicon.objc.types.NSTimeInterval` 83 | * :class:`~rubicon.objc.types.NSUInteger` 84 | * :data:`~rubicon.objc.types.NSZeroPoint` 85 | * :class:`~rubicon.objc.types.UIEdgeInsets` 86 | * :func:`~rubicon.objc.types.UIEdgeInsetsMake` 87 | * :data:`~rubicon.objc.types.UIEdgeInsetsZero` 88 | * :class:`~rubicon.objc.types.UniChar` 89 | * :class:`~rubicon.objc.types.unichar` 90 | -------------------------------------------------------------------------------- /docs/spelling_wordlist: -------------------------------------------------------------------------------- 1 | Alea 2 | alloc 3 | Autorelease 4 | autorelease 5 | autoreleased 6 | autoreleases 7 | Bugfixes 8 | callables 9 | CPython 10 | deallocated 11 | deallocating 12 | Deprecations 13 | dev 14 | Est 15 | getter 16 | getters 17 | hashable 18 | Iacta 19 | IDEs 20 | initializer 21 | inlines 22 | instantiation 23 | kwargs 24 | linters 25 | lookups 26 | macOS 27 | metaclass 28 | metaclasses 29 | mutableCopy 30 | namespace 31 | namespaces 32 | ObjC 33 | pre 34 | Pythonic 35 | rc 36 | reStructuredText 37 | runtime 38 | structs 39 | subclassed 40 | subclasses 41 | Subclasses 42 | subclassing 43 | submodules 44 | superclass 45 | superclasses 46 | Sur 47 | syntaxes 48 | Towncrier 49 | Unregister 50 | variadic 51 | Variadic 52 | Xcode 53 | 54 | # Usernames 55 | cculianu 56 | Dayof 57 | jeamland 58 | Longhanks 59 | ojii 60 | stsievert 61 | uranusjr 62 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Tutorials 3 | ========= 4 | 5 | These tutorials are step-by step guides for using Rubicon. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :titlesonly: 10 | 11 | tutorial-1 12 | tutorial-2 13 | 14 | 15 | Tutorial 1 - Your first bridge 16 | ============================== 17 | 18 | In :doc:`./tutorial-1`, you will use Rubicon to invoke an existing Objective-C 19 | library on your computer. 20 | 21 | Tutorial 2 - Writing your own class 22 | =================================== 23 | 24 | In :doc:`./tutorial-2`, you will write a Python class, and expose it to the 25 | Objective-C runtime. 26 | -------------------------------------------------------------------------------- /docs/tutorial/tutorial-1.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial-1: 2 | 3 | ================= 4 | Your first bridge 5 | ================= 6 | 7 | In this example, we're going to use Rubicon to access the Objective-C 8 | Foundation library, and the ``NSURL`` class in that library. ``NSURL`` is the 9 | class used to represent and manipulate URLs. 10 | 11 | This tutorial assumes you've set up your environment as described in the 12 | :doc:`Getting started guide `. 13 | 14 | Accessing NSURL 15 | =============== 16 | 17 | Start Python, and get a reference to an Objective-C class. In this example, 18 | we're going to use the ``NSURL`` class, Objective-C's representation of URLs: 19 | 20 | .. code-block:: pycon 21 | 22 | >>> from rubicon.objc import ObjCClass 23 | >>> NSURL = ObjCClass("NSURL") 24 | 25 | This gives us an ``NSURL`` class in Python which is transparently bridged to 26 | the ``NSURL`` class in the Objective-C runtime. Any method or property 27 | described in `Apple's documentation on NSURL 28 | `__ 29 | can be accessed over this bridge. 30 | 31 | Let's create an instance of an ``NSURL`` object. The ``NSURL`` documentation 32 | describes a static constructor ``+URLWithString:``; we can invoke this 33 | constructor as: 34 | 35 | .. code-block:: pycon 36 | 37 | >>> base = NSURL.URLWithString("https://beeware.org/") 38 | 39 | That is, the name of the method in Python is identical to the method in 40 | Objective-C. The first argument is declared as being an ``NSString *``; Rubicon 41 | converts the Python :class:`str` into an ``NSString`` instance as part of 42 | invoking the method. 43 | 44 | ``NSURL`` has another static constructor: ``+URLWithString:relativeToURL:``. We 45 | can also invoke this constructor: 46 | 47 | .. code-block:: pycon 48 | 49 | >>> full = NSURL.URLWithString("contributing/", relativeToURL=base) 50 | 51 | The second argument (``relativeToURL``) is accessed as a keyword argument. This 52 | argument is declared as being of type ``NSURL *``; since ``base`` is an 53 | instance of ``NSURL``, Rubicon can pass through this instance. 54 | 55 | Sometimes, an Objective-C method definition will use the same keyword argument 56 | name twice (for example, ``NSLayoutConstraint`` has a 57 | ``+constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:`` 58 | selector, using the ``attribute`` keyword twice). This is legal in Objective-C, 59 | but not in Python, as you can't repeat a keyword argument in a method call. In 60 | this case, you can use a ``__`` suffix on the ambiguous keyword argument to make 61 | it unique. Any content after and including the ``__`` will be stripped when 62 | making the Objective-C call: 63 | 64 | .. code-block:: pycon 65 | 66 | >>> constraint = NSLayoutConstraint.constraintWithItem( 67 | ... first_item, 68 | ... attribute__1=first_attribute, 69 | ... relatedBy=relation, 70 | ... toItem=second_item, 71 | ... attribute__2=second_attribute, 72 | ... multiplier=2.0, 73 | ... constant=1.0 74 | ... ) 75 | 76 | Instance methods 77 | ================ 78 | 79 | So far, we've been using the ``+URLWithString:`` static constructor. However, 80 | ``NSURL`` also provides an initializer method ``-initWithString:``. To use this 81 | method, you first have to instruct the Objective-C runtime to allocate memory 82 | for the instance, then invoke the initializer: 83 | 84 | .. code-block:: pycon 85 | 86 | >>> base = NSURL.alloc().initWithString("https://beeware.org/") 87 | 88 | Now that you have an instance of ``NSURL``, you'll want to manipulate it. 89 | ``NSURL`` describes an ``absoluteURL`` property; this property can be accessed 90 | as a Python attribute: 91 | 92 | .. code-block:: pycon 93 | 94 | >>> absolute = full.absoluteURL 95 | 96 | You can also invoke methods on the instance: 97 | 98 | .. code-block:: pycon 99 | 100 | >>> longer = absolute.URLByAppendingPathComponent('how/first-time/') 101 | 102 | If you want to output an object at the console, you can use the Objective-C 103 | property ``description``, or for debugging output, ``debugDescription``: 104 | 105 | .. code-block:: pycon 106 | 107 | >>> longer.description 108 | 'https://beeware.org/contributing/how/first-time/' 109 | 110 | >>> longer.debugDescription 111 | 'https://beeware.org/contributing/how/first-time/' 112 | 113 | Internally, ``description`` and ``debugDescription`` are hooked up to their 114 | Python equivalents, ``__str__()`` and ``__repr__()``, respectively: 115 | 116 | .. code-block:: pycon 117 | 118 | >>> str(absolute) 119 | 'https://beeware.org/contributing/' 120 | 121 | >>> repr(absolute) 122 | '' 123 | 124 | >>> print(absolute) 125 | https://beeware.org/contributing/ 126 | 127 | Time to take over the world! 128 | ============================ 129 | 130 | You now have access to *any* method, on *any* class, in any library, in the 131 | entire macOS or iOS ecosystem! If you can invoke something in Objective-C, you 132 | can invoke it in Python - all you need to do is: 133 | 134 | * load the library with ctypes; 135 | * register the classes you want to use; and 136 | * Use those classes as if they were written in Python. 137 | 138 | Next steps 139 | ========== 140 | 141 | The next step is to write your own classes, and expose them into the 142 | Objective-C runtime. That's the subject of the :doc:`next tutorial 143 | <./tutorial-2>`. 144 | -------------------------------------------------------------------------------- /docs/tutorial/tutorial-2.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Tutorial 2 - Writing your own class 3 | =================================== 4 | 5 | Eventually, you'll come across an Objective-C API that requires you to provide 6 | a class instance as an argument. For example, when using macOS and iOS GUI 7 | classes, you often need to define "delegate" classes to describe how a GUI 8 | element will respond to mouse clicks and key presses. 9 | 10 | Let's define a ``Handler`` class, with two methods: 11 | 12 | * an ``-initWithValue:`` constructor that accepts an integer; and 13 | 14 | * a ``-pokeWithValue:andName:`` method that accepts an integer and a 15 | string, prints the string, and returns a float that is one half of the 16 | value. 17 | 18 | The declaration for this class would be:: 19 | 20 | from rubicon.objc import NSObject, objc_method 21 | 22 | 23 | class Handler(NSObject): 24 | @objc_method 25 | def initWithValue_(self, v: int): 26 | self.value = v 27 | return self 28 | 29 | @objc_method 30 | def pokeWithValue_andName_(self, v: int, name) -> float: 31 | print("My name is", name) 32 | return v / 2.0 33 | 34 | This code has several interesting implementation details: 35 | 36 | * The ``Handler`` class extends ``NSObject``. This instructs Rubicon to 37 | construct the class in a way that it can be registered with the 38 | Objective-C runtime. 39 | 40 | * Each method that we want to expose to Objective-C is decorated with 41 | ``@objc_method``.The method names match the Objective-C descriptor that 42 | you want to expose, but with colons replaced by underscores. This matches 43 | the "long form" way of invoking methods discussed in :doc:`tutorial-1`. 44 | 45 | * The ``v`` argument on ``initWithValue_()`` uses a Python 3 type 46 | annotation to declare it's type. Objective-C is a language with static 47 | typing, so any methods defined in Python must provide this typing 48 | information. Any argument that isn't annotated is assumed to be of type 49 | ``id`` - that is, a pointer to an Objective-C object. 50 | 51 | * The ``pokeWithValue_andName_()`` method has it's integer argument 52 | annotated, and has it's return type annotated as float. Again, this is 53 | to support Objective-C typing operations. Any function that has no 54 | return type annotation is assumed to return ``id``. A return type 55 | annotation of ``None`` will be interpreted as a ``void`` method in 56 | Objective-C. The ``name`` argument doesn't need to be annotated because it 57 | will be passed in as a string, and strings are ``NSObject`` subclasses 58 | in Objective-C. 59 | 60 | * ``initWithValue_()`` is a constructor, so it returns ``self``. 61 | 62 | Having declared the class, you can then instantiate and use it: 63 | 64 | .. code-block:: pycon 65 | 66 | >>> my_handler = Handler.alloc().initWithValue(42) 67 | >>> print(my_handler.value) 68 | 42 69 | >>> print(my_handler.pokeWithValue(37, andName="Alice")) 70 | My name is Alice 71 | 18.5 72 | 73 | Objective-C properties 74 | ====================== 75 | 76 | When we defined the initializer for ``Handler``, we stored the provided value 77 | as the ``value`` attribute of the class. However, as this attribute wasn't 78 | declared to Objective-C, it won't be visible to the Objective-C runtime. 79 | You can access ``value`` from within Python - but Objective-C code won't be 80 | able to access it. 81 | 82 | To expose value to the Objective-C runtime, we need to make one small change, 83 | and explicitly declare value as an Objective-C property:: 84 | 85 | from rubicon.objc import NSObject, objc_method, objc_property 86 | 87 | class PureHandler(NSObject): 88 | value = objc_property() 89 | 90 | @objc_method 91 | def initWithValue_(self, v: int): 92 | self.value = v 93 | return self 94 | 95 | This doesn't change anything about how you access or modify the attribute - it 96 | just means that Objective-C code will be able to see the attribute as well. 97 | 98 | Class naming 99 | ============ 100 | 101 | In this revised example, you'll note that we also used a different class name 102 | - ``PureHandler``. This was deliberate, because Objective-C doesn't have any 103 | concept of namespaces. As a result, you can only define one class of any given 104 | name in a process - so, you won't be able to define a second ``Handler`` class in 105 | the same Python shell. If you try, you'll get an error: 106 | 107 | .. code-block:: pycon 108 | 109 | >>> class Handler(NSObject): 110 | ... pass 111 | Traceback (most recent call last) 112 | ... 113 | RuntimeError: An Objective-C class named b'Handler' already exists 114 | 115 | You'll need to be careful (and sometimes, painfully verbose) when choosing class 116 | names. 117 | 118 | To allow a class name to be reused, you can set the class variable 119 | :attr:`~rubicon.objc.api.ObjCClass.auto_rename` to ``True``. This option enables 120 | automatic renaming of the Objective C class if a naming collision is detected: 121 | 122 | .. code-block:: pycon 123 | 124 | >>> ObjCClass.auto_rename = True 125 | 126 | This option can also be enabled on a per-class basis by using the 127 | ``auto_rename`` argument in the class declaration: 128 | 129 | .. code-block:: pycon 130 | 131 | >>> class Handler(NSObject, auto_rename=True): 132 | ... pass 133 | 134 | If this option is used, the Objective C class name will have a numeric suffix 135 | (e.g., `Handler_2`). The Python class name will be unchanged. 136 | 137 | What, no ``__init__()``? 138 | ======================== 139 | 140 | You'll also notice that our example code *doesn't* have an ``__init__()`` 141 | method like you'd normally expect of Python code. As we're defining an 142 | Objective-C class, we need to follow the Objective-C object life cycle - which 143 | means defining initializer methods that are visible to the Objective-C runtime, 144 | and invoking them over that bridge. 145 | 146 | Next steps 147 | ========== 148 | 149 | ??? 150 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools==80.9.0", 4 | "setuptools_scm==8.3.1", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | dynamic = ["version"] 10 | name = "rubicon-objc" 11 | description = "A bridge between an Objective C runtime environment and Python." 12 | readme = "README.rst" 13 | requires-python = ">= 3.9" 14 | license = "BSD-3-Clause" 15 | license-files = [ 16 | "LICENSE", 17 | ] 18 | authors = [ 19 | {name="Russell Keith-Magee", email="russell@keith-magee.com"}, 20 | ] 21 | maintainers = [ 22 | {name="BeeWare Team", email="team@beeware.org"}, 23 | ] 24 | keywords = [ 25 | "macOS", 26 | "iOS", 27 | "Objective C", 28 | ] 29 | classifiers = [ 30 | "Development Status :: 4 - Beta", 31 | "Intended Audience :: Developers", 32 | "Programming Language :: Objective C", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Programming Language :: Python :: 3.13", 39 | "Programming Language :: Python :: 3.14", 40 | "Programming Language :: Python :: 3 :: Only", 41 | "Topic :: Software Development", 42 | ] 43 | 44 | [project.urls] 45 | Homepage = "https://beeware.org/rubicon" 46 | Funding = "https://beeware.org/contributing/membership/" 47 | Documentation = "https://rubicon-objc.readthedocs.io/en/latest/" 48 | Tracker = "https://github.com/beeware/rubicon-objc/issues" 49 | Source = "https://github.com/beeware/rubicon-objc" 50 | 51 | [project.optional-dependencies] 52 | dev = [ 53 | "pre-commit == 4.2.0", 54 | "pytest == 8.4.0", 55 | "setuptools_scm == 8.3.1", 56 | "tox == 4.26.0", 57 | ] 58 | # Docs are always built on a specific Python version; see RTD and tox config files, 59 | # and the docs contribution guide. 60 | docs = [ 61 | "furo == 2024.8.6", 62 | "pyenchant == 3.2.2", 63 | "sphinx == 8.2.3", 64 | "sphinx_tabs == 3.4.7", 65 | "sphinx-autobuild == 2024.10.3", 66 | "sphinx-copybutton == 0.5.2", 67 | "sphinxcontrib-spelling == 8.0.1", 68 | ] 69 | 70 | [tool.codespell] 71 | skip = ".git,*.pdf,*.svg" 72 | # the way to make case sensitive skips of words etc 73 | ignore-regex = "\bNd\b" 74 | ignore-words-list = "re-use,assertIn" 75 | 76 | [tool.isort] 77 | profile = "black" 78 | split_on_trailing_comma = true 79 | combine_as_imports = true 80 | 81 | [tool.setuptools_scm] 82 | # To enable SCM versioning, we need an empty tool configuration for setuptools_scm 83 | 84 | [tool.towncrier] 85 | directory = "changes" 86 | package = "rubicon.objc" 87 | filename = "docs/background/releases.rst" 88 | title_format = "{version} ({project_date})" 89 | template = "changes/template.rst" 90 | type = [ 91 | { directory = "feature", name = "Features", showcontent = true }, 92 | { directory = "bugfix", name = "Bugfixes", showcontent = true }, 93 | { directory = "removal", name = "Backward Incompatible Changes", showcontent = true }, 94 | { directory = "doc", name = "Documentation", showcontent = true }, 95 | { directory = "misc", name = "Misc", showcontent = false }, 96 | ] 97 | -------------------------------------------------------------------------------- /src/rubicon/objc/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | # Read version from SCM metadata 3 | # This will only exist in a development environment 4 | from setuptools_scm import get_version 5 | 6 | # Excluded from coverage because a pure test environment (such as the one 7 | # used by tox in CI) won't have setuptools_scm 8 | __version__ = get_version("../../..", relative_to=__file__) # pragma: no cover 9 | except (ModuleNotFoundError, LookupError): 10 | # If setuptools_scm isn't in the environment, the call to import will fail. 11 | # If it *is* in the environment, but the code isn't a git checkout (e.g., 12 | # it's been pip installed non-editable) the call to get_version() will fail. 13 | # If either of these occurs, read version from the installer metadata. 14 | 15 | # importlib.metadata.version was added in Python 3.8 16 | try: 17 | from importlib.metadata import version 18 | except ModuleNotFoundError: 19 | from importlib_metadata import version 20 | 21 | __version__ = version("rubicon-objc") 22 | 23 | # `api`, `runtime` and `types` are only included for clarity. They are not 24 | # strictly necessary, because the from-imports below also import the types and 25 | # runtime modules and implicitly add them to the rubicon.objc namespace. 26 | # 27 | # The import of collections is important, however. The classes from collections 28 | # are not meant to be used directly, instead they are registered with the 29 | # runtime module (using the for_objcclass decorator) so they are used in place 30 | # of ObjCInstance when representing Foundation collections in Python. If this 31 | # module is not imported, the registration will not take place, and Foundation 32 | # collections will not support the expected methods/operators in Python! 33 | from . import api, collections, runtime, types 34 | from .api import ( 35 | Block, 36 | NSArray, 37 | NSDictionary, 38 | NSMutableArray, 39 | NSMutableDictionary, 40 | NSObject, 41 | NSObjectProtocol, 42 | ObjCBlock, 43 | ObjCClass, 44 | ObjCInstance, 45 | ObjCMetaClass, 46 | ObjCProtocol, 47 | at, 48 | ns_from_py, 49 | objc_classmethod, 50 | objc_const, 51 | objc_ivar, 52 | objc_method, 53 | objc_property, 54 | objc_rawmethod, 55 | py_from_ns, 56 | ) 57 | from .runtime import SEL, objc_block, objc_id, send_message, send_super 58 | from .types import ( 59 | CFIndex, 60 | CFRange, 61 | CGFloat, 62 | CGGlyph, 63 | CGPoint, 64 | CGPointMake, 65 | CGRect, 66 | CGRectMake, 67 | CGSize, 68 | CGSizeMake, 69 | NSEdgeInsets, 70 | NSEdgeInsetsMake, 71 | NSInteger, 72 | NSMakePoint, 73 | NSMakeRect, 74 | NSMakeSize, 75 | NSPoint, 76 | NSRange, 77 | NSRect, 78 | NSSize, 79 | NSTimeInterval, 80 | NSUInteger, 81 | NSZeroPoint, 82 | UIEdgeInsets, 83 | UIEdgeInsetsMake, 84 | UIEdgeInsetsZero, 85 | UniChar, 86 | unichar, 87 | ) 88 | 89 | __all__ = [ 90 | "__version__", 91 | "CFIndex", 92 | "CFRange", 93 | "CGFloat", 94 | "CGGlyph", 95 | "CGPoint", 96 | "CGPointMake", 97 | "CGRect", 98 | "CGRectMake", 99 | "CGSize", 100 | "CGSizeMake", 101 | "NSEdgeInsets", 102 | "NSEdgeInsetsMake", 103 | "NSInteger", 104 | "NSMakePoint", 105 | "NSMakeRect", 106 | "NSMakeSize", 107 | "NSPoint", 108 | "NSRange", 109 | "NSRect", 110 | "NSSize", 111 | "NSTimeInterval", 112 | "NSUInteger", 113 | "NSZeroPoint", 114 | "UIEdgeInsets", 115 | "UIEdgeInsetsMake", 116 | "UIEdgeInsetsZero", 117 | "UniChar", 118 | "unichar", 119 | "SEL", 120 | "send_message", 121 | "send_super", 122 | "Block", 123 | "NSArray", 124 | "NSDictionary", 125 | "NSMutableArray", 126 | "NSMutableDictionary", 127 | "NSObject", 128 | "NSObjectProtocol", 129 | "ObjCBlock", 130 | "ObjCClass", 131 | "ObjCInstance", 132 | "ObjCMetaClass", 133 | "ObjCProtocol", 134 | "at", 135 | "ns_from_py", 136 | "objc_block", 137 | "objc_classmethod", 138 | "objc_const", 139 | "objc_id", 140 | "objc_ivar", 141 | "objc_method", 142 | "objc_property", 143 | "objc_rawmethod", 144 | "py_from_ns", 145 | "api", 146 | "collections", 147 | "runtime", 148 | "types", 149 | ] 150 | -------------------------------------------------------------------------------- /src/rubicon/objc/collections.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | from .api import ( 4 | NSArray, 5 | NSDictionary, 6 | NSMutableArray, 7 | NSMutableDictionary, 8 | NSString, 9 | ObjCInstance, 10 | for_objcclass, 11 | ns_from_py, 12 | py_from_ns, 13 | ) 14 | from .runtime import objc_id, send_message 15 | from .types import NSNotFound, NSRange, NSUInteger, unichar 16 | 17 | # All NSComparisonResult values. 18 | NSOrderedAscending = -1 19 | NSOrderedSame = 0 20 | NSOrderedDescending = 1 21 | 22 | 23 | # Some useful NSStringCompareOptions. 24 | NSLiteralSearch = 2 25 | NSBackwardsSearch = 4 26 | 27 | 28 | @for_objcclass(NSString) 29 | class ObjCStrInstance(ObjCInstance): 30 | """Provides Pythonic operations on NSString objects that mimic those of 31 | Python's str. 32 | 33 | Note that str objects consist of Unicode code points, whereas 34 | NSString objects consist of UTF-16 code units. These are not 35 | equivalent for code points greater than U+FFFF. For performance and 36 | simplicity, ObjCStrInstance objects behave as sequences of UTF-16 37 | code units, like NSString. (Individual UTF-16 code units are 38 | represented as Python str objects of length 1.) If you need to 39 | access or iterate over code points instead of UTF-16 code units, use 40 | str(nsstring) to convert the NSString to a Python str first. 41 | """ 42 | 43 | def __str__(self): 44 | return self.UTF8String.decode("utf-8") 45 | 46 | def __fspath__(self): 47 | return self.__str__() 48 | 49 | def __eq__(self, other): 50 | if isinstance(other, str): 51 | return self.isEqualToString(ns_from_py(other)) 52 | elif isinstance(other, NSString): 53 | return self.isEqualToString(other) 54 | else: 55 | return super().__eq__(other) 56 | 57 | def __ne__(self, other): 58 | return not self.__eq__(other) 59 | 60 | # Note: We cannot define a __hash__ for NSString objects; doing so would violate the Python convention that 61 | # mutable objects should not be hashable. Although we could disallow hashing for NSMutableString objects, this 62 | # would make some immutable strings unhashable as well, because immutable strings can have a runtime class that 63 | # is a subclass of NSMutableString. This is not just a theoretical possibility - for example, on OS X 10.11, 64 | # isinstance(NSString.string(), NSMutableString) is true. 65 | 66 | def _compare(self, other, want): 67 | """Helper method used to implement the comparison operators. 68 | 69 | If other is a str or NSString, it is compared to self, and True 70 | or False is returned depending on whether the result is one of 71 | the wanted values. If other is not a string, NotImplemented is 72 | returned. 73 | """ 74 | 75 | if isinstance(other, str): 76 | ns_other = ns_from_py(other) 77 | elif isinstance(other, NSString): 78 | ns_other = other 79 | else: 80 | return NotImplemented 81 | 82 | return self.compare(ns_other, options=NSLiteralSearch) in want 83 | 84 | def __lt__(self, other): 85 | return self._compare(other, {NSOrderedAscending}) 86 | 87 | def __le__(self, other): 88 | return self._compare(other, {NSOrderedAscending, NSOrderedSame}) 89 | 90 | def __ge__(self, other): 91 | return self._compare(other, {NSOrderedSame, NSOrderedDescending}) 92 | 93 | def __gt__(self, other): 94 | return self._compare(other, {NSOrderedDescending}) 95 | 96 | def __contains__(self, value): 97 | if not isinstance(value, (str, NSString)): 98 | raise TypeError( 99 | "'in ' requires str or NSString as left operand, " 100 | f"not {type(value).__module__}.{type(value).__qualname__}" 101 | ) 102 | 103 | return self.find(value) != -1 104 | 105 | def __len__(self): 106 | return self.length 107 | 108 | def __getitem__(self, key): 109 | if isinstance(key, slice): 110 | start, stop, step = key.indices(len(self)) 111 | 112 | if step == 1: 113 | return self.substringWithRange(NSRange(start, stop - start)) 114 | else: 115 | rng = range(start, stop, step) 116 | chars = (unichar * len(rng))() 117 | for chars_i, self_i in enumerate(rng): 118 | chars[chars_i] = ord(self[self_i]) 119 | return NSString.stringWithCharacters(chars, length=len(chars)) 120 | else: 121 | if key < 0: 122 | index = len(self) + key 123 | else: 124 | index = key 125 | 126 | if index not in range(len(self)): 127 | raise IndexError(f"{type(self).__name__} index out of range") 128 | 129 | return chr(self.characterAtIndex(index)) 130 | 131 | def __add__(self, other): 132 | if isinstance(other, (str, NSString)): 133 | return self.stringByAppendingString(other) 134 | else: 135 | return NotImplemented 136 | 137 | def __radd__(self, other): 138 | if isinstance(other, (str, NSString)): 139 | return ns_from_py(other).stringByAppendingString(self) 140 | else: 141 | return NotImplemented 142 | 143 | def __mul__(self, other): 144 | try: 145 | count = operator.index(other) 146 | except AttributeError: 147 | return NotImplemented 148 | 149 | if count <= 0: 150 | return ns_from_py("") 151 | else: 152 | # https://stackoverflow.com/a/4608137 153 | return self.stringByPaddingToLength( 154 | count * len(self), 155 | withString=self, 156 | startingAtIndex=0, 157 | ) 158 | 159 | def __rmul__(self, other): 160 | return self.__mul__(other) 161 | 162 | def _find(self, sub, start=None, end=None, *, reverse): 163 | if not isinstance(sub, (str, NSString)): 164 | raise TypeError( 165 | f"must be str or NSString, not {type(sub).__module__}.{type(sub).__qualname__}" 166 | ) 167 | 168 | start, end, _ = slice(start, end).indices(len(self)) 169 | 170 | if not sub: 171 | # Special case: Python considers the empty string to be contained in every string, 172 | # at the earliest position searched. NSString considers the empty string to *not* be 173 | # contained in any string. This difference is handled here. 174 | return end if reverse else start 175 | 176 | options = NSLiteralSearch 177 | if reverse: 178 | options |= NSBackwardsSearch 179 | found_range = self.rangeOfString( 180 | sub, options=options, range=NSRange(start, end - start) 181 | ) 182 | if found_range.location == NSNotFound: 183 | return -1 184 | else: 185 | return found_range.location 186 | 187 | def _index(self, sub, start=None, end=None, *, reverse): 188 | found = self._find(sub, start, end, reverse=reverse) 189 | if found == -1: 190 | raise ValueError("substring not found") 191 | else: 192 | return found 193 | 194 | def find(self, sub, start=None, end=None): 195 | return self._find(sub, start=start, end=end, reverse=False) 196 | 197 | def index(self, sub, start=None, end=None): 198 | return self._index(sub, start=start, end=end, reverse=False) 199 | 200 | def rfind(self, sub, start=None, end=None): 201 | return self._find(sub, start=start, end=end, reverse=True) 202 | 203 | def rindex(self, sub, start=None, end=None): 204 | return self._index(sub, start=start, end=end, reverse=True) 205 | 206 | # A fallback method; get the locally defined attribute if it exists; 207 | # otherwise, get the attribute from the Python-converted version 208 | # of the string 209 | def __getattr__(self, attr): 210 | try: 211 | return super().__getattr__(attr) 212 | except AttributeError: 213 | return getattr(self.__str__(), attr) 214 | 215 | 216 | @for_objcclass(NSArray) 217 | class ObjCListInstance(ObjCInstance): 218 | def __getitem__(self, item): 219 | if isinstance(item, slice): 220 | start, stop, step = item.indices(len(self)) 221 | if step == 1: 222 | return self.subarrayWithRange(NSRange(start, stop - start)) 223 | else: 224 | return ns_from_py( 225 | [self.objectAtIndex(x) for x in range(start, stop, step)] 226 | ) 227 | else: 228 | if item < 0: 229 | index = len(self) + item 230 | else: 231 | index = item 232 | 233 | if index not in range(len(self)): 234 | raise IndexError(f"{type(self).__name__} index out of range") 235 | 236 | return self.objectAtIndex(index) 237 | 238 | def __len__(self): 239 | return send_message(self.ptr, "count", restype=NSUInteger, argtypes=[]) 240 | 241 | def __iter__(self): 242 | for i in range(len(self)): 243 | yield self.objectAtIndex(i) 244 | 245 | def __contains__(self, item): 246 | return self.containsObject_(item) 247 | 248 | def __eq__(self, other): 249 | return list(self) == other 250 | 251 | def __ne__(self, other): 252 | return not self.__eq__(other) 253 | 254 | def index(self, value): 255 | idx = self.indexOfObject_(value) 256 | if idx == NSNotFound: 257 | raise ValueError(f"{value!r} is not in list") 258 | return idx 259 | 260 | def count(self, value): 261 | return len([x for x in self if x == value]) 262 | 263 | def copy(self): 264 | return ObjCInstance(send_message(self, "copy", restype=objc_id, argtypes=[])) 265 | 266 | 267 | @for_objcclass(NSMutableArray) 268 | class ObjCMutableListInstance(ObjCListInstance): 269 | def __setitem__(self, item, value): 270 | if isinstance(item, slice): 271 | arr = ns_from_py(value) 272 | if not isinstance(arr, NSArray): 273 | raise TypeError( 274 | f"{type(value).__module__}.{type(value).__qualname__} " 275 | "is not convertible to NSArray" 276 | ) 277 | 278 | start, stop, step = item.indices(len(self)) 279 | if step == 1: 280 | self.replaceObjectsInRange( 281 | NSRange(start, stop - start), withObjectsFromArray=arr 282 | ) 283 | else: 284 | indices = range(start, stop, step) 285 | if len(arr) != len(indices): 286 | raise ValueError( 287 | f"attempt to assign sequence of size {len(value)} " 288 | f"to extended slice of size {len(indices)}" 289 | ) 290 | 291 | for idx, obj in zip(indices, arr): 292 | self.replaceObjectAtIndex(idx, withObject=obj) 293 | else: 294 | if item < 0: 295 | index = len(self) + item 296 | else: 297 | index = item 298 | 299 | if index not in range(len(self)): 300 | raise IndexError(f"{type(self).__name__} assignment index out of range") 301 | 302 | self.replaceObjectAtIndex(index, withObject=value) 303 | 304 | def __delitem__(self, item): 305 | if isinstance(item, slice): 306 | start, stop, step = item.indices(len(self)) 307 | if step == 1: 308 | self.removeObjectsInRange(NSRange(start, stop - start)) 309 | else: 310 | for idx in sorted(range(start, stop, step), reverse=True): 311 | self.removeObjectAtIndex(idx) 312 | else: 313 | if item < 0: 314 | index = len(self) + item 315 | else: 316 | index = item 317 | 318 | if index not in range(len(self)): 319 | raise IndexError(f"{type(self).__name__} assignment index out of range") 320 | 321 | self.removeObjectAtIndex_(index) 322 | 323 | def copy(self): 324 | return self.mutableCopy() 325 | 326 | def append(self, value): 327 | self.addObject_(value) 328 | 329 | def extend(self, values): 330 | for value in values: 331 | self.addObject_(value) 332 | 333 | def clear(self): 334 | self.removeAllObjects() 335 | 336 | def pop(self, item=-1): 337 | value = self[item] 338 | del self[item] 339 | return value 340 | 341 | def remove(self, value): 342 | del self[self.index(value)] 343 | 344 | def reverse(self): 345 | self.setArray(self.reverseObjectEnumerator().allObjects()) 346 | 347 | def insert(self, idx, value): 348 | self.insertObject_atIndex_(value, idx) 349 | 350 | 351 | @for_objcclass(NSDictionary) 352 | class ObjCDictInstance(ObjCInstance): 353 | def __getitem__(self, item): 354 | v = self.objectForKey_(item) 355 | if v is None: 356 | raise KeyError(item) 357 | return v 358 | 359 | def __len__(self): 360 | return self.count 361 | 362 | def __iter__(self): 363 | yield from self.allKeys() 364 | 365 | def __contains__(self, item): 366 | return self.objectForKey_(item) is not None 367 | 368 | def __eq__(self, other): 369 | return py_from_ns(self) == other 370 | 371 | def __ne__(self, other): 372 | return not self.__eq__(other) 373 | 374 | def get(self, item, default=None): 375 | v = self.objectForKey_(item) 376 | if v is None: 377 | return default 378 | return v 379 | 380 | def keys(self): 381 | return self.allKeys() 382 | 383 | def values(self): 384 | return self.allValues() 385 | 386 | def items(self): 387 | for key in self.allKeys(): 388 | yield key, self.objectForKey_(key) 389 | 390 | def copy(self): 391 | return ObjCInstance(send_message(self, "copy", restype=objc_id, argtypes=[])) 392 | 393 | 394 | @for_objcclass(NSMutableDictionary) 395 | class ObjCMutableDictInstance(ObjCDictInstance): 396 | no_pop_default = object() 397 | 398 | def __setitem__(self, item, value): 399 | self.setObject_forKey_(value, item) 400 | 401 | def __delitem__(self, item): 402 | if item not in self: 403 | raise KeyError(item) 404 | 405 | self.removeObjectForKey_(item) 406 | 407 | def copy(self): 408 | return self.mutableCopy() 409 | 410 | def clear(self): 411 | self.removeAllObjects() 412 | 413 | def pop(self, item, default=no_pop_default): 414 | if item not in self: 415 | if default is not self.no_pop_default: 416 | return default 417 | else: 418 | raise KeyError(item) 419 | 420 | value = self.objectForKey_(item) 421 | self.removeObjectForKey_(item) 422 | return value 423 | 424 | def popitem(self): 425 | if len(self) == 0: 426 | raise KeyError(f"popitem(): {type(self).__name__} is empty") 427 | 428 | key = self.allKeys().firstObject() 429 | value = self.objectForKey_(key) 430 | self.removeObjectForKey_(key) 431 | return key, value 432 | 433 | def setdefault(self, key, default=None): 434 | value = self.objectForKey_(key) 435 | if value is None: 436 | value = default 437 | if value is not None: 438 | self.setObject_forKey_(default, key) 439 | return value 440 | 441 | def update(self, new=None, **kwargs): 442 | if new is not None: 443 | kwargs.update(new) 444 | 445 | for k, v in kwargs.items(): 446 | self.setObject_forKey_(v, k) 447 | -------------------------------------------------------------------------------- /src/rubicon/objc/ctypes_patch.py: -------------------------------------------------------------------------------- 1 | """This module provides a workaround to allow callback functions to return 2 | composite types (most importantly structs). 3 | 4 | Currently, ctypes callback functions (created by passing a Python callable to a 5 | CFUNCTYPE object) are only able to return what ctypes considers a "simple" type. This 6 | includes void (None), scalars (c_int, c_float, etc.), c_void_p, c_char_p, c_wchar_p, and 7 | py_object. Returning "composite" types (structs, unions, and non-"simple" pointers) is 8 | not possible. This issue has been reported on the Python bug tracker 9 | (https://github.com/python/cpython/issues/49960). 10 | 11 | For pointers, the easiest workaround is to return a c_void_p instead of the correctly 12 | typed pointer, and to cast the value on both sides. For structs and unions there is no 13 | easy workaround, which is why this somewhat hacky workaround is necessary. 14 | """ 15 | 16 | import ctypes 17 | import sys 18 | import types 19 | import warnings 20 | 21 | # This module relies on the layout of a few internal Python and ctypes 22 | # structures. Because of this, it's possible (but not all that likely) that 23 | # things will break on newer/older Python versions. 24 | if sys.version_info < (3, 6) or sys.version_info >= (3, 15): 25 | v = sys.version_info 26 | warnings.warn( 27 | "rubicon.objc.ctypes_patch has only been tested with Python 3.6 through 3.14. " 28 | f"You are using Python {v.major}.{v.minor}.{v.micro}. Most likely things will " 29 | "work properly, but you may experience crashes if Python's internals have " 30 | "changed significantly." 31 | ) 32 | 33 | 34 | # The PyTypeObject struct from "Include/object.h". 35 | # This is a forward declaration, fields are set later once PyVarObject has been declared. 36 | class PyTypeObject(ctypes.Structure): 37 | pass 38 | 39 | 40 | # The PyObject struct from "Include/object.h". 41 | class PyObject(ctypes.Structure): 42 | _fields_ = [ 43 | ("ob_refcnt", ctypes.c_ssize_t), 44 | ("ob_type", ctypes.POINTER(PyTypeObject)), 45 | ] 46 | 47 | 48 | # The PyVarObject struct from "Include/object.h". 49 | class PyVarObject(ctypes.Structure): 50 | _fields_ = [ 51 | ("ob_base", PyObject), 52 | ("ob_size", ctypes.c_ssize_t), 53 | ] 54 | 55 | 56 | # This structure is not stable across Python versions, but the few fields that 57 | # we use probably won't change. 58 | PyTypeObject._fields_ = [ 59 | ("ob_base", PyVarObject), 60 | ("tp_name", ctypes.c_char_p), 61 | ("tp_basicsize", ctypes.c_ssize_t), 62 | ("tp_itemsize", ctypes.c_ssize_t), 63 | # There are many more fields, but we're only interested in the size fields, 64 | # so we can leave out everything else. 65 | ] 66 | 67 | 68 | # The ffi_type structure from libffi's "include/ffi.h". This is a forward 69 | # declaration, because the structure contains pointers to itself. 70 | class ffi_type(ctypes.Structure): 71 | pass 72 | 73 | 74 | ffi_type._fields_ = [ 75 | ("size", ctypes.c_size_t), 76 | ("alignment", ctypes.c_ushort), 77 | ("type", ctypes.c_ushort), 78 | ("elements", ctypes.POINTER(ctypes.POINTER(ffi_type))), 79 | ] 80 | 81 | 82 | # The GETFUNC and SETFUNC typedefs from "Modules/_ctypes/ctypes.h". 83 | GETFUNC = ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p, ctypes.c_ssize_t) 84 | if sys.version_info < (3, 10): 85 | # The return type of SETFUNC is declared here as a c_void_p instead of py_object to work 86 | # around a ctypes bug (https://github.com/python/cpython/issues/81061). See the comment 87 | # in make_callback_returnable's setfunc for details. This bug was fixed in 3.10. 88 | SETFUNC = ctypes.PYFUNCTYPE( 89 | ctypes.c_void_p, ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t 90 | ) 91 | else: 92 | SETFUNC = ctypes.PYFUNCTYPE( 93 | ctypes.py_object, ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t 94 | ) 95 | 96 | 97 | if sys.version_info < (3, 13): 98 | # The PyTypeObject structure for the dict class. 99 | # This is used to determine the size of the PyDictObject structure. 100 | PyDict_Type = PyTypeObject.from_address(id(dict)) 101 | 102 | # The PyDictObject structure from "Include/dictobject.h". This structure is not 103 | # stable across Python versions, and did indeed change in recent Python 104 | # releases. Because we only care about the size of the structure and not its 105 | # actual contents, we can declare it as an opaque byte array, with the length 106 | # taken from PyDict_Type. 107 | class PyDictObject(ctypes.Structure): 108 | _fields_ = [ 109 | ("PyDictObject_opaque", (ctypes.c_ubyte * PyDict_Type.tp_basicsize)), 110 | ] 111 | 112 | # The StgDictObject structure from "Modules/_ctypes/ctypes.h". This structure is 113 | # not officially stable across Python versions, but it didn't change between being 114 | # introduced in 2009, and being replaced in 2024/Python 3.13.0a6. 115 | class StgDictObject(ctypes.Structure): 116 | _fields_ = [ 117 | ("dict", PyDictObject), 118 | ("size", ctypes.c_ssize_t), 119 | ("align", ctypes.c_ssize_t), 120 | ("length", ctypes.c_ssize_t), 121 | ("ffi_type_pointer", ffi_type), 122 | ("proto", ctypes.py_object), 123 | ("setfunc", SETFUNC), 124 | ("getfunc", GETFUNC), 125 | # There are a few more fields, but we leave them out again because 126 | # we don't need them. 127 | ] 128 | 129 | # The mappingproxyobject struct from "Objects/descrobject.c". This structure is 130 | # not officially stable across Python versions, but its layout hasn't changed 131 | # since 2001. 132 | class mappingproxyobject(ctypes.Structure): 133 | _fields_ = [ 134 | ("ob_base", PyObject), 135 | ("mapping", ctypes.py_object), 136 | ] 137 | 138 | def unwrap_mappingproxy(proxy): 139 | """Return the mapping contained in a mapping proxy object.""" 140 | 141 | if not isinstance(proxy, types.MappingProxyType): 142 | raise TypeError( 143 | "Expected a mapping proxy object, not " 144 | f"{type(proxy).__module__}.{type(proxy).__qualname__}" 145 | ) 146 | 147 | return mappingproxyobject.from_address(id(proxy)).mapping 148 | 149 | def get_stgdict_of_type(tp): 150 | """Return the given ctypes type's StgDict object. If the object's dict is 151 | not a StgDict, an error is raised. 152 | 153 | This function is roughly equivalent to the PyType_stgdict function in the 154 | ctypes source code. We cannot use that function directly, because it is not 155 | part of CPython's public C API, and thus not accessible on some systems (see 156 | #113). 157 | """ 158 | 159 | if not isinstance(tp, type): 160 | raise TypeError( 161 | "Expected a type object, not " 162 | f"{type(tp).__module__}.{type(tp).__qualname__}" 163 | ) 164 | 165 | stgdict = tp.__dict__ 166 | if isinstance(stgdict, types.MappingProxyType): 167 | # If the type's __dict__ is wrapped in a mapping proxy, we need to 168 | # unwrap it. (This appears to always be the case, so the isinstance 169 | # check above could perhaps be left out, but it doesn't hurt to check.) 170 | stgdict = unwrap_mappingproxy(stgdict) 171 | 172 | # The StgDict type is not publicly exposed anywhere, so we can't use 173 | # isinstance. Checking the name is the best we can do here. 174 | if type(stgdict).__name__ != "StgDict": 175 | raise TypeError( 176 | "The given type's dict must be a StgDict, not " 177 | f"{type(stgdict).__module__}.{type(stgdict).__qualname__}" 178 | ) 179 | 180 | return StgDictObject.from_address(id(stgdict)) 181 | 182 | else: 183 | # In Python 3.13.0a6 (https://github.com/python/cpython/issues/114314), 184 | # StgDict was replaced with a new StgInfo data type that requires less 185 | # metaclass magic. 186 | 187 | class StgInfo(ctypes.Structure): 188 | _fields_ = [ 189 | ("initialized", ctypes.c_int), 190 | ("size", ctypes.c_ssize_t), 191 | ("align", ctypes.c_ssize_t), 192 | ("length", ctypes.c_ssize_t), 193 | ("ffi_type_pointer", ffi_type), 194 | ("proto", ctypes.py_object), 195 | ("setfunc", SETFUNC), 196 | ("getfunc", GETFUNC), 197 | # There are a few more fields, but we leave them out again because 198 | # we don't need them. 199 | ] 200 | 201 | # void *PyObject_GetTypeData(PyObject *o, PyTypeObject *cls); 202 | ctypes.pythonapi.PyObject_GetTypeData.restype = ctypes.c_void_p 203 | ctypes.pythonapi.PyObject_GetTypeData.argtypes = [ctypes.c_void_p, ctypes.c_void_p] 204 | 205 | def get_stginfo_of_type(tp): 206 | """Return the given ctypes type's StgInfo object. 207 | 208 | This function is roughly equivalent to the PyStgInfo_FromType function in the 209 | ctypes source code. We cannot use that function directly, because it is not 210 | part of CPython's public C API, and thus not accessible). 211 | """ 212 | # Original code: 213 | # if (!PyObject_IsInstance((PyObject *)type, (PyObject *)state->PyCType_Type)) 214 | if not isinstance(tp, type(ctypes.Structure).__base__): 215 | raise TypeError( 216 | "Expected a ctypes structure type, " 217 | f"not {type(tp).__module__}.{type(tp).__qualname__}" 218 | ) 219 | 220 | # tp is the Python representation of the type. The StgInfo struct is the 221 | # type data stored on ctypes.CType_Type (which is the base class of 222 | # ctypes.Structure). 223 | # Original code: 224 | # StgInfo *info = PyObject_GetTypeData((PyObject *)type, state->PyCType_Type); 225 | info = ctypes.pythonapi.PyObject_GetTypeData( 226 | id(tp), 227 | id(type(ctypes.Structure).__base__), 228 | ) 229 | result = StgInfo.from_address(info) 230 | if not result.initialized: 231 | raise TypeError( 232 | f"{type(tp).__module__}.{type(tp).__qualname__} has not been " 233 | "initialized; it may be an abstract class" 234 | ) 235 | 236 | return result 237 | 238 | 239 | ctypes.pythonapi.Py_IncRef.restype = None 240 | ctypes.pythonapi.Py_IncRef.argtypes = [ctypes.POINTER(PyObject)] 241 | 242 | 243 | def make_callback_returnable(ctype): 244 | """Modify the given ctypes type so it can be returned from a callback 245 | function. 246 | 247 | This function may be used as a decorator on a struct/union declaration. 248 | 249 | The method is idempotent; it only modifies the type the first time it 250 | is invoked on a type. 251 | """ 252 | # The presence of the _rubicon_objc_ctypes_patch_getfunc attribute is a 253 | # sentinel for whether the type has been modified previously. 254 | if hasattr(ctype, "_rubicon_objc_ctypes_patch_getfunc"): 255 | return ctype 256 | 257 | # The implementation changed in 3.13.0a6; StgDict was replaced with StgInfo 258 | if sys.version_info < (3, 13): 259 | stg = get_stgdict_of_type(ctype) 260 | else: 261 | stg = get_stginfo_of_type(ctype) 262 | 263 | # Ensure that there is no existing getfunc or setfunc on the stgdict. 264 | if ctypes.cast(stg.getfunc, ctypes.c_void_p).value is not None: 265 | raise ValueError( 266 | f"The ctype {ctype.__module__}.{ctype.__name__} already has a getfunc" 267 | ) 268 | elif ctypes.cast(stg.setfunc, ctypes.c_void_p).value is not None: 269 | raise ValueError( 270 | f"The ctype {ctype.__module__}.{ctype.__name__} already has a setfunc" 271 | ) 272 | 273 | # Define the getfunc and setfunc. 274 | @GETFUNC 275 | def getfunc(ptr, size): 276 | actual_size = ctypes.sizeof(ctype) 277 | if size != 0 and size != actual_size: 278 | raise ValueError( 279 | f"getfunc for ctype {ctype}: Requested size {size} " 280 | f"does not match actual size {actual_size}" 281 | ) 282 | 283 | return ctype.from_buffer_copy(ctypes.string_at(ptr, actual_size)) 284 | 285 | @SETFUNC 286 | def setfunc(ptr, value, size): 287 | actual_size = ctypes.sizeof(ctype) 288 | if size != 0 and size != actual_size: 289 | raise ValueError( 290 | f"setfunc for ctype {ctype}: Requested size {size} " 291 | f"does not match actual size {actual_size}" 292 | ) 293 | 294 | ctypes.memmove(ptr, ctypes.addressof(value), actual_size) 295 | 296 | if sys.version_info < (3, 10): 297 | # Because of a ctypes bug (https://github.com/python/cpython/issues/81061), 298 | # returning None from a callback with restype py_object causes a reference 299 | # counting error that can crash Python. To work around this bug, the restype of 300 | # SETFUNC is declared as c_void_p instead. This way ctypes performs no automatic 301 | # reference counting for the returned object, which avoids the bug. However, 302 | # this way we have to manually convert the Python object to a pointer and adjust 303 | # its reference count. This bug was fixed in 3.10. 304 | none_ptr = ctypes.cast(id(None), ctypes.POINTER(PyObject)) 305 | # The return value of a SETFUNC is expected to have an extra reference 306 | # (which will be owned by the caller of the SETFUNC). 307 | ctypes.pythonapi.Py_IncRef(none_ptr) 308 | # The returned pointer must be returned as a plain int, not as a c_void_p, 309 | # otherwise ctypes won't recognize it and will raise a TypeError. 310 | return ctypes.cast(none_ptr, ctypes.c_void_p).value 311 | 312 | # Store the getfunc and setfunc as attributes on the ctype, so they don't 313 | # get garbage-collected. 314 | ctype._rubicon_objc_ctypes_patch_getfunc = getfunc 315 | ctype._rubicon_objc_ctypes_patch_setfunc = setfunc 316 | 317 | # Put the getfunc and setfunc into the stg fields. 318 | stg.getfunc = getfunc 319 | stg.setfunc = setfunc 320 | 321 | # Return the passed in ctype, so this function can be used as a decorator. 322 | return ctype 323 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import faulthandler 2 | import os 3 | 4 | from rubicon.objc.runtime import load_library 5 | 6 | try: 7 | import platform 8 | 9 | OSX_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split(".")[:2]) 10 | except Exception: 11 | OSX_VERSION = None 12 | 13 | try: 14 | rubiconharness = load_library( 15 | os.path.abspath("tests/objc/build/librubiconharness.dylib") 16 | ) 17 | except ValueError: 18 | raise ValueError( 19 | "Couldn't load Rubicon test harness library. Did you remember to run make?" 20 | ) 21 | 22 | faulthandler.enable() 23 | -------------------------------------------------------------------------------- /tests/objc/Altered_Example.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "Example.h" 4 | 5 | @interface Altered_Example : Example { 6 | 7 | } 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /tests/objc/Altered_Example.m: -------------------------------------------------------------------------------- 1 | #import "Altered_Example.h" 2 | #import 3 | 4 | @implementation Altered_Example 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /tests/objc/BaseExample.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "Protocols.h" 4 | 5 | @interface BaseExample : NSObject { 6 | int _baseIntField; 7 | } 8 | 9 | @property int baseIntField; 10 | 11 | +(int) staticBaseIntField; 12 | +(void) setStaticBaseIntField: (int) v; 13 | 14 | +(int) accessStaticBaseIntField; 15 | +(void) mutateStaticBaseIntFieldWithValue: (int) v; 16 | 17 | -(id) init; 18 | -(id) initWithIntValue: (int) v; 19 | 20 | -(int) accessBaseIntField; 21 | -(void) mutateBaseIntFieldWithValue: (int) v; 22 | 23 | -(void) method:(int) v withArg: (int) m; 24 | -(void) methodWithArgs: (int) m, ...; 25 | -(void) method:(int) v; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /tests/objc/BaseExample.m: -------------------------------------------------------------------------------- 1 | #import "BaseExample.h" 2 | 3 | @implementation BaseExample 4 | 5 | @synthesize baseIntField = _baseIntField; 6 | 7 | static int _staticBaseIntField = 1; 8 | 9 | +(int) staticBaseIntField 10 | { 11 | @synchronized(self) 12 | { 13 | return _staticBaseIntField; 14 | } 15 | } 16 | 17 | +(void) setStaticBaseIntField: (int) v 18 | { 19 | @synchronized(self) 20 | { 21 | _staticBaseIntField = v; 22 | } 23 | } 24 | 25 | +(int) accessStaticBaseIntField 26 | { 27 | @synchronized(self) { 28 | return _staticBaseIntField; 29 | } 30 | } 31 | 32 | +(void) mutateStaticBaseIntFieldWithValue: (int) v 33 | { 34 | @synchronized(self) { 35 | _staticBaseIntField = v; 36 | } 37 | } 38 | 39 | -(id) init 40 | { 41 | self = [super init]; 42 | 43 | if (self) { 44 | [self setBaseIntField:2]; 45 | } 46 | return self; 47 | } 48 | 49 | -(id) initWithIntValue: (int) v 50 | { 51 | self = [super init]; 52 | 53 | if (self) { 54 | [self setBaseIntField:v]; 55 | } 56 | return self; 57 | } 58 | 59 | -(int) accessBaseIntField 60 | { 61 | return self.baseIntField; 62 | } 63 | 64 | -(void) mutateBaseIntFieldWithValue: (int) v 65 | { 66 | self.baseIntField = v; 67 | } 68 | 69 | 70 | -(void) method:(int) v withArg: (int) m{ 71 | self.baseIntField = v * m; 72 | } 73 | 74 | -(void) methodWithArgs: (int) num, ... { 75 | 76 | int sum = 0; 77 | 78 | va_list args; 79 | va_start( args, num ); 80 | 81 | for( int i = 0; i < num; i++) 82 | { 83 | sum += va_arg( args, int); 84 | } 85 | 86 | va_end( args ); 87 | 88 | self.baseIntField = sum; 89 | } 90 | 91 | -(void) method:(int) v{ 92 | self.baseIntField = v; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /tests/objc/Blocks.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BlockPropertyExample : NSObject { 4 | int (^_blockProperty)(int, int); 5 | } 6 | 7 | @property (copy) int (^blockProperty)(int, int); 8 | @end 9 | 10 | typedef struct 11 | { 12 | int a; 13 | int b; 14 | } blockStruct; 15 | 16 | @interface BlockDelegate : NSObject 17 | - (void)exampleMethod:(void (^)(int, int))blockArgument; 18 | - (int)structBlockMethod:(int (^)(blockStruct))blockArgument; 19 | @end 20 | 21 | @interface BlockObjectExample : NSObject { 22 | int _value; 23 | BlockDelegate *_delegate; 24 | } 25 | 26 | @property int value; 27 | @property (retain) BlockDelegate *delegate; 28 | - (id)initWithDelegate:(BlockDelegate *)delegate; 29 | - (int)blockExample; 30 | - (int)structBlockExample; 31 | @end 32 | 33 | 34 | @interface BlockReceiverExample : NSObject 35 | - (int)receiverMethod:(int (^)(int, int))blockArgument; 36 | @end 37 | 38 | 39 | @interface BlockRoundTrip : NSObject 40 | - (int (^)(int, int))roundTrip:(int (^)(int, int))blockArgument; 41 | - (int (^)(void))roundTripNoArgs:(int (^)(void))blockArgument; 42 | @end 43 | -------------------------------------------------------------------------------- /tests/objc/Blocks.m: -------------------------------------------------------------------------------- 1 | #import "Blocks.h" 2 | 3 | @implementation BlockPropertyExample 4 | 5 | @synthesize blockProperty = _blockProperty; 6 | 7 | -(id) init 8 | { 9 | self = [super init]; 10 | 11 | if (self) { 12 | self.blockProperty = ^(int a, int b){ 13 | return a + b; 14 | }; 15 | } 16 | return self; 17 | } 18 | 19 | @end 20 | 21 | @implementation BlockObjectExample 22 | 23 | @synthesize value = _value; 24 | @synthesize delegate = _delegate; 25 | 26 | -(id) initWithDelegate:(BlockDelegate *)delegate 27 | { 28 | self = [super init]; 29 | if (self) { 30 | self.delegate = delegate; 31 | } 32 | return self; 33 | } 34 | 35 | -(int) blockExample 36 | { 37 | BlockDelegate *delegate = self.delegate; 38 | 39 | [delegate exampleMethod:^(int a, int b){ 40 | self.value = a + b; 41 | }]; 42 | return self.value; 43 | } 44 | 45 | -(int) structBlockExample 46 | { 47 | BlockDelegate *delegate = self.delegate; 48 | return [delegate structBlockMethod:^(blockStruct bs){ 49 | return bs.a + bs.b; 50 | }]; 51 | } 52 | @end 53 | 54 | @implementation BlockReceiverExample 55 | 56 | -(int) receiverMethod:(int (^)(int, int))blockArgument 57 | { 58 | return blockArgument(13, 14); 59 | } 60 | 61 | @end 62 | 63 | @implementation BlockRoundTrip 64 | 65 | - (int (^)(int, int)) roundTrip:(int (^)(int, int))blockArgument 66 | { 67 | return blockArgument; 68 | } 69 | 70 | - (int (^)(void)) roundTripNoArgs:(int (^)(void))blockArgument 71 | { 72 | return blockArgument; 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /tests/objc/Callback.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class Example; 4 | 5 | @protocol Callback 6 | 7 | - (void) poke: (Example *) example withValue: (int) value; 8 | - (void) peek: (Example *) example withValue: (int) value; 9 | - (NSString *) reverse: (NSString *) input; 10 | - (NSString *) message; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /tests/objc/DescriptionTester.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface DescriptionTester : NSObject { 4 | NSString *_descriptionString; 5 | NSString *_debugDescriptionString; 6 | } 7 | 8 | @property (retain) NSString *descriptionString; 9 | @property (retain) NSString *debugDescriptionString; 10 | 11 | -(instancetype) initWithDescriptionString:(NSString *)descriptionString debugDescriptionString:(NSString *)debugDescriptionString; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /tests/objc/DescriptionTester.m: -------------------------------------------------------------------------------- 1 | #import "DescriptionTester.h" 2 | 3 | @implementation DescriptionTester 4 | 5 | @synthesize descriptionString = _descriptionString; 6 | @synthesize debugDescriptionString = _debugDescriptionString; 7 | 8 | -(instancetype) initWithDescriptionString:(NSString *)descriptionString debugDescriptionString:(NSString *)debugDescriptionString { 9 | self = [super init]; 10 | if (self) { 11 | self.descriptionString = descriptionString; 12 | self.debugDescriptionString = debugDescriptionString; 13 | } 14 | return self; 15 | } 16 | 17 | -(NSString *) description { 18 | return self.descriptionString; 19 | } 20 | 21 | -(NSString *) debugDescription { 22 | return self.debugDescriptionString; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /tests/objc/Example.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "BaseExample.h" 4 | #import "Thing.h" 5 | #import "Callback.h" 6 | 7 | struct simple { 8 | int foo; 9 | int bar; 10 | }; 11 | 12 | struct complex { 13 | short things[4]; 14 | void (*callback)(void); 15 | struct simple s; 16 | struct complex *next; 17 | }; 18 | 19 | /* objc_msgSend on i386, x86_64, ARM64; objc_msgSend_stret on ARM32. */ 20 | struct int_sized { 21 | char data[4]; 22 | }; 23 | 24 | /* objc_msgSend on x86_64, ARM64; objc_msgSend_stret on i386, ARM32. */ 25 | struct oddly_sized { 26 | char data[5]; 27 | }; 28 | 29 | /* objc_msgSend on ARM64; objc_msgSend_stret on i386, x86_64, ARM32. */ 30 | struct large { 31 | char data[17]; 32 | }; 33 | 34 | extern NSString *const SomeGlobalStringConstant; 35 | 36 | @interface Example : BaseExample { 37 | 38 | int _intField; 39 | Thing *_thing; 40 | NSArray *_array; 41 | NSDictionary *_dict; 42 | id _callback; 43 | int _ambiguous; 44 | } 45 | 46 | #if __has_extension(objc_class_property) 47 | @property (class, readonly) int classAmbiguous; 48 | #endif 49 | 50 | @property int intField; 51 | @property (retain) Thing *thing; 52 | @property (retain) NSArray *array; 53 | @property (retain) NSDictionary *dict; 54 | @property (retain) id callback; 55 | @property (readonly) int ambiguous; 56 | 57 | +(Protocol *)callbackProtocol; 58 | 59 | +(int) staticIntField; 60 | +(void) setStaticIntField: (int) v; 61 | 62 | +(int) accessStaticIntField; 63 | +(void) mutateStaticIntFieldWithValue: (int) v; 64 | 65 | -(id) init; 66 | -(id) initWithClassChange; 67 | -(id) initWithIntValue: (int) v; 68 | -(id) initWithBaseIntValue: (int) b intValue: (int) v; 69 | 70 | -(int) accessIntField; 71 | -(void) mutateIntFieldWithValue: (int) v; 72 | 73 | -(void) setSpecialValue: (int) v; 74 | 75 | -(void) mutateThing: (Thing *) thing; 76 | -(Thing *) accessThing; 77 | 78 | -(int) instanceMethod; 79 | -(int) instanceAmbiguous; 80 | +(int) classMethod; 81 | +(int) classAmbiguous; 82 | 83 | -(NSString *) toString; 84 | -(NSString *) duplicateString:(NSString *) in; 85 | -(NSString *) smiley; 86 | 87 | -(NSNumber *) theAnswer; 88 | -(NSNumber *) twopi; 89 | 90 | -(float) areaOfSquare: (float) size; 91 | -(double) areaOfCircle: (double) diameter; 92 | -(NSDecimalNumber *) areaOfTriangleWithWidth: (NSDecimalNumber *) width andHeight: (NSDecimalNumber *) height; 93 | 94 | -(struct int_sized) intSizedStruct; 95 | -(struct oddly_sized) oddlySizedStruct; 96 | -(struct large) largeStruct; 97 | 98 | -(void) testPoke:(int) value; 99 | -(void) testPeek:(int) value; 100 | -(NSString *) getMessage; 101 | -(NSString *) reverseIt:(NSString *) input; 102 | 103 | +(NSUInteger) overloaded; 104 | +(NSUInteger) overloaded:(NSUInteger)arg1; 105 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2; 106 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg1:(NSUInteger)arg2 extraArg2:(NSUInteger)arg3; 107 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg2:(NSUInteger)arg2 extraArg1:(NSUInteger)arg3; 108 | +(NSUInteger) overloaded:(NSUInteger)arg1 orderedArg1:(NSUInteger)arg2 orderedArg2:(NSUInteger)arg3; 109 | +(NSUInteger) overloaded:(NSUInteger)arg1 duplicateArg:(NSUInteger)arg2 duplicateArg:(NSUInteger)arg3; 110 | 111 | +(struct complex) doStuffWithStruct:(struct simple)simple; 112 | 113 | -(id) processDictionary:(NSDictionary *) dict; 114 | -(id) processArray:(NSArray *) dict; 115 | 116 | -(NSSize) testThing:(int) value; 117 | 118 | @end 119 | -------------------------------------------------------------------------------- /tests/objc/Example.m: -------------------------------------------------------------------------------- 1 | #import "Example.h" 2 | #import "Altered_Example.h" 3 | #import 4 | #import 5 | 6 | NSString *const SomeGlobalStringConstant = @"Some global string constant"; 7 | 8 | @implementation Example 9 | 10 | @synthesize intField = _intField; 11 | @synthesize thing = _thing; 12 | @synthesize array = _array; 13 | @synthesize dict = _dict; 14 | @synthesize callback = _callback; 15 | @synthesize ambiguous = _ambiguous; 16 | 17 | +(Protocol *)callbackProtocol { 18 | // Since the Callback protocol is not adopted by any class in the test harness, the compiler doesn't generate 19 | // runtime info for it by default. To force the protocol to be available at runtime, we use it as an object here. 20 | return @protocol(Callback); 21 | } 22 | 23 | static int _staticIntField = 11; 24 | 25 | +(int) staticIntField 26 | { 27 | @synchronized(self) { 28 | return _staticIntField; 29 | } 30 | } 31 | 32 | +(void) setStaticIntField: (int) v 33 | { 34 | @synchronized(self) { 35 | _staticIntField = v; 36 | } 37 | } 38 | 39 | +(int) accessStaticIntField 40 | { 41 | @synchronized(self) { 42 | return _staticIntField; 43 | } 44 | } 45 | 46 | +(void) mutateStaticIntFieldWithValue: (int) v 47 | { 48 | @synchronized(self) { 49 | _staticIntField = v; 50 | } 51 | } 52 | 53 | 54 | -(id) init 55 | { 56 | self = [super initWithIntValue:22]; 57 | 58 | if (self) { 59 | [self setIntField:33]; 60 | } 61 | _ambiguous = 42; 62 | return self; 63 | } 64 | 65 | -(id) initWithClassChange 66 | { 67 | self = [super initWithIntValue:44]; 68 | 69 | if (self) { 70 | [self setIntField:55]; 71 | } 72 | _ambiguous = 37; 73 | 74 | object_setClass(self, [Altered_Example class]); 75 | return self; 76 | } 77 | 78 | -(id) initWithIntValue: (int) v 79 | { 80 | self = [super initWithIntValue:44]; 81 | 82 | if (self) { 83 | [self setIntField:v]; 84 | } 85 | _ambiguous = 42; 86 | return self; 87 | } 88 | 89 | -(id) initWithBaseIntValue: (int) b intValue: (int) v 90 | { 91 | self = [super initWithIntValue:b]; 92 | 93 | if (self) { 94 | [self setIntField:v]; 95 | } 96 | _ambiguous = 42; 97 | return self; 98 | } 99 | 100 | /* Simple methods */ 101 | -(int) accessIntField 102 | { 103 | return self.intField; 104 | } 105 | 106 | -(void) mutateIntFieldWithValue: (int) v 107 | { 108 | self.intField = v; 109 | } 110 | 111 | -(void) setSpecialValue: (int) v 112 | { 113 | self.intField = v; 114 | } 115 | 116 | /* Float/Double/Decimal argument/return value handling */ 117 | -(float) areaOfSquare: (float) size 118 | { 119 | return size * size; 120 | } 121 | 122 | -(double) areaOfCircle: (double) diameter 123 | { 124 | return diameter * M_PI; 125 | } 126 | 127 | -(NSDecimalNumber *) areaOfTriangleWithWidth: (NSDecimalNumber *) width 128 | andHeight: (NSDecimalNumber *) height 129 | { 130 | return [width decimalNumberByMultiplyingBy:[height decimalNumberByDividingBy:[NSDecimalNumber decimalNumberWithString:@"2.0"]]]; 131 | } 132 | 133 | /* Handling of struct returns of different sizes. */ 134 | -(struct int_sized) intSizedStruct { 135 | struct int_sized ret = {"abc"}; 136 | return ret; 137 | } 138 | 139 | -(struct oddly_sized) oddlySizedStruct { 140 | struct oddly_sized ret = {"abcd"}; 141 | return ret; 142 | } 143 | 144 | -(struct large) largeStruct { 145 | struct large ret = {"abcdefghijklmnop"}; 146 | return ret; 147 | } 148 | 149 | 150 | /* Handling of object references. */ 151 | -(void) mutateThing: (Thing *) thing 152 | { 153 | self.thing = thing; 154 | } 155 | 156 | -(Thing *) accessThing 157 | { 158 | return self.thing; 159 | } 160 | 161 | -(int) instanceMethod 162 | { 163 | return _ambiguous; 164 | } 165 | 166 | -(int) instanceAmbiguous 167 | { 168 | return _ambiguous; 169 | } 170 | 171 | +(int) classMethod 172 | { 173 | return 37; 174 | } 175 | 176 | +(int) classAmbiguous 177 | { 178 | return 37; 179 | } 180 | 181 | /* String argument/return value handling */ 182 | -(NSString *) toString 183 | { 184 | return [NSString stringWithFormat:@"This is an ObjC Example object"]; 185 | } 186 | 187 | -(NSString *) duplicateString:(NSString *) in 188 | { 189 | return [NSString stringWithFormat:@"%@%@", in, in]; 190 | } 191 | 192 | -(NSString *) smiley 193 | { 194 | return @"%-)"; 195 | } 196 | 197 | /* NSNumber return value */ 198 | -(NSNumber *) theAnswer 199 | { 200 | return [NSNumber numberWithInt:42]; 201 | } 202 | 203 | -(NSNumber *) twopi 204 | { 205 | return [NSNumber numberWithFloat:2.0*M_PI]; 206 | } 207 | 208 | /* Callback handling */ 209 | -(void) testPoke:(int) value 210 | { 211 | [self.callback poke:self withValue:value]; 212 | } 213 | 214 | -(void) testPeek:(int) value 215 | { 216 | [self.callback peek:self withValue:value]; 217 | } 218 | 219 | -(NSString *) getMessage 220 | { 221 | return [self.callback message]; 222 | } 223 | 224 | -(NSString *) reverseIt:(NSString *) input 225 | { 226 | return [self.callback reverse:input]; 227 | } 228 | 229 | +(NSUInteger) overloaded 230 | { 231 | return 0; 232 | } 233 | 234 | +(NSUInteger) overloaded:(NSUInteger)arg1 235 | { 236 | return arg1; 237 | } 238 | 239 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg:(NSUInteger)arg2 240 | { 241 | return arg1 + arg2; 242 | } 243 | 244 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg1:(NSUInteger)arg2 extraArg2:(NSUInteger)arg3 245 | { 246 | return arg1 + arg2 + arg3; 247 | } 248 | 249 | +(NSUInteger) overloaded:(NSUInteger)arg1 extraArg2:(NSUInteger)arg2 extraArg1:(NSUInteger)arg3 250 | { 251 | return arg1 * arg2 * arg3; 252 | } 253 | 254 | +(NSUInteger) overloaded:(NSUInteger)arg1 orderedArg1:(NSUInteger)arg2 orderedArg2:(NSUInteger)arg3 255 | { 256 | return 0; 257 | } 258 | 259 | +(NSUInteger) overloaded:(NSUInteger)arg1 duplicateArg:(NSUInteger)arg2 duplicateArg:(NSUInteger)arg3 260 | { 261 | return arg1 + 2 * arg2 + 3 * arg3; 262 | } 263 | 264 | +(struct complex) doStuffWithStruct:(struct simple)simple 265 | { 266 | return (struct complex){ 267 | .things = {1, 2, 3, 4}, 268 | .callback = NULL, 269 | .s = simple, 270 | .next = NULL, 271 | }; 272 | } 273 | 274 | +(struct simple) extractSimpleStruct:(struct complex)complex 275 | { 276 | return complex.s; 277 | } 278 | 279 | -(id) processDictionary:(NSDictionary *) dict 280 | { 281 | return [dict objectForKey:@"data"]; 282 | } 283 | 284 | -(id) processArray:(NSArray *) array 285 | { 286 | return [array objectAtIndex:1]; 287 | } 288 | 289 | -(NSSize) testThing:(int) value 290 | { 291 | return [_thing computeSize:NSMakeSize(0, value)]; 292 | } 293 | 294 | @end 295 | -------------------------------------------------------------------------------- /tests/objc/Makefile: -------------------------------------------------------------------------------- 1 | OBJCC = clang 2 | OBJCLD_SHARED = $(OBJCC) -shared 3 | 4 | OBJCLDFLAGS = -fobjc-link-runtime 5 | 6 | OUTPUT_DIR = build 7 | OBJECTS_DIR = $(OUTPUT_DIR)/objects 8 | 9 | HEADER_FILES = $(wildcard *.h) 10 | SOURCE_FILES = $(wildcard *.m) 11 | OBJ_FILES = $(addprefix $(OBJECTS_DIR)/,$(addsuffix .o,$(SOURCE_FILES))) 12 | 13 | LIB_NAME = librubiconharness.dylib 14 | 15 | all: $(OUTPUT_DIR)/$(LIB_NAME) 16 | 17 | $(OUTPUT_DIR)/$(LIB_NAME): $(OBJ_FILES) 18 | @mkdir -p $(@D) 19 | $(OBJCLD_SHARED) $(OBJCLDFLAGS) $(OBJ_FILES) $(OBJCLDLIBS) -o $@ 20 | 21 | clean: 22 | $(RM) -r $(OUTPUT_DIR) 23 | 24 | $(OBJECTS_DIR)/%.m.o: %.m $(HEADER_FILES) 25 | @mkdir -p $(@D) 26 | $(OBJCC) $(OBJCFLAGS) -c $< -o $@ 27 | -------------------------------------------------------------------------------- /tests/objc/Protocols.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol ExampleProtocol @end 4 | 5 | @protocol BaseProtocolOne @end 6 | 7 | @protocol BaseProtocolTwo @end 8 | 9 | @protocol DerivedProtocol @end 10 | -------------------------------------------------------------------------------- /tests/objc/SpecificExample.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "Example.h" 4 | 5 | @interface SpecificExample : Example { 6 | 7 | } 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /tests/objc/SpecificExample.m: -------------------------------------------------------------------------------- 1 | #import "SpecificExample.h" 2 | #import 3 | 4 | @implementation SpecificExample 5 | 6 | -(void) method:(int) v withArg: (int) m{ 7 | self.baseIntField = v + m; 8 | } 9 | 10 | -(void) methodWithArgs: (int) num, ... { 11 | 12 | int prod = 1; 13 | 14 | va_list args; 15 | va_start( args, num ); 16 | 17 | for( int i = 0; i < num; i++) 18 | { 19 | prod *= va_arg( args, int); 20 | } 21 | 22 | va_end( args ); 23 | 24 | self.baseIntField = prod; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /tests/objc/Thing.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface Thing : NSObject { 4 | NSString *_name; 5 | } 6 | 7 | @property (retain) NSString *name; 8 | 9 | -(id) initWithName: (NSString *) name; 10 | -(id) initWithName: (NSString *) name value: (int) v; 11 | 12 | -(NSString *) toString; 13 | 14 | -(NSSize) computeSize: (NSSize) input; 15 | -(NSRect) computeRect: (NSRect) input; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /tests/objc/Thing.m: -------------------------------------------------------------------------------- 1 | #import "Thing.h" 2 | 3 | @implementation Thing 4 | 5 | @synthesize name = _name; 6 | 7 | -(id) initWithName: (NSString *) name 8 | { 9 | self = [super init]; 10 | 11 | if (self) { 12 | self.name = name; 13 | } 14 | return self; 15 | } 16 | 17 | -(id) initWithName: (NSString *) name value: (int) v 18 | { 19 | self = [super init]; 20 | 21 | if (self) { 22 | self.name = [NSString stringWithFormat:@"%@ %d", name, v, NULL]; 23 | } 24 | return self; 25 | } 26 | 27 | -(NSString *) toString 28 | { 29 | return self.name; 30 | } 31 | 32 | -(NSSize) computeSize: (NSSize) input 33 | { 34 | return NSMakeSize(input.width * 2, input.height * 3); 35 | } 36 | 37 | -(NSRect) computeRect: (NSRect) input 38 | { 39 | return NSMakeRect(input.origin.x + 100, input.origin.y + 200, input.size.width * 2, input.size.height * 3); 40 | } 41 | 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /tests/test_NSArray.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | 5 | from rubicon.objc import ( 6 | NSArray, 7 | NSMutableArray, 8 | NSObject, 9 | ObjCClass, 10 | objc_method, 11 | objc_property, 12 | py_from_ns, 13 | ) 14 | from rubicon.objc.collections import ObjCListInstance 15 | 16 | 17 | class NSArrayMixinTest(unittest.TestCase): 18 | py_list = ["one", "two", "three"] 19 | 20 | def make_array(self, contents=None): 21 | a = NSMutableArray.alloc().init() 22 | if contents is not None: 23 | for value in contents: 24 | a.addObject(value) 25 | 26 | return NSArray.arrayWithArray(a) 27 | 28 | def test_getitem(self): 29 | a = self.make_array(self.py_list) 30 | 31 | for pos, value in enumerate(self.py_list): 32 | self.assertEqual(a[pos], value) 33 | 34 | with self.assertRaises(IndexError): 35 | a[len(self.py_list) + 10] 36 | 37 | with self.assertRaises(IndexError): 38 | a[-len(self.py_list) - 1] 39 | 40 | def test_len(self): 41 | a = self.make_array(self.py_list) 42 | 43 | self.assertEqual(len(a), len(self.py_list)) 44 | 45 | def test_iter(self): 46 | a = self.make_array(self.py_list) 47 | 48 | keys = list(self.py_list) 49 | for k in a: 50 | self.assertTrue(k in keys) 51 | keys.remove(k) 52 | 53 | self.assertTrue(len(keys) == 0) 54 | 55 | def test_contains(self): 56 | a = self.make_array(self.py_list) 57 | for value in self.py_list: 58 | self.assertTrue(value in a) 59 | 60 | def test_index(self): 61 | a = self.make_array(self.py_list) 62 | self.assertEqual(a.index("two"), 1) 63 | with self.assertRaises(ValueError): 64 | a.index("umpteen") 65 | 66 | def test_count(self): 67 | a = self.make_array(self.py_list) 68 | self.assertEqual(a.count("one"), 1) 69 | 70 | def test_copy(self): 71 | a = self.make_array(self.py_list) 72 | b = a.copy() 73 | self.assertEqual(b, a) 74 | self.assertEqual(b, self.py_list) 75 | 76 | with self.assertRaises(AttributeError): 77 | b.append("four") 78 | 79 | def test_equivalence(self): 80 | a = self.make_array(self.py_list) 81 | b = self.make_array(self.py_list) 82 | 83 | self.assertEqual(a, self.py_list) 84 | self.assertEqual(b, self.py_list) 85 | self.assertEqual(a, b) 86 | self.assertEqual(self.py_list, a) 87 | self.assertEqual(self.py_list, b) 88 | self.assertEqual(b, a) 89 | 90 | self.assertNotEqual(a, object()) 91 | self.assertNotEqual(a, []) 92 | self.assertNotEqual(a, self.py_list[:2]) 93 | self.assertNotEqual(a, self.py_list + ["spam", "ham"]) 94 | 95 | def test_slice_access(self): 96 | a = self.make_array(self.py_list * 2) 97 | self.assertEqual(a[1:4], ["two", "three", "one"]) 98 | self.assertEqual(a[:-2], ["one", "two", "three", "one"]) 99 | self.assertEqual(a[4:], ["two", "three"]) 100 | self.assertEqual(a[1:5:2], ["two", "one"]) 101 | 102 | def test_argument(self): 103 | Example = ObjCClass("Example") 104 | example = Example.alloc().init() 105 | 106 | a = self.make_array(self.py_list) 107 | # Call a method with an NSArray instance 108 | self.assertEqual(example.processArray(a), "two") 109 | # Call the same method with the Python list 110 | self.assertEqual(example.processArray(self.py_list), "two") 111 | 112 | def test_property(self): 113 | Example = ObjCClass("Example") 114 | example = Example.alloc().init() 115 | 116 | a = self.make_array(self.py_list) 117 | example.array = a 118 | 119 | self.assertEqual(example.array, self.py_list) 120 | self.assertIsInstance(example.array, ObjCListInstance) 121 | self.assertEqual(example.array[1], "two") 122 | 123 | 124 | class NSMutableArrayMixinTest(NSArrayMixinTest): 125 | def make_array(self, contents=None): 126 | a = NSMutableArray.alloc().init() 127 | if contents is not None: 128 | for value in contents: 129 | a.addObject(value) 130 | 131 | return a 132 | 133 | def test_setitem(self): 134 | a = self.make_array(self.py_list) 135 | 136 | a[2] = "four" 137 | self.assertEqual(a[2], "four") 138 | 139 | with self.assertRaises(IndexError): 140 | a[len(a)] = "invalid" 141 | 142 | with self.assertRaises(IndexError): 143 | a[-len(a) - 1] = "invalid" 144 | 145 | def test_del(self): 146 | a = self.make_array(self.py_list) 147 | del a[0] 148 | self.assertEqual(len(a), 2) 149 | self.assertEqual(a[0], "two") 150 | 151 | with self.assertRaises(IndexError): 152 | del a[len(a)] 153 | 154 | with self.assertRaises(IndexError): 155 | del a[-len(a) - 1] 156 | 157 | def test_append(self): 158 | a = self.make_array() 159 | a.append("an item") 160 | self.assertTrue("an item" in a) 161 | 162 | def test_extend(self): 163 | a = self.make_array() 164 | a.extend(["an item", "another item"]) 165 | self.assertTrue("an item" in a) 166 | self.assertTrue("another item" in a) 167 | 168 | def test_clear(self): 169 | a = self.make_array(self.py_list) 170 | a.clear() 171 | self.assertEqual(len(a), 0) 172 | 173 | def test_count(self): 174 | a = self.make_array(self.py_list) 175 | self.assertEqual(a.count("one"), 1) 176 | 177 | a.append("one") 178 | self.assertEqual(a.count("one"), 2) 179 | 180 | def test_copy(self): 181 | a = self.make_array(self.py_list) 182 | b = a.copy() 183 | self.assertEqual(b, a) 184 | self.assertEqual(b, self.py_list) 185 | 186 | b.append("four") 187 | 188 | def test_insert(self): 189 | a = self.make_array(self.py_list) 190 | a.insert(1, "four") 191 | self.assertEqual(a[0], "one") 192 | self.assertEqual(a[1], "four") 193 | self.assertEqual(a[2], "two") 194 | 195 | def test_pop(self): 196 | a = self.make_array(self.py_list) 197 | self.assertEqual(a.pop(), "three") 198 | self.assertEqual(a.pop(0), "one") 199 | self.assertEqual(len(a), 1) 200 | self.assertEqual(a[0], "two") 201 | 202 | def test_remove(self): 203 | a = self.make_array(self.py_list) 204 | a.remove("three") 205 | self.assertEqual(len(a), 2) 206 | self.assertEqual(a[-1], "two") 207 | with self.assertRaises(ValueError): 208 | a.remove("umpteen") 209 | 210 | def test_slice_assignment1(self): 211 | a = self.make_array(self.py_list * 2) 212 | a[2:4] = ["four", "five"] 213 | self.assertEqual(a, ["one", "two", "four", "five", "two", "three"]) 214 | 215 | def test_slice_assignment2(self): 216 | a = self.make_array(self.py_list * 2) 217 | a[::2] = ["four", "five", "six"] 218 | self.assertEqual(a, ["four", "two", "five", "one", "six", "three"]) 219 | 220 | def test_slice_assignment3(self): 221 | a = self.make_array(self.py_list * 2) 222 | a[2:4] = ["four"] 223 | self.assertEqual(a, ["one", "two", "four", "two", "three"]) 224 | 225 | def test_bad_slice_assignment1(self): 226 | a = self.make_array(self.py_list * 2) 227 | 228 | with self.assertRaises(TypeError): 229 | a[2:4] = 4 230 | 231 | def test_bad_slice_assignment2(self): 232 | a = self.make_array(self.py_list * 2) 233 | 234 | with self.assertRaises(ValueError): 235 | a[::2] = [4] 236 | 237 | def test_del_slice1(self): 238 | a = self.make_array(self.py_list * 2) 239 | del a[-2:] 240 | self.assertEqual(len(a), 4) 241 | self.assertEqual(a[0], "one") 242 | self.assertEqual(a[-1], "one") 243 | 244 | def test_del_slice2(self): 245 | a = self.make_array(self.py_list * 2) 246 | del a[::2] 247 | self.assertEqual(len(a), 3) 248 | self.assertEqual(a[0], "two") 249 | self.assertEqual(a[1], "one") 250 | self.assertEqual(a[2], "three") 251 | 252 | def test_del_slice3(self): 253 | a = self.make_array(self.py_list * 2) 254 | del a[::-2] 255 | self.assertEqual(len(a), 3) 256 | self.assertEqual(a[0], "one") 257 | self.assertEqual(a[1], "three") 258 | self.assertEqual(a[2], "two") 259 | 260 | def test_reverse(self): 261 | a = self.make_array(self.py_list) 262 | a.reverse() 263 | 264 | for pos, value in enumerate(reversed(self.py_list)): 265 | self.assertEqual(a[pos], value) 266 | 267 | 268 | class PythonObjectTest(unittest.TestCase): 269 | def test_primitive_list_attribute(self): 270 | class PrimitiveListAttrContainer(NSObject): 271 | @objc_method 272 | def init(self): 273 | self.data = [1, 2, 3] 274 | return self 275 | 276 | @objc_method 277 | def initWithList_(self, data): 278 | self.data = data 279 | return self 280 | 281 | obj1 = PrimitiveListAttrContainer.alloc().init() 282 | self.assertEqual(obj1.data, [1, 2, 3]) 283 | self.assertIsInstance(obj1.data, list) 284 | 285 | # If it's set through a method call, it becomes an objc instance 286 | obj2 = PrimitiveListAttrContainer.alloc().initWithList_([4, 5, 6]) 287 | self.assertIsInstance(obj2.data, ObjCListInstance) 288 | self.assertEqual(py_from_ns(obj2.data), [4, 5, 6]) 289 | 290 | # If it's set by direct attribute access, it becomes a Python object. 291 | obj2.data = [7, 8, 9] 292 | self.assertIsInstance(obj2.data, list) 293 | self.assertEqual(obj2.data, [7, 8, 9]) 294 | 295 | def test_primitive_list_property(self): 296 | class PrimitiveListContainer(NSObject): 297 | data = objc_property() 298 | 299 | @objc_method 300 | def init(self): 301 | self.data = [1, 2, 3] 302 | return self 303 | 304 | @objc_method 305 | def initWithList_(self, data): 306 | self.data = data 307 | return self 308 | 309 | obj1 = PrimitiveListContainer.alloc().init() 310 | self.assertIsInstance(obj1.data, ObjCListInstance) 311 | self.assertEqual(py_from_ns(obj1.data), [1, 2, 3]) 312 | 313 | obj2 = PrimitiveListContainer.alloc().initWithList_([4, 5, 6]) 314 | self.assertIsInstance(obj2.data, ObjCListInstance) 315 | self.assertEqual(py_from_ns(obj2.data), [4, 5, 6]) 316 | 317 | obj2.data = [7, 8, 9] 318 | self.assertIsInstance(obj2.data, ObjCListInstance) 319 | self.assertEqual(py_from_ns(obj2.data), [7, 8, 9]) 320 | 321 | def test_object_list_attribute(self): 322 | class ObjectListAttrContainer(NSObject): 323 | @objc_method 324 | def init(self): 325 | self.data = ["x1", "y2", "z3"] 326 | return self 327 | 328 | @objc_method 329 | def initWithList_(self, data): 330 | self.data = data 331 | return self 332 | 333 | obj1 = ObjectListAttrContainer.alloc().init() 334 | self.assertEqual(obj1.data, ["x1", "y2", "z3"]) 335 | self.assertIsInstance(obj1.data, list) 336 | 337 | # If it's set through a method call, it becomes an objc instance 338 | obj2 = ObjectListAttrContainer.alloc().initWithList_(["a4", "b5", "c6"]) 339 | self.assertEqual(obj2.data, ["a4", "b5", "c6"]) 340 | self.assertIsInstance(obj2.data, ObjCListInstance) 341 | 342 | # If it's set by direct attribute access, it becomes a Python object. 343 | obj2.data = ["i7", "j8", "k9"] 344 | self.assertEqual(obj2.data, ["i7", "j8", "k9"]) 345 | self.assertIsInstance(obj2.data, list) 346 | 347 | def test_object_list_property(self): 348 | class ObjectListContainer(NSObject): 349 | data = objc_property() 350 | 351 | @objc_method 352 | def init(self): 353 | self.data = ["x1", "y2", "z3"] 354 | return self 355 | 356 | @objc_method 357 | def initWithList_(self, data): 358 | self.data = data 359 | return self 360 | 361 | obj1 = ObjectListContainer.alloc().init() 362 | self.assertEqual(obj1.data, ["x1", "y2", "z3"]) 363 | self.assertIsInstance(obj1.data, ObjCListInstance) 364 | 365 | obj2 = ObjectListContainer.alloc().initWithList_(["a4", "b5", "c6"]) 366 | self.assertEqual(obj2.data, ["a4", "b5", "c6"]) 367 | self.assertIsInstance(obj2.data, ObjCListInstance) 368 | 369 | obj2.data = ["i7", "j8", "k9"] 370 | self.assertEqual(obj2.data, ["i7", "j8", "k9"]) 371 | self.assertIsInstance(obj2.data, ObjCListInstance) 372 | 373 | def test_multitype_list_property(self): 374 | class MultitypeListContainer(NSObject): 375 | data = objc_property() 376 | 377 | Example = ObjCClass("Example") 378 | example = Example.alloc().init() 379 | 380 | # All types can be stored in a list. 381 | obj = MultitypeListContainer.alloc().init() 382 | 383 | obj.data = [4, True, "Hello", example] 384 | self.assertIsInstance(obj.data, ObjCListInstance) 385 | self.assertEqual(py_from_ns(obj.data), [4, True, "Hello", example]) 386 | -------------------------------------------------------------------------------- /tests/test_NSDictionary.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | 5 | from rubicon.objc import ( 6 | NSDictionary, 7 | NSMutableDictionary, 8 | NSObject, 9 | ObjCClass, 10 | objc_method, 11 | objc_property, 12 | ) 13 | from rubicon.objc.collections import ObjCDictInstance 14 | 15 | 16 | class NSDictionaryMixinTest(unittest.TestCase): 17 | py_dict = { 18 | "one": "ONE", 19 | "two": "TWO", 20 | "three": "THREE", 21 | } 22 | 23 | def make_dictionary(self, contents=None): 24 | d = NSMutableDictionary.alloc().init() 25 | if contents is not None: 26 | for key, value in contents.items(): 27 | d.setObject_forKey_(value, key) 28 | 29 | return NSDictionary.dictionaryWithDictionary(d) 30 | 31 | def test_getitem(self): 32 | d = self.make_dictionary(self.py_dict) 33 | 34 | for key, value in self.py_dict.items(): 35 | self.assertEqual(d[key], value) 36 | 37 | with self.assertRaises(KeyError): 38 | d["NO SUCH KEY"] 39 | 40 | def test_iter(self): 41 | d = self.make_dictionary(self.py_dict) 42 | 43 | keys = set(self.py_dict) 44 | for k in d: 45 | keys.remove(str(k)) 46 | 47 | self.assertTrue(len(keys) == 0) 48 | 49 | def test_len(self): 50 | d = self.make_dictionary(self.py_dict) 51 | self.assertEqual(len(d), len(self.py_dict)) 52 | 53 | def test_get(self): 54 | d = self.make_dictionary(self.py_dict) 55 | 56 | self.assertEqual(d.get("one"), "ONE") 57 | self.assertEqual(d.get("two", None), "TWO") 58 | self.assertEqual(d.get("four", None), None) 59 | self.assertEqual(d.get("five", 5), 5) 60 | self.assertEqual(d.get("six", None), None) 61 | 62 | def test_contains(self): 63 | d = self.make_dictionary(self.py_dict) 64 | for key in self.py_dict: 65 | self.assertTrue(key in d) 66 | 67 | def test_copy(self): 68 | d = self.make_dictionary(self.py_dict) 69 | e = d.copy() 70 | self.assertEqual(e, d) 71 | self.assertEqual(e, self.py_dict) 72 | 73 | def test_equivalence(self): 74 | d1 = self.make_dictionary(self.py_dict) 75 | d2 = self.make_dictionary(self.py_dict) 76 | smaller_py_dict = self.py_dict.copy() 77 | del smaller_py_dict["three"] 78 | bigger_py_dict = {"four": "FOUR"} 79 | bigger_py_dict.update(self.py_dict) 80 | 81 | self.assertEqual(d1, self.py_dict) 82 | self.assertEqual(d2, self.py_dict) 83 | self.assertEqual(d1, d2) 84 | self.assertEqual(self.py_dict, d1) 85 | self.assertEqual(self.py_dict, d2) 86 | self.assertEqual(d2, d1) 87 | 88 | self.assertNotEqual(d1, object()) 89 | self.assertNotEqual(d1, {}) 90 | self.assertNotEqual(d1, smaller_py_dict) 91 | self.assertNotEqual(d1, bigger_py_dict) 92 | 93 | def test_keys(self): 94 | a = self.make_dictionary(self.py_dict) 95 | for k1, k2 in zip(sorted(a.keys()), sorted(self.py_dict.keys())): 96 | self.assertEqual(k1, k2) 97 | 98 | def test_values(self): 99 | a = self.make_dictionary(self.py_dict) 100 | for v1, v2 in zip(sorted(a.values()), sorted(self.py_dict.values())): 101 | self.assertEqual(v1, v2) 102 | 103 | def test_items(self): 104 | d = self.make_dictionary(self.py_dict) 105 | for i1, i2 in zip(sorted(d.items()), sorted(self.py_dict.items())): 106 | self.assertEqual(i1[0], i2[0]) 107 | self.assertEqual(i1[1], i2[1]) 108 | 109 | def test_argument(self): 110 | Example = ObjCClass("Example") 111 | example = Example.alloc().init() 112 | 113 | d = self.make_dictionary(self.py_dict) 114 | # Call a method with an NSDictionary instance 115 | self.assertIsNone(example.processDictionary(d)) 116 | # Call the same method with the raw Python dictionary 117 | self.assertIsNone(example.processDictionary(self.py_dict)) 118 | 119 | raw = {"data": "stuff", "other": "gadgets"} 120 | d = self.make_dictionary(raw) 121 | # Call a method with an NSDictionary instance 122 | self.assertEqual(example.processDictionary(d), "stuff") 123 | # Call the same method with the raw Python dictionary 124 | self.assertEqual(example.processDictionary(raw), "stuff") 125 | 126 | def test_property(self): 127 | Example = ObjCClass("Example") 128 | example = Example.alloc().init() 129 | 130 | d = self.make_dictionary(self.py_dict) 131 | example.dict = d 132 | 133 | self.assertEqual(example.dict, self.py_dict) 134 | self.assertIsInstance(example.dict, ObjCDictInstance) 135 | self.assertEqual(example.dict["one"], "ONE") 136 | 137 | 138 | class NSMutableDictionaryMixinTest(NSDictionaryMixinTest): 139 | def make_dictionary(self, contents=None): 140 | d = NSMutableDictionary.alloc().init() 141 | if contents is not None: 142 | for key, value in contents.items(): 143 | d.setObject_forKey_(value, key) 144 | 145 | return d 146 | 147 | def test_setitem(self): 148 | d = self.make_dictionary() 149 | for key, value in self.py_dict.items(): 150 | d[key] = value 151 | 152 | for key, value in self.py_dict.items(): 153 | self.assertEqual(d[key], value) 154 | 155 | def test_del(self): 156 | d = self.make_dictionary(self.py_dict) 157 | del d["one"] 158 | self.assertEqual(len(d), 2) 159 | with self.assertRaises(KeyError): 160 | d["one"] 161 | 162 | def test_clear(self): 163 | d = self.make_dictionary(self.py_dict) 164 | d.clear() 165 | self.assertEqual(len(d), 0) 166 | 167 | def test_copy(self): 168 | d = self.make_dictionary(self.py_dict) 169 | e = d.copy() 170 | self.assertEqual(e, d) 171 | self.assertEqual(e, self.py_dict) 172 | 173 | e["four"] = "FOUR" 174 | 175 | def test_pop1(self): 176 | d = self.make_dictionary(self.py_dict) 177 | 178 | self.assertEqual(d.pop("one"), "ONE") 179 | self.assertEqual(len(d), 2) 180 | with self.assertRaises(KeyError): 181 | d["one"] 182 | 183 | def test_pop2(self): 184 | d = self.make_dictionary(self.py_dict) 185 | 186 | with self.assertRaises(KeyError): 187 | d.pop("four") 188 | 189 | def test_pop3(self): 190 | d = self.make_dictionary(self.py_dict) 191 | 192 | self.assertEqual(d.pop("four", 4), 4) 193 | 194 | def test_popitem(self): 195 | d = self.make_dictionary(self.py_dict) 196 | 197 | keys = set(self.py_dict) 198 | 199 | while len(d) > 0: 200 | key, value = d.popitem() 201 | self.assertTrue(str(key) in keys) 202 | self.assertEqual(value, self.py_dict[str(key)]) 203 | self.assertTrue(key not in d) 204 | 205 | with self.assertRaises(KeyError): 206 | d.popitem() 207 | 208 | def test_setdefault1(self): 209 | d = self.make_dictionary(self.py_dict) 210 | 211 | self.assertEqual(d.setdefault("one", "default"), "ONE") 212 | self.assertEqual(len(d), len(self.py_dict)) 213 | 214 | def test_setdefault2(self): 215 | d = self.make_dictionary(self.py_dict) 216 | 217 | self.assertTrue("four" not in d) 218 | self.assertEqual(d.setdefault("four", "FOUR"), "FOUR") 219 | self.assertEqual(len(d), len(self.py_dict) + 1) 220 | self.assertEqual(d["four"], "FOUR") 221 | 222 | def test_setdefault3(self): 223 | d = self.make_dictionary(self.py_dict) 224 | 225 | self.assertTrue("four" not in d) 226 | self.assertEqual(d.setdefault("four"), None) 227 | self.assertEqual(len(d), len(self.py_dict)) 228 | with self.assertRaises(KeyError): 229 | d["four"] 230 | 231 | def test_update1(self): 232 | d = self.make_dictionary(self.py_dict) 233 | 234 | self.assertEqual(d, self.py_dict) 235 | d.update({"one": "two", "three": "four", "four": "FIVE"}) 236 | self.assertNotEqual(d, self.py_dict) 237 | self.assertEqual(d["one"], "two") 238 | self.assertEqual(d["two"], "TWO") 239 | self.assertEqual(d["three"], "four") 240 | self.assertEqual(d["four"], "FIVE") 241 | self.assertEqual(len(d), len(self.py_dict) + 1) 242 | 243 | def test_update2(self): 244 | d = self.make_dictionary(self.py_dict) 245 | 246 | self.assertEqual(d, self.py_dict) 247 | d.update([("one", "two"), ("three", "four"), ("four", "FIVE")]) 248 | self.assertNotEqual(d, self.py_dict) 249 | self.assertEqual(d["one"], "two") 250 | self.assertEqual(d["two"], "TWO") 251 | self.assertEqual(d["three"], "four") 252 | self.assertEqual(len(d), len(self.py_dict) + 1) 253 | 254 | def test_update3(self): 255 | d = self.make_dictionary(self.py_dict) 256 | 257 | self.assertEqual(d, self.py_dict) 258 | d.update(one="two", three="four", four="FIVE") 259 | self.assertNotEqual(d, self.py_dict) 260 | self.assertEqual(d["one"], "two") 261 | self.assertEqual(d["two"], "TWO") 262 | self.assertEqual(d["three"], "four") 263 | self.assertEqual(d["four"], "FIVE") 264 | self.assertEqual(len(d), len(self.py_dict) + 1) 265 | 266 | def test_update4(self): 267 | d = self.make_dictionary(self.py_dict) 268 | 269 | self.assertEqual(d, self.py_dict) 270 | d.update({"one": "two"}, three="four", four="FIVE") 271 | self.assertNotEqual(d, self.py_dict) 272 | self.assertEqual(d["one"], "two") 273 | self.assertEqual(d["two"], "TWO") 274 | self.assertEqual(d["three"], "four") 275 | self.assertEqual(d["four"], "FIVE") 276 | self.assertEqual(len(d), len(self.py_dict) + 1) 277 | 278 | 279 | class PythonObjectTest(unittest.TestCase): 280 | def test_primitive_dict_attribute(self): 281 | class PrimitiveDictAttrContainer(NSObject): 282 | @objc_method 283 | def init(self): 284 | self.data = {1: 2, 2: 4, 3: 6} 285 | return self 286 | 287 | @objc_method 288 | def initWithDict_(self, data): 289 | self.data = data 290 | return self 291 | 292 | obj1 = PrimitiveDictAttrContainer.alloc().init() 293 | self.assertEqual(obj1.data, {1: 2, 2: 4, 3: 6}) 294 | self.assertIsInstance(obj1.data, dict) 295 | 296 | # If it's set through a method call, it becomes an objc instance 297 | obj2 = PrimitiveDictAttrContainer.alloc().initWithDict_({4: 8, 5: 10, 6: 12}) 298 | self.assertEqual(obj2.data, {4: 8, 5: 10, 6: 12}) 299 | self.assertIsInstance(obj2.data, ObjCDictInstance) 300 | 301 | # If it's set by direct attribute access, it becomes a Python object. 302 | obj2.data = {7: 14, 8: 16, 9: 18} 303 | self.assertEqual(obj2.data, {7: 14, 8: 16, 9: 18}) 304 | self.assertIsInstance(obj2.data, dict) 305 | 306 | def test_primitive_dict_property(self): 307 | class PrimitiveDictContainer(NSObject): 308 | data = objc_property() 309 | 310 | @objc_method 311 | def init(self): 312 | self.data = {1: 2, 2: 4, 3: 6} 313 | return self 314 | 315 | @objc_method 316 | def initWithDict_(self, data): 317 | self.data = data 318 | return self 319 | 320 | obj1 = PrimitiveDictContainer.alloc().init() 321 | self.assertEqual(obj1.data, {1: 2, 2: 4, 3: 6}) 322 | self.assertIsInstance(obj1.data, ObjCDictInstance) 323 | 324 | obj2 = PrimitiveDictContainer.alloc().initWithDict_({4: 8, 5: 10, 6: 12}) 325 | self.assertEqual(obj2.data, {4: 8, 5: 10, 6: 12}) 326 | self.assertIsInstance(obj2.data, ObjCDictInstance) 327 | 328 | obj2.data = {7: 14, 8: 16, 9: 18} 329 | self.assertEqual(obj2.data, {7: 14, 8: 16, 9: 18}) 330 | self.assertIsInstance(obj2.data, ObjCDictInstance) 331 | 332 | def test_object_dict_attribute(self): 333 | class ObjectDictAttrContainer(NSObject): 334 | @objc_method 335 | def init(self): 336 | self.data = {"x": "x1", "y": "y2", "z": "z3"} 337 | return self 338 | 339 | @objc_method 340 | def initWithDict_(self, data): 341 | self.data = data 342 | return self 343 | 344 | obj1 = ObjectDictAttrContainer.alloc().init() 345 | self.assertEqual(obj1.data, {"x": "x1", "y": "y2", "z": "z3"}) 346 | self.assertIsInstance(obj1.data, dict) 347 | 348 | # If it's set through a method call, it becomes an objc instance 349 | obj2 = ObjectDictAttrContainer.alloc().initWithDict_( 350 | {"a": "a4", "b": "b5", "c": "c6"} 351 | ) 352 | self.assertEqual(obj2.data, {"a": "a4", "b": "b5", "c": "c6"}) 353 | self.assertIsInstance(obj2.data, ObjCDictInstance) 354 | 355 | # If it's set by direct attribute access, it becomes a Python object. 356 | obj2.data = {"i": "i7", "j": "j8", "k": "k9"} 357 | self.assertEqual(obj2.data, {"i": "i7", "j": "j8", "k": "k9"}) 358 | self.assertIsInstance(obj2.data, dict) 359 | 360 | def test_object_dict_property(self): 361 | class ObjectDictContainer(NSObject): 362 | data = objc_property() 363 | 364 | @objc_method 365 | def init(self): 366 | self.data = {"x": "x1", "y": "y2", "z": "z3"} 367 | return self 368 | 369 | @objc_method 370 | def initWithDict_(self, data): 371 | self.data = data 372 | return self 373 | 374 | obj1 = ObjectDictContainer.alloc().init() 375 | self.assertEqual(obj1.data, {"x": "x1", "y": "y2", "z": "z3"}) 376 | self.assertIsInstance(obj1.data, ObjCDictInstance) 377 | 378 | obj2 = ObjectDictContainer.alloc().initWithDict_( 379 | {"a": "a4", "b": "b5", "c": "c6"} 380 | ) 381 | self.assertEqual(obj2.data, {"a": "a4", "b": "b5", "c": "c6"}) 382 | self.assertIsInstance(obj2.data, ObjCDictInstance) 383 | 384 | obj2.data = {"i": "i7", "j": "j8", "k": "k9"} 385 | self.assertEqual(obj2.data, {"i": "i7", "j": "j8", "k": "k9"}) 386 | self.assertIsInstance(obj2.data, ObjCDictInstance) 387 | 388 | def test_multitype_dict_property(self): 389 | class MultitypeDictContainer(NSObject): 390 | data = objc_property() 391 | 392 | # All types can be stored in a dict. 393 | obj = MultitypeDictContainer.alloc().init() 394 | obj.data = {4: 16, True: False, "Hello": "Goodbye"} 395 | self.assertEqual(obj.data, {4: 16, True: False, "Hello": "Goodbye"}) 396 | self.assertIsInstance(obj.data, ObjCDictInstance) 397 | -------------------------------------------------------------------------------- /tests/test_async.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import sys 5 | import time 6 | import unittest 7 | 8 | from rubicon.objc.eventloop import RubiconEventLoop 9 | 10 | 11 | # Some coroutines with known behavior for testing purposes. 12 | async def do_stuff(results, x): 13 | for i in range(0, x): 14 | results.append(i) 15 | await asyncio.sleep(0.1) 16 | 17 | 18 | async def stop_loop(loop, delay): 19 | await asyncio.sleep(delay) 20 | loop.stop() 21 | 22 | 23 | class AsyncRunTests(unittest.TestCase): 24 | def setUp(self): 25 | self.loop = RubiconEventLoop() 26 | 27 | def tearDown(self): 28 | if sys.version_info < (3, 14): 29 | asyncio.set_event_loop_policy(None) 30 | self.loop.close() 31 | 32 | def test_run_until_complete(self): 33 | results = [] 34 | start = time.time() 35 | self.loop.run_until_complete(do_stuff(results, 5)) 36 | end = time.time() 37 | 38 | # The co-routine should have accumulated 5 values, 39 | # and taken at least 0.1*5 == 0.5 seconds to run. 40 | self.assertEqual(results, [0, 1, 2, 3, 4]) 41 | self.assertGreaterEqual(end - start, 0.5) 42 | 43 | def test_run_forever(self): 44 | results1 = [] 45 | results2 = [] 46 | start = time.time() 47 | self.loop.create_task(do_stuff(results1, 3)) 48 | self.loop.create_task(do_stuff(results2, 4)) 49 | self.loop.create_task(stop_loop(self.loop, 0.6)) 50 | self.loop.run_forever() 51 | end = time.time() 52 | 53 | # The co-routine should have accumulated two 54 | # independent sets of values (of different lengths). 55 | # The run duration is controlled by the stop task. 56 | self.assertEqual(results1, [0, 1, 2]) 57 | self.assertEqual(results2, [0, 1, 2, 3]) 58 | self.assertGreaterEqual(end - start, 0.6) 59 | 60 | 61 | class AsyncCallTests(unittest.TestCase): 62 | def setUp(self): 63 | self.loop = RubiconEventLoop() 64 | 65 | def tearDown(self): 66 | if sys.version_info < (3, 14): 67 | asyncio.set_event_loop_policy(None) 68 | self.loop.close() 69 | 70 | def test_call_soon(self): 71 | start = time.time() 72 | self.loop.call_soon(self.loop.stop) 73 | self.loop.run_forever() 74 | end = time.time() 75 | 76 | # The co-routine will be queued immediately, 77 | # and stop the loop immediately. 78 | self.assertLessEqual(end - start, 0.05) 79 | 80 | def test_call_later(self): 81 | start = time.time() 82 | self.loop.call_later(0.2, self.loop.stop) 83 | self.loop.run_forever() 84 | end = time.time() 85 | 86 | # The co-routine will be queued after 0.2 seconds. 87 | self.assertGreaterEqual(end - start, 0.2) 88 | self.assertLess(end - start, 0.4) 89 | 90 | def test_call_at(self): 91 | start = time.time() 92 | when = self.loop.time() + 0.2 93 | self.loop.call_at(when, self.loop.stop) 94 | self.loop.run_forever() 95 | end = time.time() 96 | 97 | # The co-routine will be queued after 0.2 seconds. 98 | self.assertGreaterEqual(end - start, 0.2) 99 | self.assertLess(end - start, 0.4) 100 | 101 | 102 | class AsyncReaderWriterTests(unittest.TestCase): 103 | def setUp(self): 104 | self.loop = asyncio.new_event_loop() 105 | self.server = None 106 | 107 | def tearDown(self): 108 | # Close the server 109 | if self.server: 110 | self.server.close() 111 | self.loop.run_until_complete(self.server.wait_closed()) 112 | self.loop.close() 113 | 114 | def test_tcp_echo(self): 115 | """A simple TCP Echo client/server works as expected.""" 116 | # This tests that you can: 117 | # * create a TCP server 118 | # * create a TCP client 119 | # * write to a socket 120 | # * read from a socket 121 | # * be notified of updates on a socket when data arrives. 122 | 123 | # Requires that port 3742 is available for use. 124 | 125 | server_messages = [] 126 | 127 | async def echo_server(reader, writer): 128 | data = await reader.read(100) 129 | message = data.decode() 130 | server_messages.append(message) 131 | 132 | writer.write(data) 133 | await writer.drain() 134 | 135 | writer.close() 136 | 137 | self.server = self.loop.run_until_complete( 138 | asyncio.start_server(echo_server, "127.0.0.1", 3742) 139 | ) 140 | 141 | client_messages = [] 142 | 143 | async def echo_client(message): 144 | reader, writer = await asyncio.open_connection("127.0.0.1", 3742) 145 | 146 | writer.write(message.encode()) 147 | 148 | data = await reader.read(100) 149 | client_messages.append(data.decode()) 150 | 151 | writer.close() 152 | 153 | self.loop.run_until_complete(echo_client("Hello, World!")) 154 | self.loop.run_until_complete(echo_client("Goodbye, World!")) 155 | 156 | self.assertEqual(server_messages, ["Hello, World!", "Goodbye, World!"]) 157 | self.assertEqual(client_messages, ["Hello, World!", "Goodbye, World!"]) 158 | 159 | 160 | class AsyncSubprocessTests(unittest.TestCase): 161 | def setUp(self): 162 | self.loop = RubiconEventLoop() 163 | 164 | def tearDown(self): 165 | if sys.version_info < (3, 14): 166 | asyncio.set_event_loop_policy(None) 167 | self.loop.close() 168 | 169 | def test_subprocess(self): 170 | async def list_dir(): 171 | proc = await asyncio.create_subprocess_shell( 172 | "ls", 173 | stdout=asyncio.subprocess.PIPE, 174 | ) 175 | 176 | entries = set() 177 | line = await proc.stdout.readline() 178 | while line: 179 | entries.add(line.decode("utf-8").strip()) 180 | line = await proc.stdout.readline() 181 | 182 | # Cleanup - close the transport. 183 | proc._transport.close() 184 | return entries 185 | 186 | task = asyncio.ensure_future(list_dir(), loop=self.loop) 187 | self.loop.run_until_complete(task) 188 | 189 | # Check for some files that should exist. 190 | # Everything in the sample set, less everything from the result, 191 | # should be an empty set. 192 | self.assertEqual( 193 | {"README.rst"} - task.result(), 194 | set(), 195 | ) 196 | -------------------------------------------------------------------------------- /tests/test_blocks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | from ctypes import Structure, c_float, c_int, c_void_p 5 | 6 | from rubicon.objc import NSObject, ObjCBlock, ObjCClass, objc_method 7 | from rubicon.objc.api import Block 8 | from rubicon.objc.runtime import objc_block 9 | 10 | 11 | class BlockTests(unittest.TestCase): 12 | def test_block_property_ctypes(self): 13 | BlockPropertyExample = ObjCClass("BlockPropertyExample") 14 | instance = BlockPropertyExample.alloc().init() 15 | result = ObjCBlock(instance.blockProperty, c_int, c_int, c_int)(1, 2) 16 | self.assertEqual(result, 3) 17 | 18 | def test_block_property_pytypes(self): 19 | BlockPropertyExample = ObjCClass("BlockPropertyExample") 20 | instance = BlockPropertyExample.alloc().init() 21 | result = ObjCBlock(instance.blockProperty, int, int, int)(1, 2) 22 | self.assertEqual(result, 3) 23 | 24 | def test_block_delegate_method_manual_ctypes(self): 25 | class DelegateManualC(NSObject): 26 | @objc_method 27 | def exampleMethod_(self, block): 28 | ObjCBlock(block, c_void_p, c_int, c_int)(2, 3) 29 | 30 | BlockObjectExample = ObjCClass("BlockObjectExample") 31 | delegate = DelegateManualC.alloc().init() 32 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 33 | result = instance.blockExample() 34 | self.assertEqual(result, 5) 35 | 36 | def test_block_delegate_method_manual_pytypes(self): 37 | class DelegateManualPY(NSObject): 38 | @objc_method 39 | def exampleMethod_(self, block): 40 | ObjCBlock(block, None, int, int)(2, 3) 41 | 42 | BlockObjectExample = ObjCClass("BlockObjectExample") 43 | delegate = DelegateManualPY.alloc().init() 44 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 45 | result = instance.blockExample() 46 | self.assertEqual(result, 5) 47 | 48 | def test_block_delegate_auto(self): 49 | class DelegateAuto(NSObject): 50 | @objc_method 51 | def exampleMethod_(self, block: objc_block): 52 | block(4, 5) 53 | 54 | BlockObjectExample = ObjCClass("BlockObjectExample") 55 | delegate = DelegateAuto.alloc().init() 56 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 57 | result = instance.blockExample() 58 | self.assertEqual(result, 9) 59 | 60 | def test_block_delegate_manual_struct(self): 61 | class BlockStruct(Structure): 62 | _fields_ = [ 63 | ("a", c_int), 64 | ("b", c_int), 65 | ] 66 | 67 | class DelegateManualStruct(NSObject): 68 | @objc_method 69 | def structBlockMethod_(self, block: objc_block) -> int: 70 | return ObjCBlock(block, int, BlockStruct)(BlockStruct(42, 43)) 71 | 72 | BlockObjectExample = ObjCClass("BlockObjectExample") 73 | delegate = DelegateManualStruct.alloc().init() 74 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 75 | result = instance.structBlockExample() 76 | self.assertEqual(result, 85) 77 | 78 | def test_block_delegate_auto_struct(self): 79 | class BlockStruct(Structure): 80 | _fields_ = [ 81 | ("a", c_int), 82 | ("b", c_int), 83 | ] 84 | 85 | class DelegateAutoStruct(NSObject): 86 | @objc_method 87 | def structBlockMethod_(self, block: objc_block) -> int: 88 | return block(BlockStruct(42, 43)) 89 | 90 | BlockObjectExample = ObjCClass("BlockObjectExample") 91 | delegate = DelegateAutoStruct.alloc().init() 92 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 93 | result = instance.structBlockExample() 94 | self.assertEqual(result, 85) 95 | 96 | def test_block_delegate_auto_struct_mismatch(self): 97 | class BadBlockStruct(Structure): 98 | _fields_ = [ 99 | ("a", c_float), 100 | ("b", c_int), 101 | ] 102 | 103 | class BadDelegateAutoStruct(NSObject): 104 | @objc_method 105 | def structBlockMethod_(self, block: objc_block) -> int: 106 | try: 107 | # block accepts an anonymous structure with 2 int arguments 108 | # Passing in BadBlockStruct should raise a type error because 109 | # the structure's shape doesn't match. 110 | return block(BadBlockStruct(2.71828, 43)) 111 | except TypeError: 112 | # Ideally, this would be raised as an ObjC error; 113 | # however, at least for now, that doesn't happen. 114 | return -99 115 | 116 | BlockObjectExample = ObjCClass("BlockObjectExample") 117 | delegate = BadDelegateAutoStruct.alloc().init() 118 | instance = BlockObjectExample.alloc().initWithDelegate_(delegate) 119 | result = instance.structBlockExample() 120 | self.assertEqual(result, -99) 121 | 122 | def test_block_receiver(self): 123 | BlockReceiverExample = ObjCClass("BlockReceiverExample") 124 | instance = BlockReceiverExample.alloc().init() 125 | 126 | values = [] 127 | 128 | def block(a: int, b: int) -> int: 129 | values.append(a + b) 130 | return 42 131 | 132 | result = instance.receiverMethod_(block) 133 | 134 | self.assertEqual(values, [27]) 135 | self.assertEqual(result, 42) 136 | 137 | def test_block_receiver_no_return_annotation(self): 138 | BlockReceiverExample = ObjCClass("BlockReceiverExample") 139 | instance = BlockReceiverExample.alloc().init() 140 | 141 | def block(a: int, b: int): 142 | return a + b 143 | 144 | with self.assertRaises(ValueError): 145 | instance.receiverMethod_(block) 146 | 147 | def test_block_receiver_missing_arg_annotation(self): 148 | BlockReceiverExample = ObjCClass("BlockReceiverExample") 149 | instance = BlockReceiverExample.alloc().init() 150 | 151 | def block(a: int, b) -> int: 152 | return a + b 153 | 154 | with self.assertRaises(ValueError): 155 | instance.receiverMethod_(block) 156 | 157 | def test_block_receiver_lambda(self): 158 | BlockReceiverExample = ObjCClass("BlockReceiverExample") 159 | instance = BlockReceiverExample.alloc().init() 160 | with self.assertRaises(ValueError): 161 | instance.receiverMethod_(lambda a, b: a + b) 162 | 163 | def test_block_receiver_explicit(self): 164 | BlockReceiverExample = ObjCClass("BlockReceiverExample") 165 | instance = BlockReceiverExample.alloc().init() 166 | 167 | values = [] 168 | 169 | block = Block(lambda a, b: values.append(a + b), None, int, int) 170 | instance.receiverMethod_(block) 171 | 172 | self.assertEqual(values, [27]) 173 | 174 | def test_block_round_trip(self): 175 | BlockRoundTrip = ObjCClass("BlockRoundTrip") 176 | instance = BlockRoundTrip.alloc().init() 177 | 178 | def block(a: int, b: int) -> int: 179 | return a + b 180 | 181 | returned_block = instance.roundTrip_(block) 182 | self.assertEqual(returned_block(8, 9), 17) 183 | 184 | def test_block_round_trip_no_arguments(self): 185 | """A block that takes no arguments can be created with both ways of 186 | specifying types.""" 187 | 188 | BlockRoundTrip = ObjCClass("BlockRoundTrip") 189 | instance = BlockRoundTrip.alloc().init() 190 | 191 | @Block 192 | def block_1() -> c_int: 193 | return 42 194 | 195 | returned_block_1 = instance.roundTripNoArgs(block_1) 196 | self.assertEqual(returned_block_1(), 42) 197 | 198 | block_2 = Block(lambda: 42, c_int) 199 | returned_block_2 = instance.roundTripNoArgs(block_2) 200 | self.assertEqual(returned_block_2(), 42) 201 | 202 | def test_block_bound_method(self): 203 | """A bound method with type annotations can be wrapped in a block.""" 204 | 205 | class Handler: 206 | def no_args(self) -> c_int: 207 | return 42 208 | 209 | def two_args(self, x: c_int, y: c_int) -> c_int: 210 | return x + y 211 | 212 | handler = Handler() 213 | no_args_block = Block(handler.no_args) 214 | two_args_block = Block(handler.two_args) 215 | 216 | BlockRoundTrip = ObjCClass("BlockRoundTrip") 217 | instance = BlockRoundTrip.alloc().init() 218 | 219 | returned_no_args_block = instance.roundTripNoArgs(no_args_block) 220 | self.assertEqual(returned_no_args_block(), 42) 221 | 222 | returned_two_args_block = instance.roundTrip(two_args_block) 223 | self.assertEqual(returned_two_args_block(12, 34), 46) 224 | -------------------------------------------------------------------------------- /tests/test_ctypes_patch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ctypes 4 | import unittest 5 | 6 | from rubicon.objc import ctypes_patch 7 | 8 | 9 | class CtypesPatchTest(unittest.TestCase): 10 | def test_patch_structure(self): 11 | """A custom structure can be patched successfully.""" 12 | 13 | class TestStruct(ctypes.Structure): 14 | _fields_ = [ 15 | ("spam", ctypes.c_int), 16 | ("ham", ctypes.c_double), 17 | ] 18 | 19 | functype = ctypes.CFUNCTYPE(TestStruct) 20 | 21 | # Before patching, the structure cannot be returned from a callback. 22 | with self.assertRaises(TypeError): 23 | 24 | @functype 25 | def get_struct_fail(): 26 | return TestStruct(123, 123) 27 | 28 | ctypes_patch.make_callback_returnable(TestStruct) 29 | 30 | # After patching, the structure can be returned from a callback. 31 | @functype 32 | def get_struct(): 33 | return TestStruct(123, 123) 34 | 35 | # After being returned from the callback, the structure's data is intact. 36 | struct = get_struct() 37 | self.assertEqual(struct.spam, 123) 38 | self.assertEqual(struct.ham, 123) 39 | 40 | def test_patch_pointer(self): 41 | """A custom pointer type can be patched successfully.""" 42 | 43 | class TestStruct(ctypes.Structure): 44 | _fields_ = [ 45 | ("spam", ctypes.c_int), 46 | ("ham", ctypes.c_double), 47 | ] 48 | 49 | pointertype = ctypes.POINTER(TestStruct) 50 | functype = ctypes.CFUNCTYPE(pointertype) 51 | 52 | original_struct = TestStruct(123, 123) 53 | 54 | # Before patching, the pointer cannot be returned from a callback. 55 | with self.assertRaises(TypeError): 56 | 57 | @functype 58 | def get_struct_fail(): 59 | return pointertype(original_struct) 60 | 61 | ctypes_patch.make_callback_returnable(pointertype) 62 | 63 | # After patching, the structure can be returned from a callback. 64 | @functype 65 | def get_struct(): 66 | return pointertype(original_struct) 67 | 68 | # After being returned from the callback, the pointer's data is intact. 69 | struct_pointer = get_struct() 70 | self.assertEqual( 71 | ctypes.addressof(struct_pointer.contents), ctypes.addressof(original_struct) 72 | ) 73 | self.assertEqual(struct_pointer.contents.spam, 123) 74 | self.assertEqual(struct_pointer.contents.ham, 123) 75 | 76 | def test_no_patch_primitives(self): 77 | """Primitive types cannot be patched.""" 78 | 79 | for tp in (ctypes.c_int, ctypes.c_double, ctypes.c_char_p, ctypes.c_void_p): 80 | with self.subTest(tp): 81 | with self.assertRaises(ValueError): 82 | ctypes_patch.make_callback_returnable(tp) 83 | 84 | def test_patch_idempotent(self): 85 | """Patching a type multiple times is equivalent to patching once.""" 86 | 87 | class TestStruct(ctypes.Structure): 88 | _fields_ = [ 89 | ("spam", ctypes.c_int), 90 | ("ham", ctypes.c_double), 91 | ] 92 | 93 | functype = ctypes.CFUNCTYPE(TestStruct) 94 | 95 | for _ in range(5): 96 | ctypes_patch.make_callback_returnable(TestStruct) 97 | 98 | # After patching, the structure can be returned from a callback. 99 | @functype 100 | def get_struct(): 101 | return TestStruct(123, 123) 102 | 103 | # After being returned from the callback, the structure's data is intact. 104 | struct = get_struct() 105 | self.assertEqual(struct.spam, 123) 106 | self.assertEqual(struct.ham, 123) 107 | 108 | def test_patched_type_returned_often(self): 109 | """Returning a patched type very often works properly without crashing 110 | anything. 111 | 112 | This checks that bpo-36880 is either fixed or worked around. 113 | """ 114 | 115 | class TestStruct(ctypes.Structure): 116 | _fields_ = [ 117 | ("spam", ctypes.c_int), 118 | ("ham", ctypes.c_double), 119 | ] 120 | 121 | functype = ctypes.CFUNCTYPE(TestStruct) 122 | 123 | ctypes_patch.make_callback_returnable(TestStruct) 124 | 125 | # After patching, the structure can be returned from a callback. 126 | @functype 127 | def get_struct(): 128 | return TestStruct(123, 123) 129 | 130 | for _ in range(10000): 131 | # After being returned from the callback, the structure's data is intact. 132 | struct = get_struct() 133 | self.assertEqual(struct.spam, 123) 134 | self.assertEqual(struct.ham, 123) 135 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # flake8 doesn't believe in pyproject.toml, so we keep the configuration here. 2 | [flake8] 3 | # https://flake8.readthedocs.org/en/latest/ 4 | exclude=\ 5 | venv*/*,\ 6 | local/*,\ 7 | .tox/* 8 | max-line-length = 119 9 | extend-ignore = 10 | # whitespace before : 11 | # See https://github.com/PyCQA/pycodestyle/issues/373 12 | E203, 13 | 14 | [tox] 15 | envlist = towncrier-check,pre-commit,docs{,-lint,-all},py{39,310,311,312,313,314} 16 | skip_missing_interpreters = true 17 | 18 | [testenv:pre-commit] 19 | package = wheel 20 | wheel_build_env = .pkg 21 | extras = dev 22 | commands = pre-commit run --all-files --show-diff-on-failure --color=always 23 | 24 | [testenv:py{,39,310,311,312,313,314}] 25 | package = wheel 26 | wheel_build_env = .pkg 27 | depends = pre-commit 28 | extras = dev 29 | allowlist_externals = 30 | make 31 | commands = 32 | make -C tests{/}objc 33 | python -m pytest {posargs:-vv --color yes} 34 | 35 | [testenv:towncrier{,-check}] 36 | deps = 37 | towncrier==24.8.0 38 | commands = 39 | check : python -m towncrier.check --compare-with origin/main 40 | !check : python -m towncrier {posargs} 41 | 42 | [docs] 43 | docs_dir = {tox_root}{/}docs 44 | build_dir = {[docs]docs_dir}{/}_build 45 | sphinx_args = --show-traceback --fail-on-warning --keep-going --jobs auto 46 | 47 | [testenv:docs{,-lint,-all,-live,-live-src}] 48 | # Docs are always built on Python 3.12. See also the RTD config and contribution docs. 49 | base_python = py312 50 | # give sphinx-autobuild time to shutdown http server 51 | suicide_timeout = 1 52 | package = wheel 53 | wheel_build_env = .pkg 54 | extras = docs 55 | passenv = 56 | # On macOS M1, you need to manually set the location of the PyEnchant 57 | # library: 58 | # export PYENCHANT_LIBRARY_PATH=/opt/homebrew/lib/libenchant-2.2.dylib 59 | PYENCHANT_LIBRARY_PATH 60 | commands = 61 | !lint-!all-!live : python -m sphinx {[docs]sphinx_args} {posargs} --builder html {[docs]docs_dir} {[docs]build_dir}{/}html 62 | lint : python -m sphinx {[docs]sphinx_args} {posargs} --builder spelling {[docs]docs_dir} {[docs]build_dir}{/}spell 63 | lint : python -m sphinx {[docs]sphinx_args} {posargs} --builder linkcheck {[docs]docs_dir} {[docs]build_dir}{/}links 64 | all : python -m sphinx {[docs]sphinx_args} {posargs} --verbose --write-all --fresh-env --builder html {[docs]docs_dir} {[docs]build_dir}{/}html 65 | live-!src : sphinx-autobuild {[docs]sphinx_args} {posargs} --builder html {[docs]docs_dir} {[docs]build_dir}{/}live 66 | live-src : sphinx-autobuild {[docs]sphinx_args} {posargs} --write-all --fresh-env --watch {tox_root}{/}src{/}rubicon{/}objc --builder html {[docs]docs_dir} {[docs]build_dir}{/}live 67 | --------------------------------------------------------------------------------