diff --git a/.github/workflows/build-wheel.yaml b/.github/workflows/build-wheel.yaml index 5491ae88..fc9bbbec 100644 --- a/.github/workflows/build-wheel.yaml +++ b/.github/workflows/build-wheel.yaml @@ -22,11 +22,11 @@ jobs: matrix: os: [ubuntu-latest, macos-13, windows-latest] arch: [auto] - requires-python: [">=3.8,<3.10", ">=3.10,<3.12"] + # Cannot set like this: ">=3.9,<3.11" here, + # which will result in two packages running within a single runner, + # potentially causing conflicts in the Python environment. + requires-python: [">=3.9,<3.10", ">=3.10,<3.11", ">=3.11,<3.12", ">=3.12,<3.13"] include: - - os: ubuntu-latest - arch: aarch64 - requires-python: ">=3.8,<3.9" - os: ubuntu-latest arch: aarch64 requires-python: ">=3.9,<3.10" @@ -36,18 +36,33 @@ jobs: - os: ubuntu-latest arch: aarch64 requires-python: ">=3.11,<3.12" + - os: ubuntu-latest + arch: aarch64 + requires-python: ">=3.12,<3.13" + - os: macos-13 + arch: universal2 + requires-python: ">=3.9,<3.10" + - os: macos-13 + arch: universal2 + requires-python: ">=3.10,<3.11" - os: macos-13 arch: universal2 - requires-python: ">=3.8,<3.10" + requires-python: ">=3.11,<3.12" - os: macos-13 arch: universal2 - requires-python: ">=3.10,<3.12" + requires-python: ">=3.12,<3.13" + - os: macos-13 + arch: arm64 + requires-python: ">=3.9,<3.10" - os: macos-13 arch: arm64 - requires-python: ">=3.8,<3.10" + requires-python: ">=3.10,<3.11" + - os: macos-13 + arch: arm64 + requires-python: ">=3.11,<3.12" - os: macos-13 arch: arm64 - requires-python: ">=3.10,<3.12" + requires-python: ">=3.12,<3.13" steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index c3e9649b..e9eb8d43 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -79,13 +79,13 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-13", "windows-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] module: ["xoscar"] exclude: - - { os: macos-13, python-version: 3.9} - { os: macos-13, python-version: 3.10} - - { os: windows-latest, python-version: 3.9} + - { os: macos-13, python-version: 3.11} - { os: windows-latest, python-version: 3.10} + - { os: windows-latest, python-version: 3.11} include: - { os: self-hosted, module: gpu, python-version: 3.11} - { os: ubuntu-20.04, module: doc-build, python-version: 3.9} @@ -98,7 +98,7 @@ jobs: submodules: recursive - name: Add msbuild to PATH if: ${{ matrix.os == 'windows-latest'}} - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v2 - name: Set up conda ${{ matrix.python-version }} uses: conda-incubator/setup-miniconda@v3 if: ${{ matrix.module != 'gpu' }} @@ -112,6 +112,12 @@ jobs: run: | conda install -c conda-forge libstdcxx-ng + # Important for python == 3.12 + - name: Update pip and setuptools + if: ${{ matrix.python-version == '3.12' }} + run: | + python -m pip install -U pip setuptools + - name: Install dependencies env: MODULE: ${{ matrix.module }} @@ -122,7 +128,7 @@ jobs: working-directory: ./python - name: Install ucx dependencies - if: ${{ (matrix.os == 'ubuntu-latest') && (matrix.python-version >= '3.9')}} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | # ucx-py move to ucxx and ucxx-cu12 can be run on CPU # conda install -c conda-forge -c rapidsai ucx-proc=*=cpu ucx ucx-py diff --git a/.gitignore b/.gitignore index 6f336bd6..3f5db384 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python build/ +.DS_Store develop-eggs/ dist/ downloads/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be11c32b..fc36f54a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.10.0 hooks: - id: black files: python/xoscar diff --git a/CMakeLists.txt b/CMakeLists.txt index d1b1ad71..ab4c6000 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,11 @@ else() option(USE_LIBUV "Build libuv transport on others" ON) endif() +if(MSVC) + add_compile_options(/utf-8) + message(STATUS "Done setting /utf-8 for MSVC") +endif() + include_directories(${CMAKE_SOURCE_DIR}) #find python3 include dir execute_process(COMMAND python -c "import sysconfig; print(sysconfig.get_path('include'))" @@ -29,8 +34,9 @@ if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") execute_process( COMMAND cmd /c - "cd ..\\..\\..\\..\\..\\third_party\\libuv&&mkdir build&&cd build && mkdir uvlib&&cmake .. -DCMAKE_INSTALL_PREFIX=uvlib&&msbuild.exe INSTALL.vcxproj" + "echo %cd% && cd ..\\..\\..\\..\\..\\third_party\\libuv && mkdir build && cd build && mkdir uvlib && cmake .. -DCMAKE_INSTALL_PREFIX=uvlib && msbuild.exe INSTALL.vcxproj" ) + message(STATUS "Done creating libuv_dir = ${libuv_dir}") endif() set(libuv_ROOT ${CMAKE_SOURCE_DIR}/third_party/libuv/build/uvlib) set(uv_HEADER_PATH ${CMAKE_SOURCE_DIR}/third_party/libuv/include) diff --git a/cpp/collective/rendezvous/include/error.h b/cpp/collective/rendezvous/include/error.h index 17645dc4..7d7e6e73 100644 --- a/cpp/collective/rendezvous/include/error.h +++ b/cpp/collective/rendezvous/include/error.h @@ -47,7 +47,7 @@ struct formatter { decltype(auto) format(const std::error_code &err, FormatContext &ctx) const { return format_to(ctx.out(), - "({}: {} - {})", + fmt::runtime("({}: {} - {})"), err.category(), err.value(), err.message()); diff --git a/cpp/collective/rendezvous/src/socket.cpp b/cpp/collective/rendezvous/src/socket.cpp index 1f7f9061..dc8ad4dc 100644 --- a/cpp/collective/rendezvous/src/socket.cpp +++ b/cpp/collective/rendezvous/src/socket.cpp @@ -16,6 +16,7 @@ limitations under the License. */ #include "error.h" #include "exception.h" #include "fmt/chrono.h" +#include "fmt/ranges.h" #include #include diff --git a/python/pyproject.toml b/python/pyproject.toml index 68d9e2a0..1edffbfb 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools<64", + "setuptools<64; python_version<'3.12'", + "setuptools>=75; python_version>='3.12'", "packaging", "wheel", "oldest-supported-numpy", @@ -42,6 +43,6 @@ markers = [ ] [tool.cibuildwheel] -build = ["cp38-*", "cp39-*", "cp310-*", "cp311-*"] -skip = "pp* *musllinux* *i686 cp36* cp38-win32 cp39-win32 cp310-win32 cp311-win32" +build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*"] +skip = "pp* *musllinux* *i686 cp36* cp38-win32 cp39-win32 cp310-win32 cp311-win32 cp312-win32" manylinux-x86_64-image = "manylinux2014" diff --git a/python/setup.cfg b/python/setup.cfg index 0e5093fa..c66ef75a 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -7,15 +7,15 @@ maintainer = Qin Xuye maintainer_email = qinxuye@xprobe.io license = Apache License 2.0 url = http://github.com/xorbitsai/xoscar -python_requires = >=3.8 +python_requires = >=3.9 classifier = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries @@ -31,7 +31,6 @@ install_requires = cloudpickle>=1.5.0 psutil>=5.9.0 tblib>=1.7.0 - pickle5; python_version<"3.8" uvloop>=0.14.0; sys_platform!="win32" packaging @@ -53,7 +52,6 @@ dev = sphinx pydata-sphinx-theme>=0.3.0 sphinx-intl>=0.9.9 - mock>=4.0.0; python_version<"3.8" flake8>=3.8.0 black doc = @@ -82,6 +80,8 @@ omit = xoscar/nvutils.py *.pxd */tests/* +disable_warnings = + include-ignored [coverage:report] exclude_lines = diff --git a/python/setup.py b/python/setup.py index 438dae23..d104f282 100644 --- a/python/setup.py +++ b/python/setup.py @@ -17,6 +17,7 @@ import re import subprocess import sys +import sysconfig from distutils.command.build_ext import build_ext as _du_build_ext from distutils.file_util import copy_file, move_file from pathlib import Path @@ -28,6 +29,7 @@ from packaging.version import Version from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +from setuptools.command.install_lib import install_lib from setuptools.extension import Library try: @@ -143,6 +145,45 @@ def build_long_description(): "win-arm64": "ARM64", } +TARGET_TO_PLAT = { + 'x86': 'win32', + 'x64': 'win-amd64', + 'arm': 'win-arm32', + 'arm64': 'win-arm64', +} + + +# Copied from https://github.com/pypa/setuptools/blob/main/setuptools/_distutils/util.py#L50 +def get_host_platform(): + """ + Return a string that identifies the current platform. Use this + function to distinguish platform-specific build directories and + platform-specific built distributions. + """ + + # This function initially exposed platforms as defined in Python 3.9 + # even with older Python versions when distutils was split out. + # Now it delegates to stdlib sysconfig, but maintains compatibility. + return sysconfig.get_platform() + + +def get_platform(): + if os.name == 'nt': + target = os.environ.get('VSCMD_ARG_TGT_ARCH') + return TARGET_TO_PLAT.get(target) or get_host_platform() + return get_host_platform() + + +plat_specifier = ".{}-{}".format(get_platform(), sys.implementation.cache_tag) + + +def get_build_lib(): + return os.path.join("build", "lib" + plat_specifier) + + +def get_build_temp(): + return os.path.join("build", 'temp' + plat_specifier) + # A CMakeExtension needs a sourcedir instead of a file list. # The name must be the _single_ output extension from the CMake build. @@ -154,6 +195,18 @@ def __init__(self, name: str, sourcedir: str = "") -> None: class CMakeBuild(build_ext): + def finalize_options(self): + """ + For python 3.12, the build_temp and build_lib dirs are temp dirs which are depended on your OS, + which leads to that cannot find the copy directory during C++ compiled process. + However, for Python < 3.12, these two dirs can be automatically located in the `build` directory of the project directory. + Therefore, in order to be compatible with all Python versions, + directly using fixed dirs here by coping source codes from `setuptools`. + """ + self.build_temp = get_build_temp() + self.build_lib = get_build_lib() + super().finalize_options() + def copy_extensions_to_source(self): build_py = self.get_finalized_command('build_py') for ext in self.extensions: diff --git a/python/xoscar/aio/__init__.py b/python/xoscar/aio/__init__.py index d7b1d58f..1cee6138 100644 --- a/python/xoscar/aio/__init__.py +++ b/python/xoscar/aio/__init__.py @@ -11,15 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import asyncio -import sys - from .file import AioFileObject from .lru import alru_cache from .parallelism import AioEvent - -if sys.version_info[:2] < (3, 9): - from ._threads import to_thread - - asyncio.to_thread = to_thread diff --git a/python/xoscar/aio/_threads.py b/python/xoscar/aio/_threads.py deleted file mode 100644 index 6324577a..00000000 --- a/python/xoscar/aio/_threads.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2022-2023 XProbe Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import contextvars -import functools -from asyncio import events - -__all__ = ("to_thread",) - - -async def to_thread(func, *args, **kwargs): - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Return a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) diff --git a/python/xoscar/backends/communication/socket.py b/python/xoscar/backends/communication/socket.py index 816859f2..61742f7e 100644 --- a/python/xoscar/backends/communication/socket.py +++ b/python/xoscar/backends/communication/socket.py @@ -17,6 +17,7 @@ import asyncio import concurrent.futures as futures +import logging import os import socket import sys @@ -30,7 +31,7 @@ from ..._utils import to_binary from ...constants import XOSCAR_UNIX_SOCKET_DIR from ...serialization import AioDeserializer, AioSerializer, deserialize -from ...utils import classproperty, implements, is_v6_ip +from ...utils import classproperty, implements, is_py_312, is_v6_ip from .base import Channel, ChannelType, Client, Server from .core import register_client, register_server from .utils import read_buffers, write_buffers @@ -38,6 +39,9 @@ _is_windows: bool = sys.platform.startswith("win") +logger = logging.getLogger(__name__) + + class SocketChannel(Channel): __slots__ = "reader", "writer", "_channel_type", "_send_lock", "_recv_lock" @@ -131,11 +135,23 @@ async def join(self, timeout=None): if timeout is None: await self._aio_server.serve_forever() else: - future = asyncio.create_task(self._aio_server.serve_forever()) - try: - await asyncio.wait_for(future, timeout=timeout) - except (futures.TimeoutError, asyncio.TimeoutError): - future.cancel() + if is_py_312(): + # For python 3.12, there's a bug for `serve_forever`: + # https://github.com/python/cpython/issues/123720, + # which is unable to be cancelled. + # Here is really a simulation of `wait_for` + task = asyncio.create_task(self._aio_server.serve_forever()) + await asyncio.sleep(timeout) + if task.done(): + logger.warning(f"`serve_forever` should never be done.") + else: + task.cancel() + else: + future = asyncio.create_task(self._aio_server.serve_forever()) + try: + await asyncio.wait_for(future, timeout=timeout) + except (futures.TimeoutError, asyncio.TimeoutError, TimeoutError): + future.cancel() @implements(Server.on_connected) async def on_connected(self, *args, **kwargs): @@ -161,7 +177,10 @@ async def on_connected(self, *args, **kwargs): @implements(Server.stop) async def stop(self): self._aio_server.close() - await self._aio_server.wait_closed() + # Python 3.12: # https://github.com/python/cpython/issues/104344 + # `wait_closed` leads to hang + if not is_py_312(): + await self._aio_server.wait_closed() # close all channels await asyncio.gather( *(channel.close() for channel in self._channels if not channel.closed) diff --git a/python/xoscar/backends/message.pyx b/python/xoscar/backends/message.pyx index 71db25e4..3f728165 100644 --- a/python/xoscar/backends/message.pyx +++ b/python/xoscar/backends/message.pyx @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio from enum import Enum from types import TracebackType from typing import Any, Type @@ -21,7 +22,9 @@ from tblib import pickling_support from ..core cimport ActorRef, BufferRef from ..serialization.core cimport Serializer + from ..utils import wrap_exception + from .._utils cimport new_random_id # make sure traceback can be pickled @@ -245,6 +248,11 @@ cdef class ErrorMessage(_MessageBase): if issubclass(self.error_type, _AsCauseBase): return self.error.with_traceback(self.traceback) + # for being compatible with Python 3.12 `asyncio.wait_for` + # https://github.com/python/cpython/pull/113850 + if isinstance(self.error, asyncio.CancelledError): + return asyncio.CancelledError(f"[address={self.address}, pid={self.pid}]").with_traceback(self.traceback) + return wrap_exception( self.error, (_AsCauseBase,), diff --git a/python/xoscar/utils.py b/python/xoscar/utils.py index 6c60f973..5c6cb851 100644 --- a/python/xoscar/utils.py +++ b/python/xoscar/utils.py @@ -30,6 +30,7 @@ import time import uuid from abc import ABC +from functools import lru_cache from types import TracebackType from typing import Callable, Type, Union @@ -464,6 +465,11 @@ def is_linux(): return sys.platform.startswith("linux") +@lru_cache +def is_py_312(): + return sys.version_info[:2] == (3, 12) + + def is_v4_zero_ip(ip_port_addr: str) -> bool: return ip_port_addr.split("://")[-1].startswith("0.0.0.0:") diff --git a/third_party/fmt b/third_party/fmt index 13156e54..c95722ad 160000 --- a/third_party/fmt +++ b/third_party/fmt @@ -1 +1 @@ -Subproject commit 13156e54bf91e44641ce3aac041d31f9a15a8042 +Subproject commit c95722ad629092b7c5e8bcc4ce4019c413f53711