agent-readable 0.1.0__tar.gz → 0.1.1__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.
Files changed (20) hide show
  1. {agent_readable-0.1.0 → agent_readable-0.1.1}/AGENT-PROMPT.md +6 -0
  2. {agent_readable-0.1.0 → agent_readable-0.1.1}/PKG-INFO +28 -8
  3. {agent_readable-0.1.0 → agent_readable-0.1.1}/README.md +26 -6
  4. agent_readable-0.1.1/docs/agent_help_vs_help.gif +0 -0
  5. agent_readable-0.1.0/examples/module_support.py → agent_readable-0.1.1/examples/modules_and_functions.py +22 -4
  6. {agent_readable-0.1.0 → agent_readable-0.1.1}/pyproject.toml +2 -2
  7. {agent_readable-0.1.0 → agent_readable-0.1.1}/src/agent_readable/__main__.py +15 -11
  8. {agent_readable-0.1.0 → agent_readable-0.1.1}/src/agent_readable/_protocol.py +55 -1
  9. {agent_readable-0.1.0 → agent_readable-0.1.1}/tests/test_cli.py +20 -3
  10. {agent_readable-0.1.0 → agent_readable-0.1.1}/tests/test_protocol.py +105 -0
  11. {agent_readable-0.1.0 → agent_readable-0.1.1}/.github/workflows/publish.yml +0 -0
  12. {agent_readable-0.1.0 → agent_readable-0.1.1}/.github/workflows/test.yml +0 -0
  13. {agent_readable-0.1.0 → agent_readable-0.1.1}/.gitignore +0 -0
  14. {agent_readable-0.1.0 → agent_readable-0.1.1}/LICENSE +0 -0
  15. {agent_readable-0.1.0 → agent_readable-0.1.1}/examples/any_class.py +0 -0
  16. {agent_readable-0.1.0 → agent_readable-0.1.1}/examples/duck_type.py +0 -0
  17. {agent_readable-0.1.0 → agent_readable-0.1.1}/examples/sqlite_connection.py +0 -0
  18. {agent_readable-0.1.0 → agent_readable-0.1.1}/examples/temperature.py +0 -0
  19. {agent_readable-0.1.0 → agent_readable-0.1.1}/src/agent_readable/__init__.py +0 -0
  20. {agent_readable-0.1.0 → agent_readable-0.1.1}/tests/__init__.py +0 -0
@@ -15,6 +15,12 @@ print(agent_help(ClassName))
15
15
  # or
16
16
  print(agent_help(package.module))
17
17
  # or: python -m agent_readable package.module
18
+
19
+ # functions and methods also work
20
+ print(agent_help(package.module.func))
21
+ print(agent_help(ClassName.method))
22
+ # or: python -m agent_readable package.module:func
23
+ # or: python -m agent_readable package.module:ClassName.method
18
24
  ```
19
25
 
20
26
  Treat the output as the canonical usage guide. Follow do/don't rules, anti-patterns, and lifecycle constraints exactly. Do not invent behavior that contradicts it.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-readable
3
- Version: 0.1.0
4
- Summary: A lightweight protocol for exposing agent-oriented documentation from Python classes and modules
3
+ Version: 0.1.1
4
+ Summary: A lightweight Python protocol for agent-oriented documentation
5
5
  Project-URL: Repository, https://github.com/zydo/agent-readable
6
6
  Author: zydo and agent-readable contributors
7
7
  License-Expression: MIT
@@ -28,6 +28,13 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  A lightweight Python protocol for exposing agent-oriented documentation from classes and modules.
30
30
 
31
+ <!-- markdownlint-disable MD033 -->
32
+ <p align="center">
33
+ <strong><code>logging.Logger</code> compared with <code>agent_help()</code> and <code>help()</code></strong><br>
34
+ <img src="docs/agent_help_vs_help.gif" alt="agent_help vs help">
35
+ </p>
36
+ <!-- markdownlint-enable MD033 -->
37
+
31
38
  ## Problem
32
39
 
33
40
  AI coding agents recognize established libraries from their training data, but they hallucinate when APIs change, when libraries are new, or when correct usage depends on rules that aren't visible from the API surface — pre-conditions, lifecycle order, anti-patterns, *"use `call()` for non-streaming, `stream()` for streaming."*
@@ -404,9 +411,9 @@ Instances of the Logger class represent a single logging channel. A
404
411
 
405
412
  No mixin, no duck-typing — just pass any class to `agent_help()`.
406
413
 
407
- ## Example 5: Modules
414
+ ## Example 5: Modules, functions, and methods
408
415
 
409
- `agent_help()` also works on modules — it generates a summary with the module docstring, public functions, and classes. Full example: [`examples/module_support.py`](examples/module_support.py).
416
+ `agent_help()` also works on modules — it generates a summary with the module docstring, public functions, and classes. Full example: [`examples/modules_and_functions.py`](examples/modules_and_functions.py).
410
417
 
411
418
  ```python
412
419
  import sys
@@ -429,7 +436,7 @@ Demonstrates both shapes of module support:
429
436
  2. A stdlib module (pathlib).
430
437
 
431
438
  Run this file to see both outputs:
432
- python examples/module_support.py
439
+ python examples/modules_and_functions.py
433
440
 
434
441
  ## Public API
435
442
 
@@ -445,7 +452,7 @@ Run this file to see both outputs:
445
452
  - If usage is ambiguous, prefer the simplest documented usage pattern.
446
453
  ````
447
454
 
448
- You can also pass any stdlib or third-party module — same `module_support.py` shows it.
455
+ You can also pass any stdlib or third-party module — same `modules_and_functions.py` shows it.
449
456
 
450
457
  ```python
451
458
  import pathlib
@@ -494,6 +501,15 @@ operating systems.
494
501
 
495
502
  Modules support less customization than classes — there is no mixin inheritance or `__agent_notes__()`. You can override the auto-generated output entirely by setting a module-level `__agent_help__` attribute (callable or string), but this is discouraged since it replaces the auto-generated summary — signatures, purpose, and public API listing are all lost. Prefer clear docstrings on the module and its functions/classes instead.
496
503
 
504
+ You can also pass a function or method directly — `agent_help()` renders the signature, full docstring, and usage rules. The output is close to `help()` for a single callable; the bigger wins are still on classes and modules.
505
+
506
+ ```python
507
+ import pathlib
508
+ from agent_readable import agent_help
509
+
510
+ print(agent_help(pathlib.Path.read_text))
511
+ ```
512
+
497
513
  ```python
498
514
  import sys
499
515
 
@@ -515,9 +531,13 @@ python -m agent_readable agent_readable:AgentReadableMixin
515
531
 
516
532
  # Any module
517
533
  python -m agent_readable pathlib
534
+
535
+ # A function or method
536
+ python -m agent_readable json:dumps
537
+ python -m agent_readable pathlib:Path.read_text
518
538
  ```
519
539
 
520
- Outputs agent-oriented documentation for the given class or module to stdout.
540
+ Outputs agent-oriented documentation for the given class, module, function, or method to stdout.
521
541
 
522
542
  ## FAQ
523
543
 
@@ -575,7 +595,7 @@ Classes that define a `@classmethod` named `__agent_help__` returning a `str` ar
575
595
  The two dunders intentionally encode different composition rules:
576
596
 
577
597
  | Aspect | `__agent_help__()` | `__agent_notes__()` |
578
- |-----------------|-------------------------------------------------|-----------------------------------------------------------------------------|
598
+ | --------------- | ----------------------------------------------- | --------------------------------------------------------------------------- |
579
599
  | Semantics | **Replacement** — returned string IS the output | **Additive** — appended to auto-generated docs |
580
600
  | Composition | Single class wins (the one closest in MRO) | Accumulated across the MRO; leaf class wins on conflict (header marks this) |
581
601
  | When to use | Total control over the rendered text | "Auto-doc + my extra do/don't rules" |
@@ -2,6 +2,13 @@
2
2
 
3
3
  A lightweight Python protocol for exposing agent-oriented documentation from classes and modules.
4
4
 
5
+ <!-- markdownlint-disable MD033 -->
6
+ <p align="center">
7
+ <strong><code>logging.Logger</code> compared with <code>agent_help()</code> and <code>help()</code></strong><br>
8
+ <img src="docs/agent_help_vs_help.gif" alt="agent_help vs help">
9
+ </p>
10
+ <!-- markdownlint-enable MD033 -->
11
+
5
12
  ## Problem
6
13
 
7
14
  AI coding agents recognize established libraries from their training data, but they hallucinate when APIs change, when libraries are new, or when correct usage depends on rules that aren't visible from the API surface — pre-conditions, lifecycle order, anti-patterns, *"use `call()` for non-streaming, `stream()` for streaming."*
@@ -378,9 +385,9 @@ Instances of the Logger class represent a single logging channel. A
378
385
 
379
386
  No mixin, no duck-typing — just pass any class to `agent_help()`.
380
387
 
381
- ## Example 5: Modules
388
+ ## Example 5: Modules, functions, and methods
382
389
 
383
- `agent_help()` also works on modules — it generates a summary with the module docstring, public functions, and classes. Full example: [`examples/module_support.py`](examples/module_support.py).
390
+ `agent_help()` also works on modules — it generates a summary with the module docstring, public functions, and classes. Full example: [`examples/modules_and_functions.py`](examples/modules_and_functions.py).
384
391
 
385
392
  ```python
