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 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,5 @@
1
+ """Public API for inspecting Python objects."""
2
+
3
+ from .inspection import FieldInfo, InspectionResult, MethodInfo, inspect
4
+
5
+ __all__ = ["FieldInfo", "InspectionResult", "MethodInfo", "inspect"]
@@ -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,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ kindof/__init__.py
4
+ kindof/inspection.py
5
+ kindof.egg-info/PKG-INFO
6
+ kindof.egg-info/SOURCES.txt
7
+ kindof.egg-info/dependency_links.txt
8
+ kindof.egg-info/top_level.txt
9
+ tests/test_inspection.py
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)