Metadata-Version: 2.4
Name: reannotate
Version: 0.1.2
Summary: An extension to annotationlib to assist in creating new annotate functions
Project-URL: Homepage, https://github.com/DavidCEllis/Reannotate
Author: David C Ellis
License-Expression: MIT AND PSF-2.0
License-File: LICENSE
License-File: LICENSE.PSF
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# Reannotate

This library acts as an extension to the new deferred annotations that arrived as part of
PEP-649/749 in Python 3.14.

Its main purpose is to make it possible to manipulate PEP-649/749 annotations in order to
recreate `__annotate__` functions that support all of the new annotations formats. It
should be as simple to manipulate and change annotations to create new `__annotate__`
functions with `reannotate` as it was to manipulate and create new `__annotations__` under
older versions of Python.

It also makes it easy to retrieve annotations and evaluate them individually.

Unlike `Format.FORWARDREF`, `get_deferred_annotations` will always return
`DeferredAnnotation` objects as the values of the annotations dictionary.

## Usage

### Retrieving deferred annotations

`get_deferred_annotations` is provided to retrieve deferred annotations from an annotated
object:

```python
from pprint import pp
from reannotate import get_deferred_annotations

class Example:
    a: int
    b: list[unknown]
    c: str | undefined

annos = get_deferred_annotations(Example)

pp(annos)
```

```python
{'a': DeferredAnnotation('int'),
 'b': DeferredAnnotation('list[unknown]'),
 'c': DeferredAnnotation('str | undefined')}
```

To use the `DeferredAnnotation` objects, they have an `.evaluate()` method that supports
the standard `annotationlib` formats:

```python
from annotationlib import Format

print(annos['a'].evaluate(format=Format.VALUE))
print(annos['b'].evaluate(format=Format.FORWARDREF))
print(annos['c'].evaluate(format=Format.STRING))
```

```python
<class 'int'>
list[ForwardRef('unknown', is_class=True, owner=<class '__main__.Example'>)]
str | undefined
```

If a value is defined at a later point, the annotation can then be evaluated fully.

```python
unknown = float

print(annos['b'].evaluate())
print(annos['b'].is_resolved)  # True if a DeferredAnnotation has been fully evaluated
```

```python
list[float]
True
```

### Creating a new `__annotate__` callable

Instances of the `ReAnnotate` class are intended to act as `__annotate__` callables.

```python
from annotationlib import call_annotate_function, Format
from reannotate import get_deferred_annotations, ReAnnotate

class Example:
    a: int
    b: list[undefined]

annos = get_deferred_annotations(Example)

new_annos = ReAnnotate(annos)

print(call_annotate_function(new_annos, format=Format.FORWARDREF))
```

```python
{'a': <class 'int'>, 'b': list[ForwardRef('undefined', is_class=True, owner=<class '__main__.Example'>)]}
```

### Handling Unions and Generics with forward references

`reannotate` provides `get_origin` and `get_args` functions, analogous to those provided
by `typing` that can get the origin and arguments of genericised annotations even if there
are forward references.

Unlike `typing` the objects will be returned in `DeferredAnnotation` format. This allows
for some of them to be forward references.

Note: This relies on the assumption that the objects in question are types, and as such
`|` indicates a union

```python
from reannotate import get_deferred_annotations, get_origin, get_args

class Example:
    a: undefined | bytes | str
    b: unknown[str]

annos = get_deferred_annotations(Example)
a_anno = annos['a']
b_anno = annos['b']

print(get_origin(a_anno))
print(get_args(a_anno))
print()

print(get_origin(b_anno))
print(get_args(b_anno))
```

```python
DeferredAnnotation('typing.Union')
(DeferredAnnotation('undefined'), DeferredAnnotation('bytes'), DeferredAnnotation('str'))

DeferredAnnotation('unknown')
(DeferredAnnotation('str'),)
```

The primary purpose of these functions is to allow for extracting arguments from generics
to create new annotations. For example, using the argument to `InitVar` as the annotation
for `__init__` in something like dataclasses.

## How does this differ from `Format.FORWARDREF`

### Resolution

The `FORWARDREF` format always attempts to resolve annotations at runtime as far as
possible, this means that the `ForwardRef` objects can be contained inside other objects
and made more difficult to resolve. This resolution makes them unsuitable to use to
generate new `__annotate__` callables.

```python
from annotationlib import get_annotations, Format
from reannotate import get_deferred_annotations

class Example:
    a: list[ref]

print(get_annotations(Example, format=Format.FORWARDREF)['a'])
print(get_deferred_annotations(Example)['a'])
```

```python
list[ForwardRef('ref', is_class=True, owner=<class '__main__.Example'>)]
DeferredAnnotation('list[ref]')
```

In this case if `ref` is defined later, the `DeferredAnnotation` can be resolved using
`.evaluate()`, but resolving the annotation from the `ForwardRef` format requires
evaluating the reference inside the `GenericAlias` for `list`.

`DeferredAnnotation` also keeps the full string for the annotation and as such can be used
to generate new `STRING` format annotations.

## Use case examples

### A 'type' attribute on dataclass-like fields that evaluates

With Python 3.14 annotations, dataclasses can now accept forward references without
needing to use `__future__` annotations.

Take for example a self referential class:

