Skip to content

c108.abc

Runtime introspection and type-validation utilities for Python objects.

This module provides lightweight object summaries, deep memory sizing, and flexible attribute search to aid debugging and diagnostics. Includes decorators and inline helpers to validate function parameters and object attributes against type hints.

ObjectInfo dataclass

Summarize an object with its type, size, unit, and human-friendly presentation.

Lightweight, heuristic-based object inspection for quick diagnostics, logging, and REPL exploration. This is designed for simplistic stats and one-line string conversion, NOT a replacement for profiling tools or exact memory analysis.

Prioritizes simplicity and readability over precision. Deep size calculation is opt-in due to performance cost on large/nested objects.

Provides a lightweight summary of an object, including its type, a human-oriented size measure, unit labels, and optionally a deep byte size.

Attributes:

Name Type Description
type type

The object's type (class for instances, or the type object itself).

size int | float | list[int | float]

Human-oriented measure: - numbers, bytes-like: int (bytes) - str: int (characters) - containers (Sequence/Set/Mapping): int (items_count) - image-like: list[int, int, float] (width, height, megapixels) - class objects: int (attrs_count) - user-defined instances with attrs: list[int, int] (attrs_count, deep)

unit str | list[str]

Unit label(s) matching the structure of size. Note: a plain str is treated as a scalar unit, not a sequence.

deep_size int | None

Deep size in bytes (like pympler.deep_sizeof) computed via c108.abc.deep_sizeof() function for most objects; None for classes or when not computed.

Init vars

fully_qualified (bool): If true, class_name is fully qualified; builtins are never fully qualified.

Raises:

Type Description
ValueError

If size and unit are sequences of different lengths.

See Also

:mod:~.dictify: Comprehensive object-to-dictionary conversion toolkit.

Source code in c108/abc.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
@dataclass(frozen=True)
class ObjectInfo:
    """
    Summarize an object with its type, size, unit, and human-friendly presentation.

    Lightweight, heuristic-based object inspection for quick diagnostics,
    logging, and REPL exploration. This is designed for simplistic stats and one-line
    string conversion, NOT a replacement for profiling tools or exact memory analysis.

    Prioritizes simplicity and readability over precision. Deep size calculation is opt-in
    due to performance cost on large/nested objects.

    Provides a lightweight summary of an object, including its type, a human-oriented
    size measure, unit labels, and optionally a deep byte size.

    Attributes:
        type (type): The object's type (class for instances, or the type object itself).
        size (int | float | list[int|float]): Human-oriented measure:
            - numbers, bytes-like: int (bytes)
            - str: int (characters)
            - containers (Sequence/Set/Mapping): int (items_count)
            - image-like: list[int, int, float] (width, height, megapixels)
            - class objects: int (attrs_count)
            - user-defined instances with attrs: list[int, int] (attrs_count, deep)
        unit (str | list[str]): Unit label(s) matching the structure of size.
            Note: a plain str is treated as a scalar unit, not a sequence.
        deep_size (int | None): Deep size in bytes (like pympler.deep_sizeof) computed
            via c108.abc.deep_sizeof() function for most objects; None for classes or
            when not computed.

    Init vars:
        fully_qualified (bool): If true, class_name is fully qualified; builtins are never fully qualified.

    Raises:
        ValueError: If size and unit are sequences of different lengths.

    See Also:
        :mod:`~.dictify`: Comprehensive object-to-dictionary conversion toolkit.
    """

    type: type
    size: int | float | list[int | float] = field(default_factory=list)
    unit: str | list[str] = field(default_factory=list)
    deep_size: int | None = None

    fully_qualified: InitVar[bool] = False

    def __post_init__(self, fully_qualified: bool):
        """
        Post-initialization validation and options.

        For frozen dataclasses, we must use object.__setattr__() to set attributes.
        """
        # Store fully_qualified using the frozen workaround
        object.__setattr__(self, "_fully_qualified", fully_qualified)

        # Validate runtime logic constraints
        # Both size and unit must be sequences (and not str/bytes) to validate length
        if isinstance(self.size, abc.Sequence) and not isinstance(
            self.size, (str, bytes, bytearray)
        ):
            if isinstance(self.unit, abc.Sequence) and not isinstance(
                self.unit, (str, bytes, bytearray)
            ):
                if len(self.size) != len(self.unit):
                    raise ValueError(
                        f"size and unit must be same length, but got "
                        f"len(size)={len(self.size)}, len(unit)={len(self.unit)}"
                    )

    @classmethod
    def from_object(
        cls, obj: Any, fully_qualified: bool = False, deep_size: bool = False
    ) -> "ObjectInfo":
        """
        Build an ObjectInfo summary of 'obj'.

        Heuristics according to 'obj' type:
          - Numbers: size=N bytes (shallow), unit="bytes".
          - str: size=N chars, unit="chars".
          - bytes/bytearray/memoryview: size=N bytes, unit="bytes".
          - Sequence/Set/Mapping: size=N items, unit="items".
          - Image-like: size=[width, height, Mpx], unit=["width","height","Mpx"].
          - Class (type): size=N attrs, unit="attrs"; deep_size=None.
          - Instance with attrs: size=[N attrs, deep bytes], unit=["attrs","bytes"].
          - Other/no-attrs: size = shallow bytes, unit="bytes"
          - Any obj: get deep size via c108.abc.deep_sizeof() if deep_size=True;
                     None for classes or when deep_size=False.

        Parameters:
          - obj: object to summarize.
          - fully_qualified: whether class_name should be fully qualified for non-builtin types.
          - deep_size: whether to compute deep size (can be expensive for large objects).

        Returns:
          - ObjectInfo with populated size, unit, deep_size, and type.
        """

        def __get_deep_size(o):
            try:
                deep_size_ = deep_sizeof(o) if deep_size else None
            except:
                deep_size_ = None
            return deep_size_

        def __get_shallow_size(o):
            try:
                size_ = sys.getsizeof(o)
            except:
                size_ = None
            return size_

        # Scalars
        if isinstance(obj, (int, float, bool, complex)):
            b = __get_shallow_size(obj)  # shallow bytes, used for human-facing size
            return cls(
                size=b,
                unit="bytes",
                deep_size=__get_deep_size(obj),
                type=type(obj),
                fully_qualified=fully_qualified,
            )
        elif isinstance(obj, str):
            # Human-facing size is chars; deep bytes can be useful to compare memory footprint
            return cls(
                size=len(obj),
                unit="chars",
                deep_size=__get_deep_size(obj),
                type=type(obj),
                fully_qualified=fully_qualified,
            )
        elif isinstance(obj, (bytes, bytearray, memoryview)):
            n = len(obj)
            return cls(
                size=n,
                unit="bytes",
                deep_size=__get_deep_size(obj),
                type=type(obj),
                fully_qualified=fully_qualified,
            )

        # Containers
        elif isinstance(obj, (abc.Sequence, abc.Set, abc.Mapping)):
            return cls(
                size=len(obj),
                unit="items",
                deep_size=__get_deep_size(obj),
                type=type(obj),
                fully_qualified=fully_qualified,
            )

        # Images
        elif _acts_like_image(obj):
            width, height = obj.size
            mega_px = width * height / 1e6
            return cls(
                size=[width, height, mega_px],
                unit=["width", "height", "Mpx"],
                deep_size=__get_deep_size(obj),
                type=type(obj),
                fully_qualified=fully_qualified,
            )

        # Class objects
        elif type(obj) is type:
            attrs = search_attrs(
                obj,
                format="list",
                include_methods=False,
                include_private=False,
                include_properties=False,
                skip_errors=True,
            )
            return cls(
                type=obj,
                size=len(attrs),
                unit="attrs",
                deep_size=None,
                fully_qualified=fully_qualified,
            )

        # Instances with attributes
        elif attrs := search_attrs(
            obj,
            format="list",
            include_methods=False,
            include_private=False,
            include_properties=False,
            skip_errors=True,
        ):
            return cls(
                type=type(obj),
                size=len(attrs),
                unit="attrs",
                deep_size=__get_deep_size(obj),
                fully_qualified=fully_qualified,
            )

        # Other instances with no attrs found
        else:
            return cls(
                type=type(obj),
                size=__get_shallow_size(obj),
                unit="bytes",
                deep_size=__get_deep_size(obj),
                fully_qualified=fully_qualified,
            )

    def to_str(self, deep_size: bool = False) -> str:
        """
        Human-readable one-line summary.

        Parameters:
            deep_size: If True and deep_size is available, append deep bytes info.

        Examples:
            "<int> 28 bytes"
            "<str> 11 chars"
            "<list> 3 items"
            "<list> 3 items, 256 deep bytes"
            "<PIL.Image.Image> 640⨯480 W⨯H, 0.307 Mpx"
            "<PIL.Image.Image> 640⨯480 W⨯H, 0.307 Mpx, 1228800 deep bytes"
            "<MyClass> 4 attrs"
            "<MyClass> 4 attrs, 1024 deep bytes"

        Raises:
              ValueError: If size and unit lengths mismatch.
        """
        # Handle list-based size/unit pairs
        if isinstance(self.size, list) and isinstance(self.unit, list):
            # List lengths should be checked in __post_init__()

            if _acts_like_image(self.type):
                # Special image formatting: width⨯height W⨯H, Mpx
                width, height, mega_px = self.size
                base_str = (
                    f"<{self._class_name}> {width}{height} W⨯H, {round(mega_px, ndigits=3)} Mpx"
                )
            else:
                # Generic list formatting: join size-unit pairs
                size_unit_pairs = [f"{s} {u}" for s, u in zip(self.size, self.unit)]
                base_str = f"<{self._class_name}> {', '.join(size_unit_pairs)}"
        else:
            # Single size/unit pair
            base_str = f"<{self._class_name}> {self.size} {self.unit}"

        # Consistently append deep_size info if requested and available
        if deep_size and self.deep_size is not None:
            base_str += f", {self.deep_size} deep bytes"

        return base_str

    def to_dict(self, include_none_attrs: bool = False) -> dict[str, Any]:
        """
        Export as dictionary.

        Args:
            include_none_attrs: If True, include fields with None values (like deep_size when not computed).

        Returns:
            Dictionary with keys: type, size, unit, and optionally deep_size.

        Examples:
            >>> info = ObjectInfo.from_object("hello")
            >>> info.to_dict()
            {'type': <class 'str'>, 'size': 5, 'unit': 'chars'}
        """
        result = {
            "type": self.type,
            "size": self.size,
            "unit": self.unit,
        }

        if include_none_attrs or self.deep_size is not None:
            result["deep_size"] = self.deep_size

        return result

    def __str__(self) -> str:
        """Default string representation using to_str() with default formatting."""
        return self.to_str()

    def __repr__(self) -> str:
        """Developer-friendly representation."""
        return (
            f"ObjectInfo(type={self.type.__name__}, size={self.size}, "
            f"unit={self.unit}, deep_size={self.deep_size})"
        )

    @property
    def _class_name(self) -> str:
        """Return a display name for 'type' (fully qualified for non-builtin types if enabled)."""
        return class_name(
            self.type,
            fully_qualified=self._fully_qualified,
            fully_qualified_builtins=False,
        )

__post_init__(fully_qualified)

Post-initialization validation and options.

For frozen dataclasses, we must use object.setattr() to set attributes.

Source code in c108/abc.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def __post_init__(self, fully_qualified: bool):
    """
    Post-initialization validation and options.

    For frozen dataclasses, we must use object.__setattr__() to set attributes.
    """
    # Store fully_qualified using the frozen workaround
    object.__setattr__(self, "_fully_qualified", fully_qualified)

    # Validate runtime logic constraints
    # Both size and unit must be sequences (and not str/bytes) to validate length
    if isinstance(self.size, abc.Sequence) and not isinstance(
        self.size, (str, bytes, bytearray)
    ):
        if isinstance(self.unit, abc.Sequence) and not isinstance(
            self.unit, (str, bytes, bytearray)
        ):
            if len(self.size) != len(self.unit):
                raise ValueError(
                    f"size and unit must be same length, but got "
                    f"len(size)={len(self.size)}, len(unit)={len(self.unit)}"
                )

__repr__()

Developer-friendly representation.

Source code in c108/abc.py
330
331
332
333
334
335
def __repr__(self) -> str:
    """Developer-friendly representation."""
    return (
        f"ObjectInfo(type={self.type.__name__}, size={self.size}, "
        f"unit={self.unit}, deep_size={self.deep_size})"
    )

__str__()

Default string representation using to_str() with default formatting.

Source code in c108/abc.py
326
327
328
def __str__(self) -> str:
    """Default string representation using to_str() with default formatting."""
    return self.to_str()

from_object(obj, fully_qualified=False, deep_size=False) classmethod

Build an ObjectInfo summary of 'obj'.

Heuristics according to 'obj' type: - Numbers: size=N bytes (shallow), unit="bytes". - str: size=N chars, unit="chars". - bytes/bytearray/memoryview: size=N bytes, unit="bytes". - Sequence/Set/Mapping: size=N items, unit="items". - Image-like: size=[width, height, Mpx], unit=["width","height","Mpx"]. - Class (type): size=N attrs, unit="attrs"; deep_size=None. - Instance with attrs: size=[N attrs, deep bytes], unit=["attrs","bytes"]. - Other/no-attrs: size = shallow bytes, unit="bytes" - Any obj: get deep size via c108.abc.deep_sizeof() if deep_size=True; None for classes or when deep_size=False.

