A powerful Python library for extending existing types with additional functionality while preserving their original behavior, type information and subtype invariances.
- Type Wrapping: Seamlessly wrap existing Python types with new functionality and preservation of subtype invariances when using methods of supertype
- Custom Initialization: Control object initialization with special handling
- Attribute Preservation: Maintains both
__dict__
and__slots__
attributes - Memory Efficient: Uses weak references for caching
- Debug Support: Built-in debug printing capabilities for development
- Async Support: Full support for asynchronous methods and operations
pip install python-newtype
import pytest
import re
from newtype import NewType, newtype_exclude
class EmailStr(NewType(str)):
# you can define `__slots__` to save space
__slots__ = (
'_local_part',
'_domain_part',
)
def __init__(self, value: str):
super().__init__()
if "@" not in value:
raise TypeError("`EmailStr` requires a '@' symbol within")
self._local_part, self._domain_part = value.split("@")
@newtype_exclude
def __str__(self):
return f"<Email - Local Part: {self.local_part}; Domain Part: {self.domain_part}>"
@property
def local_part(self):
"""Return the local part of the email address."""
return self._local_part
@property
def domain_part(self):
"""Return the domain part of the email address."""
return self._domain_part
@property
def full_email(self):
"""Return the full email address."""
return str(self)
@classmethod
def from_string(cls, email: str):
"""Create an EmailStr instance from a string."""
return cls(email)
@staticmethod
def is_valid_email(email: str) -> bool:
"""Check if the provided string is a valid email format."""
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return re.match(email_regex, email) is not None
def test_emailstr_replace():
"""`EmailStr` uses `str.replace(..)` as its own method, returning an instance of `EmailStr`
if the resultant `str` instance is a value `EmailStr`.
"""
peter_email = EmailStr("[email protected]")
smith_email = EmailStr("[email protected]")
with pytest.raises(Exception):
# this raises because `peter_email` is no longer an instance of `EmailStr`
peter_email = peter_email.replace("[email protected]", "petergmail.com")
# this works because the entire email can be 'replaced'
james_email = smith_email.replace("[email protected]", "[email protected]")
# comparison with `str` is built-in
assert james_email == "[email protected]"
# `james_email` is still an `EmailStr`
assert isinstance(james_email, EmailStr)
# this works because the local part can be 'replaced'
jane_email = james_email.replace("james", "jane")
# `jane_email` is still an `EmailStr`
assert isinstance(jane_email, EmailStr)
assert jane_email == "[email protected]"
def test_emailstr_properties_methods():
"""Test the property, class method, and static method of EmailStr."""
# Test property
email = EmailStr("[email protected]")
# `property` is not coerced to `EmailStr`
assert email.full_email == "<Email - Local Part: test; Domain Part: example.com>"
assert isinstance(email.full_email, str)
# `property` is not coerced to `EmailStr`
assert not isinstance(email.full_email, EmailStr)
assert email.local_part == "test"
assert email.domain_part == "example.com"
# Test class method
email_from_string = EmailStr.from_string("[email protected]")
# `property` is not coerced to `EmailStr`
assert (
email_from_string.full_email
== "<Email - Local Part: classmethod; Domain Part: example.com>"
)
assert email_from_string.local_part == "classmethod"
assert email_from_string.domain_part == "example.com"
# Test static method
assert EmailStr.is_valid_email("[email protected]") is True
assert EmailStr.is_valid_email("invalid-email.com") is False
def test_email_str__slots__():
email = EmailStr("[email protected]")
with pytest.raises(AttributeError):
email.hi = "bye"
assert email.hi == "bye"
For detailed documentation, visit py-nt.asyncmove.com.
- Python 3.8 or higher
- C compiler (for building extensions)
- Development packages:
make install-dev-deps
git clone https://github.com/jymchng/python-newtype-dev.git
cd python-newtype-dev
make build
git clone https://github.com/jymchng/python-newtype-dev.git
cd python-newtype-dev
make install
# Run all tests
make test
# Run with debug output
make test-debug
# Run specific test suite
make test-custom
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
Special thanks to all contributors who have helped shape this project.