386
393
  import sys
@@ -403,7 +410,7 @@ Demonstrates both shapes of module support:
403
410
  2. A stdlib module (pathlib).
404
411
 
405
412
  Run this file to see both outputs:
406
- python examples/module_support.py
413
+ python examples/modules_and_functions.py
407
414
 
408
415
  ## Public API
409
416
 
@@ -419,7 +426,7 @@ Run this file to see both outputs:
419
426
  - If usage is ambiguous, prefer the simplest documented usage pattern.
420
427
  ````
421
428
 
422
- You can also pass any stdlib or third-party module — same `module_support.py` shows it.
429
+ You can also pass any stdlib or third-party module — same `modules_and_functions.py` shows it.
423
430
 
424
431
  ```python
425
432
  import pathlib
@@ -468,6 +475,15 @@ operating systems.
468
475
 
469
476
  Modules support less customization than classes — there is no mixin inheritance or `__agent_notes__()`. You can override the auto-generated output entirely by setting a module-level `__agent_help__` attribute (callable or string), but this is discouraged since it replaces the auto-generated summary — signatures, purpose, and public API listing are all lost. Prefer clear docstrings on the module and its functions/classes instead.
470
477
 
478
+ You can also pass a function or method directly — `agent_help()` renders the signature, full docstring, and usage rules. The output is close to `help()` for a single callable; the bigger wins are still on classes and modules.
479
+
480
+ ```python
481
+ import pathlib
482
+ from agent_readable import agent_help
483
+
484
+ print(agent_help(pathlib.Path.read_text))
485
+ ```
486
+
471
487
  ```python
472
488
  import sys
473
489
 
@@ -489,9 +505,13 @@ python -m agent_readable agent_readable:AgentReadableMixin
489
505
 
490
506
  # Any module
491
507
  python -m agent_readable pathlib
