@@ -52,6 +52,19 @@ bool HasAttrDirect(PyObject* pyclass, PyObject* pyname, bool mustBeCPyCppyy = fa
5252 return false ;
5353}
5454
55+ bool HasAttrInMRO (PyObject *pyclass, PyObject *pyname)
56+ {
57+ // Check base classes in the MRO (skipping the class itself) for a CPyCppyy overload.
58+ PyObject *mro = ((PyTypeObject *)pyclass)->tp_mro ;
59+ if (mro && PyTuple_Check (mro)) {
60+ for (Py_ssize_t i = 1 ; i < PyTuple_GET_SIZE (mro); ++i) {
61+ if (HasAttrDirect (PyTuple_GET_ITEM (mro, i), pyname, /* mustBeCPyCppyy=*/ true ))
62+ return true ;
63+ }
64+ }
65+ return false ;
66+ }
67+
5568PyObject* GetAttrDirect (PyObject* pyclass, PyObject* pyname) {
5669// get an attribute without causing getattr lookups
5770 PyObject* dct = PyObject_GetAttr (pyclass, PyStrings::gDict );
@@ -63,6 +76,18 @@ PyObject* GetAttrDirect(PyObject* pyclass, PyObject* pyname) {
6376 return nullptr ;
6477}
6578
79+ // -----------------------------------------------------------------------------
80+ static bool IsIntegerType (const std::string &resname)
81+ {
82+ // Check if the resolved return type is an integer type suitable for __len__,
83+ // which requires a non-negative int in Python.
84+ static const std::set<std::string> sIntegerTypes = {" short" , " unsigned short" , " int" , " unsigned int" ,
85+ " long" , " unsigned long" , " long long" , " unsigned long long" ,
86+ " char" , " signed char" , " unsigned char" , " wchar_t" };
87+ std::string resolved = Cppyy::ResolveName (resname);
88+ return sIntegerTypes .find (resolved) != sIntegerTypes .end ();
89+ }
90+
6691// -----------------------------------------------------------------------------
6792inline bool IsTemplatedSTLClass (const std::string& name, const std::string& klass) {
6893// Scan the name of the class and determine whether it is a template instantiation.
@@ -1755,12 +1780,37 @@ bool CPyCppyy::Pythonize(PyObject* pyclass, const std::string& name)
17551780 Utility::AddToClass (pyclass, pybool_name, (PyCFunction)NullCheckBool, METH_NOARGS);
17561781 }
17571782
1758- // for STL containers, and user classes modeled after them
1759- // the attribute must be a CPyCppyy overload, otherwise the check gives false
1760- // positives in the case where the class has a non-function attribute that is
1761- // called "size".
1762- if (HasAttrDirect (pyclass, PyStrings::gSize , /* mustBeCPyCppyy=*/ true )) {
1763- Utility::AddToClass (pyclass, " __len__" , " size" );
1783+ // for STL containers, and user classes modeled after them. Guard the alias to
1784+ // __len__ by verifying that size() returns an integer type and the class has
1785+ // begin()/end() or operator[] (i.e. is container-like). This prevents bool()
1786+ // returning False for valid objects whose size() returns non-integer types like
1787+ // std::optional<std::size_t>.
1788+ if (HasAttrDirect (pyclass, PyStrings::gSize , /* mustBeCPyCppyy=*/ true ) || HasAttrInMRO (pyclass, PyStrings::gSize )) {
1789+ bool sizeIsInteger = false ;
1790+ PyObject *pySizeMethod = PyObject_GetAttr (pyclass, PyStrings::gSize );
1791+ if (pySizeMethod && CPPOverload_Check (pySizeMethod)) {
1792+ auto *ol = (CPPOverload *)pySizeMethod;
1793+ // size() is not typically overloaded; check the first overload's return type
1794+ if (ol->HasMethods ()) {
1795+ PyObject *pyrestype =
1796+ ol->fMethodInfo ->fMethods [0 ]->Reflex (Cppyy::Reflex::RETURN_TYPE, Cppyy::Reflex::AS_STRING);
1797+ if (pyrestype) {
1798+ sizeIsInteger = IsIntegerType (CPyCppyy_PyText_AsString (pyrestype));
1799+ Py_DECREF (pyrestype);
1800+ }
1801+ }
1802+ }
1803+ Py_XDECREF (pySizeMethod);
1804+
1805+ if (sizeIsInteger) {
1806+ bool hasIterators = (HasAttrDirect (pyclass, PyStrings::gBegin ) || HasAttrInMRO (pyclass, PyStrings::gBegin )) &&
1807+ (HasAttrDirect (pyclass, PyStrings::gEnd ) || HasAttrInMRO (pyclass, PyStrings::gEnd ));
1808+ bool hasSubscript = HasAttrDirect (pyclass, PyStrings::gGetItem ) ||
1809+ !Cppyy::GetMethodIndicesFromName (klass->fCppType , " operator[]" ).empty ();
1810+ if (hasIterators || hasSubscript) {
1811+ Utility::AddToClass (pyclass, " __len__" , " size" );
1812+ }
1813+ }
17641814 }
17651815
17661816 if (HasAttrDirect (pyclass, PyStrings::gContains )) {
0 commit comments