Skip to content

Commit

Permalink
restructure. optimize
Browse files Browse the repository at this point in the history
  • Loading branch information
MirS0bhan committed Sep 1, 2024
1 parent 8bb1992 commit a501040
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 47 deletions.
64 changes: 64 additions & 0 deletions test/test_benchmark/test_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pydantic
from typing import List, Optional
from timeit import timeit
import random
import string


# Define the Address and User models
class Address(pydantic.BaseModel):
street: str
city: str
zipcode: str


class User(pydantic.BaseModel):
name: str
age: int
address: Address
friends: List[str]
email: Optional[str] = None
phone: Optional[str] = None


# Helper function to generate random strings
def random_string(length=10):
return ''.join(random.choices(string.ascii_letters, k=length))


# Helper function to generate random data for User objects
def generate_random_data(num_objects):
data = []
for _ in range(num_objects):
user_data = {
"name": random_string(),
"age": random.randint(18, 80),
"address": {
"street": random_string(),
"city": random_string(),
"zipcode": random_string(5)
},
"friends": [random_string() for _ in range(5)],
"email": random_string() + "@example.com" if random.choice([True, False]) else None,
"phone": "".join(random.choices(string.digits, k=10)) if random.choice([True, False]) else None
}
data.append(user_data)
return data


# Function to benchmark the creation of User objects using pre-generated data
def benchmark_user_creation(data):
for user_data in data:
user = User(**user_data)


# Run the benchmark
if __name__ == "__main__":
num_objects = 10000 # Number of User objects to create

# Generate the random data first
data = generate_random_data(num_objects)

# Time the creation of User objects using the pre-generated data
time_taken = timeit(lambda: benchmark_user_creation(data), number=1)
print(f"Time taken to create {num_objects} User objects: {time_taken:.2f} seconds")
Empty file added test/test_codebase/dump_type.py
Empty file.
42 changes: 42 additions & 0 deletions test/test_codebase/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import unittest
from typing import List, Optional

from vlidt import BaseModel


# Define a subclass of BaseModel to test the functionality

class Person(BaseModel):
name: str
age: int
hobbies: Optional[List[str]] = None


class TestBaseModel(unittest.TestCase):
def test_valid_data(self):
# Test with valid data
person = Person(name="John", age=30, hobbies=["reading", "cycling"])
self.assertEqual(person.name, "John")
self.assertEqual(person.age, 30)
self.assertEqual(person.hobbies, ["reading", "cycling"])

def test_invalid_type(self):
# Test with invalid data (age should be int, not str)
with self.assertRaises(TypeError):
person = Person(name="John", age="thirty", hobbies=["reading", "cycling"])

def test_optional_field(self):
# Test with None in the optional field
person = Person(name="Jane", age=25, hobbies=None)
self.assertEqual(person.name, "Jane")
self.assertEqual(person.age, 25)
self.assertIsNone(person.hobbies)

def test_missing_required_field(self):
# Test with a missing required field (name)
with self.assertRaises(TypeError):
person = Person(age=30, hobbies=["reading", "cycling"])


if __name__ == "__main__":
unittest.main()
104 changes: 104 additions & 0 deletions test/test_codebase/test_dump_load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import unittest
from typing import List, Optional
from dataclasses import dataclass

# Assuming load and dump are part of your custom implementation
from vlidt import load, dump, BaseModel # Replace `your_module` with the actual module name


class Address(BaseModel):
street: str
city: str
zipcode: str


class User(BaseModel):
name: str
age: int
address: Address
friends: List[str]
email: Optional[str] = None # Optional field
phone: Optional[str] = None # Optional field


class TestSerialization(unittest.TestCase):

def setUp(self):
# Base data without optional fields
self.user_data = {
"name": "John Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"zipcode": "12345"
},
"friends": ["Alice", "Bob"]
}

# Base instance without optional fields(BaseModel)
self.user_instance = User(
name="John Doe",
age=30,
address=Address(
street="123 Main St",
city="Anytown",
zipcode="12345"
),
friends=["Alice", "Bob"]
)

def test_dump(self):
dumped_data = dump(self.user_instance)
#self.assertEqual(dumped_data, self.user_data)


def test_load(self):
loaded_instance = load(User, self.user_data)
self.assertEqual(loaded_instance, self.user_instance)

def test_dump_and_load(self):
dumped_data = dump(self.user_instance)
loaded_instance = load(User, dumped_data)
self.assertEqual(loaded_instance, self.user_instance)

def test_optional_fields(self):
# Data with optional fields included
user_data_with_optional = {
"name": "John Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"zipcode": "12345"
},
"friends": ["Alice", "Bob"],
"email": "john.doe@example.com",
"phone": "123-456-7890"
}

# Instance with optional fields included
user_instance_with_optional = User(
name="John Doe",
age=30,
address=Address(
street="123 Main St",
city="Anytown",
zipcode="12345"
),
friends=["Alice", "Bob"],
email="john.doe@example.com",
phone="123-456-7890"
)

# Dump test
dumped_data = dump(user_instance_with_optional)
self.assertEqual(dumped_data, user_data_with_optional)

# Load test
loaded_instance = load(User, user_data_with_optional)
self.assertEqual(loaded_instance, user_instance_with_optional)


if __name__ == "__main__":
unittest.main()
5 changes: 3 additions & 2 deletions vlidt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .validata import BaseModel
from .base import dump, load
from .base import BaseModel
from .lump import dump, load
from .validation import validate
11 changes: 11 additions & 0 deletions vlidt/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from abc import ABC, abstractmethod


class BaseModelAbc(ABC):
@abstractmethod
def __type__(self):
NotImplemented()

@abstractmethod
def __type__(self):
NotImplemented()
54 changes: 10 additions & 44 deletions vlidt/base.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,21 @@
from typing_validation import validate as vli
from typing import Generator, Any, Type, Dict
from dataclasses import fields, Field, dataclass, is_dataclass
from dataclasses import dataclass

from .abc import BaseModelAbc
from .lump import dump_type, collect_sub_dataclasses
from .validation import validate


@dataclass
class BaseModel:
class BaseModel(BaseModelAbc):
"""Base class for data models that automatically becomes a dataclass."""
def __post_init__(self):
validate(self)

def __init_subclass__(cls, **kwargs):
# every class has inherite BaseModel make it dataclass
dataclass(cls)


def iterate_fields(cls: Type[BaseModel]) -> Generator[Any, Field, None]:
"""Yield field values and Field objects for a given dataclass."""
for fld in fields(cls):
yield getattr(cls, fld.name), fld


def iterate_fields_type(cls: Type[BaseModel]) -> Generator[Any, Field, None]:
"""Yield field values and their corresponding types from a dataclass."""
for value, fld in iterate_fields(cls):
yield getattr(cls, fld.name), fld.type


def validate(cls):
"""Validate each field in a dataclass using the provided validation function."""
return list(map(
lambda x: vli(*x),
iterate_fields_type(cls)
))


def dump(dt: Type[BaseModel]) -> Dict:
"""Convert a dataclass instance into a dictionary representation."""
return {
fld.name: val if not is_dataclass(val) else dump(val) for val, fld in iterate_fields(dt)
}


def load(dt: Type[BaseModel], data: Dict) -> BaseModel:
"""Create a dataclass instance from a dictionary representation."""
kwargs = {
fld.name: data[fld.name] if not is_dataclass(fld.type) else load(fld.type, data[fld.name])
for fld in fields(dt)
}
return dt(**kwargs)
cls.__type__ = dump_type(cls)
cls.__sub_dataclasses__ = collect_sub_dataclasses(cls)


def dump_type(dt: Type[BaseModel]) -> Dict:
"""Return a dictionary mapping field names to their types for a dataclass."""
return {
fld.name: fld.type if not is_dataclass(fld.type) else dump_type(fld.type) for fld in fields(dt)
}
42 changes: 42 additions & 0 deletions vlidt/lump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from .abc import BaseModelAbc as BaseModel

from typing import Type, Dict
from dataclasses import fields, is_dataclass


def dump(dt: Type[BaseModel]) -> Dict:
"""Convert a dataclass instance into a dictionary representation."""
kwarg = dt.__dict__.copy()
for name, typ in dt.__sub_dataclasses__.items():
kn = kwarg[name]
if type(kn) is not dict:
kwarg[name] = dump(kn)
return kwarg


def load(dt: Type[BaseModel], data: Dict) -> BaseModel:
"""Create a dataclass instance from a dictionary representation."""
if is_dataclass(data):
return data
# Check if the dataclass has fields
for name, dtcls in dt.__sub_dataclasses__.items():
data[name] = load(dtcls, data[name])

return dt(**data)


def dump_type(dt: Type[BaseModel]) -> Dict:
"""Return a dictionary mapping field names to their types for a dataclass."""
return {
fld.name: fld.type if not is_dataclass(fld.type) else dump_type(fld.type)
for fld in fields(dt)
}


def collect_sub_dataclasses(dt):
return {
fld.name: fld.type
for fld in filter(lambda fld: is_dataclass(fld.type), fields(dt))
}


15 changes: 15 additions & 0 deletions vlidt/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Generator, Any, Type
from dataclasses import fields, Field


def iterate_fields(cls) -> Generator[Any, Field, None]:
"""Yield field values and Field objects for a given dataclass."""
for fld in fields(cls):
fld.compare
yield getattr(cls, fld.name), fld


def iterate_fields_type(cls) -> Generator[Any, Field, None]:
"""Yield field values and their corresponding types from a dataclass."""
for value, fld in iterate_fields(cls):
yield getattr(cls, fld.name), fld.type
1 change: 0 additions & 1 deletion vlidt/validata.py

This file was deleted.

11 changes: 11 additions & 0 deletions vlidt/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing_validation import validate as vli

from .utils import iterate_fields_type


def validate(cls):
"""Validate each field in a dataclass using the provided validation function."""
return list(map(
lambda x: vli(*x),
iterate_fields_type(cls)
))

0 comments on commit a501040

Please sign in to comment.