From 6080ef50602b53bfe87dce604bab357feed022da Mon Sep 17 00:00:00 2001 From: root Date: Tue, 18 Feb 2025 16:03:43 +0100 Subject: [PATCH 1/8] fixing enum assignment issue --- newtype/extensions/newtype_meth.c | 21 ++++++++++++++++++++- newtype/newtype.py | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/newtype/extensions/newtype_meth.c b/newtype/extensions/newtype_meth.c index 6a62537..666d29e 100644 --- a/newtype/extensions/newtype_meth.c +++ b/newtype/extensions/newtype_meth.c @@ -158,31 +158,48 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, first_elem = PyTuple_GetItem(args, 0); Py_XINCREF( first_elem); // Increment reference count of the first element - + DEBUG_PRINT("`first_elem`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(first_elem))); } else { // `args` is empty here, then we are done actually + DEBUG_PRINT("`args` is empty\n"); goto done; }; if (PyObject_IsInstance(first_elem, (PyObject*)self->cls)) { init_args = PyObject_GetAttrString(first_elem, NEWTYPE_INIT_ARGS_STR); init_kwargs = PyObject_GetAttrString(first_elem, NEWTYPE_INIT_KWARGS_STR); + DEBUG_PRINT("`init_args`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(init_args))); + DEBUG_PRINT("`init_kwargs`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); } else { // first element is not the subtype, so we are done also + DEBUG_PRINT("`first_elem` is not the subtype\n"); goto done; } Py_XDECREF(first_elem); } else { // `self->obj` is not NULL + DEBUG_PRINT("`self->obj` is not NULL\n"); init_args = PyObject_GetAttrString(self->obj, NEWTYPE_INIT_ARGS_STR); init_kwargs = PyObject_GetAttrString(self->obj, NEWTYPE_INIT_KWARGS_STR); + DEBUG_PRINT("`init_args`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(init_args))); + DEBUG_PRINT("`init_kwargs`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); } Py_ssize_t args_len = PyTuple_Size(init_args); + DEBUG_PRINT("`args_len`: %zd\n", args_len); Py_ssize_t combined_args_len = 1 + args_len; + DEBUG_PRINT("`combined_args_len`: %zd\n", combined_args_len); args_combined = PyTuple_New(combined_args_len); + DEBUG_PRINT("`args_combined`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(args_combined))); if (args_combined == NULL) { Py_XDECREF(init_args); Py_XDECREF(init_kwargs); Py_DECREF(result); + DEBUG_PRINT("`args_combined` is NULL\n"); return NULL; // Use return NULL instead of Py_RETURN_NONE } @@ -195,11 +212,13 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, for (Py_ssize_t i = 0; i < args_len; i++) { PyObject* item = PyTuple_GetItem(init_args, i); // Borrowed reference if (item == NULL) { + DEBUG_PRINT("`item` is NULL\n"); Py_DECREF(args_combined); Py_XDECREF(init_args); Py_XDECREF(init_kwargs); return NULL; } + DEBUG_PRINT("`item`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(item))); Py_INCREF(item); // Increase reference count PyTuple_SET_ITEM(args_combined, i + 1, diff --git a/newtype/newtype.py b/newtype/newtype.py index 47f1cfb..fe88b89 100644 --- a/newtype/newtype.py +++ b/newtype/newtype.py @@ -227,7 +227,8 @@ def __init_subclass__(cls, **init_subclass_context: Any) -> None: else: if k == "__dict__": continue - setattr(cls, k, v) + # (TODO: Fix enum issue) + # setattr(cls, k, v) cls.__init__ = NewTypeInit(constructor) # type: ignore[method-assign] def __new__(cls, value: Any = None, *_args: Any, **_kwargs: Any) -> "BaseNewType": From 67ce730e293418533bc41ba438509e44d24b7f34 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Feb 2025 18:13:45 +0100 Subject: [PATCH 2/8] looks like the enum issue is fixed, now is to clean up, set `result` to have init_kwargs, then set up `args_combined` and form it --- examples/newtype_enums.py | 10 ++ newtype/extensions/newtype_init.c | 4 + newtype/extensions/newtype_meth.c | 240 +++++++++++++++++++++++++----- 3 files changed, 214 insertions(+), 40 deletions(-) diff --git a/examples/newtype_enums.py b/examples/newtype_enums.py index 7b5990d..e81790b 100644 --- a/examples/newtype_enums.py +++ b/examples/newtype_enums.py @@ -14,6 +14,14 @@ class ENV(NewType(str), Enum): # type: ignore[misc] PREPROD = "PREPROD" PROD = "PROD" +print(type(ENV.LOCAL)) +print(type(ENV.LOCAL).__mro__) +print(super(ENV, ENV.LOCAL).__self_class__) +print(super(ENV, ENV.LOCAL).__thisclass__) +print(super(super(ENV, ENV.LOCAL).__self_class__)) +print(super(ENV)) +print(str.__str__(ENV.LOCAL)) + class RegularENV(str, Enum): LOCAL = "LOCAL" @@ -58,6 +66,7 @@ class RollYourOwnNewTypeEnum(ENVVariant, Enum): # type: ignore[no-redef] PREPROD = "PREPROD" PROD = "PROD" + # mypy doesn't raise errors here def test_nt_env_replace() -> None: @@ -71,6 +80,7 @@ def test_nt_env_replace() -> None: # nevermind about the reason why we want to do so env = env.replace(ENV.LOCAL, ENV.DEV) # reveal_type(env) # Revealed type is "newtype_enums.ENV" + print(f"`env`: {env}; type: {type(env)}") # replacement is successful assert env is ENV.DEV diff --git a/newtype/extensions/newtype_init.c b/newtype/extensions/newtype_init.c index fa7c11c..3e2b7ed 100644 --- a/newtype/extensions/newtype_init.c +++ b/newtype/extensions/newtype_init.c @@ -101,6 +101,7 @@ static PyObject* NewTypeInit_call(NewTypeInitObject* self, PyObject* func; if (self->has_get) { + DEBUG_PRINT("`self->has_get`: %d\n", self->has_get); if (self->obj == NULL && self->cls == NULL) { // free standing function PyErr_SetString( @@ -117,6 +118,8 @@ static PyObject* NewTypeInit_call(NewTypeInitObject* self, self->func_get, self->obj, self->cls, NULL); } } else { + DEBUG_PRINT("`self->func_get`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(self->func_get))); func = self->func_get; } @@ -179,6 +182,7 @@ static PyObject* NewTypeInit_call(NewTypeInitObject* self, result = PyObject_Call(func, args, kwds); } else { PyErr_SetString(PyExc_TypeError, "Invalid type object in descriptor"); + DEBUG_PRINT("`self->cls` is not a valid type object\n"); result = NULL; } diff --git a/newtype/extensions/newtype_meth.c b/newtype/extensions/newtype_meth.c index 666d29e..db73859 100644 --- a/newtype/extensions/newtype_meth.c +++ b/newtype/extensions/newtype_meth.c @@ -94,6 +94,10 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, self->func_get, Py_None, self->wrapped_cls, NULL); } else { DEBUG_PRINT("`self->obj` is not NULL\n"); + DEBUG_PRINT("`self->wrapped_cls`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(self->wrapped_cls))); + DEBUG_PRINT("`self->obj`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(self->obj))); func = PyObject_CallFunctionObjArgs( self->func_get, self->obj, self->wrapped_cls, NULL); } @@ -145,7 +149,10 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, { // now we try to build an instance of the subtype DEBUG_PRINT("`result` is an instance of `self->wrapped_cls`\n"); PyObject *init_args, *init_kwargs; - PyObject *new_inst, *args_combined; + PyObject* new_inst; + PyObject* args_combined = NULL; + PyObject* super_class_new_args = NULL; + Py_ssize_t args_len = 0; if (self->obj == NULL) { PyObject* first_elem; @@ -188,62 +195,214 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); } - Py_ssize_t args_len = PyTuple_Size(init_args); - DEBUG_PRINT("`args_len`: %zd\n", args_len); - Py_ssize_t combined_args_len = 1 + args_len; - DEBUG_PRINT("`combined_args_len`: %zd\n", combined_args_len); - args_combined = PyTuple_New(combined_args_len); - DEBUG_PRINT("`args_combined`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(args_combined))); - if (args_combined == NULL) { - Py_XDECREF(init_args); - Py_XDECREF(init_kwargs); - Py_DECREF(result); - DEBUG_PRINT("`args_combined` is NULL\n"); - return NULL; // Use return NULL instead of Py_RETURN_NONE - } - - // Set the first item of the new tuple to `result` - PyTuple_SET_ITEM(args_combined, - 0, - result); // `result` is now owned by `args_combined` - - // Copy items from `init_args` to `args_combined` - for (Py_ssize_t i = 0; i < args_len; i++) { - PyObject* item = PyTuple_GetItem(init_args, i); // Borrowed reference - if (item == NULL) { - DEBUG_PRINT("`item` is NULL\n"); - Py_DECREF(args_combined); + if (init_args != NULL) { + DEBUG_PRINT("`init_args` is not NULL\n"); + args_len = PyTuple_Size(init_args); + DEBUG_PRINT("`args_len`: %zd\n", args_len); + Py_ssize_t combined_args_len = 1 + args_len; + DEBUG_PRINT("`combined_args_len`: %zd\n", combined_args_len); + args_combined = PyTuple_New(combined_args_len); + DEBUG_PRINT("`args_combined`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(args_combined))); + if (args_combined == NULL) { Py_XDECREF(init_args); Py_XDECREF(init_kwargs); - return NULL; + Py_DECREF(result); + DEBUG_PRINT("`args_combined` is NULL\n"); + return NULL; // Use return NULL instead of Py_RETURN_NONE } - DEBUG_PRINT("`item`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(item))); - Py_INCREF(item); // Increase reference count + // Set the first item of the new tuple to `result` PyTuple_SET_ITEM(args_combined, - i + 1, - item); // `item` is now owned by `args_combined` + 0, + result); // `result` is now owned by `args_combined` + + // Copy items from `init_args` to `args_combined` + for (Py_ssize_t i = 0; i < args_len; i++) { + PyObject* item = PyTuple_GetItem(init_args, i); // Borrowed reference + if (item == NULL) { + DEBUG_PRINT("`item` is NULL\n"); + Py_DECREF(args_combined); + Py_XDECREF(init_args); + Py_XDECREF(init_kwargs); + return NULL; + } + DEBUG_PRINT("`item`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(item))); + Py_INCREF(item); // Increase reference count + PyTuple_SET_ITEM(args_combined, + i + 1, + item); // `item` is now owned by `args_combined` + } + DEBUG_PRINT("`args_combined`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(args_combined))); } - DEBUG_PRINT("`args_combined`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(args_combined))); + + if (init_args == NULL || init_kwargs == NULL) { + DEBUG_PRINT("`init_args` or `init_kwargs` is NULL\n"); + }; if (init_kwargs != NULL) { DEBUG_PRINT("`init_kwargs`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); }; - // Call the function or constructor + // If `args_combined` is NULL, create a new tuple with one item + // and set `result` as the first item of the tuple + if (init_args == NULL) { + DEBUG_PRINT("`init_args` is NULL\n"); + + if (PyObject_SetAttrString( + self->obj, NEWTYPE_INIT_ARGS_STR, PyTuple_New(0)) + < 0) + { + result = NULL; + goto done; + } + if (PyObject_SetAttrString( + self->obj, NEWTYPE_INIT_KWARGS_STR, PyDict_New()) + < 0) + { + result = NULL; + goto done; + } + // goto done; + DEBUG_PRINT("`args_combined` is NULL\n"); + DEBUG_PRINT("`init_kwargs`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); + DEBUG_PRINT("`self->wrapped_cls`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(self->wrapped_cls))); + + super_class_new_args = PyTuple_New(2); + Py_XINCREF(self->wrapped_cls); + PyTuple_SET_ITEM(super_class_new_args, 0, self->wrapped_cls); + Py_XINCREF(self->obj); + PyTuple_SET_ITEM(super_class_new_args, 1, self->obj); + if (super_class_new_args == NULL) { + DEBUG_PRINT("`super_class_new_args` is NULL\n"); + Py_XDECREF(result); + Py_XDECREF(self->obj); + Py_XDECREF(args_combined); + return NULL; + }; + + if (!PyTuple_CheckExact(super_class_new_args)) { + DEBUG_PRINT("`super_class_new_args` is not a tuple\n"); + Py_DECREF(super_class_new_args); + Py_DECREF(result); + Py_DECREF(self->obj); + Py_DECREF(args_combined); + return NULL; + } else { + DEBUG_PRINT("`super_class_new_args` is a tuple\n"); + // Check if the first item is the wrapped class + if (Py_Is(PyTuple_GetItem(super_class_new_args, 0), self->wrapped_cls)) + { + DEBUG_PRINT( + "`super_class_new_args`'s first item is the wrapped class\n"); + } else { + DEBUG_PRINT( + "`super_class_new_args`'s first item is not the wrapped class\n"); + } + // Check if the second item is the object + if (Py_Is(PyTuple_GetItem(super_class_new_args, 1), self->obj)) { + DEBUG_PRINT("`super_class_new_args`'s second item is the object\n"); + } else { + DEBUG_PRINT( + "`super_class_new_args`'s second item is not the object\n"); + } + // Check length of `super_class_new_args` + if (PyTuple_Size(super_class_new_args) == 2) { + DEBUG_PRINT("`super_class_new_args` has 2 items\n"); + } else { + DEBUG_PRINT("`super_class_new_args` has %zd items\n", + PyTuple_Size(super_class_new_args)); + } + DEBUG_PRINT("WHAT THE FUCK\n"); + + if (super_class_new_args == NULL) { + DEBUG_PRINT("`super_class_new_args` is NULL\n"); + Py_XDECREF(result); + Py_XDECREF(self->obj); + Py_XDECREF(args_combined); + return NULL; + } else { + DEBUG_PRINT("`super_class_new_args` is not NULL\n"); + Py_XINCREF(super_class_new_args); + DEBUG_PRINT("WHAT THE FUCK\n"); + } + DEBUG_PRINT("WHAT THE FUCK\n"); + DEBUG_PRINT("`self`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr((PyObject*)self))); + PyObject* super_inst = + PyObject_CallFunctionObjArgs((PyObject*)&PySuper_Type, + Py_TYPE(self->obj), + (PyObject*)self->obj, + NULL); + if (super_inst == NULL) { + DEBUG_PRINT("`super_inst` is NULL\n"); + if (PyErr_Occurred()) { + PyErr_Print(); + } + Py_XDECREF(result); + Py_XDECREF(self->obj); + Py_XDECREF(args_combined); + return NULL; + } + DEBUG_PRINT("WHAT THE FUCK\n"); + + PyObject* wrapped_cls_inst = PyObject_CallFunctionObjArgs( + self->wrapped_cls, + self->obj, + init_kwargs == NULL ? init_args : PyTuple_New(0), + init_kwargs, + NULL); + if (wrapped_cls_inst == NULL) { + DEBUG_PRINT("`wrapped_cls_inst` is NULL\n"); + Py_XDECREF(result); + Py_XDECREF(self->obj); + Py_XDECREF(args_combined); + return NULL; + } else { + DEBUG_PRINT("`wrapped_cls_inst` is not NULL\n"); + DEBUG_PRINT("`wrapped_cls_inst`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(wrapped_cls_inst))); + } + + args_combined = PyTuple_New(1); // Allocate tuple with one element + // Py_INCREF(self->obj); + // PyTuple_SET_ITEM(args_combined, 0, self->obj); + Py_INCREF(result); + PyTuple_SET_ITEM(args_combined, 0, result); + DEBUG_PRINT("`args_combined`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(args_combined))); + // Tackle this might be easier + // FAILED examples/newtype_enums.py::test_nt_env_replace - TypeError: + // > cannot extend + new_inst = + PyObject_Call((PyObject*)self->cls, args_combined, init_kwargs); + if (new_inst == NULL) { + DEBUG_PRINT("`new_inst` is NULL\n"); + Py_DECREF(result); + Py_DECREF(self->obj); + Py_DECREF(args_combined); + return NULL; + } + Py_DECREF(result); + Py_DECREF(self->obj); + Py_DECREF(args_combined); + DEBUG_PRINT("`new_inst`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(new_inst))); + return new_inst; + } + } + new_inst = PyObject_Call((PyObject*)self->cls, args_combined, init_kwargs); // Clean up - Py_DECREF(args_combined); // Decrement reference count of `args_combined` + Py_XDECREF(args_combined); // Decrement reference count of `args_combined` Py_XDECREF(init_args); Py_XDECREF(init_kwargs); - // Ensure proper error propagation - if (new_inst == NULL) { - return NULL; - } + DEBUG_PRINT("`new_inst`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(new_inst))); // Only proceed if we have all required objects and dictionaries if (self->obj != NULL && result != NULL && new_inst != NULL @@ -446,6 +605,7 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, done: Py_XINCREF(result); + DEBUG_PRINT("DONE! `result`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(result))); return result; } From b8ed3fe48f3eb6d9dc5250e88fe9517b724385e2 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Feb 2025 18:16:38 +0100 Subject: [PATCH 3/8] cleaned up the codes --- newtype/extensions/newtype_meth.c | 139 ++++-------------------------- 1 file changed, 15 insertions(+), 124 deletions(-) diff --git a/newtype/extensions/newtype_meth.c b/newtype/extensions/newtype_meth.c index db73859..eee2474 100644 --- a/newtype/extensions/newtype_meth.c +++ b/newtype/extensions/newtype_meth.c @@ -151,7 +151,6 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, PyObject *init_args, *init_kwargs; PyObject* new_inst; PyObject* args_combined = NULL; - PyObject* super_class_new_args = NULL; Py_ssize_t args_len = 0; if (self->obj == NULL) { @@ -264,135 +263,27 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, result = NULL; goto done; } - // goto done; - DEBUG_PRINT("`args_combined` is NULL\n"); - DEBUG_PRINT("`init_kwargs`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(init_kwargs))); - DEBUG_PRINT("`self->wrapped_cls`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(self->wrapped_cls))); - - super_class_new_args = PyTuple_New(2); - Py_XINCREF(self->wrapped_cls); - PyTuple_SET_ITEM(super_class_new_args, 0, self->wrapped_cls); - Py_XINCREF(self->obj); - PyTuple_SET_ITEM(super_class_new_args, 1, self->obj); - if (super_class_new_args == NULL) { - DEBUG_PRINT("`super_class_new_args` is NULL\n"); - Py_XDECREF(result); - Py_XDECREF(self->obj); - Py_XDECREF(args_combined); - return NULL; - }; - if (!PyTuple_CheckExact(super_class_new_args)) { - DEBUG_PRINT("`super_class_new_args` is not a tuple\n"); - Py_DECREF(super_class_new_args); + args_combined = PyTuple_New(1); // Allocate tuple with one element + Py_INCREF(result); + PyTuple_SET_ITEM(args_combined, 0, result); + DEBUG_PRINT("`args_combined`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(args_combined))); + new_inst = + PyObject_Call((PyObject*)self->cls, args_combined, init_kwargs); + if (new_inst == NULL) { + DEBUG_PRINT("`new_inst` is NULL\n"); Py_DECREF(result); Py_DECREF(self->obj); Py_DECREF(args_combined); return NULL; - } else { - DEBUG_PRINT("`super_class_new_args` is a tuple\n"); - // Check if the first item is the wrapped class - if (Py_Is(PyTuple_GetItem(super_class_new_args, 0), self->wrapped_cls)) - { - DEBUG_PRINT( - "`super_class_new_args`'s first item is the wrapped class\n"); - } else { - DEBUG_PRINT( - "`super_class_new_args`'s first item is not the wrapped class\n"); - } - // Check if the second item is the object - if (Py_Is(PyTuple_GetItem(super_class_new_args, 1), self->obj)) { - DEBUG_PRINT("`super_class_new_args`'s second item is the object\n"); - } else { - DEBUG_PRINT( - "`super_class_new_args`'s second item is not the object\n"); - } - // Check length of `super_class_new_args` - if (PyTuple_Size(super_class_new_args) == 2) { - DEBUG_PRINT("`super_class_new_args` has 2 items\n"); - } else { - DEBUG_PRINT("`super_class_new_args` has %zd items\n", - PyTuple_Size(super_class_new_args)); - } - DEBUG_PRINT("WHAT THE FUCK\n"); - - if (super_class_new_args == NULL) { - DEBUG_PRINT("`super_class_new_args` is NULL\n"); - Py_XDECREF(result); - Py_XDECREF(self->obj); - Py_XDECREF(args_combined); - return NULL; - } else { - DEBUG_PRINT("`super_class_new_args` is not NULL\n"); - Py_XINCREF(super_class_new_args); - DEBUG_PRINT("WHAT THE FUCK\n"); - } - DEBUG_PRINT("WHAT THE FUCK\n"); - DEBUG_PRINT("`self`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr((PyObject*)self))); - PyObject* super_inst = - PyObject_CallFunctionObjArgs((PyObject*)&PySuper_Type, - Py_TYPE(self->obj), - (PyObject*)self->obj, - NULL); - if (super_inst == NULL) { - DEBUG_PRINT("`super_inst` is NULL\n"); - if (PyErr_Occurred()) { - PyErr_Print(); - } - Py_XDECREF(result); - Py_XDECREF(self->obj); - Py_XDECREF(args_combined); - return NULL; - } - DEBUG_PRINT("WHAT THE FUCK\n"); - - PyObject* wrapped_cls_inst = PyObject_CallFunctionObjArgs( - self->wrapped_cls, - self->obj, - init_kwargs == NULL ? init_args : PyTuple_New(0), - init_kwargs, - NULL); - if (wrapped_cls_inst == NULL) { - DEBUG_PRINT("`wrapped_cls_inst` is NULL\n"); - Py_XDECREF(result); - Py_XDECREF(self->obj); - Py_XDECREF(args_combined); - return NULL; - } else { - DEBUG_PRINT("`wrapped_cls_inst` is not NULL\n"); - DEBUG_PRINT("`wrapped_cls_inst`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(wrapped_cls_inst))); - } - - args_combined = PyTuple_New(1); // Allocate tuple with one element - // Py_INCREF(self->obj); - // PyTuple_SET_ITEM(args_combined, 0, self->obj); - Py_INCREF(result); - PyTuple_SET_ITEM(args_combined, 0, result); - DEBUG_PRINT("`args_combined`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(args_combined))); - // Tackle this might be easier - // FAILED examples/newtype_enums.py::test_nt_env_replace - TypeError: - // > cannot extend - new_inst = - PyObject_Call((PyObject*)self->cls, args_combined, init_kwargs); - if (new_inst == NULL) { - DEBUG_PRINT("`new_inst` is NULL\n"); - Py_DECREF(result); - Py_DECREF(self->obj); - Py_DECREF(args_combined); - return NULL; - } - Py_DECREF(result); - Py_DECREF(self->obj); - Py_DECREF(args_combined); - DEBUG_PRINT("`new_inst`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(new_inst))); - return new_inst; } + Py_DECREF(result); + Py_DECREF(self->obj); + Py_DECREF(args_combined); + DEBUG_PRINT("`new_inst`: %s\n", + PyUnicode_AsUTF8(PyObject_Repr(new_inst))); + return new_inst; } new_inst = PyObject_Call((PyObject*)self->cls, args_combined, init_kwargs); From 66439aabbe1b0e55807877b8f0a223bc78f1c325 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Feb 2025 18:52:35 +0100 Subject: [PATCH 4/8] solved the slots issue and enum issue together --- examples/newtype_enums.py | 7 ------ newtype/newtype.py | 9 ++++--- tests/test_newtype_enums.py | 47 +++++++++++++++++++++++++++++++++++++ tests/test_slots.py | 6 +++++ 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 tests/test_newtype_enums.py diff --git a/examples/newtype_enums.py b/examples/newtype_enums.py index e81790b..b340a60 100644 --- a/examples/newtype_enums.py +++ b/examples/newtype_enums.py @@ -14,13 +14,6 @@ class ENV(NewType(str), Enum): # type: ignore[misc] PREPROD = "PREPROD" PROD = "PROD" -print(type(ENV.LOCAL)) -print(type(ENV.LOCAL).__mro__) -print(super(ENV, ENV.LOCAL).__self_class__) -print(super(ENV, ENV.LOCAL).__thisclass__) -print(super(super(ENV, ENV.LOCAL).__self_class__)) -print(super(ENV)) -print(str.__str__(ENV.LOCAL)) class RegularENV(str, Enum): diff --git a/newtype/newtype.py b/newtype/newtype.py index fe88b89..d2a820f 100644 --- a/newtype/newtype.py +++ b/newtype/newtype.py @@ -180,7 +180,7 @@ class BaseNewType(base_type): # type: ignore[valid-type, misc] if hasattr(base_type, "__slots__"): __slots__ = ( - *base_type.__slots__, + # *base_type.__slots__, NEWTYPE_INIT_ARGS_STR, NEWTYPE_INIT_KWARGS_STR, ) @@ -224,11 +224,14 @@ def __init_subclass__(cls, **init_subclass_context: Any) -> None: and not func_is_excluded(v) ): setattr(cls, k, NewTypeMethod(v, base_type)) + else: if k == "__dict__": continue - # (TODO: Fix enum issue) - # setattr(cls, k, v) + try: + setattr(cls, k, v) + except AttributeError: + continue cls.__init__ = NewTypeInit(constructor) # type: ignore[method-assign] def __new__(cls, value: Any = None, *_args: Any, **_kwargs: Any) -> "BaseNewType": diff --git a/tests/test_newtype_enums.py b/tests/test_newtype_enums.py new file mode 100644 index 0000000..9758742 --- /dev/null +++ b/tests/test_newtype_enums.py @@ -0,0 +1,47 @@ +from enum import Enum + +import pytest +from newtype import NewType + + +class ENV(NewType(str), Enum): # type: ignore[misc] + + LOCAL = "LOCAL" + DEV = "DEV" + SIT = "SIT" + UAT = "UAT" + PREPROD = "PREPROD" + PROD = "PROD" + + +# mypy doesn't raise errors here +def test_nt_env_replace() -> None: + + env = ENV.LOCAL + + assert env is ENV.LOCAL + assert env is not ENV.DEV + assert isinstance(env, ENV) + + # let's say now we want to replace the environment + # nevermind about the reason why we want to do so + env = env.replace(ENV.LOCAL, ENV.DEV) + # reveal_type(env) # Revealed type is "newtype_enums.ENV" + + # replacement is successful + assert env is ENV.DEV + assert env is not ENV.LOCAL + + # still an `ENV` + assert isinstance(env, ENV) + assert isinstance(env, str) + + with pytest.raises(ValueError): + # cannot replace with something that is not a `ENV` + env = env.replace(ENV.DEV, "NotAnEnv") + + # reveal_type(env) # Revealed type is "newtype_enums.ENV" + + with pytest.raises(ValueError): + # cannot even make 'DEV' -> 'dev' + env = env.lower() diff --git a/tests/test_slots.py b/tests/test_slots.py index 42c851d..ccff6fd 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -25,6 +25,9 @@ def test_base_slots(): base = Base("TestName") assert base.name == "TestName" + with pytest.raises(AttributeError): + base.__dict__ + with pytest.raises(AttributeError): base.age = 30 # Should raise an error since 'age' is not a defined slot @@ -35,6 +38,9 @@ def test_derived_slots(): assert derived.name == "TestName" assert derived.age == 25 + with pytest.raises(AttributeError): + derived.__dict__ + with pytest.raises(AttributeError): derived.address = ( "123 Street" # Should raise an error since 'address' is not a defined slot From b3297d7836f05e067b56f4b5923aa0fa5b3dcd3d Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Feb 2025 18:59:16 +0100 Subject: [PATCH 5/8] update tests/build_test_pyvers_docker_images.sh and ready to merge to main ONLY after doing version update --- tests/build_test_pyvers_docker_images.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/build_test_pyvers_docker_images.sh b/tests/build_test_pyvers_docker_images.sh index c440127..804bd6e 100755 --- a/tests/build_test_pyvers_docker_images.sh +++ b/tests/build_test_pyvers_docker_images.sh @@ -2,7 +2,6 @@ # Create logs directory if it doesn't exist mkdir -p ./tests/logs -make build # Build Docker images in parallel with logging docker build -t python-newtype-test-mul-vers:3.8 -f ./tests/Dockerfile-test-py3.8 . > ./tests/logs/py3.8-test.log 2>&1 & From 7cc246ebce82f96a4a24992686a5242d89d4246c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Feb 2025 07:58:12 +0100 Subject: [PATCH 6/8] newtype int enum is done --- Makefile | 5 +- examples/newtype_enums_int.py | 85 +++++++++++++++++++++++++++++++ newtype/extensions/newtype_meth.c | 2 - pyproject.toml | 1 + tests/test_newtype_enums.py | 80 +++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 examples/newtype_enums_int.py diff --git a/Makefile b/Makefile index 1ee8c63..5eac13d 100644 --- a/Makefile +++ b/Makefile @@ -124,9 +124,12 @@ test-str: test-async: $(PYTHON) -m pytest tests/test_async.py $(PYTEST_FLAGS) +test-enums: + $(PYTHON) -m pytest tests/test_newtype_enums.py $(PYTEST_FLAGS) + # Run memory leak tests test-leak: - $(PYTHON) -m pytest --enable-leak-tracking -W error --stacks 10 tests/test_newtype_init.py $(PYTEST_FLAGS) + $(PYTHON) -m pytest --enable-leak-tracking -W error tests/test_newtype_init.py $(PYTEST_FLAGS) # Run a specific test file (usage: make test-file FILE=test_newtype.py) test-file: diff --git a/examples/newtype_enums_int.py b/examples/newtype_enums_int.py new file mode 100644 index 0000000..f78f30d --- /dev/null +++ b/examples/newtype_enums_int.py @@ -0,0 +1,85 @@ +from enum import Enum +from weakref import WeakValueDictionary + +import pytest + +from newtype import NewType + + +class GenericWrappedBoundedInt(NewType(int)): + MAX_VALUE: int = 0 + + __CONCRETE_BOUNDED_INTS__ = WeakValueDictionary() + + def __new__(cls, value: int): + inst = super().__new__(cls, value % cls.MAX_VALUE) + return inst + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(int(self)) + + def __class_getitem__(cls, idx=MAX_VALUE): + if not isinstance(idx, int): + raise TypeError(f"cannot make `BoundedInt[{idx}]`") + + if idx not in cls.__CONCRETE_BOUNDED_INTS__: + + class ConcreteBoundedInt(cls): + MAX_VALUE = idx + + cls.__CONCRETE_BOUNDED_INTS__[idx] = ConcreteBoundedInt + + return cls.__CONCRETE_BOUNDED_INTS__[idx] + + +class Severity(GenericWrappedBoundedInt[5], Enum): + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + CRITICAL = 4 + + +def test_severity(): + assert Severity.DEBUG == 0 + assert Severity.INFO == 1 + assert Severity.WARNING == 2 + assert Severity.ERROR == 3 + assert Severity.CRITICAL == 4 + + with pytest.raises(AttributeError, match=r"[c|C]annot\s+reassign\s+\w+"): + Severity.ERROR += 1 + + severity = Severity.ERROR + assert severity == 3 + + severity += 1 + assert severity == 4 + assert severity != 3 + assert isinstance(severity, int) + assert isinstance(severity, Severity) + assert severity is not Severity.ERROR + assert severity is Severity.CRITICAL + + severity -= 1 + assert severity == 3 + assert severity != 4 + assert isinstance(severity, int) + assert isinstance(severity, Severity) + assert severity is Severity.ERROR + assert severity is not Severity.CRITICAL + + severity = Severity.DEBUG + assert severity == 0 + assert str(severity.value) == "0" + with pytest.raises(ValueError, match=r"\d+ is not a valid Severity"): + severity -= 1 + + severity = Severity.CRITICAL + assert severity == 4 + assert str(severity.value) == "4" + with pytest.raises(ValueError, match=r"\d+ is not a valid Severity"): + severity += 1 diff --git a/newtype/extensions/newtype_meth.c b/newtype/extensions/newtype_meth.c index eee2474..94f2aeb 100644 --- a/newtype/extensions/newtype_meth.c +++ b/newtype/extensions/newtype_meth.c @@ -96,8 +96,6 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self, DEBUG_PRINT("`self->obj` is not NULL\n"); DEBUG_PRINT("`self->wrapped_cls`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(self->wrapped_cls))); - DEBUG_PRINT("`self->obj`: %s\n", - PyUnicode_AsUTF8(PyObject_Repr(self->obj))); func = PyObject_CallFunctionObjArgs( self->func_get, self->obj, self->wrapped_cls, NULL); } diff --git a/pyproject.toml b/pyproject.toml index 3300418..54601a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,7 @@ exclude = [ "examples/newtype_enums.py", "examples/mutable.py", "examples/pydantic-compat.py", + "examples/newtype_enums_int.py", ] [tool.ruff.format] diff --git a/tests/test_newtype_enums.py b/tests/test_newtype_enums.py index 9758742..ab98654 100644 --- a/tests/test_newtype_enums.py +++ b/tests/test_newtype_enums.py @@ -1,4 +1,5 @@ from enum import Enum +from weakref import WeakValueDictionary import pytest from newtype import NewType @@ -45,3 +46,82 @@ def test_nt_env_replace() -> None: with pytest.raises(ValueError): # cannot even make 'DEV' -> 'dev' env = env.lower() + + +class GenericWrappedBoundedInt(NewType(int)): + MAX_VALUE: int = 0 + + __CONCRETE_BOUNDED_INTS__ = WeakValueDictionary() + + def __new__(cls, value: int): + inst = super().__new__(cls, value % cls.MAX_VALUE) + return inst + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(int(self)) + + def __class_getitem__(cls, idx=MAX_VALUE): + if not isinstance(idx, int): + raise TypeError(f"cannot make `BoundedInt[{idx}]`") + + if idx not in cls.__CONCRETE_BOUNDED_INTS__: + + class ConcreteBoundedInt(cls): + MAX_VALUE = idx + + cls.__CONCRETE_BOUNDED_INTS__[idx] = ConcreteBoundedInt + + return cls.__CONCRETE_BOUNDED_INTS__[idx] + + +class Severity(GenericWrappedBoundedInt[5], Enum): + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + CRITICAL = 4 + + +def test_severity(): + assert Severity.DEBUG == 0 + assert Severity.INFO == 1 + assert Severity.WARNING == 2 + assert Severity.ERROR == 3 + assert Severity.CRITICAL == 4 + + with pytest.raises(AttributeError, match=r"[c|C]annot\s+reassign\s+\w+"): + Severity.ERROR += 1 + + severity = Severity.ERROR + assert severity == 3 + + severity += 1 + assert severity == 4 + assert severity != 3 + assert isinstance(severity, int) + assert isinstance(severity, Severity) + assert severity is not Severity.ERROR + assert severity is Severity.CRITICAL + + severity -= 1 + assert severity == 3 + assert severity != 4 + assert isinstance(severity, int) + assert isinstance(severity, Severity) + assert severity is Severity.ERROR + assert severity is not Severity.CRITICAL + + severity = Severity.DEBUG + assert severity == 0 + assert str(severity.value) == "0" + with pytest.raises(ValueError, match=r"\d+ is not a valid Severity"): + severity -= 1 + + severity = Severity.CRITICAL + assert severity == 4 + assert str(severity.value) == "4" + with pytest.raises(ValueError, match=r"\d+ is not a valid Severity"): + severity += 1 From 055d7484e7928d9b2cfda8f1506c2a057da5b4d4 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Feb 2025 07:59:55 +0100 Subject: [PATCH 7/8] newtype int enum is done --- examples/newtype_enums.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/newtype_enums.py b/examples/newtype_enums.py index b340a60..b23dda3 100644 --- a/examples/newtype_enums.py +++ b/examples/newtype_enums.py @@ -73,7 +73,6 @@ def test_nt_env_replace() -> None: # nevermind about the reason why we want to do so env = env.replace(ENV.LOCAL, ENV.DEV) # reveal_type(env) # Revealed type is "newtype_enums.ENV" - print(f"`env`: {env}; type: {type(env)}") # replacement is successful assert env is ENV.DEV From 3bb3419e056b4bf393e492ff5512748d4c379465 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Feb 2025 08:16:01 +0100 Subject: [PATCH 8/8] Release version 0.1.6 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 54601a7..fe7fe4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry_dynamic_versioning.backend" [tool.poetry] name = "python-newtype" -version = "0.1.5" +version = "0.1.6" homepage = "https://github.com/jymchng/python-newtype-dev" repository = "https://github.com/jymchng/python-newtype-dev" license = "MIT"