Fitting data into types¶
The base feature of typefit is to fit data into Python data structures. By example:
from typing import NamedTuple, Text
from typefit import typefit
class Item(NamedTuple):
id: int
title: Text
item = typefit(Item, {"id": 42, "title": "Item title yay"})
assert item.title == "Item title yay"
It will use typing annotations in order to understand what kind of data structure you expect and then it will create an instance of the specified type based on the provided data.
Fitting Enum¶
Typefit can also be used to fit data to an Enum.
from typefit import typefit
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
item = typefit(Color, 2)
assert item == Color.GREEN
typefit will return the attribute of the enum class if the value is valid or else it will raise a ValueError exception.
Fitting mappings¶
The concept is as simple as that but it can become pretty powerful. Firstly, typing definitions can be recursive, so this would work:
from dataclasses import dataclass, field
from typing import List, Text
from typefit import typefit
@dataclass
class Comment:
text: Text
children: List["Comment"] = field(default_factory=list)
data = {
"text": "Hello",
"children": [
{
"text": "Howdy",
},
{
"text": "Hello to you too",
},
]
}
comment = typefit(Comment, data)
You’ll notice that we switched from typing.NamedTuple
to a
dataclass. Both approaches work, that’s up to you to decide which fits your
needs most.
Note
This example uses a forward reference as type because the Comment
is not defined at the time we need it. This means that Python will have to
be able to resolve this reference later, meaning that this class has to be
importable. If hidden inside a 2-nd order function it won’t work.
Custom field names¶
If you don’t want your fields to bear the exact name as they have in the JSON that you are deserializing, you can specify customized names:
from dataclasses import dataclass, field
from typing import Text
from typefit import typefit, meta, other_field
@dataclass
class Info:
some_thing: Text = field(metadata=meta(source=other_field('someThing')))
x: Info = typefit(Info, {"someThing": "foo"})
assert x.some_thing == "foo"
Parsing narrow types¶
This system also allows you to parse input and coerce it into a more pythonic
object. A typical example for that would be to parse a date. Typefit uses
pendulum
in order to parse dates and will return a Pendulum date time
object.
from typefit import narrows, typefit
data = "2019-01-01T00:00:00Z"
date = typefit(narrows.DateTime, data)
assert date.month == 1
Note
Date narrow types depend on the pendulum
package, however Typefit
doesn’t list it as a dependency. If you want to be able to use those, you
need to install pendulum
for your project.
Narrows are types that will help narrow-down the data you are parsing to a Python function. Of course, you might not want to limit yourself to builtin narrow types.
Custom type¶
You can provide a custom narrow type simply by creating a type whose
constructor will have exactly a single argument which is properly annotated
(with a simple type like int
or Text
but not a generic like Union
or List
).
from typing import Text
from typefit import typefit
class Name:
def __init__(self, full_name: Text):
split = full_name.split(' ')
if len(split) != 2:
raise ValueError('Too many names')
self.first_name, self.last_name = split
name = typefit(Name, "Rémy Sanchez")
print(name.first_name)
print(name.last_name)
Wrapper type¶
However sometimes you just want to wrap a type that already exists but doesn’t have the right arguments in its constructor. That’s the case of the date-related narrows described above. Let’s dissect one.
import pendulum
class TimeStamp(pendulum.DateTime):
def __new__(cls, date: int):
return pendulum.from_timestamp(date)
You’ll probably ask why is there some funny business with __new__
instead
of just created a function that will parse and return the desired value. The
answer is that you’re doing type annotations so you must provide valid types
otherwise you’ll confuse your static type checker, which loses the interest of
annotating types in a first place.
Injection¶
You might not want everything in your objects to be exclusively parsed from the input but also provide some context so that methods implemented on each object can actually interact with the outside world.
Context¶
The first type of injection is context injection. Typically you’ll start by defining a field in your class that needs to be injected:
from dataclasses import dataclass, field
from typing import Text
from typefit import typefit, meta
@dataclass
class Info:
some_thing: Text
other_thing: Text = field(metadata=meta(context='other_thing'))
def do_something(self):
print(self.some_thing)
print(self.other_thing)
Then you can pass a context to the typefit function:
x: Info = typefit(Info, {"some_thing": "foo"}, context={"other_thing": "bar"})
x.do_something()
Let’s note that the context will be provided to all objects recursively and it will not be fitted or anything.
A use-case for this for example could be to provide a database connection or some kind of resource handler to all the fitted objects that need it so that they can have methods of their own.
Root¶
The second type of injection is root injection. This is useful when you want to have a reference to the root object from your children. This allows them to access the full tree and thus to see what happens in their siblings.
That can be useful for example if you parse a configuration file and each object from this configuration file wants to access methods from sibling objects to do some kind of validation or cross-reference.
Here’s an example of how a child can access its siblings:
from dataclasses import dataclass, field
from typing import Text, List
from typefit import typefit, meta
@dataclass
class Child:
name: Text
root: 'Parent' = field(metadata=meta(inject_root=True))
def list_siblings(self):
names = [s.name for s in self.root.children]
print(f"Siblings are named: {', '.join(names)}")
@dataclass
class Parent:
children: List[Child]
family = typefit(Parent, {
"children": [
{"name": "Alice"},
{"name": "Bob"},
{"name": "Charlie"},
]
})
family.children[0].list_siblings()
Reference¶
You’ll find here the reference of the public API. Private function are documented in the source code but might change or disappear without notice. The current API is not stable but will try to change as little as possible before version 1.
Typefit¶
The core typefit module only exposes one function.
- typefit.typefit(t: Type[T] | UnionType, value: Any, context: Mapping[str, Any] | None = None) T ¶
Fits a JSON-decoded value into native Python type-annotated objects.
This uses the default sane settings but it might not be up to your taste. By example, errors will be reported in the logging module using ANSI escape codes for syntactical coloration, however depending on the situation you might not want that.
If you want more flexibility and configuration, you can use the
Fitter
directly.- Parameters:
t –
Type to fit the value into. Currently supported types are:
Simple builtins like
int
,float
,typing.Text
,typing.bool
Enumerations which are subclass of
enum.Enum
.Custom types. The constructor needs to accept exactly one parameter and that parameter should have a typing annotation.
typing.Union
to define several possible typestyping.List
to declare a list and the type of list values
value – Value to be fit into the type
context – Custom context whose values can be injected into fitted object through the use if
typefit.meta.meta()
’s context argument.
- Returns:
If the value fits, a value of the right type is returned.
- Return type:
T
- Raises:
ValueError – When the fitting cannot be done, a
ValueError
is raised.
See also
- class typefit.Fitter(no_unwanted_keys: bool = False, error_reporter: ErrorReporter | None = None, context: Mapping[str, Any] | None = None)¶
Core class responsible for the fitting of objects.
Create an instance with the configuration you want
Use the
fit()
method to do your fittings
Notes
Overall orchestrator of the fitting. A lot of the logic happens in the nodes, but this class is responsible for executing the logic in the right order and also holds configuration.
- __init__(no_unwanted_keys: bool = False, error_reporter: ErrorReporter | None = None, context: Mapping[str, Any] | None = None)¶
Constructs the instance.
- Parameters:
no_unwanted_keys – If set to
True
, it will not be allowed to have unwanted keys when fitting mappings into dataclasses/named tuples.error_reporter – Error reporting for when a validation fails. By default no report is made but you might want to arrange reporting for your needs, otherwise you’re going to be debugging in the blind.
context – Custom context whose values can be injected into fitted object through the use if
typefit.meta.meta()
’s context argument.
- fit(t: Type[T], value: Any) T ¶
Fits data into a type. The data is expected to be JSON-decoded values (strings, ints, bools, etc).
On failure a ValueError will arise and if an error reporter is set it will be sent the node to generate the error report.
Notes
You’ll notice down there some “root_injector” business and might wonder what the fuck is this. We have a feature that allows to inject the “root” object (aka the one that we’re about to return) into marked fields down the line. However all the child objects are built before the root is built itself. As a result, all those fields are initially filled up by “None” and then when we finally have our root object constructed we go back into each of those objects to set correct values.
In order to do this in an orthogonal way, each node has the possibility every time they encounter a place to inject a root field to add a callable (which is obviously gonna be a 2nd-order function) into the root_injectors attribute of this class. Meaning that at this point all we got to do is to call all the root injectors (no need to recurse into anything, yay).
- Parameters:
t – Type you want to fit the value into
value – Value you want to fit into a type
- Raises:
ValueError –
Narrows¶
Narrows are data types that integrate some form of parsing to generate Python objects from more generic types like strings. All those classes accept exactly one argument to their constructor which is the data structure to convert.
- class typefit.narrows.Date(date: str)¶
Parses a date and returns a standard pendulum Date.
- class typefit.narrows.DateTime(date: str)¶
Parses a date/time and returns a standard pendulum DateTime.
- class typefit.narrows.TimeStamp(date: int)¶
Parses a Unix timestamp and returns a standard pendulum DateTime.
Meta¶
The meta module allows to specify meta-information on fields and types in order to affect the way that typefit will deal with them.
- class typefit.meta.Source(value_from_json: Callable[[Mapping[str, Any]], Any], value_to_json: Callable[[str, Any], Dict])¶
Provides a way back and forth to convert the data from and to a JSON structure. Since the conversion from JSON is able to dig into any number of fields from the original mapping, the conversion to JSON will have to produce a dictionary as output, even if it has only one key.
- value_from_json: Callable[[Mapping[str, Any]], Any]¶
Alias for field number 0
- value_to_json: Callable[[str, Any], Dict]¶
Alias for field number 1
- typefit.meta.meta(source: Source | None = None, inject_root: bool = False, context: str | None = None)¶
Generates the field metadata for a field based on what arguments are provided. By example, to source a field into a field with another name in the JSON data, you can do something like:
>>> @dataclass >>> class Foo: >>> x: int = field(metadata=meta(source=other_field('x')))
See also
- Parameters:
source – Source function, that given the mapping as input will provide the value as output. If the value isn’t found in the mapping, a KeyError should arise.
inject_root – Whether or not Typefit should inject the root object into the field. This is useful when you want to have a reference to the root object from a child object.
context – Instead of getting this value from the parsed object, Typefit will inject this key from the context into the field. This is useful when you want to inject a value from the context into the object.