Skip to content
Open
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
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ AccessModifierOffset: -4
ColumnLimit: 100

Language: Cpp
Standard: c++17
Standard: c++20
AlignAfterOpenBracket: Align
AlignEscapedNewlines: Right
AllowAllArgumentsOnNextLine: false
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ concurrency:
env:
OPTREE_CXX_WERROR: "OFF"
_GLIBCXX_USE_CXX11_ABI: "1"
MACOSX_DEPLOYMENT_TARGET: "15.0"
IPHONEOS_DEPLOYMENT_TARGET: "16.3"
PYTHONUNBUFFERED: "1"
PYTHON_TAG: "py3" # to be updated
PYTHON_VERSION: "3" # to be updated
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
run: make clang-format

- name: clang-tidy
run: make clang-tidy CMAKE_CXX_STANDARD=17
run: make clang-tidy CMAKE_CXX_STANDARD=20

- name: cpplint
run: make cpplint
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests-with-pydebug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ env:
CMAKE_BUILD_TYPE: "Debug"
OPTREE_CXX_WERROR: "ON"
_GLIBCXX_USE_CXX11_ABI: "1"
MACOSX_DEPLOYMENT_TARGET: "15.0"
PYTHONUNBUFFERED: "1"
PYTHON: "python" # to be updated
PYTHON_TAG: "py3" # to be updated
Expand Down
32 changes: 1 addition & 31 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ env:
CMAKE_BUILD_TYPE: "Debug"
OPTREE_CXX_WERROR: "ON"
_GLIBCXX_USE_CXX11_ABI: "1"
MACOSX_DEPLOYMENT_TARGET: "15.0"
FULL_TEST_PYTHON_VERSIONS: "3.12;3.13"
PYTHONUNBUFFERED: "1"
PYTHON: "python" # to be updated
Expand Down Expand Up @@ -194,37 +195,6 @@ jobs:
echo "::endgroup::"
echo "pybind11_VERSION=HEAD" | tee -a "${GITHUB_ENV}"

- name: Test installable with C++17
shell: bash
if: runner.os != 'Windows'
run: |
(
set -ex
${{ env.PYTHON }} -m venv venv
source venv/bin/activate
OPTREE_CXX_WERROR=OFF CMAKE_CXX_STANDARD=17 \
${{ env.PYTHON }} -m pip install -v .
pushd tests
${{ env.PYTHON }} -X dev -Walways -Werror -c 'import optree'
popd
rm -rf venv
)

if [[ "$?" -ne 0 ]]; then
echo "::error::Failed to install with C++17." >&2
exit 1
fi
CORE_DUMP_FILES="$(
find . -type d -path "./venv" -prune \
-o '(' -iname "core.*.[1-9]*" -o -iname "core_*.dmp" ')' -print
)"
if [[ -n "${CORE_DUMP_FILES}" ]]; then
echo "::error::Coredump files found, indicating a crash during tests." >&2
echo "Coredump files:" >&2
ls -alh ${CORE_DUMP_FILES} >&2
exit 1
fi