Parameters:

Name Type Description Default
- obj

object to summarize.

required
- fully_qualified

whether class_name should be fully qualified for non-builtin types.

required
- deep_size

whether to compute deep size (can be expensive for large objects).

required

Returns:

Type Description
ObjectInfo
  • ObjectInfo with populated size, unit, deep_size, and type.
Source code in c108/abc.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
@classmethod
def from_object(
    cls, obj: Any, fully_qualified: bool = False, deep_size: bool = False
) -> "ObjectInfo":
    """
    Build an ObjectInfo summary of 'obj'.

    Heuristics according to 'obj' type:
      - Numbers: size=N bytes (shallow), unit="bytes".
      - str: size=N chars, unit="chars".
      - bytes/bytearray/memoryview: size=N bytes, unit="bytes".
      - Sequence/Set/Mapping: size=N items, unit="items".
      - Image-like: size=[width, height, Mpx], unit=["width","height","Mpx"].
      - Class (type): size=N attrs, unit="attrs"; deep_size=None.
      - Instance with attrs: size=[N attrs, deep bytes], unit=["attrs","bytes"].
      - Other/no-attrs: size = shallow bytes, unit="bytes"
      - Any obj: get deep size via c108.abc.deep_sizeof() if deep_size=True;
                 None for classes or when deep_size=False.

    Parameters:
      - obj: object to summarize.
      - fully_qualified: whether class_name should be fully qualified for non-builtin types.
      - deep_size: whether to compute deep size (can be expensive for large objects).

    Returns:
      - ObjectInfo with populated size, unit, deep_size, and type.
    """

    def __get_deep_size(o):
        try:
            deep_size_ = deep_sizeof(o) if deep_size else None
        except:
            deep_size_ = None
        return deep_size_

    def __get_shallow_size(o):
        try:
            size_ = sys.getsizeof(o)
        except:
            size_ = None
        return size_

    # Scalars
    if isinstance(obj, (int, float, bool, complex)):
        b = __get_shallow_size(obj)  # shallow bytes, used for human-facing size
        return cls(
            size=b,
            unit="bytes",
            deep_size=__get_deep_size(obj),
            type=type(obj),
            fully_qualified=fully_qualified,
        )
    elif isinstance(obj, str):
        # Human-facing size is chars; deep bytes can be useful to compare memory footprint
        return cls(
            size=len(obj),
            unit="chars",
            deep_size=__get_deep_size(obj),
            type=type(obj),
            fully_qualified=fully_qualified,
        )
    elif isinstance(obj, (bytes, bytearray, memoryview)):
        n = len(obj)
        return cls(
            size=n,
            unit="bytes",
            deep_size=__get_deep_size(obj),
            type=type(obj),
            fully_qualified=fully_qualified,
        )

    # Containers
    elif isinstance(obj, (abc.Sequence, abc.Set, abc.Mapping)):
        return cls(
            size=len(obj),
            unit="items",
            deep_size=__get_deep_size(obj),
            type=type(obj),
            fully_qualified=fully_qualified,
        )

    # Images
    elif _acts_like_image(obj):
        width, height = obj.size
        mega_px = width * height / 1e6
        return cls(
            size=[width, height, mega_px],
            unit=["width", "height", "Mpx"],
            deep_size=__get_deep_size(obj),
            type=type(obj),
            fully_qualified=fully_qualified,
        )

    # Class objects
    elif type(obj) is type:
        attrs = search_attrs(
            obj,
            format="list",
            include_methods=False,
            include_private=False,
            include_properties=False,
            skip_errors=True,
        )
        return cls(
            type=obj,
            size=len(attrs),
            unit="attrs",
            deep_size=None,
            fully_qualified=fully_qualified,
        )

    # Instances with attributes
    elif attrs := search_attrs(
        obj,
        format="list",
        include_methods=False,
        include_private=False,
        include_properties=False,
        skip_errors=True,
    ):
        return cls(
            type=type(obj),
            size=len(attrs),
            unit="attrs",
            deep_size=__get_deep_size(obj),
            fully_qualified=fully_qualified,
        )

    # Other instances with no attrs found
    else:
        return cls(
            type=type(obj),
            size=__get_shallow_size(obj),
            unit="bytes",
            deep_size=__get_deep_size(obj),
            fully_qualified=fully_qualified,
        )

to_dict(include_none_attrs=False)

Export as dictionary.

Parameters:

Name Type Description Default
include_none_attrs bool

If True, include fields with None values (like deep_size when not computed).

False

Returns:

Type Description
dict[str, Any]

Dictionary with keys: type, size, unit, and optionally deep_size.

Examples:

>>> info = ObjectInfo.from_object("hello")
>>> info.to_dict()
{'type': <class 'str'>, 'size': 5, 'unit': 'chars'}
Source code in c108/abc.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
def to_dict(self, include_none_attrs: bool = False) -> dict[str, Any]:
    """
    Export as dictionary.

    Args:
        include_none_attrs: If True, include fields with None values (like deep_size when not computed).

    Returns:
        Dictionary with keys: type, size, unit, and optionally deep_size.

    Examples:
        >>> info = ObjectInfo.from_object("hello")
        >>> info.to_dict()
        {'type': <class 'str'>, 'size': 5, 'unit': 'chars'}
    """
    result = {
        "type": self.type,
        "size": self.size,
        "unit": self.unit,
    }

    if include_none_attrs or self.deep_size is not None:
        result["deep_size"] = self.deep_size

    return result

to_str(deep_size=False)

Human-readable one-line summary.

Parameters:

Name Type Description Default
deep_size bool

If True and deep_size is available, append deep bytes info.

False

Examples:

" 28 bytes" " 11 chars" " 3 items" " 3 items, 256 deep bytes" " 640⨯480 W⨯H, 0.307 Mpx" " 640⨯480 W⨯H, 0.307 Mpx, 1228800 deep bytes" " 4 attrs" " 4 attrs, 1024 deep bytes"

Raises:

Type Description
ValueError

If size and unit lengths mismatch.

