Skip to content

feature: Expose typeguards for Object properties #462

Description

@dangotbanned

Is your feature request related to a problem? Please describe.

I've been working on some extensions which has been fun (thanks for griffe ❤️)

However, a stumbling block has been that the griffe.Object.is_* properties aren't playing nicely with type checkers:

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import griffe


def on_module(mod: griffe.Module) -> None:
    for member in mod.members.values():
        if member.is_class:
            _bases = member.bases
        elif member.is_module:
            _imports_future_annotations = member.imports_future_annotations
Show me red squiggles

Image

Describe the solution you'd like

I like the color red, but not in my IDE.

Luckily, the typing spec has blessed is with (Type Narrowing) which can help avoid them:

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import griffe

if TYPE_CHECKING:
    import sys

    if sys.version_info >= (3, 13):
        from typing import TypeIs
    else:
        from typing_extensions import TypeIs


def is_class(obj: Any) -> TypeIs[griffe.Class]:
    return obj is griffe.Kind.CLASS


def is_module(obj: Any) -> TypeIs[griffe.Module]:
    return obj is griffe.Kind.MODULE


def on_module(mod: griffe.Module) -> None:
    for member in mod.members.values():
        if is_class(member):
            _bases = member.bases
        elif is_module(member):
            _imports_future_annotations = member.imports_future_annotations
Show me happy typing

Image

Note

is_module and is_class are simple examples - the idea can extend to any kind of narrowing you'd use with griffe

Ideally, these guards would be accessible either at the top-level or their own namespace:

  • griffe.is_*
  • griffe.<somewhere-else>.is_*

Examples from elsewhere:

Describe alternatives you've considered

  • Ignore the issue
  • Avoid the issue
    • Use isinstance (or identity checks) inline
      • Can get verbose, fast
    • Rolling your own TypeIs guards
      • This is what I'm doing now
      • In theory, keeping in sync with griffe may be a concern
      • Doing this in a low-overhead way is usually best understood by the author
        • E.g. I'd need to read the source and find out about Kind to know that's a cheaper check than isinstance
But wait, can't we just define these on the existing properties?

I wish 😔, more details here.

In short, you can define a method to be typeguard - but the narrowing applies to the first non-self|cls argument.

The properties don't fit into this narrow window sadly

Additional context

I did a quick search for ignore, and found these guys that demonstrate the issue:

Show examples within griffe

A healthy chunk of mixins.py

if member.is_attribute:
member = cast("Attribute", member)

if not member.is_alias and member.is_class:
_apply_recursively(member, processed) # ty:ignore[invalid-argument-type]

if not member.is_alias and (member.is_module or member.is_class):
_apply_recursively(member, processed) # ty:ignore[invalid-argument-type]

if self.members[name].is_alias:
return self.members[name].target_path # ty:ignore[unresolved-attribute]

while target.is_alias:
if target.path in paths_seen:
raise CyclicAliasError([*paths_seen, target.path])
paths_seen[target.path] = None
target = target.target # ty:ignore[unresolved-attribute]
return target # ty:ignore[invalid-return-type]

and self.members["annotations"].is_alias
and self.members["annotations"].target_path == "__future__.annotations" # ty:ignore[unresolved-attribute]

bases: list[Class] = [base for base in self.resolved_bases if base.is_class] # ty:ignore[invalid-assignment]

Metadata

Metadata

Assignees

Labels

featureNew feature or request

Type

No type

Fields

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