Skip to content

Commit 21db26e

Browse files
authored
Merge pull request #21 from jymchng/feat/fix-emailstr-slots
tests passed
2 parents a514f06 + 1e00807 commit 21db26e

File tree

10 files changed

+222
-78
lines changed

10 files changed

+222
-78
lines changed

LICENSE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Jim Chng <jimchng@outlook.com>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
2. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ test-leak:
131131
test-file:
132132
$(PYTHON) -m pytest $(FILE) $(PYTEST_FLAGS)
133133

134+
test-e2e:
135+
bash tests/build_test_pyvers_docker_images.sh
136+
134137
# Development workflow targets
135138
dev: clean build test
136139

examples/email_str.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
import pytest
2+
3+
14
import re
25

3-
import pytest
46

57
from newtype import NewType, newtype_exclude
68

79

810
class EmailStr(NewType(str)):
11+
# you can define `__slots__` to save space
12+
__slots__ = (
13+
'_local_part',
14+
'_domain_part',
15+
)
16+
917
def __init__(self, value: str):
10-
super().__init__(value)
18+
super().__init__()
1119
if "@" not in value:
1220
raise TypeError("`EmailStr` requires a '@' symbol within")
1321
self._local_part, self._domain_part = value.split("@")
@@ -98,6 +106,14 @@ def test_emailstr_properties_methods():
98106
assert EmailStr.is_valid_email("invalid-email.com") is False
99107

100108

109+
def test_email_str__slots__():
110+
email = EmailStr("test@example.com")
111+
112+
with pytest.raises(AttributeError):
113+
email.hi = "bye"
114+
assert email.hi == "bye"
115+
116+
101117
# Run the tests
102118
if __name__ == "__main__":
103119
pytest.main([__file__])

examples/email_str_def.png

110 KB
Loading

examples/email_str_init.png

47.3 KB
Loading

examples/email_str_replace.png

56.3 KB
Loading

newtype/newtype.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ def func_is_excluded(func):
111111
return getattr(func, NEWTYPE_EXCLUDE_FUNC_STR, False)
112112

113113