```python
from dataclasses import dataclass, fields

@dataclass
class Example:
    examples: list[Example]
```

While this now works, the dataclass 'field' for 'examples' is fixed with the forward
reference contained in the 'type' attribute.

```python
examples_field = fields(Example)[0]
print(examples_field.type)
```

Output:

```python
list[ForwardRef('Example', is_class=True, owner=<class '__main__.Example'>)]
```

Using `reannotate`, this can be avoided. Here is the same example but using
[ducktools-classbuilder](https://github.com/DavidCEllis/ducktools-classbuilder) instead of
`dataclasses`:

```python
from ducktools.classbuilder.prefab import get_attributes, prefab

@prefab
class Example:
    examples: list[Example]

examples_attribute = get_attributes(Example)['examples']
print(examples_attribute.type)
```

Output:

```python
list[__main__.Example]
```

This is because internally, `ducktools-classbuilder` uses reannotate's
`get_deferred_annotations` instead of `Format.FORWARDREF` and evaluates them only when
`.type` is accessed.

### Adding fields automatically to a dataclass

With the new annotations in Python 3.14 it is no longer always possible to retrieve
`__annotations__`. To correctly handle inserting a field into a dataclass it is necessary
to create a new `__annotate__` function.

Using `get_deferred_annotations` and `ReAnnotate`, this can now be done in a similar
fashion as it was possible prior to Python 3.14.

```python
from annotationlib import get_annotations, Format
from dataclasses import dataclass, field
from functools import wraps

from reannotate import get_deferred_annotations, ReAnnotate

def debug_dataclass(cls):
    # Gets all annotations in an unevaluated format
    annos = get_deferred_annotations(cls)

    # Standard objects can be provided and will be converted to `DeferredAnnotation` values
    annos |= {"_used_kwargs": dict[str, object]}

    # ReAnnotate instances are callables that replace the `__annotate__` function
    cls.__annotate__ = ReAnnotate(annos)
    cls._used_kwargs = field(init=False, repr=False, compare=False)

    new_cls = dataclass(cls, slots=True)
    dc_init = new_cls.__init__

    @wraps(dc_init)
    def new_init(self, *args, **kwargs):
        dc_init(self, *args, **kwargs)
        self._used_kwargs = kwargs

    new_cls.__init__ = new_init

    return new_cls

@debug_dataclass
class Example:
    answer: int = 42
    name: str = "Zaphod"
    mystery: Unknown = field(default=None, repr=False)

print(Example()._used_kwargs)  # {}
print(Example(54, name="Dent")._used_kwargs)  # {'name': 'Dent'}

# Define Unknown here and it will allow the annotations to evaluate
Unknown = None | str
print(get_annotations(Example))
# {'answer': <class 'int'>, 'name': <class 'str'>, 'mystery': None | str, '_used_kwargs': dict[str, object]}
```

### Checking which annotations can be evaluated

With the `FORWARDREF` format, it is not simple to know which annotations would fail to
evaluate as forward references can be contained in other arbitrary objects.

`DeferredAnnotation` instances have an `.is_resolved` property which indicates if the
annotation has been fully evaluated.

```python
from annotationlib import Format
from reannotate import get_deferred_annotations

def f(a: str, b: list[undefined]): ...

annos = get_deferred_annotations(f)

print(annos['a'].evaluate(format=Format.FORWARDREF))  # <class 'str'>
print(annos['a'].is_resolved)  # True
print(annos['b'].evaluate(format=Format.FORWARDREF))  # list[ForwardRef('undefined', ...)]
print(annos['b'].is_resolved)  # False
```

## What about...

### Metaclasses

`call_annotate_deferred` is provided to retrieve deferred annotations in the same way that
`call_annotate_function` is used to retrieve standard annotations.

### `__future__` annotations

Deferred annotations are intended to act like regular annotations when called with the
standard annotation evaluation methods in order to create new `__annotate__` functions
that behave like the original.

If `__future__` annotations are used, `get_deferred_annotations` will still get
`DeferredAnnotation` objects, but all formats will evaluate to strings, as they do for
`__future__` annotations with `annotationlib.get_annotations`.

### Literal string annotations

Literal strings in annotations are treated as if they are from `__future__` annotations.
They will not have an associated evaluation context to prevent accidental attempts at
evaluation. This is done to be consistent with how they would be returned from
`get_annotations` without `eval_str`.

### Type Aliases

Like `get_annotations`, type aliases inside `DeferredAnnotation` objects will not be
evaluated.

```python
from reannotate import get_deferred_annotations

type Vector = list[float]

def f(v: Vector): ...

v_anno = get_deferred_annotations(f)['v']
print(v_anno.evaluate())  # Vector
```

## What about getting this in the stdlib?

Ideally I would like to get this kind of functionality from the stdlib, as currently it
relies on a number of private functions from `annotationlib`. This started as a fork of
`annotationlib` to add deferred annotations as a `Format` supported directly.

As part of changing this to a third party module, the `Format` enum value was dropped and
`make_annotate_function` created `__annotate__` functions are replaced with the
`ReAnnotate` callable in order to support retrieving deferred annotations from generated
`annotate` callables.

You can read
[this discourse thread](https://discuss.python.org/t/add-a-format-deferred-option-for-pep-649-749-annotations/104001)
for the origins of this.