Source code in c108/abc.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def to_str(self, deep_size: bool = False) -> str:
    """
    Human-readable one-line summary.

    Parameters:
        deep_size: If True and deep_size is available, append deep bytes info.

    Examples:
        "<int> 28 bytes"
        "<str> 11 chars"
        "<list> 3 items"
        "<list> 3 items, 256 deep bytes"
        "<PIL.Image.Image> 640⨯480 W⨯H, 0.307 Mpx"
        "<PIL.Image.Image> 640⨯480 W⨯H, 0.307 Mpx, 1228800 deep bytes"
        "<MyClass> 4 attrs"
        "<MyClass> 4 attrs, 1024 deep bytes"

    Raises:
          ValueError: If size and unit lengths mismatch.
    """
    # Handle list-based size/unit pairs
    if isinstance(self.size, list) and isinstance(self.unit, list):
        # List lengths should be checked in __post_init__()

        if _acts_like_image(self.type):
            # Special image formatting: width⨯height W⨯H, Mpx
            width, height, mega_px = self.size
            base_str = (
                f"<{self._class_name}> {width}{height} W⨯H, {round(mega_px, ndigits=3)} Mpx"
            )
        else:
            # Generic list formatting: join size-unit pairs
            size_unit_pairs = [f"{s} {u}" for s, u in zip(self.size, self.unit)]
            base_str = f"<{self._class_name}> {', '.join(size_unit_pairs)}"
    else:
        # Single size/unit pair
        base_str = f"<{self._class_name}> {self.size} {self.unit}"

    # Consistently append deep_size info if requested and available
    if deep_size and self.deep_size is not None:
        base_str += f", {self.deep_size} deep bytes"

    return base_str

classgetter(func=None, *, cache=False)

classgetter(func: Callable[[type[ClsT]], T]) -> ClassGetter[T]
classgetter(func: None = None, *, cache: bool = False) -> Callable[[Callable[[type[ClsT]], T]], ClassGetter[T]]

Decorator for read-only class-level properties.

Creates a ClassGetter descriptor that allows accessing class-level computed values without parentheses, similar to @property but for class attributes instead of instance attributes.

The decorated method is read-only: attempting to assign to it on an instance will raise AttributeError. However, class-level assignment will replace the descriptor entirely (standard Python behavior).

Can be used with or without arguments

@classgetter def all(cls): ...

@classgetter(cache=True) def all(cls): ...

Parameters:

Name Type Description Default
func Callable[[type], T] | None

Function to wrap (when used without arguments)

None
cache bool

If True, cache the computed value per class. Useful for expensive computations that don't change at runtime. Default: False.

False

Returns:

Type Description
ClassGetter[T] | Callable[[Callable[[type], T]], ClassGetter[T]]

ClassGetter descriptor instance, or a decorator function if

ClassGetter[T] | Callable[[Callable[[type], T]], ClassGetter[T]]

called with keyword arguments.

Examples:

Basic usage: >>> class AWS: ... s3 = "s3" ... s3a = "s3a" ... ... @classgetter ... def all(cls): ... return tuple(v for k, v in vars(cls).items() ... if isinstance(v, str) and not k.startswith('_')) ... >>> AWS.all # No parentheses! ('s3', 's3a')

With caching for expensive computations: >>> class DatabaseSchemes: ... postgres = "postgresql" ... mysql = "mysql" ... sqlite = "sqlite" ... ... @classgetter(cache=True) ... def all(cls): ... return tuple(v for k, v in vars(cls).items() ... if isinstance(v, str) and not k.startswith('_')) ... >>> DatabaseSchemes.all # Computed once ('postgresql', 'mysql', 'sqlite') >>> DatabaseSchemes.all # Returned from cache ('postgresql', 'mysql', 'sqlite')

Instance access is prevented: >>> aws = AWS() >>> aws.all = "new_value" Traceback (most recent call last): ... AttributeError: 'all' is a read-only class attribute

Class-level replacement is allowed (standard Python behavior): >>> AWS.all = ("s3", "s3a", "s3n") # Replaces the descriptor >>> AWS.all ('s3', 's3a', 's3n')

Note
  • The wrapped function receives the class (not instance) as first argument
  • Instance assignment is blocked: obj.attr = value raises AttributeError
  • Class assignment replaces descriptor: Class.attr = value is allowed
  • Caching is per-class, so subclasses maintain separate caches
  • The descriptor is created at class definition time (decoration time)
  • Type checkers will understand the return type through proper annotations
  • PyCharm and other Type checkers with weak descriptors inspection may comlain for cls not callable
Source code in c108/abc.py
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
def classgetter(
    func: Callable[[type], T] | None = None,
    *,
    cache: bool = False,
) -> ClassGetter[T] | Callable[[Callable[[type], T]], ClassGetter[T]]:
    """
    Decorator for read-only class-level properties.

    Creates a ClassGetter descriptor that allows accessing class-level
    computed values without parentheses, similar to @property but for
    class attributes instead of instance attributes.

    The decorated method is read-only: attempting to assign to it on an
    instance will raise AttributeError. However, class-level assignment
    will replace the descriptor entirely (standard Python behavior).

    Can be used with or without arguments:
        @classgetter
        def all(cls): ...

        @classgetter(cache=True)
        def all(cls): ...

    Args:
        func: Function to wrap (when used without arguments)
        cache: If True, cache the computed value per class. Useful for
               expensive computations that don't change at runtime.
               Default: False.

    Returns:
        ClassGetter descriptor instance, or a decorator function if
        called with keyword arguments.

    Examples:
        Basic usage:
            >>> class AWS:
            ...     s3 = "s3"
            ...     s3a = "s3a"
            ...
            ...     @classgetter
            ...     def all(cls):
            ...         return tuple(v for k, v in vars(cls).items()
            ...                     if isinstance(v, str) and not k.startswith('_'))
            ...
            >>> AWS.all  # No parentheses!
            ('s3', 's3a')

        With caching for expensive computations:
            >>> class DatabaseSchemes:
            ...     postgres = "postgresql"
            ...     mysql = "mysql"
            ...     sqlite = "sqlite"
            ...
            ...     @classgetter(cache=True)
            ...     def all(cls):
            ...         return tuple(v for k, v in vars(cls).items()
            ...                     if isinstance(v, str) and not k.startswith('_'))
            ...
            >>> DatabaseSchemes.all  # Computed once
            ('postgresql', 'mysql', 'sqlite')
            >>> DatabaseSchemes.all  # Returned from cache
            ('postgresql', 'mysql', 'sqlite')

        Instance access is prevented:
            >>> aws = AWS()
            >>> aws.all = "new_value"
            Traceback (most recent call last):
            ...
            AttributeError: 'all' is a read-only class attribute

        Class-level replacement is allowed (standard Python behavior):
            >>> AWS.all = ("s3", "s3a", "s3n")  # Replaces the descriptor
            >>> AWS.all
            ('s3', 's3a', 's3n')

    Note:
        - The wrapped function receives the class (not instance) as first argument
        - **Instance assignment is blocked**: obj.attr = value raises AttributeError
        - **Class assignment replaces descriptor**: Class.attr = value is allowed
        - Caching is per-class, so subclasses maintain separate caches
        - The descriptor is created at class definition time (decoration time)
        - Type checkers will understand the return type through proper annotations
        - PyCharm and other Type checkers with weak descriptors inspection may comlain for cls not callable
    """

    def decorator(f: Callable[[type], T]) -> ClassGetter[T]:
        sig = inspect.signature(f)
        params = list(sig.parameters.values())

        if len(params) != 1:
            raise TypeError(
                f"@classgetter expects a function with exactly one parameter (cls), "
                f"but {f.__name__!r} has {len(params)} parameters"
            )

        return ClassGetter(f, cache=cache)

    if func is None:
        return decorator
    else:
        return decorator(func)

