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 ADDED
@@ -0,0 +1,5 @@
1
+ """Public API for inspecting Python objects."""
2
+
3
+ from .inspection import FieldInfo, InspectionResult, MethodInfo, inspect
4
+
5
+ __all__ = ["FieldInfo", "InspectionResult", "MethodInfo", "inspect"]
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ kindof