From fd7f65341310d3cca7f2017371fe4ed7f1c2ef0a Mon Sep 17 00:00:00 2001 From: Juan-Pablo Scaletti Date: Sat, 30 Mar 2024 18:17:29 -0500 Subject: [PATCH] Fix packaging issue --- pyproject.toml | 2 +- src/fodantic/__init__.py | 1 + src/{fodantic.py => fodantic/form.py} | 182 +------------------------- src/fodantic/form_field.py | 131 ++++++++++++++++++ src/fodantic/wrapper.py | 61 +++++++++ 5 files changed, 198 insertions(+), 179 deletions(-) create mode 100644 src/fodantic/__init__.py rename src/{fodantic.py => fodantic/form.py} (54%) create mode 100644 src/fodantic/form_field.py create mode 100644 src/fodantic/wrapper.py diff --git a/pyproject.toml b/pyproject.toml index 807929a..fb103fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "juanpablo@jpscaletti.com"}, diff --git a/src/fodantic/__init__.py b/src/fodantic/__init__.py new file mode 100644 index 0000000..34834a2 --- /dev/null +++ b/src/fodantic/__init__.py @@ -0,0 +1 @@ +from .form import * # noqa diff --git a/src/fodantic.py b/src/fodantic/form.py similarity index 54% rename from src/fodantic.py rename to src/fodantic/form.py index 301c93b..22ac902 100644 --- a/src/fodantic.py +++ b/src/fodantic/form.py @@ -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"] @@ -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 diff --git a/src/fodantic/form_field.py b/src/fodantic/form_field.py new file mode 100644 index 0000000..8ec0f2f --- /dev/null +++ b/src/fodantic/form_field.py @@ -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 diff --git a/src/fodantic/wrapper.py b/src/fodantic/wrapper.py new file mode 100644 index 0000000..2f3fe2c --- /dev/null +++ b/src/fodantic/wrapper.py @@ -0,0 +1,61 @@ +""" +Fodantic +Copyright (c) 2024 Juan-Pablo Scaletti +""" +import typing as t + + +class DataWrapper: + def __init__(self, source: t.Any): + """ + A utility class for wrapping request data and providing a consistent interface + for updating, accessing single values, or lists of values. + + ## Arguments: + + - source: The underlying data source. Can be a Multidict implementation + or a regular dict. + + """ + 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