Skip to main content

Typical: Take Typing Further.

Project description

Typical: Take Typing Further. :duck:

image image image image image codecov Code style: black

Take Typing Further with Typical. Make your annotations work for you.

Quickstart

Typical is exceptionally light-weight (<30KB) 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:

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:

  1. 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)
    
  2. 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.

Documentation

Full documentation coming soon!

Happy Typing :duck:

How to Contribute

  1. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
  2. Create a branch on Github for your issue or fork the repository on GitHub to start making your changes to the master branch.
  3. Write a test which shows that the bug was fixed or that the feature works as expected.
  4. Send a pull request and bug the maintainer until it gets merged and published. :)

Project details


Release history Release notifications | RSS feed

This version

1.3.1

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typical-1.3.1.tar.gz (16.7 kB view details)

Uploaded Source

Built Distribution

typical-1.3.1-py2.py3-none-any.whl (13.3 kB view details)

Uploaded Python 2 Python 3

File details

Details for the file typical-1.3.1.tar.gz.

File metadata

  • Download URL: typical-1.3.1.tar.gz
  • Upload date:
  • Size: 16.7 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

Hashes for typical-1.3.1.tar.gz
Algorithm Hash digest
SHA256 0f6fe8487b4807a0416ea966a9c237efdaec736b24612f0ed34ff816e1f256c4
MD5 28c2776921fde9d4c643c5e31da92382
BLAKE2b-256 3fed4789da44b4edb713b53ee8e928e247f8af8b7b6d347176a1b36451063558

See more details on using hashes here.

File details

Details for the file typical-1.3.1-py2.py3-none-any.whl.

File metadata

  • Download URL: typical-1.3.1-py2.py3-none-any.whl
  • Upload date:
  • Size: 13.3 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

Hashes for typical-1.3.1-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 d7b94f49e83b6cef2fe58a74fb900a419d6a34fabff37b2c37e8c7e00c2d17d6
MD5 c3eeebbf63d251006f9e82edd8de5162
BLAKE2b-256 cc9c03e3b2de80fee0424f1b342aab0e7359980f9af19b4e098c6b5a57c445c7

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page