114+
def can_subclass_have___slots__(base_type: T) -> bool:
115+
try:
116+
117+
class _Test(base_type): # type: ignore[valid-type, misc]
118+
__slots__ = ("a", "b")
119+
120+
_Test()
121+
except TypeError:
122+
return False
123+
return True
124+
125+
114126
def NewType(base_type: T, **context: "Dict[str, Any]") -> T: # noqa: N802, C901
115127
"""Create a new type that preserves type information through all operations.
116128
@@ -175,6 +187,12 @@ class BaseNewType(base_type): # type: ignore[valid-type, misc]
175187
NEWTYPE_INIT_ARGS_STR,
176188
NEWTYPE_INIT_KWARGS_STR,
177189
)
190+
else:
191+
if can_subclass_have___slots__(base_type):
192+
__slots__ = (
193+
NEWTYPE_INIT_ARGS_STR,
194+
NEWTYPE_INIT_KWARGS_STR,
195+
)
178196

179197
def __init_subclass__(cls, **context) -> None:
180198
"""Initialize a subclass of BaseNewType.
@@ -190,8 +208,9 @@ def __init_subclass__(cls, **context) -> None:
190208
**context: Additional context for subclass initialization
191209
"""
192210
super().__init_subclass__(**context)
211+
NEWTYPE_LOGGER.debug(f"cls: {cls}")
193212
NEWTYPE_LOGGER.debug(base_type, base_type.__dict__)
194-
NEWTYPE_LOGGER.debug("cls.__dict__: ", cls.__dict__)
213+
NEWTYPE_LOGGER.debug(f"cls.__dict__: {cls.__dict__}")
195214

196215
constructor = cls.__init__
197216
original_cls_dict = {}
@@ -201,19 +220,18 @@ def __init_subclass__(cls, **context) -> None:
201220
for k, v in base_type.__dict__.items():
202221
if callable(v) and (k not in object.__dict__) and (k not in original_cls_dict):
203222
NEWTYPE_LOGGER.debug(
204-
"callable(v) and (k not in object.__dict__) and \
205-
(k not in original_cls_dict) ; k: ",
206-
k,
223+
f"callable(v) and (k not in object.__dict__) and \
224+
(k not in original_cls_dict) ; k: {k}"
207225
)
208226
setattr(cls, k, NewTypeMethod(v, base_type))
209227
elif k not in object.__dict__:
210228
if k == "__dict__":
211229
continue
212230
setattr(cls, k, v)
213231
NEWTYPE_LOGGER.debug("Set")
214-
NEWTYPE_LOGGER.debug("original_cls_dict: ", original_cls_dict)
232+
NEWTYPE_LOGGER.debug(f"original_cls_dict: {original_cls_dict}")
215233
for k, v in original_cls_dict.items():
216-
NEWTYPE_LOGGER.debug("k in original_cls_dict: ", k)
234+
NEWTYPE_LOGGER.debug(f"k in original_cls_dict: {k}")
217235
if (
218236
callable(v)
219237
and k != "__init__"
@@ -228,10 +246,23 @@ def __init_subclass__(cls, **context) -> None:
228246
NEWTYPE_LOGGER.debug("Set")
229247
NEWTYPE_LOGGER.debug("Setting cls.__init__")
230248
cls.__init__ = NewTypeInit(constructor) # type: ignore[method-assign]
231-
if hasattr(cls, "__slots__"):
232-
NEWTYPE_LOGGER.debug("cls.__slots__: ", cls.__slots__)
233249
NEWTYPE_LOGGER.debug("Setting cls.__init__ completed")
234-
NEWTYPE_LOGGER.debug("cls.__dict__: ", cls.__dict__, " at end of __init_subclass__")
250+
# if hasattr(cls, "__slots__"):
251+
# NEWTYPE_LOGGER.debug(f"cls.__slots__: {cls.__slots__}")
252+
# BaseNewType.__slots__ = (
253+
# *(base_type.__slots__ if hasattr(base_type, "__slots__") else ()),
254+
# *cls.__slots__,
255+
# NEWTYPE_INIT_ARGS_STR,
256+
# NEWTYPE_INIT_KWARGS_STR,
257+
# )
258+
# NEWTYPE_LOGGER.debug(
259+
# f"cls.__slots__: {cls.__slots__}, at end of `__init_subclass__`"
260+
# )
261+
# NEWTYPE_LOGGER.debug(f"cls.__dict__: {cls.__dict__}, at end of `__init_subclass__`")
262+
if hasattr(BaseNewType, "__slots__"):
263+
NEWTYPE_LOGGER.debug(
264+
f"BaseNewType.__slots__: {BaseNewType.__slots__}, at end of `__init_subclass__`"
265+
)
235266

236267
def __new__(cls, value=None, *_args, **_kwargs):
237268
"""Create a new instance of BaseNewType.
@@ -244,7 +275,6 @@ def __new__(cls, value=None, *_args, **_kwargs):
244275
*_args: Additional positional arguments
245276
**_kwargs: Additional keyword arguments
246277
"""
247-
NEWTYPE_LOGGER.debug("cls, cls.__new__: ", cls, cls.__new__)
248278
if base_type.__new__ == object.__new__:
249279
NEWTYPE_LOGGER.debug(
250280
"base_type.__new__ == object.__new__; base_type.__new__ = ", base_type.__new__
@@ -272,9 +302,9 @@ def __new__(cls, value=None, *_args, **_kwargs):
272302
if v is not UNDEFINED:
273303
setattr(inst, k, v)
274304
else:
275-
NEWTYPE_LOGGER.debug(
276-
"base_type.__new__ != object.__new__; base_type.__new__ = ", base_type.__new__
277-
)
305+
# NEWTYPE_LOGGER.debug(
306+
# "base_type.__new__ != object.__new__; base_type.__new__ = ", base_type.__new__
307+
# )
278308
inst = cast("BaseNewType", base_type.__new__(cls, value))
279309
return inst
280310

0 commit comments

Comments
 (0)