Skip to content
Merged
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
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions examples/newtype_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ENV(NewType(str), Enum): # type: ignore[misc]
PREPROD = "PREPROD"
PROD = "PROD"


class RegularENV(str, Enum):

LOCAL = "LOCAL"
Expand Down Expand Up @@ -58,6 +59,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:

Expand Down
85 changes: 85 additions & 0 deletions examples/newtype_enums_int.py
Original file line number Diff line number Diff line change
@@ -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"<BoundedInt[MAX_VALUE={self.MAX_VALUE}]: {super().__repr__()}>"

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
4 changes: 4 additions & 0 deletions newtype/extensions/newtype_init.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
136 changes: 102 additions & 34 deletions newtype/extensions/newtype_meth.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ 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)));
func = PyObject_CallFunctionObjArgs(
self->func_get, self->obj, self->wrapped_cls, NULL);
}
Expand Down Expand Up @@ -145,7 +147,9 @@ 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;
Py_ssize_t args_len = 0;

if (self->obj == NULL) {
PyObject* first_elem;
Expand All @@ -158,73 +162,136 @@ 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);
Py_ssize_t combined_args_len = 1 + args_len;
args_combined = PyTuple_New(combined_args_len);
if (args_combined == NULL) {
Py_XDECREF(init_args);
Py_XDECREF(init_kwargs);
Py_DECREF(result);
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) {
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
}
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;
}

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;
}
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
Expand Down Expand Up @@ -427,6 +494,7 @@ static PyObject* NewTypeMethod_call(NewTypeMethodObject* self,

done:
Py_XINCREF(result);
DEBUG_PRINT("DONE! `result`: %s\n", PyUnicode_AsUTF8(PyObject_Repr(result)));
return result;
}

Expand Down
8 changes: 6 additions & 2 deletions newtype/newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -224,10 +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
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":
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -146,6 +146,7 @@ exclude = [
"examples/newtype_enums.py",
"examples/mutable.py",
"examples/pydantic-compat.py",
"examples/newtype_enums_int.py",
]

[tool.ruff.format]
Expand Down
1 change: 0 additions & 1 deletion tests/build_test_pyvers_docker_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand Down
Loading
Loading