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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions