diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index e0dbb062eb0372..54c4044e60a4e5 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2586,6 +2586,25 @@ class T "" "" """ self.expect_failure(block, err, lineno=1) + def test_can_convert_module_getattr(self): + function = self.parse_function(""" + module m + __getattr__ + name: object + / + """) + self.assertEqual(function.kind, FunctionKind.METHOD_GETATTR) + + def test_can_convert_class_getattr(self): + function = self.parse_function(""" + module m + class m.T "PyObject *" "" + m.T.__getattr__ + name: object + / + """, signatures_in_block=3, function_index=2) + self.assertEqual(function.kind, FunctionKind.METHOD_GETATTR) + def test_cannot_specify_pydefault_without_default(self): err = "You can't specify py_default without specifying a default value!" block = """ diff --git a/Misc/NEWS.d/next/Tools-Demos/2025-10-20-16-49-22.gh-issue-140382.lzLQCh.rst b/Misc/NEWS.d/next/Tools-Demos/2025-10-20-16-49-22.gh-issue-140382.lzLQCh.rst new file mode 100644 index 00000000000000..3ff26142aa4ffe --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2025-10-20-16-49-22.gh-issue-140382.lzLQCh.rst @@ -0,0 +1 @@ +Argument clinic now supports :meth:`module.__getattr__`. diff --git a/Modules/clinic/zlibmodule.c.h b/Modules/clinic/zlibmodule.c.h index 6fba75339b3206..14cce7d542a7f9 100644 --- a/Modules/clinic/zlibmodule.c.h +++ b/Modules/clinic/zlibmodule.c.h @@ -1379,6 +1379,43 @@ zlib_crc32_combine(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } +PyDoc_STRVAR(zlib___getattr____doc__, +"__getattr__($module, name, /)\n" +"--\n" +"\n" +"Module __getattr__"); + +#define ZLIB___GETATTR___METHODDEF \ + {"__getattr__", (PyCFunction)zlib___getattr__, METH_O, zlib___getattr____doc__}, + +static PyObject * +zlib___getattr___impl(PyObject *module, const char *name); + +static PyObject * +zlib___getattr__(PyObject *module, PyObject *arg) +{ + PyObject *return_value = NULL; + const char *name; + + if (!PyUnicode_Check(arg)) { + _PyArg_BadArgument("__getattr__", "argument", "str", arg); + goto exit; + } + Py_ssize_t name_length; + name = PyUnicode_AsUTF8AndSize(arg, &name_length); + if (name == NULL) { + goto exit; + } + if (strlen(name) != (size_t)name_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + return_value = zlib___getattr___impl(module, name); + +exit: + return return_value; +} + #ifndef ZLIB_COMPRESS_COPY_METHODDEF #define ZLIB_COMPRESS_COPY_METHODDEF #endif /* !defined(ZLIB_COMPRESS_COPY_METHODDEF) */ @@ -1402,4 +1439,4 @@ zlib_crc32_combine(PyObject *module, PyObject *const *args, Py_ssize_t nargs) #ifndef ZLIB_DECOMPRESS___DEEPCOPY___METHODDEF #define ZLIB_DECOMPRESS___DEEPCOPY___METHODDEF #endif /* !defined(ZLIB_DECOMPRESS___DEEPCOPY___METHODDEF) */ -/*[clinic end generated code: output=fa5fc356f3090cce input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a012669e48021aa4 input=a9049054013a1b77]*/ diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index 6bac09aa6c2a6c..9dc7c523450ce4 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -2015,15 +2015,20 @@ zlib_crc32_combine_impl(PyObject *module, unsigned int crc1, return crc32_combine(crc1, crc2, len); } +/*[clinic input] +zlib.__getattr__ + + name: str + / + +Module __getattr__ +[clinic start generated code]*/ + static PyObject * -zlib_getattr(PyObject *self, PyObject *args) +zlib___getattr___impl(PyObject *module, const char *name) +/*[clinic end generated code: output=5ec48f62426a4b95 input=59a69e9dcff70564]*/ { - PyObject *name; - if (!PyArg_UnpackTuple(args, "__getattr__", 1, 1, &name)) { - return NULL; - } - - if (PyUnicode_Check(name) && PyUnicode_EqualToUTF8(name, "__version__")) { + if (strcmp(name, "__version__") == 0) { if (PyErr_WarnEx(PyExc_DeprecationWarning, "'__version__' is deprecated and slated for removal in Python 3.20", 1) < 0) { @@ -2032,7 +2037,7 @@ zlib_getattr(PyObject *self, PyObject *args) return PyUnicode_FromString("1.0"); } - PyErr_Format(PyExc_AttributeError, "module 'zlib' has no attribute %R", name); + PyErr_Format(PyExc_AttributeError, "module 'zlib' has no attribute %s", name); return NULL; } @@ -2046,7 +2051,7 @@ static PyMethodDef zlib_methods[] = ZLIB_CRC32_COMBINE_METHODDEF ZLIB_DECOMPRESS_METHODDEF ZLIB_DECOMPRESSOBJ_METHODDEF - {"__getattr__", zlib_getattr, METH_VARARGS, "Module __getattr__"}, + ZLIB___GETATTR___METHODDEF {NULL, NULL} }; diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py index bc21ae84e1c332..f367bc3896dcc9 100644 --- a/Tools/clinic/libclinic/converters.py +++ b/Tools/clinic/libclinic/converters.py @@ -7,7 +7,7 @@ from libclinic import fail, Null, unspecified, unknown from libclinic.function import ( Function, Parameter, - CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, + CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, METHOD_GETATTR, GETTER, SETTER) from libclinic.codegen import CRenderData, TemplateDict from libclinic.converter import ( @@ -1090,7 +1090,7 @@ def correct_name_for_self( f: Function, parser: bool = False ) -> tuple[str, str]: - if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}: + if f.kind in {CALLABLE, METHOD_INIT, METHOD_GETATTR, GETTER, SETTER}: if f.cls: return "PyObject *", "self" return "PyObject *", "module" diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 0d83baeba9e508..caec19b19df71a 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -17,7 +17,7 @@ from libclinic.function import ( Module, Class, Function, Parameter, FunctionKind, - CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, + CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, METHOD_GETATTR, GETTER, SETTER) from libclinic.converter import ( converters, legacy_converters) @@ -45,7 +45,6 @@ __float__ __floordiv__ __ge__ -__getattr__ __getattribute__ __getitem__ __gt__ @@ -598,6 +597,8 @@ def normalize_function_kind(self, fullname: str) -> None: self.kind = METHOD_NEW elif name == '__init__': self.kind = METHOD_INIT + elif name == '__getattr__': + self.kind = METHOD_GETATTR def resolve_return_converter( self, full_name: str, forced_converter: str @@ -1533,7 +1534,7 @@ def format_docstring(self) -> str: assert self.function is not None f = self.function # For the following special cases, it does not make sense to render a docstring. - if f.kind in {METHOD_INIT, METHOD_NEW, GETTER, SETTER} and not f.docstring: + if f.kind in {METHOD_INIT, METHOD_NEW, METHOD_GETATTR, GETTER, SETTER} and not f.docstring: return f.docstring # Enforce the summary line! diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index f981f0bcaf89f0..c1c8e4256d6e6a 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -58,6 +58,7 @@ class FunctionKind(enum.Enum): CLASS_METHOD = enum.auto() METHOD_INIT = enum.auto() METHOD_NEW = enum.auto() + METHOD_GETATTR = enum.auto() GETTER = enum.auto() SETTER = enum.auto() @@ -74,6 +75,7 @@ def __repr__(self) -> str: CLASS_METHOD: Final = FunctionKind.CLASS_METHOD METHOD_INIT: Final = FunctionKind.METHOD_INIT METHOD_NEW: Final = FunctionKind.METHOD_NEW +METHOD_GETATTR: Final = FunctionKind.METHOD_GETATTR GETTER: Final = FunctionKind.GETTER SETTER: Final = FunctionKind.SETTER @@ -161,7 +163,7 @@ def methoddef_flags(self) -> str | None: case FunctionKind.STATIC_METHOD: flags.append('METH_STATIC') case _ as kind: - acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER} + acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER, FunctionKind.METHOD_GETATTR} assert kind in acceptable_kinds, f"unknown kind: {kind!r}" if self.coexist: flags.append('METH_COEXIST')