Skip to content

Trouble using PrimaryKey annotation in model #1

@felnne

Description

@felnne

Hi,

I am having trouble creating a Model with a non-automatic primary key using the PrimaryKey type annotation using Python 3.11.

E.g. for this model I want to make the 'file_identifier' column the primary key but the automatic 'id' column/logic is used instead:

from sqlorm import Model, PrimaryKey


class CachedRecord(Model):
    table = "record"

    file_identifier: PrimaryKey[str]
    file_revision: str
    sha1: str

Tracking that logic back, the mapper primary_key property returns None.

I can trace that back to the process_mapped_attributes() method which fails on the 'if annotated type' check (https://github.com/hyperflask/sqlorm/blob/main/src/sqlorm/model.py#L45) because .__class__ equates as a string (both literally and as a type), rather than an annotated type.

Asking AI it's said this is due to a change in Python 3.11+:

You’re hitting Python 3.11’s postponed evaluation of annotations: many annotations are stored as strings, and typing.Annotated no longer appears as t._AnnotatedAlias.
Fix by resolving annotations with typing.get_type_hints(..., include_extras=True) and using typing.get_origin/get_args to detect Annotated and Optional.

I don't fully understand what that's saying but I was able to workaround this problem with a modified version of its suggested code [1].

I imagine that code isn't suitable to use as-is though or if there's a better way to solve the problem. I may very well have done something wrong in my usage.

Apart from this, I've found this library really easy to use, and really closely matches what I've wanted as a database layer, so thanks for realising this.

# extra imports
import sys
from typing import get_type_hints, get_origin, get_args

class ModelMetaclass(abc.ABCMeta):
    # ...

    @staticmethod
    def process_mapped_attributes(dct):
        module_name = dct.get("__module__", None)
        globalns = sys.modules[module_name].__dict__ if module_name and module_name in sys.modules else {}
        raw_annotations = dct.get("__annotations__", {}) or {}
        try:
            annotations = get_type_hints(
                type("Tmp", (), {"__annotations__": raw_annotations}),
                globalns=globalns,
                localns=None,
                include_extras=True,
            )
        except Exception:
            annotations = raw_annotations

        mapped_attrs = {}
        for name, annotation in annotations.items():
            primary_key = False
            nullable = None

            if get_origin(annotation) is t.Annotated:
                ann_args = get_args(annotation)
                base = ann_args[0] if ann_args else annotation
                meta = ann_args[1:] if len(ann_args) > 1 else ()
                primary_key = PrimaryKeyColumn in meta
                annotation = base

        # ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions