Skip to content

More updates to USAGE and README #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,6 @@ Each support package contains:

* ``VERSIONS``, a text file describing the specific versions of code used to build the
support package;
* ``platform-site``, a folder that contains site customization scripts that can be used
to make your local Python install look like it is an on-device install for each of the
underlying target architectures supported by the platform. This is needed because when
you run ``pip`` you'll be on a macOS machine with a specific architecture; if ``pip``
tries to install a binary package, it will install a macOS binary wheel (which won't
work on iOS/tvOS/watchOS). However, if you add the ``platform-site`` folder to your
``PYTHONPATH`` when invoking pip, the site customization will make your Python install
return ``platform`` and ``sysconfig`` responses consistent with on-device behavior,
which will cause ``pip`` to install platform-appropriate packages.
* ``Python.xcframework``, a multi-architecture build of the Python runtime library

On iOS/tvOS/watchOS, the ``Python.xcframework`` contains a
Expand All @@ -105,6 +96,32 @@ needed to build packages. This is required because Xcode uses the ``xcrun``
alias to dynamically generate the name of binaries, but a lot of C tooling
expects that ``CC`` will not contain spaces.

Each slice of an iOS/tvOS/watchOS XCframework also contains a
``platform-config`` folder with a subfolder for each supported architecture in
that slice. These subfolders can be used to make a macOS Python environment
behave as if it were on an iOS/tvOS/watchOS device. This works in one of two
ways:

1. **A sitecustomize.py script**. If the ``platform-config`` subfolder is on
your ``PYTHONPATH`` when a Python interpreter is started, a site
customization will be applied that patches methods in ``sys``, ``sysconfig``
and ``platform`` that are used to identify the system.

2. **A make_cross_venv.py script**. If you call ``make_cross_venv.py``,
providing the location of a virtual environment, the script will add some
files to the ``site-packages`` folder of that environment that will
automatically apply the same set of patches as the ``sitecustomize.py``
script whenever the environment is activated, without any need to modify
``PYTHONPATH``. If you use ``build`` to create an isolated PEP 517
environment to build a wheel, these patches will also be applied to the
isolated build environment that is created.

iOS distributions also contain a copy of the iOS ``testbed`` project - an Xcode
project that can be used to run test suites of Python code. See the `CPython
documentation on testing packages
<https://docs.python.org/3/using/ios.html#testing-a-python-package>`__ for
details on how to use this testbed.

For a detailed instructions on using the support package in your own project,
see the `usage guide <./USAGE.md>`__

Expand Down
113 changes: 79 additions & 34 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,60 +25,105 @@ guides:
For tvOS and watchOS, you should be able to broadly follow the instructions in
the iOS guide.

### Using Objective C

Once you've added the Python XCframework to your project, you'll need to
initialize the Python runtime in your Objective C code (This is step 10 of the
iOS guide linked above). This initialization should generally be done as early
as possible in the application's lifecycle, but definitely needs to be done
before you invoke Python code.

As a *bare minimum*, you can do the following:

1. Import the Python C API headers:
```objc
#include <Python/Python.h>
```

2. Initialize the Python interpreter:
```objc
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
NSString *pythonHome = [NSString stringWithFormat:@"%@/python", resourcePath, nil];
NSString *pythonPath = [NSString stringWithFormat:@"%@/lib/python3.13", python_home, nil];
NSString *libDynloadPath = [NSString stringWithFormat:@"%@/lib/python3.13/lib-dynload", python_home, nil];
NSString *appPath = [NSString stringWithFormat:@"%@/app", resourcePath, nil];

setenv("PYTHONHOME", pythonHome, 1);
setenv("PYTHONPATH", [NSString stringWithFormat:@"%@:%@:%@", pythonpath, libDynloadPath, appPath, nil]);

Py_Initialize();

// we now have a Python interpreter ready to be used
```
References to a specific Python version should reflect the version of
Python you are using.

