Skip to content

Commit

Permalink
Fix packaging issue
Browse files Browse the repository at this point in the history
  • Loading branch information
jpsca committed Mar 30, 2024
1 parent ea597b4 commit fd7f653
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 179 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ requires = ["setuptools"]

[project]
name = "fodantic"
version = "0.0.1"
version = "0.0.2"
description = "Pydantic-based HTTP forms"
authors = [
{name = "Juan-Pablo Scaletti", email = "[email protected]"},
Expand Down
1 change: 1 addition & 0 deletions src/fodantic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .form import * # noqa
182 changes: 4 additions & 178 deletions src/fodantic.py → src/fodantic/form.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Fodantic
Copyright (c) Juan-Pablo Scaletti
Copyright (c) 2024 Juan-Pablo Scaletti
"""

import typing as t

from pydantic import BaseModel, ValidationError
from pydantic.fields import FieldInfo
from pydantic_core import ErrorDetails

from .form_field import FormField
from .wrapper import DataWrapper


__all__ = ["formable", "Form"]

Expand Down Expand Up @@ -199,181 +200,6 @@ def _get_model_value(self, name: str) -> t.Any:
return getattr(self.model, name, None)


class DataWrapper:
def __init__(self, source: t.Any):
"""
A utility class for wrapping request data and providing uniform access methods.
This class abstracts away the differences between various request data sources, providing
a consistent interface for accessing single values or lists of values.
## Arguments:
- source (Any): The underlying data source.
"""
self.source = source
self.get = self._get_get_method()
self.getall = self._get_getall_method()

def update(self, data: dict[str, t.Any]) -> t.Any:
if hasattr(self.source, "update"):
self.source.update(data)
else:
for key, value in data.items():
setattr(self.source, key, value)

return self.source

def _get_get_method(self) -> t.Callable[[str], t.Any]:
if self.source and hasattr(self.source, "get"):
return self.source.get

def get_fallback(name: str) -> t.Any:
if not self.source:
return None
return getattr(self.source, name)

return get_fallback

def _get_getall_method(self) -> t.Callable[[str], list[t.Any]]:
if self.source:
# WebOb, Bottle, and Proper uses `getall`
if hasattr(self.source, "getall"):
return self.source.getall
# Django, Flask (Werkzeug), cgi.FieldStorage, etc. uses `getlist`
if hasattr(self.source, "getlist"):
return self.source.getlist

def getall_fallback(name: str) -> list[t.Any]:
if not self.source:
return []
values = self.get(name)
if values is None:
return []
return [values]

return getall_fallback


class FormField:
def __init__(self, *, name: str, info: FieldInfo, form: Form):
"""
A form field
## Attributes:
- model_name:
The name used in the pydantic Model (different than the one in the HTML form if
a prefix is used).
- is_required:
Whether the field is required or optional.
- is_multiple:
Whether the field expects a list of values instead of just one.
- annotation:
The type annotation of the field.
- default:
The default value of the field.
- default_factory:
The factory function used to construct the default for the field.
- alias:
The alias name of the field.
- alias_priority:
The priority of the field's alias.
- validation_alias:
The validation alias of the field.
- serialization_alias:
The serialization alias of the field.
- title:
The title of the field.
- description:
The description of the field.
- examples:
List of examples of the field.
"""
self._form = form

self.model_name = name
self.is_required = info.is_required()
self.annotation_origin = t.get_origin(info.annotation)
self.is_multiple = self.annotation_origin in (list, tuple)

self.annotation = info.annotation
self.default = info.default
self.default_factory = info.default_factory
self.alias = info.alias
self.alias_priority = info.alias_priority or 2
self.validation_alias = info.validation_alias
self.serialization_alias = info.serialization_alias
self.title = info.title
self.description = info.description
self.examples = info.examples

self.error: ErrorDetails | None = None

_str = ", ".join(str(info).replace(" required", " is_required").split(" "))
self._str = f"name='{self.name}', {_str}"

def __repr__(self) -> str:
return f"FormField({self._str})"

@property
def name(self) -> str:
return f"{self._form.prefix}{self.model_name}"

@property
def alias_name(self) -> str:
if self.alias:
return f"{self._form.prefix}{self.alias}"
return ""

@property
def value(self) -> t.Any:
model_value = self._form._get_model_value(self.model_name)
if model_value is None:
return [] if self.is_multiple else ""
return model_value

def extract_value(self, reqdata: t.Any) -> str | bool | None | list[str]:
return (
self._extract_many(reqdata)
if self.is_multiple
else self._extract_one(reqdata)
)

def get_default(self) -> t.Any:
if self.default_factory:
return self.default_factory()
return self.default

# Private

def _extract_one(self, reqdata: t.Any) -> str | bool | None:
value = reqdata.get(self.name)
alias_value = reqdata.get(self.alias_name)
if self.alias_priority > 1:
value, alias_value = alias_value, value

extracted = alias_value if value is None else value

if self.annotation == bool or self.annotation_origin == bool:
if extracted is None:
return False
if extracted == "":
return True
return bool(extracted)

return extracted

def _extract_many(self, reqdata: t.Any) -> list[str]:
value = reqdata.getall(self.name)
alias_value = reqdata.getall(self.alias_name)
if self.alias_priority > 1:
value, alias_value = alias_value, value

return alias_value if value == [] else value


class FormableBaseModel(BaseModel):
@classmethod
Expand Down
131 changes: 131 additions & 0 deletions src/fodantic/form_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Fodantic
Copyright (c) 2024 Juan-Pablo Scaletti
"""
import typing as t


if t.TYPE_CHECKING:
from pydantic.fields import FieldInfo
from pydantic_core import ErrorDetails

from .form import Form


class FormField:
def __init__(self, *, name: str, info: "FieldInfo", form: "Form"):
"""
A form field
## Attributes:
- model_name:
The name used in the pydantic Model (different than the one in the HTML form if
a prefix is used).
- is_required:
Whether the field is required or optional.
- is_multiple:
Whether the field expects a list of values instead of just one.
- annotation:
The type annotation of the field.
- default:
The default value of the field.
- default_factory:
The factory function used to construct the default for the field.
- alias:
The alias name of the field.
- alias_priority:
The priority of the field's alias.
- validation_alias:
The validation alias of the field.
- serialization_alias:
The serialization alias of the field.
- title:
The title of the field.
- description:
The description of the field.
- examples:
List of examples of the field.
"""
self._form = form

self.model_name = name
self.is_required = info.is_required()
self.annotation_origin = t.get_origin(info.annotation)
self.is_multiple = self.annotation_origin in (list, tuple)

self.annotation = info.annotation
self.default = info.default
self.default_factory = info.default_factory
self.alias = info.alias
self.alias_priority = info.alias_priority or 2
self.validation_alias = info.validation_alias
self.serialization_alias = info.serialization_alias
self.title = info.title
self.description = info.description
self.examples = info.examples

self.error: "ErrorDetails | None" = None

_str = ", ".join(str(info).replace(" required", " is_required").split(" "))
self._str = f"name='{self.name}', {_str}"

def __repr__(self) -> str:
return f"FormField({self._str})"

@property
def name(self) -> str:
return f"{self._form.prefix}{self.model_name}"

@property
def alias_name(self) -> str:
if self.alias:
return f"{self._form.prefix}{self.alias}"
return ""

@property
def value(self) -> t.Any:
model_value = self._form._get_model_value(self.model_name)
if model_value is None:
return [] if self.is_multiple else ""
return model_value

def extract_value(self, reqdata: t.Any) -> str | bool | None | list[str]:
return (
self._extract_many(reqdata)
if self.is_multiple
else self._extract_one(reqdata)
)

def get_default(self) -> t.Any:
if self.default_factory:
return self.default_factory()
return self.default

# Private

def _extract_one(self, reqdata: t.Any) -> str | bool | None:
value = reqdata.get(self.name)
alias_value = reqdata.get(self.alias_name)
if self.alias_priority > 1:
value, alias_value = alias_value, value

extracted = alias_value if value is None else value

if self.annotation == bool or self.annotation_origin == bool:
if extracted is None:
return False
if extracted == "":
return True
return bool(extracted)

return extracted

def _extract_many(self, reqdata: t.Any) -> list[str]:
value = reqdata.getall(self.name)
alias_value = reqdata.getall(self.alias_name)
if self.alias_priority > 1:
value, alias_value = alias_value, value

return alias_value if value == [] else value
Loading

0 comments on commit fd7f653

Please sign in to comment.