508
+
509
+ # A function or method
510
+ python -m agent_readable json:dumps
511
+ python -m agent_readable pathlib:Path.read_text
492
512
  ```
493
513
 
494
- Outputs agent-oriented documentation for the given class or module to stdout.
514
+ Outputs agent-oriented documentation for the given class, module, function, or method to stdout.
495
515
 
496
516
  ## FAQ
497
517
 
@@ -549,7 +569,7 @@ Classes that define a `@classmethod` named `__agent_help__` returning a `str` ar
549
569
  The two dunders intentionally encode different composition rules:
550
570
 
551
571
  | Aspect | `__agent_help__()` | `__agent_notes__()` |
552
- |-----------------|-------------------------------------------------|-----------------------------------------------------------------------------|
572
+ | --------------- | ----------------------------------------------- | --------------------------------------------------------------------------- |
553
573
  | Semantics | **Replacement** — returned string IS the output | **Additive** — appended to auto-generated docs |
554
574
  | Composition | Single class wins (the one closest in MRO) | Accumulated across the MRO; leaf class wins on conflict (header marks this) |
555
575
  | When to use | Total control over the rendered text | "Auto-doc + my extra do/don't rules" |
@@ -1,12 +1,14 @@
1
1
  """
2
- Example: Using agent_help() on modules.
2
+ Example: Using agent_help() on modules, functions, and methods.
3
3
 
4
- Demonstrates both shapes of module support:
4
+ Demonstrates non-class targets:
5
5
  1. A custom module (this file itself).
6
6
  2. A stdlib module (pathlib).
7
+ 3. A function (connect, defined below).
8
+ 4. A method (Query.execute, defined below).
7
9
 
8
- Run this file to see both outputs:
9
- python examples/module_support.py
10
+ Run this file to see all outputs:
11
+ python examples/modules_and_functions.py
10
12
  """
11
13
 
12
14
  import os
@@ -47,3 +49,19 @@ if __name__ == "__main__":
47
49
  print("=== agent_help(pathlib) — stdlib module ===")
48
50
  print()
49
51
  print(agent_help(pathlib))
52
+
53
+ print()
54
+ print("=" * 72)
55
+ print()
56
+
57
+ print("=== agent_help(connect) — function ===")
58
+ print()
59
+ print(agent_help(connect))
60
+
61
+ print()
62
+ print("=" * 72)
63
+ print()
64
+
65
+ print("=== agent_help(Query.execute) — method ===")
66
+ print()
67
+ print(agent_help(Query.execute))
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-readable"
7
- version = "0.1.0"
8
- description = "A lightweight protocol for exposing agent-oriented documentation from Python classes and modules"
7
+ version = "0.1.1"
8
+ description = "A lightweight Python protocol for agent-oriented documentation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
@@ -1,21 +1,24 @@
1
1
  """CLI: ``python -m agent_readable package.module:ClassName``,
2
2
  ``python -m agent_readable package.module.ClassName``,
3
+ ``python -m agent_readable package.module:Class.method``,
3
4
  or ``python -m agent_readable package.module``."""
4
5
 
5
6
  from __future__ import annotations
6
7
 
7
8
  import importlib
9
+ import inspect
8
10
  import sys
9
11
  import types
12
+ from typing import Any
10
13
 
11
14
  from . import agent_help
12
15
 
13
16
 
14
- def _resolve(dotted_path: str) -> type | types.ModuleType:
15
- """Import and resolve a target path to a class or module.
17
+ def _resolve(dotted_path: str) -> Any:
18
+ """Import and resolve a target path to a class, module, function, or method.
16
19
 
17
- Accepts ``package.module:ClassName``, ``package.module.ClassName``, or
18
- ``package.module``.
20
+ Accepts ``package.module:ClassName``, ``package.module.ClassName``,
21
+ ``package.module:Class.method``, or ``package.module``.
19
22
  """
20
23
  if ":" in dotted_path:
21
24
  module_path, _, attr = dotted_path.partition(":")
@@ -24,25 +27,26 @@ def _resolve(dotted_path: str) -> type | types.ModuleType:
24
27
  else:
25
28
  return importlib.import_module(dotted_path)
26
29
 
27
- module = importlib.import_module(module_path)
30
+ target: Any = importlib.import_module(module_path)
28
31
 
29
32
  for part in attr.split(".") if attr else []:
30
- module = getattr(module, part)
33
+ target = getattr(target, part)
31
34
 
32
- if not isinstance(module, type) and not isinstance(module, types.ModuleType):
35
+ if not (isinstance(target, (type, types.ModuleType)) or inspect.isroutine(target)):
33
36
  raise TypeError(
34
- f"{dotted_path!r} resolved to {type(module).__name__}, "
35
- "expected a class or module"
37
+ f"{dotted_path!r} resolved to {type(target).__name__}, "
38
+ "expected a class, module, function, or method"
36
39
  )
37
40
 
38
- return module
41
+ return target
39
42
 
40
43
 
41
44
  def main() -> None:
42
45
  if len(sys.argv) < 2:
43
46
  print(
44
47
  "Usage: python -m agent_readable "
45
- "(package.module:ClassName | package.module.ClassName | package.module)",
48
+ "(package.module:Target | package.module.Target | package.module) "
49
+ "where Target is a class, function, or Class.method",
46
50
  file=sys.stderr,
47
51
  )
48
52
  sys.exit(1)
@@ -66,7 +66,7 @@ class AgentReadableMixin:
66
66
 
67
67
  def agent_help(obj: Any) -> str:
68
68
  """
69
- Return agent-oriented help for a class, instance, or module.
69
+ Return agent-oriented help for a class, instance, module, function, or method.
70
70
 
71
71
  Dispatch for classes/instances:
72
72
 
@@ -89,6 +89,11 @@ def agent_help(obj: Any) -> str:
89
89
  or string), it is used directly. Otherwise auto-generated docs are produced
90
90
  via ``_module_doc()``. Module ``__agent_notes__`` is not part of the
91
91
  protocol — modules don't have an MRO to accumulate over.
92
+
93
+ For functions and methods (anything ``inspect.isroutine`` accepts): an
94
+ ``__agent_help__`` attribute on the routine — callable or string — is used
95
+ directly if present; otherwise ``_function_doc()`` renders signature, full
96
+ docstring, and agent usage rules.
92
97
  """
93
98
  if inspect.ismodule(obj):
94
99
  fn = getattr(obj, "__agent_help__", None)
@@ -104,6 +109,20 @@ def agent_help(obj: Any) -> str:
104
109
  return fn
105
110
  return _module_doc(obj)
106
111
 
112
+ if inspect.isroutine(obj):
113
+ override = getattr(obj, "__agent_help__", None)
114
+ if callable(override):
115
+ try:
116
+ result = override()
117
+ if isinstance(result, str):
118
+ return result
119
+ return str(result)
120
+ except Exception:
121
+ pass
122
+ if isinstance(override, str):
123
+ return override
124
+ return _function_doc(obj)
125
+
107
126
  target = obj if inspect.isclass(obj) else obj.__class__
108
127
 
109
128
  fn = getattr(target, "__agent_help__", None)
@@ -183,6 +202,41 @@ def _collect_module_api(module: types.ModuleType) -> list[str]:
183
202
  return lines
184
203
 
185
204
 
205
+ def _function_doc(fn: Any) -> str:
206
+ """Generate compact Markdown documentation for a function or method."""
207
+ parts: list[str] = []
208
+
209
+ short_name = getattr(fn, "__name__", None) or "function"
210
+ display_name = getattr(fn, "__qualname__", None) or short_name
211
+ parts.append(f"# {display_name}")
212
+ parts.append("")
213
+
214
+ sig = _safe_signature(fn)
215
+ parts.append("## Signature")
216
+ parts.append("")
217
+ parts.append("```python")
218
+ parts.append(f"{short_name}{sig}")
219
+ parts.append("```")
220
+ parts.append("")
221
+
222
+ doc = inspect.getdoc(fn)
223
+ if doc:
224
+ parts.append("## Purpose")
225
+ parts.append("")
226
+ parts.append(doc)
227
+ parts.append("")
228
+
229
+ parts.append("## Agent usage rules")
230
+ parts.append("")
231
+ parts.append("- Call with the documented signature.")
232
+ parts.append("- Do not invent unsupported behavior.")
233
+ parts.append(
234
+ "- If usage is ambiguous, prefer the simplest documented usage pattern."
235
+ )
236
+
237
+ return "\n".join(parts)
238
+
239
+
186
240
  def _base_agent_doc(cls: type) -> str:
187
241
  """
188
242
  Generate compact Markdown documentation for AI coding agents.
@@ -71,9 +71,26 @@ def test_cli_nonexistent_attribute():
71
71
  _run_main("tests.test_cli:NonExistent")
72
72
 
73
73
 
74
- def test_cli_non_class_target():
75
- with pytest.raises(TypeError, match="expected a class or module"):
76
- _run_main("agent_readable:agent_help")
74
+ def test_cli_function_target(capsys):
75
+ code = _run_main("agent_readable:agent_help")
76
+ assert code == 0
77
+ out = capsys.readouterr().out
78
+ assert "agent_help" in out
79
+ assert "## Signature" in out
80
+
81
+
82
+ def test_cli_method_target(capsys):
83
+ code = _run_main("tests.test_cli:Cache.get")
84
+ assert code == 0
85
+ out = capsys.readouterr().out
86
+ assert "Cache.get" in out
87
+ assert "## Signature" in out
88
+ assert "Retrieve a value by key." in out
89
+
90
+
91
+ def test_cli_invalid_target_type():
92
+ with pytest.raises(TypeError, match="expected a class, module, function"):
93
+ _run_main("agent_readable:__version__")
77
94
 
78
95
 
79
96
  def test_cli_module(capsys):
@@ -584,3 +584,108 @@ def test_agent_help_module_invalid_attr_falls_back():
584
584
  result = agent_help(mod)
585
585
  assert "# mymod" in result
586
586
  assert "Fallback doc." in result
587
+
588
+
589
+ # -- Function / method tests -------------------------------------------------
590
+
591
+
592
+ def test_agent_help_function_basic():
593
+ def greet(name: str) -> str:
594
+ """Say hello to someone."""
595
+ return f"Hello, {name}"
596
+
597
+ result = agent_help(greet)
598
+ assert result.startswith("#") and "greet" in result.splitlines()[0]
599
+ assert "## Signature" in result
600
+ assert "greet(name: str) -> str" in result
601
+ assert "## Purpose" in result
602
+ assert "Say hello to someone." in result
603
+ assert "## Agent usage rules" in result
604
+
605
+
606
+ def test_agent_help_unbound_method_keeps_self():
607
+ class Pool:
608
+ def rotated(self, n: int) -> "Pool":
609
+ """Rotate the pool by n positions."""
610
+ return self
611
+
612
+ result = agent_help(Pool.rotated)
613
+ assert "Pool.rotated" in result.splitlines()[0]
614
+ assert "rotated(self, n: int)" in result
615
+ assert "Rotate the pool by n positions." in result
616
+
617
+
618
+ def test_agent_help_bound_method_strips_self():
619
+ class Pool:
620
+ def rotated(self, n: int) -> "Pool":
621
+ """Rotate the pool by n positions."""
622
+ return self
623
+
624
+ result = agent_help(Pool().rotated)
625
+ assert "rotated(n: int)" in result
626
+ assert "self" not in result.split("## Signature", 1)[1].split("##", 1)[0]
627
+
628
+
629
+ def test_agent_help_classmethod_strips_cls():
630
+ class Pool:
631
+ @classmethod
632
+ def of(cls, n: int) -> "Pool":
633
+ """Construct a pool of size n."""
634
+ return cls()
635
+
636
+ result = agent_help(Pool.of)
637
+ assert "of(n: int)" in result
638
+ assert "Construct a pool of size n." in result
639
+
640
+
641
+ def test_agent_help_function_without_docstring():
642
+ def f(x: int) -> int:
643
+ return x
644
+
645
+ result = agent_help(f)
646
+ assert result.startswith("#") and "f" in result.splitlines()[0]
647
+ assert "## Purpose" not in result
648
+ assert "## Agent usage rules" in result
649
+
650
+
651
+ def test_agent_help_function_custom_callable_override():
652
+ def f():
653
+ """Auto doc."""
654
+
655
+ f.__agent_help__ = lambda: "Custom function help."
656
+ assert agent_help(f) == "Custom function help."
657
+
658
+
659
+ def test_agent_help_function_custom_string_override():
660
+ def f():
661
+ """Auto doc."""
662
+
663
+ f.__agent_help__ = "String function help."
664
+ assert agent_help(f) == "String function help."
665
+
666
+
667
+ def test_agent_help_function_non_string_return():
668
+ def f():
669
+ """Auto doc."""
670
+
671
+ f.__agent_help__ = lambda: 99
672
+ assert agent_help(f) == "99"
673
+
674
+
675
+ def test_agent_help_function_override_exception_falls_back():
676
+ def f():
677
+ """Fallback doc."""
678
+
679
+ def boom():
680
+ raise RuntimeError("boom")
681
+
682
+ f.__agent_help__ = boom
683
+ result = agent_help(f)
684
+ assert "Fallback doc." in result
685
+ assert "## Signature" in result
686
+
687
+
688
+ def test_agent_help_builtin_function():
689
+ result = agent_help(len)
690
+ assert "# len" in result
691
+ assert "## Signature" in result
File without changes