deep_sizeof(obj, *, format='int', exclude_types=(), exclude_ids=None, max_depth=None, seen=None, on_error='skip')

Calculate the deep memory size of an object including all referenced objects.

This function recursively traverses object references to compute total memory usage, similar to pympler.asizeof but using only Python stdlib. It handles circular references and avoids double-counting shared objects.

Parameters:

Name Type Description Default
obj Any

Any Python object to measure.

required
format Literal['int', 'dict']

Output format. Default "int" returns total bytes as integer. Use "dict" for detailed breakdown including per-type analysis, object count, and maximum depth reached.

'int'
exclude_types tuple[type, ...]

Tuple of types to exclude from size calculation. Useful for excluding large shared objects like modules. Objects of these types contribute 0 bytes to the total.

()
exclude_ids set[int] | None

Set of specific object IDs (from id()) to exclude. Useful for excluding particular instances rather than entire types. More fine-grained than exclude_types.

None
max_depth int | None

Maximum recursion depth. None (default) means unlimited. Useful for preventing deep recursion on heavily nested structures. When limit is reached, objects at that depth are counted shallowly.

None
seen set[int] | None

Set of object IDs already counted. Pass the same set across multiple deep_sizeof() calls to measure exclusive sizes and avoid double-counting shared references between objects.

None
on_error Literal['skip', 'raise', 'warn']

How to handle objects that raise exceptions during size calculation: - "skip" (default): Skip problematic objects, continue traversal. In dict format, tracks errors in 'errors' field. - "raise": Re-raise the first exception encountered. - "warn": Issue warnings for problematic objects but continue.

'skip'

Returns:

Name Type Description
int int | dict[str, Any]

Total size in bytes (when format="int")

dict int | dict[str, Any]

Detailed breakdown (when format="dict") containing: - total_bytes (int): Total size in bytes - by_type (dict[type, int]): Bytes per type object (not string names) - object_count (int): Number of objects successfully traversed - max_depth_reached (int): Deepest nesting level encountered - errors (dict[type, int]): Count of errors by exception type object (e.g., {TypeError: 3, AttributeError: 1}) - problematic_types (set[type]): Type objects that raised exceptions during sizeof or attribute access

Raises:

Type Description
RecursionError

If Python's recursion limit is exceeded during traversal. Consider using max_depth parameter to prevent this.

TypeError

Only when on_error="raise" and an object doesn't implement sizeof properly.

AttributeError

Only when on_error="raise" and attribute access fails on an object with unusual attribute handling.

Examples:

Basic usage: >>> data = {'items': [1, 2, 3], 'nested': {'key': 'value'}} >>> size = deep_sizeof(data) >>> size > sys.getsizeof(data) True

Detailed breakdown with error tracking: >>> info = deep_sizeof(data, format="dict") >>> info['total_bytes'] 723 >>> info['by_type'] {: 368, : 183, : 88, : 84} >>> info['errors'] {} >>> info['problematic_types'] set()

Handling buggy objects: >>> class BuggyClass: ... def sizeof(self): ... raise RuntimeError("Broken!") >>> obj = {'good': [1, 2], 'bad': BuggyClass()} >>> >>> # Default: skip errors and continue >>> size = deep_sizeof(obj) # Returns size of 'good' parts only >>> >>> # Get details about what failed >>> info = deep_sizeof(obj, format="dict") >>> info['errors'] {: 1} >>> info['problematic_types'] {} >>> >>> # Stop on first error >>> deep_sizeof(obj, on_error="raise") Traceback (most recent call last): ... RuntimeError: Broken!

Exclude specific types: >>> size_no_strings = deep_sizeof(data, exclude_types=(str,))

Limit recursion depth: >>> deeply_nested_obj = [[[0]]] >>> size = deep_sizeof(deeply_nested_obj, max_depth=10)

Exclude specific objects: >>> global_cache = {...} >>> size = deep_sizeof(obj, exclude_ids={id(global_cache)})

Warning mode for debugging: >>> import warnings >>> with warnings.catch_warnings(record=True) as w: ... size = deep_sizeof(obj, on_error="warn") ... if w: ... print(f"Encountered {len(w)} problematic objects") Encountered 1 problematic objects

Note
  • Circular references are handled automatically via internal tracking
  • Module objects are typically excluded by default in implementations
  • When on_error="skip", problematic objects contribute 0 bytes but traversal continues to their children when possible
  • The 'errors' and 'problematic_types' fields are only included in dict format output
  • The function is designed for diagnostic purposes, not for precise memory profiling. Use dedicated profiling tools for production analysis.
  • Error tracking uses actual type objects, not string names, ensuring robustness when same type names exist in different modules. Use type.module and type.name if string representation is needed.
