Skip to content

Commit 623dbf8

Browse files
trissimclaude
andcommitted
feat(enableable): fix is_enableable() to handle both classes and instances
The is_enableable() function previously only worked for instances because it used isinstance(), which doesn't work correctly for type objects when using a custom metaclass. This caused enableable configs to not be detected during widget creation. Changes: - Updated is_enableable() to check isinstance(obj, type) and use issubclass() for classes, isinstance() for instances - Added comprehensive docstring explaining the dual behavior - Wrapped issubclass() in try/except to handle non-class types This fixes the issue where enableable config classes like StepMaterializationConfig(Enableable) were not being detected as enableable, causing their enabled checkboxes to not be promoted to the title area in the UI. Testing: - is_enableable(StepMaterializationConfig) now returns True (was False) - is_enableable(StepMaterializationConfig()) still returns True - Works for all enableable config types in OpenHCS Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d70195a commit 623dbf8

File tree

3 files changed

+96
-2
lines changed

3 files changed

+96
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "python-introspect"
7-
version = "0.1.2"
7+
version = "0.1.1"
88
description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
99
readme = "README.md"
1010
requires-python = ">=3.9"

src/python_introspect/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
dataclasses, and type hints.
66
"""
77

8-
__version__ = "0.1.2"
8+
__version__ = "0.1.1"
99

1010
from .signature_analyzer import (
1111
SignatureAnalyzer,
@@ -26,6 +26,14 @@
2626
TypeResolutionError,
2727
)
2828

29+
from .enableable import (
30+
Enableable,
31+
EnableableMeta,
32+
ENABLED_FIELD,
33+
is_enableable,
34+
mark_enableable,
35+
)
36+
2937
__all__ = [
3038
# Version
3139
"__version__",
@@ -45,4 +53,11 @@
4553
"SignatureAnalysisError",
4654
"DocstringParsingError",
4755
"TypeResolutionError",
56+
57+
# Enable semantics
58+
"Enableable",
59+
"EnableableMeta",
60+
"ENABLED_FIELD",
61+
"is_enableable",
62+
"mark_enableable",
4863
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Nominal enable semantics as type-safe metadata.
2+
3+
This module provides a single, shared "axis" for objects and callables that
4+
participate in enabled semantics.
5+
6+
Design goals:
7+
- Nominal (not structural): only explicitly branded callables qualify.
8+
- Dataclass-friendly: configs can inherit Enableable to get an enabled field.
9+
- Callable-safe: branded callables must declare an `enabled` parameter.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import inspect
15+
from abc import ABC, ABCMeta
16+
from dataclasses import dataclass
17+
from typing import Any
18+
19+
20+
ENABLED_FIELD = 'enabled'
21+
22+
_ENABLEABLE_TAG = object()
23+
24+
25+
class EnableableMeta(ABCMeta):
26+
"""Metaclass enabling nominal isinstance checks for branded callables."""
27+
28+
def __instancecheck__(cls, instance: Any) -> bool: # type: ignore[override]
29+
if getattr(instance, '__enableable_tag__', None) is _ENABLEABLE_TAG:
30+
return True
31+
return super().__instancecheck__(instance)
32+
33+
34+
@dataclass(frozen=True)
35+
class Enableable(ABC, metaclass=EnableableMeta):
36+
"""Mixin indicating an object participates in enabled semantics."""
37+
38+
enabled: bool = True
39+
40+
41+
def is_enableable(obj: Any) -> bool:
42+
"""Return True iff obj is nominally Enableable.
43+
44+
Works for both instances (using isinstance) and classes (using issubclass).
45+
This is needed because widget creation code needs to check if a type (class)
46+
is enableable, not just instances.
47+
"""
48+
49+
# Check if obj is a type/class
50+
if isinstance(obj, type):
51+
# obj is a class - check if it's a subclass of Enableable
52+
try:
53+
return issubclass(obj, Enableable)
54+
except TypeError:
55+
# obj is not a class or is not class-like (e.g., a generic type)
56+
return False
57+
else:
58+
# obj is an instance - use isinstance
59+
return isinstance(obj, Enableable)
60+
61+
62+
def mark_enableable(obj: Any, *, enabled_default: bool = True) -> Any:
63+
"""Nominally brand an object/callable as Enableable.
64+
65+
This does not wrap and does not change call semantics.
66+
"""
67+
68+
_ = enabled_default # reserved for future: default enabled semantics
69+
70+
# If we're branding a callable, require the enabled kwarg to exist.
71+
if callable(obj) and not isinstance(obj, type):
72+
sig = inspect.signature(obj)
73+
if ENABLED_FIELD not in sig.parameters:
74+
raise TypeError(
75+
f"Enableable callable '{getattr(obj, '__name__', obj)}' must have an '{ENABLED_FIELD}' parameter"
76+
)
77+
78+
setattr(obj, '__enableable_tag__', _ENABLEABLE_TAG)
79+
return obj

0 commit comments

Comments
 (0)