Skip to main content

Create type hints dynamically with ease

Project description

Dyntypes

Dyntypes is a library that lets you create types based on runtime values, allowing you to create more API patterns that were previously impossible.

How it Works

Python lets you create .pyi type stub files. These are like regular python files, except they're only used by type checkers to see what types a file has. They override the original type definitions whilst not affecting the runtime. They're intended for use in C extensions where features such as docstrings are inaccessible, however they can be used to replace the types of an existing Python file.

Usage Example

import typing

type AssetFilename = str

ASSET_FOLDER = Path("./assets")
def load_asset(name: AssetFilename) -> str:
    with open(f"{ASSET_FOLDER}/filename") as f:
        return f.read()

def generate_types():
    codegen = Codegen()
    asset_names = [file.name for file in ASSET_FOLDER.iterdir()]
    codegen.set_type_alias(AssetName, typing.Literal[*asset_names]) # redefine the alias with a literal of all the file names
    codegen.save() # This writes the type stub files to disk

For examples see the examples folder on GitHub.

Documentation

Codegen()

The main core of the library is the Codegen object. This holds the references to all the types you've defined and lets you save them with .save().

Aliases

Dyntypes supports redefining type aliases in stubs.

This lets create a loosely types alias, and then narrow it in the type stub.c

When redefining type alises you want the base alias to

Alias Example

import typing
type AssetFilename = str

ASSET_FOLDER = Path("./assets")
def load_asset(name: AssetFilename) -> str:
    with open(f"{ASSET_FOLDER}/filename") as f:
        return f.read()

codegen = Codegen()

asset_names = [file.name for file in ASSET_FOLDER.iterdir()]
codegen.set_type_alias(AssetName, typing.Literal[*asset_names])

In this example, we're create a type alias that will store all the valid asset IDs held in a folder.

We define it as the loosest possible type, a string. Then we redefine it in the type stubs as a literal of all the files in that folder.

This means that when using this interface, we'll be able to get autocomplete on what is and isn't a valid filename

Function Overloads

Dyntypes creating overloads for functions for specific use cases.

The order of overloads is also important, they will be checked first to last.

codegen.overload_func(get_user, id="admin", return_type=User)
codegen.overload_func(get_user, id=str, return_type=None)

In this example: we're saying that if the ID is admin it's valid, but any other string should return None.

If we defined this the other way round, the string would be checked first and it would indicate that every string should return None.

Overload Example

import typing

type AssetFilename = str

ASSET_FOLDER = Path("./assets")
def load_asset(name: AssetFilename) -> str:
    with open(f"{ASSET_FOLDER}/filename") as f:
        return f.read()

codegen = Codegen()

asset_names = [file.name for file in ASSET_FOLDER.iterdir()]
codegen.set_type_alias(AssetName, typing.Literal[*asset_names])

In this example, we're create a type alias that will store all the valid asset IDs held in a folder.

We define it as the loosest possible type, a string. Then we redefine it in the type stubs as a literal of all the files in that folder.

This means that when using this interface, we'll be able to get autocomplete on what is and isn't a valid filename/

Notes

Literal Shorthand

As a shorthand, any value type such as string or int will automatically be converted into a Literal, so the following two lines are equivelent.

codegen.overload_func(get_version, return_type=typing.Literal["1.0.0"])
codegen.overload_func(get_version, return_type="1.0.0")

This is performed for: int, str, bytes, bool and None.

Built-in types

Most built-in types support using dynamic values in more ways than you'd expect

union_values = []
union_values.append(str)
union_values.append(int)
t.Union[*types]

first_100_numbers =  list(range(100))
t.Literal[*first_100_numbers]

If IDE complains about using dynamic values in literals, you can put # type: ignore on the same line to suppress them.

Known Issues

Root Level

Dyntypes only supports generating types at the root level of the module, so any function or type alias defined inside a a class will be removed.

def foo():
    type Bar = int # ❌: not in module body
    def bar(): # ❌: not in module body
        ...

class Foo:
    def foo(): # ❌: not in module body
        ...

if True:
    def foo(): # ❌: not in module body
        ...

type Bar = int # ️✅: will work correctly
def bar(): # ️✅: will work correctly
    ...

While support for type hinting objects inside classes may be added in a future version, conditional or nested functions are not planned.

Imported Types

Dyntypes currently doesn't support imported types directly due to limitations with how we convert values into their AST representation

While support for this is planned, it can currently be worked around by defining a type alias.

type MyAlias = t.Any

codegen.set_type_alias(MyAlias, io.BufferedReader)  # ❌: will not work due to import

type BufferedReader = io.BufferedReader
codegen.set_type_alias(MyAlias, BufferedReader)  # ️✅: current workaround

Function Overloads with the argument return_type

Due to the way overload_func is defined, you can't overload an argument named return_type.

This is a deliberate limitation to make using the API easier for the majority cases.

Support may be added for this use case if an actual use case is found.

Project details


Download files

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

Source Distribution

dyntypes-1.0.1.tar.gz (10.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dyntypes-1.0.1-py3-none-any.whl (8.7 kB view details)

Uploaded Python 3

File details

Details for the file dyntypes-1.0.1.tar.gz.

File metadata

  • Download URL: dyntypes-1.0.1.tar.gz
  • Upload date:
  • Size: 10.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.4

File hashes

Hashes for dyntypes-1.0.1.tar.gz
Algorithm Hash digest
SHA256 bbe2803b66cd30b1a2fb6f30714b41a821731d281f7a924351bd4f26e79e4419
MD5 0328e9b5f207999c1cfbe1b599066d0d
BLAKE2b-256 b4ead6164bb4eb38cca75511b4e91dc9b666bfb5da8de521ad7bbc1ad84e3666

See more details on using hashes here.

File details

Details for the file dyntypes-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: dyntypes-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 8.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.4

File hashes

Hashes for dyntypes-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 19c92b2231400827e104fece2caf19c1350a3dfc32e93e9c694a0f8c1a861d87
MD5 96173301d76e75e40d6044339f07e77b
BLAKE2b-256 0c7f44b24406529e9f0740d1300d7fde800e9ad1760d86d1ab4ea06eb8fab481

See more details on using hashes here.

Supported by

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