Skip to content

confit.draft

MetaDraft

Bases: type

A metaclass for Draft that allows the user to create specify the type the Draft should become when instantiated.

In addition to allowing static typing, this metaclass also provides a way to validate the Draft object when used in combination with pydantic validation.

Examples:

from confit import Draft


@validate_arguments
def make_hi(name, prefix) -> str:
    return prefix + " " + name


@validate_arguments
def print_hi(param: Draft[str]):
    val = param.instantiate(prefix="Hello")
    print(val)


print_hi(make_hi.draft(name="John"))
Source code in confit/draft.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class MetaDraft(type):
    """
    A metaclass for Draft that allows the user to create specify
    the type the Draft should become when instantiated.

    In addition to allowing static typing, this metaclass also
    provides a way to validate the Draft object when used in
    combination with pydantic validation.

    Examples
    --------

    ```python
    from confit import Draft


    @validate_arguments
    def make_hi(name, prefix) -> str:
        return prefix + " " + name


    @validate_arguments
    def print_hi(param: Draft[str]):
        val = param.instantiate(prefix="Hello")
        print(val)


    print_hi(make_hi.draft(name="John"))
    ```
    """

    def __init__(cls, name, bases, dct):
        super().__init__(name, bases, dct)
        cls.type_ = Any

    @functools.lru_cache(maxsize=None)
    def __getitem__(self, item):
        # TODO: allow to specify which parameters will be filled
        # eg. Draft[int, ["name"]] declares the library only allow to fill the
        # "name" parameter
        new_type = MetaDraft(self.__name__, (self,), {})
        new_type.type_ = item
        return new_type

    def validate(cls, value, config=None):
        if not isinstance(value, Draft):
            raise ConfitValidationError(
                [
                    ErrorWrapper(
                        exc=TypeError(f"Expected {cls}, got {value.__class__}"),
                        loc=(),
                    ),
                ],
                model=cls,
                name=cls.__name__,
            )
        actual = value._func
        try:
            return_type = inspect.signature(actual).return_annotation
            if return_type is not inspect.Signature.empty:
                actual = return_type
                cast(Type[cls.type_], actual)
            elif isinstance(actual, type):
                cast(Type[cls.type_], actual)
            else:  # pragma: no cover
                cast(Union[Type[cls.type_], Callable[..., cls.type_]], actual)
        except pydantic.ValidationError as e:
            e = to_legacy_error(e, None)
            e = ConfitValidationError(
                [
                    ErrorWrapper(
                        exc=TypeError(f"Expected {cls}, got {Draft[actual]}"),
                        loc=e.raw_errors[0]._loc,
                    ),
                ],
                model=cls,
                name=cls.__name__,
            )
            raise e
        return value

    def __get_validators__(cls):
        yield cls.validate

    def __get_pydantic_core_schema__(cls, source, handler):
        return core_schema.no_info_plain_validator_function(cls.validate)

    def __repr__(self):
        return f"Draft[{self.type_.__qualname__}]"

    def __instancecheck__(cls, obj):
        if isinstance(type(obj), MetaDraft):
            return obj.type_ == cls.type_ or cls.type_ is Any
        return False

Draft

Bases: Generic[R]

A Draft is a placeholder for a value that has not been instantiated yet, likely because it is missing an argument that will be provided later by the library.

Source code in confit/draft.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
class Draft(Generic[R], metaclass=MetaDraft):
    """
    A Draft is a placeholder for a value that has not been instantiated yet, likely
    because it is missing an argument that will be provided later by the library.
    """

    def __init__(
        self,
        func: Callable[P, R],
        kwargs: Dict[str, Any],
    ):
        self._func = func
        self._kwargs = kwargs

    def instantiate(self, **kwargs) -> R:
        """
        Finalize the Draft object into an instance of the expected type
        using the provided arguments. The new arguments are merged with the
        existing ones, with the old ones taking precedence. The rationale
        for this is that the user makes the Draft, and the library
        completes any missing arguments.
        """
        if not isinstance(self, Draft):
            return self

        # Order matters: priority is given to the kwargs provided
        # by the user, so most likely when the Partial is instantiated
        res = self._func(**{**kwargs, **self._kwargs})
        return res

    def _raise_draft_error(self):
        raise TypeError(
            f"This {self} has not been instantiated "
            f"yet, likely because it was missing an argument."
        )

    def __call__(self, *args, **kwargs):
        self._raise_draft_error()

    def __getattr__(self, name):
        if name.startswith("__"):
            raise AttributeError(name)
        self._raise_draft_error()

    def __repr__(self):
        return f"Draft[{self._func.__qualname__}]"

instantiate(**kwargs)

Finalize the Draft object into an instance of the expected type using the provided arguments. The new arguments are merged with the existing ones, with the old ones taking precedence. The rationale for this is that the user makes the Draft, and the library completes any missing arguments.

Source code in confit/draft.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def instantiate(self, **kwargs) -> R:
    """
    Finalize the Draft object into an instance of the expected type
    using the provided arguments. The new arguments are merged with the
    existing ones, with the old ones taking precedence. The rationale
    for this is that the user makes the Draft, and the library
    completes any missing arguments.
    """
    if not isinstance(self, Draft):
        return self

    # Order matters: priority is given to the kwargs provided
    # by the user, so most likely when the Partial is instantiated
    res = self._func(**{**kwargs, **self._kwargs})
    return res