kindof 0.1.0__tar.gz
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-0.1.0/PKG-INFO +74 -0
- kindof-0.1.0/README.md +61 -0
- kindof-0.1.0/kindof/__init__.py +5 -0
- kindof-0.1.0/kindof/inspection.py +233 -0
- kindof-0.1.0/kindof.egg-info/PKG-INFO +74 -0
- kindof-0.1.0/kindof.egg-info/SOURCES.txt +9 -0
- kindof-0.1.0/kindof.egg-info/dependency_links.txt +1 -0
- kindof-0.1.0/kindof.egg-info/top_level.txt +1 -0
- kindof-0.1.0/pyproject.toml +36 -0
- kindof-0.1.0/setup.cfg +4 -0
- kindof-0.1.0/tests/test_inspection.py +134 -0
kindof-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
kindof-0.1.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# kindof
|
|
2
|
+
|
|
3
|
+
`kindof` provides a small, typed API for describing Python objects at runtime.
|
|
4
|
+
It is useful when an application needs stable metadata or a JSON-compatible
|
|
5
|
+
description without serializing an object's values directly.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from kindof import inspect
|
|
9
|
+
|
|
10
|
+
description = inspect(instance)
|
|
11
|
+
print(description.schema)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The result exposes `type`, `fields`, `methods`, `signatures`, `annotations`, and
|
|
15
|
+
a recursive JSON-compatible `schema`. Instance fields, class fields,
|
|
16
|
+
properties, inherited members, and safely introspectable method signatures are
|
|
17
|
+
included. Unsupported or dynamic members are omitted.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
python -m pip install kindof
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick example
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from kindof import inspect
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class User:
|
|
32
|
+
name: str
|
|
33
|
+
active: bool = True
|
|
34
|
+
|
|
35
|
+
def greet(self, punctuation: str = "!") -> str:
|
|
36
|
+
return f"{self.name}{punctuation}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
user = User()
|
|
40
|
+
user.name = "Ada"
|
|
41
|
+
result = inspect(user)
|
|
42
|
+
|
|
43
|
+
assert result.fields["name"].required is False
|
|
44
|
+
assert result.methods["greet"].signature == "(self, punctuation: str = '!') -> str"
|
|
45
|
+
assert result.schema["properties"]["name"] == {"type": "string"}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See [the API guide](docs/api.md) for the complete result model and schema
|
|
49
|
+
mapping, and [the examples](docs/examples.md) for more usage patterns.
|
|
50
|
+
|
|
51
|
+
## Releasing
|
|
52
|
+
|
|
53
|
+
Releases are published to PyPI through GitHub Actions using trusted publishing.
|
|
54
|
+
The repository's `pypi` environment supplies the OIDC authorization; no PyPI
|
|
55
|
+
token is stored in the repository. To publish a release:
|
|
56
|
+
|
|
57
|
+
1. Confirm the version in `pyproject.toml`.
|
|
58
|
+
2. Create and publish a GitHub release whose tag is exactly `v<version>` (for
|
|
59
|
+
example, `v0.1.0`).
|
|
60
|
+
3. The release workflow checks the tag against the package metadata, builds and
|
|
61
|
+
validates the source distribution and wheel, and publishes both to PyPI.
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kindof
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kindof"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Typed, JSON-compatible object inspection for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "wispsy"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["kindof*"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
addopts = "-q"
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
target-version = "py39"
|
|
29
|
+
line-length = 88
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = ["E", "F", "I", "UP"]
|
|
33
|
+
|
|
34
|
+
[tool.mypy]
|
|
35
|
+
python_version = "3.10"
|
|
36
|
+
strict = true
|
kindof-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Optional, Union
|
|
3
|
+
|
|
4
|
+
from kindof import inspect
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Parent:
|
|
8
|
+
inherited: str = "parent"
|
|
9
|
+
|
|
10
|
+
def inherited_method(self, value: int) -> str:
|
|
11
|
+
return str(value)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class User(Parent):
|
|
15
|
+
name: str
|
|
16
|
+
age: int = 0
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str) -> None:
|
|
19
|
+
self.name = name
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def display_name(self) -> str:
|
|
23
|
+
return self.name.upper()
|
|
24
|
+
|
|
25
|
+
def greet(self, punctuation: Optional[str] = None) -> str:
|
|
26
|
+
return self.name + (punctuation or "!")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Complex:
|
|
30
|
+
text: str
|
|
31
|
+
values: list[int]
|
|
32
|
+
mapping: dict[str, int]
|
|
33
|
+
maybe: Optional[str]
|
|
34
|
+
choice: Union[int, str]
|
|
35
|
+
unknown: Any
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def make(cls, value: int) -> "Complex":
|
|
39
|
+
return cls()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class WithSlots:
|
|
43
|
+
__slots__ = ("value",)
|
|
44
|
+
value: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_inspect_collects_fields_methods_and_inheritance() -> None:
|
|
48
|
+
result = inspect(User("Ada"))
|
|
49
|
+
|
|
50
|
+
assert result.type.endswith(".User")
|
|
51
|
+
assert set(result.fields) >= {"name", "age", "inherited", "display_name"}
|
|
52
|
+
assert set(result.methods) >= {"greet", "inherited_method"}
|
|
53
|
+
assert (
|
|
54
|
+
result.signatures["greet"]
|
|
55
|
+
== "(self, punctuation: Optional[str] = None) -> str"
|
|
56
|
+
)
|
|
57
|
+
assert result.fields["display_name"].kind == "property"
|
|
58
|
+
assert result.annotations["name"] == "str"
|
|
59
|
+
assert result.fields["name"].required is False
|
|
60
|
+
assert result.fields["inherited"].default == "parent"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_inspecting_a_class_does_not_read_instance_values() -> None:
|
|
64
|
+
result = inspect(User)
|
|
65
|
+
|
|
66
|
+
assert result.fields["name"].required is True
|
|
67
|
+
assert result.fields["name"].default is None
|
|
68
|
+
assert result.fields["age"].default == 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_schema_and_to_dict_are_json_compatible() -> None:
|
|
72
|
+
payload = inspect(User).to_dict()
|
|
73
|
+
|
|
74
|
+
assert payload["schema"]["type"] == "object"
|
|
75
|
+
assert payload["schema"]["properties"]["name"] == {"type": "string"}
|
|
76
|
+
assert payload["schema"]["properties"]["age"] == {"type": "integer"}
|
|
77
|
+
assert payload["schema"]["properties"]["display_name"] == {}
|
|
78
|
+
json.dumps(payload)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_schema_handles_recursive_containers_unions_and_any() -> None:
|
|
82
|
+
schema = inspect(Complex).schema
|
|
83
|
+
|
|
84
|
+
assert schema["properties"]["values"] == {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {"type": "integer"},
|
|
87
|
+
}
|
|
88
|
+
assert schema["properties"]["mapping"] == {"type": "object"}
|
|
89
|
+
assert schema["properties"]["maybe"] == {"type": "string"}
|
|
90
|
+
assert schema["properties"]["choice"] == {
|
|
91
|
+
"anyOf": [{"type": "integer"}, {"type": "string"}]
|
|
92
|
+
}
|
|
93
|
+
assert schema["properties"]["unknown"] == {}
|
|
94
|
+
assert set(schema["required"]) >= {
|
|
95
|
+
"text",
|
|
96
|
+
"values",
|
|
97
|
+
"mapping",
|
|
98
|
+
"maybe",
|
|
99
|
+
"choice",
|
|
100
|
+
"unknown",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_method_metadata_includes_annotations_and_classmethod_kind() -> None:
|
|
105
|
+
result = inspect(Complex)
|
|
106
|
+
|
|
107
|
+
assert result.methods["make"].kind == "classmethod"
|
|
108
|
+
assert result.methods["make"].signature == "(value: int) -> 'Complex'"
|
|
109
|
+
assert result.methods["make"].annotations == {"value": "int", "return": "Complex"}
|
|
110
|
+
assert result.signatures["make"] == result.methods["make"].signature
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_slots_only_objects_are_safe_to_inspect() -> None:
|
|
114
|
+
result = inspect(WithSlots())
|
|
115
|
+
|
|
116
|
+
assert result.fields["value"].required is True
|
|
117
|
+
assert result.schema["properties"]["value"] == {"type": "integer"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_private_and_callable_instance_values_are_omitted() -> None:
|
|
121
|
+
class Dynamic:
|
|
122
|
+
visible: int = 1
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self.extra = object()
|
|
126
|
+
self._private = "hidden"
|
|
127
|
+
self.callback = lambda: None
|
|
128
|
+
|
|
129
|
+
result = inspect(Dynamic())
|
|
130
|
+
|
|
131
|
+
assert "extra" in result.fields
|
|
132
|
+
assert "_private" not in result.fields
|
|
133
|
+
assert "callback" not in result.fields
|
|
134
|
+
assert isinstance(result.to_dict()["fields"]["extra"]["default"], str)
|