Source code in c108/abc.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def deep_sizeof(
    obj: Any,
    *,
    format: Literal["int", "dict"] = "int",
    exclude_types: tuple[type, ...] = (),
    exclude_ids: set[int] | None = None,
    max_depth: int | None = None,
    seen: set[int] | None = None,
    on_error: Literal["skip", "raise", "warn"] = "skip",
) -> int | dict[str, Any]:
    """
    Calculate the deep memory size of an object including all referenced objects.

    This function recursively traverses object references to compute total memory
    usage, similar to pympler.asizeof but using only Python stdlib. It handles
    circular references and avoids double-counting shared objects.

    Args:
        obj: Any Python object to measure.
        format: Output format. Default "int" returns total bytes as integer.
            Use "dict" for detailed breakdown including per-type analysis,
            object count, and maximum depth reached.
        exclude_types: Tuple of types to exclude from size calculation.
            Useful for excluding large shared objects like modules.
            Objects of these types contribute 0 bytes to the total.
        exclude_ids: Set of specific object IDs (from id()) to exclude.
            Useful for excluding particular instances rather than entire types.
            More fine-grained than exclude_types.
        max_depth: Maximum recursion depth. None (default) means unlimited.
            Useful for preventing deep recursion on heavily nested structures.
            When limit is reached, objects at that depth are counted shallowly.
        seen: Set of object IDs already counted. Pass the same set across
            multiple deep_sizeof() calls to measure exclusive sizes and avoid
            double-counting shared references between objects.
        on_error: How to handle objects that raise exceptions during size calculation:
            - "skip" (default): Skip problematic objects, continue traversal. In dict
              format, tracks errors in 'errors' field.
            - "raise": Re-raise the first exception encountered.
            - "warn": Issue warnings for problematic objects but continue.

    Returns:
        int: Total size in bytes (when format="int")
        dict: Detailed breakdown (when format="dict") containing:
            - total_bytes (int): Total size in bytes
            - by_type (dict[type, int]): Bytes per type object (not string names)
            - object_count (int): Number of objects successfully traversed
            - max_depth_reached (int): Deepest nesting level encountered
            - errors (dict[type, int]): Count of errors by exception type object
              (e.g., {TypeError: 3, AttributeError: 1})
            - problematic_types (set[type]): Type objects that raised exceptions
              during __sizeof__ or attribute access

    Raises:
        RecursionError: If Python's recursion limit is exceeded during traversal.
            Consider using max_depth parameter to prevent this.
        TypeError: Only when on_error="raise" and an object doesn't implement
            __sizeof__ properly.
        AttributeError: Only when on_error="raise" and attribute access fails
            on an object with unusual attribute handling.

    Examples:
        Basic usage:
            >>> data = {'items': [1, 2, 3], 'nested': {'key': 'value'}}
            >>> size = deep_sizeof(data)
            >>> size > sys.getsizeof(data)
            True

        Detailed breakdown with error tracking:
            >>> info = deep_sizeof(data, format="dict")
            >>> info['total_bytes']
            723
            >>> info['by_type']
            {<class 'dict'>: 368, <class 'str'>: 183, <class 'list'>: 88, <class 'int'>: 84}
            >>> info['errors']
            {}
            >>> info['problematic_types']
            set()

        Handling buggy objects:
            >>> class BuggyClass:
            ...     def __sizeof__(self):
            ...         raise RuntimeError("Broken!")
            >>> obj = {'good': [1, 2], 'bad': BuggyClass()}
            >>>
            >>> # Default: skip errors and continue
            >>> size = deep_sizeof(obj)  # Returns size of 'good' parts only
            >>>
            >>> # Get details about what failed
            >>> info = deep_sizeof(obj, format="dict")
            >>> info['errors']
            {<class 'RuntimeError'>: 1}
            >>> info['problematic_types']
            {<class 'c108.abc.BuggyClass'>}
            >>>
            >>> # Stop on first error
            >>> deep_sizeof(obj, on_error="raise")
            Traceback (most recent call last):
            ...
            RuntimeError: Broken!

        Exclude specific types:
            >>> size_no_strings = deep_sizeof(data, exclude_types=(str,))

        Limit recursion depth:
            >>> deeply_nested_obj = [[[0]]]
            >>> size = deep_sizeof(deeply_nested_obj, max_depth=10)

        Exclude specific objects:
            >>> global_cache = {...}
            >>> size = deep_sizeof(obj, exclude_ids={id(global_cache)})

        Warning mode for debugging:
            >>> import warnings
            >>> with warnings.catch_warnings(record=True) as w:
            ...     size = deep_sizeof(obj, on_error="warn")
            ...     if w:
            ...         print(f"Encountered {len(w)} problematic objects")
            Encountered 1 problematic objects

    Note:
        - Circular references are handled automatically via internal tracking
        - Module objects are typically excluded by default in implementations
        - When on_error="skip", problematic objects contribute 0 bytes but
          traversal continues to their children when possible
        - The 'errors' and 'problematic_types' fields are only included in
          dict format output
        - The function is designed for diagnostic purposes, not for precise
          memory profiling. Use dedicated profiling tools for production analysis.
        - Error tracking uses actual type objects, not string names, ensuring
          robustness when same type names exist in different modules.
          Use type.__module__ and type.__name__ if string representation is needed.
    """
    # Initialize tracking structures
    if seen is None:
        seen = set()

    if exclude_ids is None:
        exclude_ids = set()

    # Detailed format tracking
    by_type = defaultdict(int) if format == "dict" else None
    error_counts = defaultdict(int) if format == "dict" else None
    problematic_types = set() if format == "dict" else None
    object_count = [0] if format == "dict" else None
    max_depth_tracker = [0] if format == "dict" else None

    # Perform recursive calculation
    total_bytes = _deep_sizeof_recursive(
        obj=obj,
        seen=seen,
        exclude_types=exclude_types,
        exclude_ids=exclude_ids,
        max_depth=max_depth,
        current_depth=0,
        on_error=on_error,
        by_type=by_type,
        error_counts=error_counts,
        problematic_types=problematic_types,
        object_count=object_count,
        max_depth_tracker=max_depth_tracker,
    )

    # Return appropriate format
    if format == "int":
        return total_bytes
    else:
        return {
            "total_bytes": total_bytes,
            "by_type": dict(by_type),
            "object_count": object_count[0],
            "max_depth_reached": max_depth_tracker[0],
            "errors": dict(error_counts),
            "problematic_types": problematic_types,
        }

isbuiltin(obj)

Check if an object is a built-in type or instance of a built-in type.

This function identifies core Python value types (int, str, list, dict, etc.) and their instances, excluding meta-programming utilities, functions, and modules.

Parameters:

Name Type Description Default
obj Any

Any Python object to check.

required

Returns:

Name Type Description
bool bool

True if obj is a built-in type or instance of a built-in type.

Examples:

>>> isbuiltin(int)          # Built-in type
True
>>> isbuiltin(42)           # Instance of built-in type
True
>>> isbuiltin([1, 2, 3])    # Instance of built-in type
True
>>> isbuiltin(len)          # Built-in function
False
>>> isbuiltin(property)     # Descriptor helper
True
>>> isbuiltin(object())     # Instance of built-in type
True
Note
  • Returns False for functions, methods, modules, and descriptor helpers
  • Returns False for user-defined classes and their instances
  • Focuses on core value types rather than meta-programming utilities
Source code in c108/abc.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
def isbuiltin(obj: Any) -> bool:
    """
    Check if an object is a built-in type or instance of a built-in type.

    This function identifies core Python value types (int, str, list, dict, etc.)
    and their instances, excluding meta-programming utilities, functions, and modules.

    Args:
        obj: Any Python object to check.

    Returns:
        bool: True if obj is a built-in type or instance of a built-in type.

    Examples:
        >>> isbuiltin(int)          # Built-in type
        True
        >>> isbuiltin(42)           # Instance of built-in type
        True
        >>> isbuiltin([1, 2, 3])    # Instance of built-in type
        True
        >>> isbuiltin(len)          # Built-in function
        False
        >>> isbuiltin(property)     # Descriptor helper
        True
        >>> isbuiltin(object())     # Instance of built-in type
        True

    Note:
        - Returns False for functions, methods, modules, and descriptor helpers
        - Returns False for user-defined classes and their instances
        - Focuses on core value types rather than meta-programming utilities
    """
    try:
        # Handle class objects (types)
        if isinstance(obj, type):
            return getattr(obj, "__module__", None) == "builtins"

        # Exclude functions, methods, built-in callables, and modules
        if (
            inspect.isfunction(obj)
            or inspect.ismethod(obj)
            or inspect.isbuiltin(obj)
            or inspect.ismodule(obj)
        ):
            return False

        # Exclude descriptor helpers
        if isinstance(obj, (property, staticmethod, classmethod)):
            return False

        # Check if instance's class is from builtins
        obj_class = getattr(obj, "__class__", None)
        if obj_class is None:
            return False

        return getattr(obj_class, "__module__", None) == "builtins"

    except (AttributeError, TypeError, RuntimeError):
        return False

