kindof 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kindof/__init__.py +5 -0
- kindof/inspection.py +233 -0
- kindof-0.1.0.dist-info/METADATA +74 -0
- kindof-0.1.0.dist-info/RECORD +6 -0
- kindof-0.1.0.dist-info/WHEEL +5 -0
- kindof-0.1.0.dist-info/top_level.txt +1 -0
kindof/__init__.py
ADDED
kindof/inspection.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Runtime object inspection with a stable, JSON-compatible result."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect as _inspect
|
|
6
|
+
import json
|
|
7
|
+
import types
|
|
8
|
+
import typing
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
_UNION_TYPE = getattr(types, "UnionType", None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _type_name(value: Any) -> str:
|
|
16
|
+
if value is Any:
|
|
17
|
+
return "Any"
|
|
18
|
+
if isinstance(value, str):
|
|
19
|
+
return value
|
|
20
|
+
origin = typing.get_origin(value)
|
|
21
|
+
if origin is typing.Union or (
|
|
22
|
+
_UNION_TYPE is not None and origin is _UNION_TYPE
|
|
23
|
+
):
|
|
24
|
+
return " | ".join(_type_name(item) for item in typing.get_args(value))
|
|
25
|
+
if origin is not None:
|
|
26
|
+
name = getattr(origin, "__name__", str(origin))
|
|
27
|
+
args = ", ".join(_type_name(item) for item in typing.get_args(value))
|
|
28
|
+
return f"{name}[{args}]" if args else name
|
|
29
|
+
return getattr(value, "__qualname__", getattr(value, "__name__", str(value)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json_value(value: Any) -> Any:
|
|
33
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
34
|
+
return value
|
|
35
|
+
try:
|
|
36
|
+
json.dumps(value)
|
|
37
|
+
except (TypeError, ValueError):
|
|
38
|
+
return repr(value)
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _safe_annotations(cls: type[Any]) -> dict[str, Any]:
|
|
43
|
+
try:
|
|
44
|
+
return dict(typing.get_type_hints(cls))
|
|
45
|
+
except (NameError, TypeError, ValueError):
|
|
46
|
+
annotations: dict[str, Any] = {}
|
|
47
|
+
for base in reversed(cls.__mro__):
|
|
48
|
+
annotations.update(getattr(base, "__annotations__", {}))
|
|
49
|
+
return annotations
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _safe_signature(member: Any) -> str | None:
|
|
53
|
+
try:
|
|
54
|
+
return str(_inspect.signature(member))
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class FieldInfo:
|
|
61
|
+
"""Description of an inspectable field."""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
type: str
|
|
65
|
+
default: Any = None
|
|
66
|
+
required: bool = False
|
|
67
|
+
kind: str = "field"
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"name": self.name,
|
|
72
|
+
"type": self.type,
|
|
73
|
+
"default": _json_value(self.default),
|
|
74
|
+
"required": self.required,
|
|
75
|
+
"kind": self.kind,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class MethodInfo:
|
|
81
|
+
"""Description of an inspectable method."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
signature: str | None
|
|
85
|
+
annotations: dict[str, str] = field(default_factory=dict)
|
|
86
|
+
kind: str = "method"
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"name": self.name,
|
|
91
|
+
"signature": self.signature,
|
|
92
|
+
"annotations": dict(self.annotations),
|
|
93
|
+
"kind": self.kind,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class InspectionResult:
|
|
99
|
+
"""Structured inspection data returned by :func:`inspect`."""
|
|
100
|
+
|
|
101
|
+
type: str
|
|
102
|
+
fields: dict[str, FieldInfo]
|
|
103
|
+
methods: dict[str, MethodInfo]
|
|
104
|
+
signatures: dict[str, str]
|
|
105
|
+
annotations: dict[str, str]
|
|
106
|
+
schema: dict[str, Any]
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict[str, Any]:
|
|
109
|
+
"""Return a recursively JSON-compatible representation."""
|
|
110
|
+
return {
|
|
111
|
+
"type": self.type,
|
|
112
|
+
"fields": {name: value.to_dict() for name, value in self.fields.items()},
|
|
113
|
+
"methods": {name: value.to_dict() for name, value in self.methods.items()},
|
|
114
|
+
"signatures": dict(self.signatures),
|
|
115
|
+
"annotations": dict(self.annotations),
|
|
116
|
+
"schema": self.schema,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _schema_for(annotation: Any) -> dict[str, Any]:
|
|
121
|
+
origin = typing.get_origin(annotation)
|
|
122
|
+
args = typing.get_args(annotation)
|
|
123
|
+
if annotation is Any:
|
|
124
|
+
return {}
|
|
125
|
+
if origin is typing.Union or (
|
|
126
|
+
_UNION_TYPE is not None and origin is _UNION_TYPE
|
|
127
|
+
):
|
|
128
|
+
non_none = [item for item in args if item is not type(None)]
|
|
129
|
+
if len(non_none) != len(args):
|
|
130
|
+
return _schema_for(non_none[0]) if len(non_none) == 1 else {
|
|
131
|
+
"anyOf": [_schema_for(item) for item in non_none]
|
|
132
|
+
}
|
|
133
|
+
return {"anyOf": [_schema_for(item) for item in args]}
|
|
134
|
+
if origin in (list, tuple, set, frozenset):
|
|
135
|
+
return {"type": "array", "items": _schema_for(args[0]) if args else {}}
|
|
136
|
+
if origin is dict:
|
|
137
|
+
return {"type": "object"}
|
|
138
|
+
primitive = {str: "string", int: "integer", float: "number", bool: "boolean"}
|
|
139
|
+
if annotation in primitive:
|
|
140
|
+
return {"type": primitive[annotation]}
|
|
141
|
+
return {"type": "object"}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def inspect(obj: Any) -> InspectionResult:
|
|
145
|
+
"""Inspect an object or class and return stable metadata."""
|
|
146
|
+
cls = obj if _inspect.isclass(obj) else type(obj)
|
|
147
|
+
annotations = _safe_annotations(cls)
|
|
148
|
+
values = (
|
|
149
|
+
{}
|
|
150
|
+
if _inspect.isclass(obj)
|
|
151
|
+
else vars(obj)
|
|
152
|
+
if hasattr(obj, "__dict__")
|
|
153
|
+
else {}
|
|
154
|
+
)
|
|
155
|
+
fields: dict[str, FieldInfo] = {}
|
|
156
|
+
|
|
157
|
+
for name, annotation in annotations.items():
|
|
158
|
+
class_member = getattr(cls, name, None)
|
|
159
|
+
if callable(class_member):
|
|
160
|
+
continue
|
|
161
|
+
member = getattr(obj, name, class_member)
|
|
162
|
+
has_class_default = (
|
|
163
|
+
name in cls.__dict__
|
|
164
|
+
and not isinstance(class_member, types.MemberDescriptorType)
|
|
165
|
+
and not isinstance(class_member, property)
|
|
166
|
+
)
|
|
167
|
+
has_default = name in values or has_class_default
|
|
168
|
+
fields[name] = FieldInfo(
|
|
169
|
+
name=name,
|
|
170
|
+
type=_type_name(annotation),
|
|
171
|
+
default=values.get(name, member),
|
|
172
|
+
required=not has_default,
|
|
173
|
+
kind="property" if isinstance(class_member, property) else "field",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for name, value in values.items():
|
|
177
|
+
if name.startswith("_") or name in fields or callable(value):
|
|
178
|
+
continue
|
|
179
|
+
fields[name] = FieldInfo(name, _type_name(type(value)), value)
|
|
180
|
+
|
|
181
|
+
for name, member in _inspect.getmembers(cls):
|
|
182
|
+
if name.startswith("_") or name in fields:
|
|
183
|
+
continue
|
|
184
|
+
if isinstance(member, property):
|
|
185
|
+
fields[name] = FieldInfo(
|
|
186
|
+
name,
|
|
187
|
+
_type_name(annotations.get(name, Any)),
|
|
188
|
+
kind="property",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
methods: dict[str, MethodInfo] = {}
|
|
192
|
+
signatures: dict[str, str] = {}
|
|
193
|
+
for name, member in _inspect.getmembers(cls, callable):
|
|
194
|
+
if name.startswith("_"):
|
|
195
|
+
continue
|
|
196
|
+
signature = _safe_signature(member)
|
|
197
|
+
if signature is None:
|
|
198
|
+
continue
|
|
199
|
+
try:
|
|
200
|
+
method_annotations = {
|
|
201
|
+
key: _type_name(value)
|
|
202
|
+
for key, value in typing.get_type_hints(member).items()
|
|
203
|
+
}
|
|
204
|
+
except (NameError, TypeError, ValueError):
|
|
205
|
+
method_annotations = {}
|
|
206
|
+
descriptor = cls.__dict__.get(name)
|
|
207
|
+
kind = "classmethod" if isinstance(descriptor, classmethod) else "method"
|
|
208
|
+
methods[name] = MethodInfo(name, signature, method_annotations, kind)
|
|
209
|
+
signatures[name] = signature
|
|
210
|
+
|
|
211
|
+
schema_properties = {
|
|
212
|
+
name: _schema_for(
|
|
213
|
+
annotations.get(name, type(values[name]) if name in values else Any)
|
|
214
|
+
)
|
|
215
|
+
for name in fields
|
|
216
|
+
}
|
|
217
|
+
schema: dict[str, Any] = {
|
|
218
|
+
"type": "object",
|
|
219
|
+
"title": cls.__name__,
|
|
220
|
+
"properties": schema_properties,
|
|
221
|
+
}
|
|
222
|
+
required = [name for name, info in fields.items() if info.required]
|
|
223
|
+
if required:
|
|
224
|
+
schema["required"] = required
|
|
225
|
+
|
|
226
|
+
return InspectionResult(
|
|
227
|
+
type=f"{cls.__module__}.{cls.__qualname__}",
|
|
228
|
+
fields=fields,
|
|
229
|
+
methods=methods,
|
|
230
|
+
signatures=signatures,
|
|
231
|
+
annotations={name: _type_name(value) for name, value in annotations.items()},
|
|
232
|
+
schema=schema,
|
|
233
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kindof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed, JSON-compatible object inspection for Python.
|
|
5
|
+
Author: wispsy
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# kindof
|
|
15
|
+
|
|
16
|
+
`kindof` provides a small, typed API for describing Python objects at runtime.
|
|
17
|
+
It is useful when an application needs stable metadata or a JSON-compatible
|
|
18
|
+
description without serializing an object's values directly.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from kindof import inspect
|
|
22
|
+
|
|
23
|
+
description = inspect(instance)
|
|
24
|
+
print(description.schema)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The result exposes `type`, `fields`, `methods`, `signatures`, `annotations`, and
|
|
28
|
+
a recursive JSON-compatible `schema`. Instance fields, class fields,
|
|
29
|
+
properties, inherited members, and safely introspectable method signatures are
|
|
30
|
+
included. Unsupported or dynamic members are omitted.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```shell
|
|
35
|
+
python -m pip install kindof
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick example
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from kindof import inspect
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class User:
|
|
45
|
+
name: str
|
|
46
|
+
active: bool = True
|
|
47
|
+
|
|
48
|
+
def greet(self, punctuation: str = "!") -> str:
|
|
49
|
+
return f"{self.name}{punctuation}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
user = User()
|
|
53
|
+
user.name = "Ada"
|
|
54
|
+
result = inspect(user)
|
|
55
|
+
|
|
56
|
+
assert result.fields["name"].required is False
|
|
57
|
+
assert result.methods["greet"].signature == "(self, punctuation: str = '!') -> str"
|
|
58
|
+
assert result.schema["properties"]["name"] == {"type": "string"}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [the API guide](docs/api.md) for the complete result model and schema
|
|
62
|
+
mapping, and [the examples](docs/examples.md) for more usage patterns.
|
|
63
|
+
|
|
64
|
+
## Releasing
|
|
65
|
+
|
|
66
|
+
Releases are published to PyPI through GitHub Actions using trusted publishing.
|
|
67
|
+
The repository's `pypi` environment supplies the OIDC authorization; no PyPI
|
|
68
|
+
token is stored in the repository. To publish a release:
|
|
69
|
+
|
|
70
|
+
1. Confirm the version in `pyproject.toml`.
|
|
71
|
+
2. Create and publish a GitHub release whose tag is exactly `v<version>` (for
|
|
72
|
+
example, `v0.1.0`).
|
|
73
|
+
3. The release workflow checks the tag against the package metadata, builds and
|
|
74
|
+
validates the source distribution and wheel, and publishes both to PyPI.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
kindof/__init__.py,sha256=Nqem3vKa9xsq23AlMYux1s8q6XN0sO2NZDTulJ069BI,192
|
|
2
|
+
kindof/inspection.py,sha256=rVCY7xVYtE2wkUQ3YVzHTkGV7SqAlxJZuC5agnVxq_k,7460
|
|
3
|
+
kindof-0.1.0.dist-info/METADATA,sha256=dN3d-lTBDUyBJmvqmJqYayYr7qH01jRQ5D1wjn4zRnQ,2190
|
|
4
|
+
kindof-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
kindof-0.1.0.dist-info/top_level.txt,sha256=YKX7qQz4dXkaFuTbdB6bxtX0WC4IHaqy3D3F9TvE-zU,7
|
|
6
|
+
kindof-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kindof
|