Typical: Take Typing Further.
Project description
Typical: Take Typing Further. :duck:
Take Typing Further with Typical. Make your annotations work for you.
Quickstart
Typical is exceptionally light-weight (<50KB) and has only one dependency - python-dateutil, which it uses to parse date-strings into datetime objects.
In order to install, simply pip3 install typical
and annotate to your
heart's content! :duck:
Motivations
In the world of web-services development, type-safety becomes necessary for the sanity of your code and your fellow developers. This is not to say that static-typing is the solution - When it comes to the external entrypoints to your code, not even a compiler is going to help you.
With Python3, type annotations were introduced. With Python3.7, the library was completely re-written for performance and ease-of-use. Type annotations are here to stay and I couldn't be happier about it.
However, there is one place where annotations fall down. There is no provided path for ensuring the type-safety of your methods, functions, and classes. This means if you're receiving data from an external source, (such as with a web service) you still need to do this work yourself.
Until now.
Automatic, Guaranteed Duck-Typing
Behold, the power of Typical:
>>> import typic
>>>
>>> @typic.al
>>> def multi(a: int, b: int):
... return a * b
...
>>> multi('2', '3')
6
Take it further...
>>> import dataclasses
>>> import enum
>>> import typic
>>>
>>> class DuckType(str, enum.Enum):
... MAL = 'mallard'
... BLK = 'black'
... WHT = 'white'
...
>>> @typic.al
... @dataclasses.dataclass
... class Duck:
... type: DuckType
... name: str
...
>>> donald = Duck('white', 'Donald')
>>> donald.type
<DuckType.WHT: 'white'>
This is all fine and dandy, but can we go... further? :thinking:
>>> class DuckRegistry:
... """A Registry for all the ducks"""
...
... @typic.al
... def __init__(self, *duck: Duck):
... self._reg = {x.name: x for x in duck}
...
... @typic.al
... def add(self, duck: Duck):
... self._reg[duck.name] = duck
...
... @typic.al
... def find(self, name: str):
... """Try to find a duck by its name. Otherwise, try with type."""
... if name not in self._reg:
... matches = [x for x in self._reg.values() if x.type == name]
... if matches:
... return matches[-1] if len(matches) == 1 else matches
... return self._reg[name]
...
>>> registry = DuckRegistry({'type': 'black', 'name': 'Daffy'})
>>> registry.find('Daffy')
Duck(type=<DuckType.BLK: 'black'>, name='Daffy')
>>> registry.add({'type': 'white', 'name': 'Donald'})
>>> registry.find('Donald')
Duck(type=<DuckType.WHT: 'white'>, name='Donald')
>>> registry.add({'type': 'goose', 'name': 'Maynard'})
Traceback (most recent call last):
...
ValueError: 'goose' is not a valid DuckType
What Just Happended Here?
When we wrap a callable with @typic.al
, the wrapper reads the
signature of the callable and automatically coerces the incoming data to
the type which is annotated. This includes varargs (*args
and
**kwargs
). This means that you no longer need to do the work of
converting incoming data yourself. You just need to signal what you
expect the data to be with an annotation and Typical will do the
rest.
The ValueError
we see in the last operation is what we can expect when
attempting to supply an invalid value for the Enum class we used above.
Rather than have to write code to cast this data and handle stuff that's
invalid, you can rest easy in the guarantee that the data you expect is
the data you'll get.
What's Supported?
As of this version, Typical can parse the following inputs into valid Python types and classes:
- JSON
- YAML (if PyYAML is installed)
- Python code (via ast.literal_eval)
- Date-strings and Unix Timestamps (via python-dateutil)
- Custom
NewType
declarations.
Limitations
Forward Refs
A "forward reference" is a reference to a type which has either not yet
been defined, or is not available within the module which the annotation
lives. This is noted by encapsulating the annotation in quotes, e.g.:
foo: 'str' = 'bar'
. Beware of using such syntax in combination with
Typical. Typical makes use of typing.get_type_hints
, which scans the
namespace(s) available to the given object to resolve annotations. If
the annotation is unavailable, a NameError
will be raised. This
behavior is considered valid. If you wish to make use of Typical for
type-coercion, make sure the annotated type is in the namespace of the
object you're wrapping and avoid Forward References if at all possible.
Special Forms
There is a subset of type annotations which are 'suscriptable' - meaning you can specify what other types this annotation may resolve to. In a few of those cases, the intended type for the incoming data is too ambiguous to resolve. The following annotations are special forms which cannot be supported:
- Union
- Any
Because these signal an unclear resolution, Typical will ignore this flavor of annotation, leaving it to the developer to determine the appropriate action.
Updates
New in version 1.1.0: typing.Optional
and typing.ClassVar
are now
supported.
New in version 1.2.0: Values set to annotated attributes are automagically resolved.
New in version 1.3.0:
-
Custom coercers may now be registered, e.g.:
import typic class MyCustomClass: def __init__(self, value): self.value = value @classmethod def factory(cls, value): return cls(value) def custom_class_coercer(value, annotation: MyCustomClass): return annotation.factory(value) def ismycustomclass(obj) -> bool: return obj is MyCustomClass typic.register(custom_class_coercer, ismycustomclass)
-
Squashed a few bugs:
- Nested calls of
Coercer.coerce_value
didn't account for values that didn't need coercion. This sometimes broke evaluation, and definitely resulted in sub-optimal type resolution performance. - In the final attempt to coerce a custom class, calling
typic.evals.safe_eval
could reveal that a value is null. In this case, we should respect whether the annotation was optional. - Sometimes people are using a version of PyYAML that's older than 5.1. We should support that.
- Nested calls of
New in version 1.3.1:
- Improved caching strategy and resolution times.
New in version 1.3.2:
- Resolution time is better than ever.
- Custom Unions are now supported via registering custom coercers with
typic.register
, as a result of raising the priority of user-registered coercers.
New in version 1.4.0:
-
A new wrapper has been added to simplify dataclass usage:
import typic @typic.klass class Foo: bar: str
is equivalent to
import dataclasses import typic @typic.al @dataclasses.dataclass class Foo: bar: str
All standard dataclass syntax is supported with
typic.klass
New in version 1.4.1:
- Fixed a nasty bug in wrapped classes that resulted in infinite recursion.
New in version 1.5.0:
- Frozen dataclasses are now supported.
New in version 1.9.0:
- Introducing
typic.bind
:- An optimized version of
inspect.Signature.bind
which will also coerce inputs given.
- An optimized version of
typic.al
is now up to ~30% faster on wrapped callables.
New in version 1.9.1:
- Squashed a bug that broke annotation resolution when wrapping bound methods of classes.
New in version 1.9.2:
- Added the
delay
keyword-arg to wrappers to allow user to delay annotation resolution until the first call of the wrapped object. - Added the
coerce
keyword-arg totypic.bind
to allow users to bind args without coercing them.
New in version 1.10.0:
- Added the ability to resolve delayed annotations with a
module-level callable, i.e.:
import typic @typic.klass(delay=True) class SomeClass: some_attr: str typic.resolve()
This is useful in more complex typing situations, such as in the library iambic, where a single coercer is registered to handle type coercion for all models. In those cases, you may wish to resolve your annotations after you have registered your coercer.
Documentation
Full documentation coming soon!
Happy Typing :duck:
How to Contribute
- Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
- Create a branch on Github for your issue or fork the repository on GitHub to start making your changes to the master branch.
- Write a test which shows that the bug was fixed or that the feature works as expected.
- Send a pull request and bug the maintainer until it gets merged and published. :)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file typical-1.10.5.tar.gz
.
File metadata
- Download URL: typical-1.10.5.tar.gz
- Upload date:
- Size: 22.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/41.0.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.7.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 707360d74f9e1379a5088e1150f298d8b03c22498c6cdee2a53f45cbdcf2bc0e |
|
MD5 | afa7eeb1af083ea0f0035d6016f1c764 |
|
BLAKE2b-256 | d4f62620617c2b1bdfa157aaf8e2fb0b95da3d1d1f3a8452f27268c66d5f3543 |
File details
Details for the file typical-1.10.5-py2.py3-none-any.whl
.
File metadata
- Download URL: typical-1.10.5-py2.py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/41.0.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.7.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a2f52a63447bfc44bb4112fb08ce8b73b2b5379f99f3a6b9538ba0b0e2d240a1 |
|
MD5 | a80091e89e59ad0a13524dcee38eff35 |
|
BLAKE2b-256 | 980b6172c5af704a1aae94ca9a27286637d21eacd4ec1242ea54aaf70c7673a8 |