search_attrs(obj, *, format='list', exclude_none=False, include_inherited=True, include_methods=False, include_private=False, include_properties=False, attr_type=None, pattern=None, skip_errors=True, sort=False)

search_attrs(obj: Any, *, format: Literal['list'] = 'list', exclude_none: bool = False, include_inherited: bool = True, include_methods: bool = False, include_private: bool = False, include_properties: bool = False, attr_type: type | tuple[type, ...] | None = None, pattern: str | None = None, skip_errors: bool = True, sort: bool = False) -> list[str]
search_attrs(obj: Any, *, format: Literal['dict'], exclude_none: bool = False, include_inherited: bool = True, include_methods: bool = False, include_private: bool = False, include_properties: bool = False, attr_type: type | tuple[type, ...] | None = None, pattern: str | None = None, skip_errors: bool = True, sort: bool = False) -> dict[str, Any]
search_attrs(obj: Any, *, format: Literal['items'], exclude_none: bool = False, include_inherited: bool = True, include_methods: bool = False, include_private: bool = False, include_properties: bool = False, attr_type: type | tuple[type, ...] | None = None, pattern: str | None = None, skip_errors: bool = True, sort: bool = False) -> list[tuple[str, Any]]

Search for attributes in an object with flexible filtering and output formats.

By default, returns only public, non-callable data attribute names. Use parameters to expand or narrow the search, and choose output format.

Parameters:

Name Type Description Default
obj Any

The object to inspect for attributes

required
format Literal['list', 'dict', 'items']

Output format: - "list": list of unique attribute names (default) - "dict": dictionary mapping names to values (keys are unique) - "items": list of (name, value) tuples with unique names, compatible with dict() constructor

'list'
exclude_none bool

If True, excludes attributes with None values

False
include_inherited bool

If True, includes attributes from parent classes. If False, only returns attributes in obj.dict (instance attrs)

True
include_methods bool

If True, includes callable attributes (methods, functions)

False
include_private bool

If True, includes private attributes (starting with '_'). Does not include dunder or mangled attributes.

False
include_properties bool

If True, includes property descriptors

False
attr_type type | tuple[type, ...] | None

Optional type or tuple of types to filter by attribute value type. Only attributes whose values are instances of these types are included.

None
pattern str | None

Optional regex pattern to filter attribute names. Must match the entire name (use '.pattern.' for substring matching)

None
skip_errors bool

If True, silently skips attributes that raise errors on access. If False, raises AttributeError on access failures.

True
sort bool

If True, sorts attribute names alphabetically. Default False preserves dir() order.

False

Returns:

Type Description
list[str] | dict[str, Any] | list[tuple[str, Any]]
  • If format="list": list[str] of attribute names
list[str] | dict[str, Any] | list[tuple[str, Any]]
  • If format="dict": dict[str, Any] mapping names to values
list[str] | dict[str, Any] | list[tuple[str, Any]]
  • If format="items": list[tuple[str, Any]] of (name, value) pairs

Raises:

Type Description
AttributeError

If skip_errors=False and attribute access fails

ValueError

If pattern is an invalid regex or format is invalid

Notes
  • Always excludes dunder attributes (name)
  • Always excludes mangled attributes (_ClassName__attr) unless include_private=True
  • Built-in primitive types return empty list/dict
  • Properties are checked by descriptor type, not by accessing values
  • When exclude_none=True or attr_type is set, properties are evaluated

Examples:

>>> class MyClass:
...     public = 1
...     _private = 2
...     none_val = None
...     @property
...     def prop(self):
...         return 3
...     def method(self):
...         pass
>>> obj = MyClass()
>>> search_attrs(obj)
['public', 'none_val']
>>> search_attrs(obj, format="dict")
{'public': 1, 'none_val': None}
>>> search_attrs(obj, format="items")
[('public', 1), ('none_val', None)]
>>> search_attrs(obj, include_private=True)
['public', '_private', 'none_val']
>>> search_attrs(obj, include_properties=True, format="dict")
{'public': 1, 'none_val': None, 'prop': 3}
>>> search_attrs(obj, exclude_none=True)
['public']
>>> search_attrs(obj, pattern=r'pub.*')
['public']
>>> search_attrs(obj, attr_type=int, format="dict")
{'public': 1}
>>> search_attrs(obj, include_methods=True, pattern=r'.*method.*')
['method']
Source code in c108/abc.py
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
def search_attrs(
    obj: Any,
    *,
    format: Literal["list", "dict", "items"] = "list",
    exclude_none: bool = False,
    include_inherited: bool = True,
    include_methods: bool = False,
    include_private: bool = False,
    include_properties: bool = False,
    attr_type: type | tuple[type, ...] | None = None,
    pattern: str | None = None,
    skip_errors: bool = True,
    sort: bool = False,
) -> list[str] | dict[str, Any] | list[tuple[str, Any]]:
    """
    Search for attributes in an object with flexible filtering and output formats.

    By default, returns only public, non-callable data attribute names. Use parameters
    to expand or narrow the search, and choose output format.

    Args:
        obj: The object to inspect for attributes
        format: Output format:
            - "list": list of unique attribute names (default)
            - "dict": dictionary mapping names to values (keys are unique)
            - "items": list of (name, value) tuples with unique names,
               compatible with dict() constructor
        exclude_none: If True, excludes attributes with None values
        include_inherited: If True, includes attributes from parent classes.
                          If False, only returns attributes in obj.__dict__ (instance attrs)
        include_methods: If True, includes callable attributes (methods, functions)
        include_private: If True, includes private attributes (starting with '_').
                        Does not include dunder or mangled attributes.
        include_properties: If True, includes property descriptors
        attr_type: Optional type or tuple of types to filter by attribute value type.
                  Only attributes whose values are instances of these types are included.
        pattern: Optional regex pattern to filter attribute names.
                 Must match the entire name (use '.*pattern.*' for substring matching)
        skip_errors: If True, silently skips attributes that raise errors on access.
                    If False, raises AttributeError on access failures.
        sort: If True, sorts attribute names alphabetically.
             Default False preserves dir() order.

    Returns:
        - If format="list": list[str] of attribute names
        - If format="dict": dict[str, Any] mapping names to values
        - If format="items": list[tuple[str, Any]] of (name, value) pairs

    Raises:
        AttributeError: If skip_errors=False and attribute access fails
        ValueError: If pattern is an invalid regex or format is invalid

    Notes:
        - Always excludes dunder attributes (__name__)
        - Always excludes mangled attributes (_ClassName__attr) unless include_private=True
        - Built-in primitive types return empty list/dict
        - Properties are checked by descriptor type, not by accessing values
        - When exclude_none=True or attr_type is set, properties are evaluated

    Examples:
        >>> class MyClass:
        ...     public = 1
        ...     _private = 2
        ...     none_val = None
        ...     @property
        ...     def prop(self):
        ...         return 3
        ...     def method(self):
        ...         pass
        >>> obj = MyClass()
        >>> search_attrs(obj)
        ['public', 'none_val']
        >>> search_attrs(obj, format="dict")
        {'public': 1, 'none_val': None}
        >>> search_attrs(obj, format="items")
        [('public', 1), ('none_val', None)]
        >>> search_attrs(obj, include_private=True)
        ['public', '_private', 'none_val']
        >>> search_attrs(obj, include_properties=True, format="dict")
        {'public': 1, 'none_val': None, 'prop': 3}
        >>> search_attrs(obj, exclude_none=True)
        ['public']
        >>> search_attrs(obj, pattern=r'pub.*')
        ['public']
        >>> search_attrs(obj, attr_type=int, format="dict")
        {'public': 1}
        >>> search_attrs(obj, include_methods=True, pattern=r'.*method.*')
        ['method']
    """

    def _search_attrs_empty_result(format: str) -> list | dict:
        """Return an appropriate empty result based on format."""
        if format == "dict":
            return {}
        else:
            return []

    def _search_attrs_is_property(obj: Any, attr_name: str) -> bool:
        """Check if an attribute is a property descriptor."""
        try:
            if inspect.isclass(obj):
                # Inspecting a class - look at the class itself
                descriptor = getattr(obj, attr_name, None)
            else:
                # Inspecting an instance - look at its type
                descriptor = getattr(type(obj), attr_name, None)
            return isinstance(descriptor, property)
        except (AttributeError, TypeError):
            return False

    # Validate format
    if format not in ("list", "dict", "items"):
        raise ValueError(
            f"format must be 'list', 'dict', or 'items' literal, got {fmt_value(format)}"
        )

    # Compile pattern if provided
    compiled_pattern = None
    if pattern is not None:
        try:
            compiled_pattern = re.compile(pattern)
        except re.error as e:
            raise ValueError(f"Invalid regex pattern: {pattern!r}") from e

    # Built-in types that should return empty results
    ignored_types = (
        int,
        float,
        bool,
        str,
        list,
        tuple,
        dict,
        set,
        frozenset,
        bytes,
        bytearray,
        complex,
        memoryview,
        range,
        type(None),
    )

    # Return empty for primitives
    if isinstance(obj, ignored_types) or (inspect.isclass(obj) and obj in ignored_types):
        return _search_attrs_empty_result(format)

    # Get attribute source based on include_inherited
    if include_inherited:
        try:
            # dir() returns sorted list, but we want definition order
            # Build attribute list manually from __dict__ and MRO
            attr_list = []
            seen_attrs = set()

            # Get the MRO (Method Resolution Order)
            if inspect.isclass(obj):
                mro = obj.__mro__
            else:
                mro = type(obj).__mro__

            # First, add instance attributes (if it's an instance)
            if not inspect.isclass(obj) and hasattr(obj, "__dict__"):
                for attr in obj.__dict__.keys():
                    if attr not in seen_attrs:
                        attr_list.append(attr)
                        seen_attrs.add(attr)

            # Then traverse MRO to get class attributes in definition order
            for klass in mro:
                if klass is object:
                    continue
                if hasattr(klass, "__dict__"):
                    for attr in klass.__dict__.keys():
                        if attr not in seen_attrs:
                            attr_list.append(attr)
                            seen_attrs.add(attr)
        except (TypeError, AttributeError):
            return _search_attrs_empty_result(format)
    else:
        # Only instance attributes
        if hasattr(obj, "__dict__"):
            attr_list = list(obj.__dict__.keys())
        elif hasattr(obj, "__slots__"):
            # Handle __slots__ without __dict__
            attr_list = list(obj.__slots__)
        else:
            return _search_attrs_empty_result(format)

    result_names = []
    result_values = []
    seen = set()

    for attr_name in attr_list:
        # Skip if already processed
        if attr_name in seen:
            continue

        # Always skip dunder
        if attr_name.startswith("__") and attr_name.endswith("__"):
            continue

        # Handle private/mangled filtering
        if not include_private:
            # Skip all private (starts with _)
            if attr_name.startswith("_"):
                continue

        # Pattern matching
        if compiled_pattern and not compiled_pattern.fullmatch(attr_name):
            continue

        # Check if it's a property
        is_property = _search_attrs_is_property(obj, attr_name)

        if is_property and not include_properties:
            continue

        # Get attribute value (needed for type checking, None checking, callable checking)
        # Also needed for dict/tuples format
        # For properties, only access value if we have value-based filters or need the value for output
        need_value = (
            format != "list"
            or exclude_none
            or attr_type is not None
            or (not include_methods and not is_property)
        )

        if need_value:
            try:
                attr_value = getattr(obj, attr_name)
            except Exception as e:
                if skip_errors:
                    continue
                # Re-raise the original exception to preserve the message
                raise
        else:
            attr_value = None  # Won't be used

        # Check if callable (method/function)
        if not include_methods:
            # For properties, we already know they're not methods, skip the check
            if not is_property:
                is_callable = callable(attr_value)
                if is_callable:
                    continue

        # Check None exclusion
        if exclude_none and attr_value is None:
            continue

        # Check type filtering
        if attr_type is not None:
            if not isinstance(attr_value, attr_type):
                continue

        result_names.append(attr_name)
        if format != "list":
            result_values.append(attr_value)
        seen.add(attr_name)

    if sort:
        if format == "list":
            result_names.sort()
        elif format == "dict":
            # Sort by keys
            result_names, result_values = (
                zip(*sorted(zip(result_names, result_values))) if result_names else ([], [])
            )
            result_names = list(result_names)
            result_values = list(result_values)
        else:  # items
            pairs = sorted(zip(result_names, result_values))
            result_names = [name for name, _ in pairs]
            result_values = [value for _, value in pairs]

    # Return in requested format
    if format == "list":
        return result_names
    elif format == "dict":
        return dict(zip(result_names, result_values))
    else:  # items
        return list(zip(result_names, result_values))