- name: Test buildable without Python frontend
if: runner.os != 'Windows'
run: |
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ endif()
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 20) # for likely/unlikely attributes
endif()
if (CMAKE_CXX_STANDARD VERSION_LESS 17)
message(FATAL_ERROR "C++17 or higher is required")
if (CMAKE_CXX_STANDARD VERSION_LESS 20)
message(FATAL_ERROR "C++20 or higher is required")
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
message(STATUS "Use C++ standard: C++${CMAKE_CXX_STANDARD}")
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The following options are available while building the Python C extension from t
```bash
export CMAKE_COMMAND="/path/to/custom/cmake"
export CMAKE_BUILD_TYPE="Debug"
export CMAKE_CXX_STANDARD="20" # C++17 is tested on Linux/macOS (C++20 is required on Windows)
export CMAKE_CXX_STANDARD="20"
export OPTREE_CXX_WERROR="OFF"
export _GLIBCXX_USE_CXX11_ABI="1"
export pybind11_DIR="/path/to/custom/pybind11"
Expand Down
62 changes: 28 additions & 34 deletions include/optree/exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,52 @@ limitations under the License.

#pragma once

#include <cstddef> // std::size_t
#include <optional> // std::optional, std::nullopt
#include <sstream> // std::ostringstream
#include <stdexcept> // std::logic_error
#include <string> // std::string, std::char_traits
#include <cstddef> // std::size_t
#include <format> // std::format
#include <source_location> // std::source_location
#include <stdexcept> // std::logic_error
#include <string> // std::string, std::char_traits
#include <string_view> // std::string_view

namespace optree {

constexpr std::size_t CURRENT_FILE_PATH_SIZE = std::char_traits<char>::length(__FILE__);
constexpr std::size_t CURRENT_FILE_PATH_SIZE =
std::char_traits<char>::length(std::source_location::current().file_name());
constexpr std::size_t CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE =
std::char_traits<char>::length("include/optree/exceptions.h");
static_assert(CURRENT_FILE_PATH_SIZE >= CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE,
"SOURCE_PATH_PREFIX_SIZE must be greater than 0.");
constexpr std::size_t SOURCE_PATH_PREFIX_SIZE =
CURRENT_FILE_PATH_SIZE - CURRENT_FILE_RELPATH_FROM_PROJECT_ROOT_SIZE;
// NOLINTNEXTLINE[bugprone-reserved-identifier]
#define __FILE_RELPATH_FROM_PROJECT_ROOT__ ((const char *)&(__FILE__[SOURCE_PATH_PREFIX_SIZE]))

constexpr std::string_view RelpathFromProjectRoot(const std::string_view &abspath) {
return abspath.substr(SOURCE_PATH_PREFIX_SIZE);
}
constexpr std::string_view RelpathFromProjectRoot(
const std::source_location &source_location = std::source_location::current()) {
return RelpathFromProjectRoot(source_location.file_name());
}

class InternalError : public std::logic_error {
public:
explicit InternalError(const std::string &message) noexcept(noexcept(std::logic_error{message}))
: std::logic_error{message} {}
explicit InternalError(const std::string &message,
const std::string &file,
const std::size_t &lineno,
const std::optional<std::string> function =
std::nullopt) noexcept(noexcept(std::logic_error{message}))
: InternalError([&message, &file, &lineno, &function]() -> std::string {
std::ostringstream oss{};
oss << message << " (";
if (function) [[likely]] {
oss << "function `" << *function << "` ";
}
oss << "at file " << file << ":" << lineno << ")\n\n"
<< "Please file a bug report at https://github.com/metaopt/optree/issues.";
return oss.str();
}()) {}
explicit InternalError(
const std::string_view &message,
const std::source_location &source_location = std::source_location::current())
: std::logic_error{
std::format("{} (in function `{}` at file {}:{}:{})\n\n"
"Please file a bug report at https://github.com/metaopt/optree/issues.",
message,
source_location.function_name(),
RelpathFromProjectRoot(source_location),
source_location.line(),
source_location.column())} {}
};

#define VA_FUNC2_(__0, __1, NAME, ...) NAME
#define VA_FUNC3_(__0, __1, __2, NAME, ...) NAME

#if !defined(__GNUC__)
# define __PRETTY_FUNCTION__ std::nullopt // NOLINT[bugprone-reserved-identifier]
#endif

#define INTERNAL_ERROR0_() INTERNAL_ERROR1_("Unreachable code.")
#define INTERNAL_ERROR1_(message) \
throw optree::InternalError((message), \
__FILE_RELPATH_FROM_PROJECT_ROOT__, \
__LINE__, \
__PRETTY_FUNCTION__)
#define INTERNAL_ERROR1_(message) throw optree::InternalError(message)
#define INTERNAL_ERROR(...) \
VA_FUNC2_(__0 __VA_OPT__(, ) __VA_ARGS__, INTERNAL_ERROR1_, INTERNAL_ERROR0_)(__VA_ARGS__)

Expand Down
96 changes: 64 additions & 32 deletions include/optree/pytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ limitations under the License.
#pragma once

#include <exception> // std::rethrow_exception, std::current_exception
#include <format> // std::format, std::format_to, std::formatter
#include <string> // std::string
#include <type_traits> // std::enable_if_t, std::is_base_of_v
#include <type_traits> // std::is_base_of_v
#include <unordered_map> // std::unordered_map
#include <utility> // std::move, std::pair, std::make_pair

Expand Down Expand Up @@ -49,6 +50,29 @@ inline Py_ALWAYS_INLINE std::string PyRepr(const std::string &string) {
return static_cast<std::string>(py::repr(py::str(string)));
}

template <typename CharT>
struct std::formatter<py::handle, CharT> {
template <class ParseContext>
constexpr ParseContext::iterator parse(const ParseContext &context) {
return context.begin();
}
template <class FormatContext>
FormatContext::iterator format(const py::handle &object, FormatContext &context) const {
return std::format_to(context.out(), "{}", PyRepr(object));
}
};
template <typename CharT>
struct std::formatter<py::object, CharT> {
template <class ParseContext>
constexpr ParseContext::iterator parse(const ParseContext &context) {
return context.begin();
}
template <class FormatContext>
FormatContext::iterator format(const py::object &object, FormatContext &context) const {
return std::format_to(context.out(), "{}", PyRepr(object));
}
};

// The maximum size of the type cache.
constexpr py::ssize_t MAX_TYPE_CACHE_SIZE = 4096;

Expand Down Expand Up @@ -105,15 +129,19 @@ inline Py_ALWAYS_INLINE py::ssize_t DictGetSize(const py::handle &dict) {
#endif
}

template <typename T, typename = std::enable_if_t<std::is_base_of_v<py::object, T>>>
inline Py_ALWAYS_INLINE T TupleGetItemAs(const py::handle &tuple, const py::ssize_t &index) {
template <typename T>
inline Py_ALWAYS_INLINE T TupleGetItemAs(const py::handle &tuple, const py::ssize_t &index)
requires(std::is_base_of_v<py::object, T>)
{
return py::reinterpret_borrow<T>(PyTuple_GET_ITEM(tuple.ptr(), index));
}
inline Py_ALWAYS_INLINE py::object TupleGetItem(const py::handle &tuple, const py::ssize_t &index) {
return TupleGetItemAs<py::object>(tuple, index);
}
template <typename T, typename = std::enable_if_t<std::is_base_of_v<py::object, T>>>
inline Py_ALWAYS_INLINE T ListGetItemAs(const py::handle &list, const py::ssize_t &index) {
template <typename T>
inline Py_ALWAYS_INLINE T ListGetItemAs(const py::handle &list, const py::ssize_t &index)
requires(std::is_base_of_v<py::object, T>)
{
#if PY_VERSION_HEX >= 0x030D00A4 // Python 3.13.0a4
PyObject * const item = PyList_GetItemRef(list.ptr(), index);
if (item == nullptr) [[unlikely]] {
Expand All @@ -127,8 +155,10 @@ inline Py_ALWAYS_INLINE T ListGetItemAs(const py::handle &list, const py::ssize_
inline Py_ALWAYS_INLINE py::object ListGetItem(const py::handle &list, const py::ssize_t &index) {
return ListGetItemAs<py::object>(list, index);
}
template <typename T, typename = std::enable_if_t<std::is_base_of_v<py::object, T>>>
inline Py_ALWAYS_INLINE T DictGetItemAs(const py::handle &dict, const py::handle &key) {
template <typename T>
inline Py_ALWAYS_INLINE T DictGetItemAs(const py::handle &dict, const py::handle &key)
requires(std::is_base_of_v<py::object, T>)
{
#if PY_VERSION_HEX >= 0x030D00A1 // Python 3.13.0a1
PyObject *value = nullptr;
if (PyDict_GetItemRef(dict.ptr(), key.ptr(), &value) < 0) [[unlikely]] {
Expand Down Expand Up @@ -167,31 +197,31 @@ inline Py_ALWAYS_INLINE void DictSetItem(const py::handle &dict,

inline Py_ALWAYS_INLINE void AssertExactList(const py::handle &object) {
if (!PyList_CheckExact(object.ptr())) [[unlikely]] {
throw py::value_error("Expected an instance of list, got " + PyRepr(object) + ".");
throw py::value_error(std::format("Expected an instance of list, got {}.", object));
}
}
inline Py_ALWAYS_INLINE void AssertExactTuple(const py::handle &object) {
if (!PyTuple_CheckExact(object.ptr())) [[unlikely]] {
throw py::value_error("Expected an instance of tuple, got " + PyRepr(object) + ".");
throw py::value_error(std::format("Expected an instance of tuple, got {}.", object));
}
}
inline Py_ALWAYS_INLINE void AssertExactDict(const py::handle &object) {
if (!PyDict_CheckExact(object.ptr())) [[unlikely]] {
throw py::value_error("Expected an instance of dict, got " + PyRepr(object) + ".");
throw py::value_error(std::format("Expected an instance of dict, got {}.", object));
}
}

inline Py_ALWAYS_INLINE void AssertExactOrderedDict(const py::handle &object) {
if (!py::type::handle_of(object).is(PyOrderedDictTypeObject)) [[unlikely]] {
throw py::value_error("Expected an instance of collections.OrderedDict, got " +
PyRepr(object) + ".");
throw py::value_error(
std::format("Expected an instance of collections.OrderedDict, got {}.", object));
}
}

inline Py_ALWAYS_INLINE void AssertExactDefaultDict(const py::handle &object) {
if (!py::type::handle_of(object).is(PyDefaultDictTypeObject)) [[unlikely]] {
throw py::value_error("Expected an instance of collections.defaultdict, got " +
PyRepr(object) + ".");
throw py::value_error(
std::format("Expected an instance of collections.defaultdict, got {}.", object));
}
}

Expand All @@ -200,16 +230,16 @@ inline Py_ALWAYS_INLINE void AssertExactStandardDict(const py::handle &object) {
py::type::handle_of(object).is(PyOrderedDictTypeObject) ||
py::type::handle_of(object).is(PyDefaultDictTypeObject))) [[unlikely]] {
throw py::value_error(
"Expected an instance of dict, collections.OrderedDict, or collections.defaultdict, "
"got " +
PyRepr(object) + ".");
std::format("Expected an instance of dict, collections.OrderedDict, "
"or collections.defaultdict, got {}.",
object));
}
}

inline Py_ALWAYS_INLINE void AssertExactDeque(const py::handle &object) {
if (!py::type::handle_of(object).is(PyDequeTypeObject)) [[unlikely]] {
throw py::value_error("Expected an instance of collections.deque, got " + PyRepr(object) +
".");
throw py::value_error(
std::format("Expected an instance of collections.deque, got {}.", object));
}
}

Expand Down Expand Up @@ -292,23 +322,24 @@ inline Py_ALWAYS_INLINE bool IsNamedTuple(const py::handle &object) {
}
inline Py_ALWAYS_INLINE void AssertExactNamedTuple(const py::handle &object) {
if (!IsNamedTupleInstance(object)) [[unlikely]] {
throw py::value_error("Expected an instance of collections.namedtuple, got " +
PyRepr(object) + ".");
throw py::value_error(
std::format("Expected an instance of collections.namedtuple, got {}.", object));
}
}
inline py::tuple NamedTupleGetFields(const py::handle &object) {
py::handle type;
if (PyType_Check(object.ptr())) [[unlikely]] {
type = object;
if (!IsNamedTupleClass(type)) [[unlikely]] {
throw py::type_error("Expected a collections.namedtuple type, got " + PyRepr(object) +
".");
throw py::type_error(
std::format("Expected a collections.namedtuple type, got {}.", object));
}
} else [[likely]] {
type = py::type::handle_of(object);
if (!IsNamedTupleClass(type)) [[unlikely]] {
throw py::type_error("Expected an instance of collections.namedtuple type, got " +
PyRepr(object) + ".");
throw py::type_error(
std::format("Expected an instance of collections.namedtuple type, got {}.",
object));
}
}
return EVALUATE_WITH_LOCK_HELD(py::getattr(type, Py_Get_ID(_fields)), type);
Expand Down Expand Up @@ -395,8 +426,8 @@ inline Py_ALWAYS_INLINE bool IsStructSequence(const py::handle &object) {
}
inline Py_ALWAYS_INLINE void AssertExactStructSequence(const py::handle &object) {
if (!IsStructSequenceInstance(object)) [[unlikely]] {
throw py::value_error("Expected an instance of PyStructSequence type, got " +
PyRepr(object) + ".");
throw py::value_error(
std::format("Expected an instance of PyStructSequence type, got {}.", object));
}
}
inline py::tuple StructSequenceGetFieldsImpl(const py::handle &type) {
Expand Down Expand Up @@ -433,13 +464,13 @@ inline py::tuple StructSequenceGetFields(const py::handle &object) {
if (PyType_Check(object.ptr())) [[unlikely]] {
type = object;
if (!IsStructSequenceClass(type)) [[unlikely]] {
throw py::type_error("Expected a PyStructSequence type, got " + PyRepr(object) + ".");
throw py::type_error(std::format("Expected a PyStructSequence type, got {}.", object));
}
} else [[likely]] {
type = py::type::handle_of(object);
if (!IsStructSequenceClass(type)) [[unlikely]] {
throw py::type_error("Expected an instance of PyStructSequence type, got " +
PyRepr(object) + ".");
throw py::type_error(
std::format("Expected an instance of PyStructSequence type, got {}.", object));
}
}

Expand Down Expand Up @@ -490,8 +521,9 @@ inline void TotalOrderSort(py::list &list) { // NOLINT[runtime/references]
const auto sort_key_fn = py::cpp_function([](const py::object &obj) -> py::tuple {
const py::handle cls = py::type::handle_of(obj);
const py::str qualname{EVALUATE_WITH_LOCK_HELD(
PyStr(py::getattr(cls, Py_Get_ID(__module__))) + "." +
PyStr(py::getattr(cls, Py_Get_ID(__qualname__))),
std::format("{}.{}",
PyStr(py::getattr(cls, Py_Get_ID(__module__))),
PyStr(py::getattr(cls, Py_Get_ID(__qualname__)))),
cls)};
return py::make_tuple(qualname, obj);
});
Expand Down
Loading
Loading