Again - this is the *bare minimum* initialization. In practice, you will likely
need to configure other aspects of the Python interpreter using the
`PyPreConfig` and `PyConfig` mechanisms. Consult the [Python documentation on
interpreter configuration](https://docs.python.org/3/c-api/init_config.html) for
more details on the configuration options that are available. You may find the
[bootstrap mainline code used by
Briefcase](https://github.com/beeware/briefcase-iOS-Xcode-template/blob/main/%7B%7B%20cookiecutter.format%20%7D%7D/%7B%7B%20cookiecutter.class_name%20%7D%7D/main.m)
a helpful point of comparison.

### Using Swift

If you want to use Swift instead of Objective C, the bare minimum initialization
code will look something like this:

1. Import the Python framework:
```swift
import Python
```

2. Initialize the Python interpreter:
```swift
guard let pythonHome = Bundle.main.path(forResource: "python", ofType: nil) else { return }
guard let pythonPath = Bundle.main.path(forResource: "python/lib/python3.13", ofType: nil) else { return }
guard let libDynloadPath = Bundle.main.path(forResource: "python/lib/python3.13/lib-dynload", ofType: nil) else { return }
let appPath = Bundle.main.path(forResource: "app", ofType: nil)

setenv("PYTHONHOME", pythonHome, 1)
setenv("PYTHONPATH", [pythonPath, libDynloadPath, appPath].compactMap { $0 }.joined(separator: ":"), 1)
Py_Initialize()
// we now have a Python interpreter ready to be used
```

Again, references to a specific Python version should reflect the version of
Python you are using; and you will likely need to use `PyPreConfig` and
`PreConfig` APIs.

## Accessing the Python runtime

There are 2 ways to access the Python runtime in your project code.

### Embedded C API

You can use the [Python Embedded C
API](https://docs.python.org/3/extending/embedding.html) to instantiate a Python
interpreter. This is the approach taken by Briefcase; you may find the bootstrap
mainline code generated by Briefcase a helpful guide to what is needed to start
an interpreter and run Python code.
API](https://docs.python.org/3/extending/embedding.html) to invoke Python code
and interact with Python objects. This is a raw C API that is accesible to both
Objective C and Swift.

### PythonKit

An alternate approach is to use
If you're using Swift, an alternate approach is to use
[PythonKit](https://github.com/pvieito/PythonKit). PythonKit is a package that
provides a Swift API to running Python code.

To use PythonKit in your project, add the Python Apple Support package to your
project as described above; then:

1. Add PythonKit to your project using the Swift Package manager. See the
PythonKit documentation for details.
project and instantiate a Python interpreter as described above; then add
PythonKit to your project using the Swift Package manager (see the [PythonKit
documentation](https://github.com/pvieito/PythonKit) for details).

2. In your Swift code, initialize the Python runtime. This should generally be
done as early as possible in the application's lifecycle, but definitely
needs to be done before you invoke Python code. References to a specific
Python version should reflect the version of Python you are using:
Once you've done this, you can import PythonKit:
```swift
import Python

guard let pythonHome = Bundle.main.path(forResource: "python", ofType: nil) else { return }
guard let pythonPath = Bundle.main.path(forResource: "python/lib/python3.13", ofType: nil) else { return }
guard let libDynloadPath = Bundle.main.path(forResource: "python/lib/python3.13/lib-dynload", ofType: nil) else { return }
let appPath = Bundle.main.path(forResource: "app", ofType: nil)

setenv("PYTHONHOME", pythonHome, 1)
setenv("PYTHONPATH", [pythonPath, libDynloadPath, appPath].compactMap { $0 }.joined(separator: ":"), 1)
Py_Initialize()
// we now have a Python interpreter ready to be used
import PythonKit
```

3. Invoke Python code in your app. For example:
and use the PythonKit Swift API to interact with Python code:
```swift
import PythonKit

let sys = Python.import("sys")
print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)")
print("Python Encoding: \(sys.getdefaultencoding().upper())")
print("Python Path: \(sys.path)")

_ = Python.import("math") // verifies `lib-dynload` is found and signed successfully
```

To integrate 3rd party python code and dependencies, you will need to make sure
`PYTHONPATH` contains their paths; once this has been done, you can run
`Python.import("<lib name>")`. to import that module from inside swift.
160 changes: 160 additions & 0 deletions patch/Python/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,164 @@ module Python {
umbrella header "Python.h"
export *
link "Python"

exclude header "datetime.h"
exclude header "dynamic_annotations.h"
exclude header "errcode.h"
exclude header "frameobject.h"
exclude header "marshal.h"
exclude header "opcode_ids.h"
exclude header "opcode.h"
exclude header "osdefs.h"
exclude header "py_curses.h"
exclude header "pyconfig-arm64.h"
exclude header "pyconfig-x86_64.h"
exclude header "pyconfig-arm32_64.h"
exclude header "pydtrace.h"
exclude header "pyexpat.h"
exclude header "structmember.h"

exclude header "cpython/frameobject.h"
exclude header "cpython/pthread_stubs.h"
exclude header "cpython/pyatomic_msc.h"
exclude header "cpython/pyatomic_std.h"
exclude header "cpython/pystats.h"

exclude header "internal/mimalloc/mimalloc.h"
exclude header "internal/mimalloc/mimalloc/atomic.h"
exclude header "internal/mimalloc/mimalloc/internal.h"
exclude header "internal/mimalloc/mimalloc/prim.h"
exclude header "internal/mimalloc/mimalloc/track.h"
exclude header "internal/mimalloc/mimalloc/types.h"

exclude header "internal/pycore_abstract.h"
exclude header "internal/pycore_asdl.h"
exclude header "internal/pycore_ast_state.h"
exclude header "internal/pycore_ast.h"
exclude header "internal/pycore_atexit.h"
exclude header "internal/pycore_audit.h"
exclude header "internal/pycore_backoff.h"
exclude header "internal/pycore_bitutils.h"
exclude header "internal/pycore_blocks_output_buffer.h"
exclude header "internal/pycore_brc.h"
exclude header "internal/pycore_bytes_methods.h"
exclude header "internal/pycore_bytesobject.h"
exclude header "internal/pycore_call.h"
exclude header "internal/pycore_capsule.h"
exclude header "internal/pycore_cell.h"
exclude header "internal/pycore_ceval_state.h"
exclude header "internal/pycore_ceval.h"
exclude header "internal/pycore_code.h"
exclude header "internal/pycore_codecs.h"
exclude header "internal/pycore_compile.h"
exclude header "internal/pycore_complexobject.h"
exclude header "internal/pycore_condvar.h"
exclude header "internal/pycore_context.h"
exclude header "internal/pycore_critical_section.h"
exclude header "internal/pycore_crossinterp_data_registry.h"
exclude header "internal/pycore_crossinterp.h"
exclude header "internal/pycore_debug_offsets.h"
exclude header "internal/pycore_descrobject.h"
exclude header "internal/pycore_dict_state.h"
exclude header "internal/pycore_dict.h"
exclude header "internal/pycore_dtoa.h"
exclude header "internal/pycore_emscripten_signal.h"
exclude header "internal/pycore_emscripten_trampoline.h"
exclude header "internal/pycore_exceptions.h"
exclude header "internal/pycore_faulthandler.h"
exclude header "internal/pycore_fileutils_windows.h"
exclude header "internal/pycore_fileutils.h"
exclude header "internal/pycore_floatobject.h"
exclude header "internal/pycore_flowgraph.h"
exclude header "internal/pycore_format.h"
exclude header "internal/pycore_frame.h"
exclude header "internal/pycore_freelist_state.h"
exclude header "internal/pycore_freelist.h"
exclude header "internal/pycore_function.h"
exclude header "internal/pycore_gc.h"
exclude header "internal/pycore_genobject.h"
exclude header "internal/pycore_getopt.h"
exclude header "internal/pycore_gil.h"
exclude header "internal/pycore_global_objects_fini_generated.h"
exclude header "internal/pycore_global_objects.h"
exclude header "internal/pycore_global_strings.h"
exclude header "internal/pycore_hamt.h"
exclude header "internal/pycore_hashtable.h"
exclude header "internal/pycore_identifier.h"
exclude header "internal/pycore_import.h"
exclude header "internal/pycore_importdl.h"
exclude header "internal/pycore_index_pool.h"
exclude header "internal/pycore_initconfig.h"
exclude header "internal/pycore_instruction_sequence.h"
exclude header "internal/pycore_instruments.h"
exclude header "internal/pycore_interp.h"
exclude header "internal/pycore_intrinsics.h"
exclude header "internal/pycore_jit.h"
exclude header "internal/pycore_list.h"
exclude header "internal/pycore_llist.h"
exclude header "internal/pycore_lock.h"
exclude header "internal/pycore_long.h"
exclude header "internal/pycore_magic_number.h"
exclude header "internal/pycore_memoryobject.h"
exclude header "internal/pycore_mimalloc.h"
exclude header "internal/pycore_modsupport.h"
exclude header "internal/pycore_moduleobject.h"
exclude header "internal/pycore_namespace.h"
exclude header "internal/pycore_object_alloc.h"
exclude header "internal/pycore_object_deferred.h"
exclude header "internal/pycore_object_stack.h"
exclude header "internal/pycore_object_state.h"
exclude header "internal/pycore_object.h"
exclude header "internal/pycore_obmalloc_init.h"
exclude header "internal/pycore_obmalloc.h"
exclude header "internal/pycore_opcode_metadata.h"
exclude header "internal/pycore_opcode_utils.h"
exclude header "internal/pycore_optimizer.h"
exclude header "internal/pycore_parking_lot.h"
exclude header "internal/pycore_parser.h"
exclude header "internal/pycore_pathconfig.h"
exclude header "internal/pycore_pyarena.h"
exclude header "internal/pycore_pyatomic_ft_wrappers.h"
exclude header "internal/pycore_pybuffer.h"
exclude header "internal/pycore_pyerrors.h"
exclude header "internal/pycore_pyhash.h"
exclude header "internal/pycore_pylifecycle.h"
exclude header "internal/pycore_pymath.h"
exclude header "internal/pycore_pymem_init.h"
exclude header "internal/pycore_pymem.h"
exclude header "internal/pycore_pystate.h"
exclude header "internal/pycore_pystats.h"
exclude header "internal/pycore_pythonrun.h"
exclude header "internal/pycore_pythread.h"
exclude header "internal/pycore_qsbr.h"
exclude header "internal/pycore_range.h"
exclude header "internal/pycore_runtime_init_generated.h"
exclude header "internal/pycore_runtime_init.h"
exclude header "internal/pycore_runtime.h"
exclude header "internal/pycore_semaphore.h"
exclude header "internal/pycore_setobject.h"
exclude header "internal/pycore_signal.h"
exclude header "internal/pycore_sliceobject.h"
exclude header "internal/pycore_stackref.h"
exclude header "internal/pycore_strhex.h"
exclude header "internal/pycore_structseq.h"
exclude header "internal/pycore_symtable.h"
exclude header "internal/pycore_sysmodule.h"
exclude header "internal/pycore_time.h"
exclude header "internal/pycore_token.h"
exclude header "internal/pycore_traceback.h"
exclude header "internal/pycore_tracemalloc.h"
exclude header "internal/pycore_tstate.h"
exclude header "internal/pycore_tuple.h"
exclude header "internal/pycore_typeobject.h"
exclude header "internal/pycore_typevarobject.h"
exclude header "internal/pycore_ucnhash.h"
exclude header "internal/pycore_unicodeobject_generated.h"
exclude header "internal/pycore_unicodeobject.h"
exclude header "internal/pycore_unionobject.h"
exclude header "internal/pycore_uniqueid.h"
exclude header "internal/pycore_uop_ids.h"
exclude header "internal/pycore_uop_metadata.h"
exclude header "internal/pycore_warnings.h"
exclude header "internal/pycore_weakref.h"
}