pymethodbook 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.
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install -e .
28
+ pip install pytest
29
+
30
+ - name: Validate curated data
31
+ run: python scripts/validate_data.py
32
+
33
+ - name: Run tests
34
+ run: python -m pytest tests/
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
@@ -0,0 +1,41 @@
1
+ # Contributing to pymethodbook
2
+
3
+ Thanks for considering a contribution! The easiest way to help is adding a new method entry — it requires zero Python knowledge.
4
+
5
+ ## Adding a method
6
+
7
+ 1. Open the relevant file in `pymethodbook/data/` (e.g. `list.json` for a list method). If the type doesn't have a file yet, create one.
8
+ 2. Add an entry following this exact shape:
9
+
10
+ ​```json
11
+ "method_name": {
12
+ "signature": "type.method_name(args)",
13
+ "description": "One clear sentence on what it does.",
14
+ "example": "input_code\n# -> expected_output",
15
+ "category": "mutating | read-only"
16
+ }
17
+ ​```
18
+
19
+ 3. Validate it:
20
+
21
+ ​```bash
22
+ python scripts/validate_data.py
23
+ ​```
24
+
25
+ 4. Run the test suite to confirm nothing broke:
26
+
27
+ ​```bash
28
+ python -m pytest tests/
29
+ ​```
30
+
31
+ 5. Open a pull request. Please keep one method (or one type file) per PR so it's easy to review.
32
+
33
+ ## Adding a new type (e.g. `str`, `set`)
34
+
35
+ 1. Create `pymethodbook/data/<type>.json` following the schema above.
36
+ 2. Run `python scripts/validate_data.py` — it automatically picks up any new JSON file in `data/`.
37
+ 3. No changes needed to `core.py`, `render.py`, or `cli.py` — they already work generically across every file in `data/`.
38
+
39
+ ## Reporting bugs or suggesting features
40
+
41
+ Open a GitHub issue with a clear description and, for bugs, the exact command you ran and what happened.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Logeshwar Chandrasekar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymethodbook
3
+ Version: 0.1.0
4
+ Summary: A terminal-first reference tool for Python's built-in methods — curated descriptions, runnable examples, and a searchable CLI.
5
+ Project-URL: Homepage, https://github.com/logeshchandrasekar/pymethodbook
6
+ Project-URL: Repository, https://github.com/logeshchandrasekar/pymethodbook
7
+ Project-URL: Issues, https://github.com/logeshchandrasekar/pymethodbook/issues
8
+ Author-email: Logesh Chandrasekar <logeshwarchandrasekar@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: builtins,cheatsheet,cli,documentation,python,reference
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Documentation
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: rich>=13.0
26
+ Requires-Dist: typer>=0.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # pymethodbook
30
+
31
+ A terminal-first reference tool for Python's built-in methods — curated descriptions, runnable examples, and a searchable CLI. No more digging through docs for what `list.sort()` actually does.
32
+
33
+ ## Why
34
+
35
+ `dir()` and `help()` give you names and terse docstrings. pymethodbook gives you a one-line description, a runnable example, and a category tag for every method — right in your terminal, in code or from the command line.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install pymethodbook
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### As a CLI
46
+
47
+ ```bash
48
+ pymethodbook explain list append
49
+ pymethodbook list dict
50
+ pymethodbook search copy
51
+ ```
52
+
53
+ ### As a library
54
+
55
+ ```python
56
+ from pymethodbook import explain, enlist, search
57
+
58
+ explain("list", "append")
59
+ enlist("dict")
60
+ search("copy")
61
+ ```
62
+
63
+ ## Currently covers
64
+
65
+ - `list` — 11 methods
66
+ - `dict` — 11 methods
67
+
68
+ More types (`str`, `set`, `tuple`) are on the roadmap — see [ROADMAP.md](ROADMAP.md).
69
+
70
+ ## Contributing
71
+
72
+ Adding a method doesn't require touching any Python code — see [CONTRIBUTING.md](CONTRIBUTING.md).
73
+
74
+ ## License
75
+
76
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,48 @@
1
+ # pymethodbook
2
+
3
+ A terminal-first reference tool for Python's built-in methods — curated descriptions, runnable examples, and a searchable CLI. No more digging through docs for what `list.sort()` actually does.
4
+
5
+ ## Why
6
+
7
+ `dir()` and `help()` give you names and terse docstrings. pymethodbook gives you a one-line description, a runnable example, and a category tag for every method — right in your terminal, in code or from the command line.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install pymethodbook
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### As a CLI
18
+
19
+ ```bash
20
+ pymethodbook explain list append
21
+ pymethodbook list dict
22
+ pymethodbook search copy
23
+ ```
24
+
25
+ ### As a library
26
+
27
+ ```python
28
+ from pymethodbook import explain, enlist, search
29
+
30
+ explain("list", "append")
31
+ enlist("dict")
32
+ search("copy")
33
+ ```
34
+
35
+ ## Currently covers
36
+
37
+ - `list` — 11 methods
38
+ - `dict` — 11 methods
39
+
40
+ More types (`str`, `set`, `tuple`) are on the roadmap — see [ROADMAP.md](ROADMAP.md).
41
+
42
+ ## Contributing
43
+
44
+ Adding a method doesn't require touching any Python code — see [CONTRIBUTING.md](CONTRIBUTING.md).
45
+
46
+ ## License
47
+
48
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
1
+ # Roadmap
2
+
3
+ Currently supported types: `list`, `dict` (v1).
4
+
5
+ ## v2
6
+ - `int` / `float` numeric methods
7
+ - `frozenset`
8
+ - String formatting mini-language reference
9
+
10
+ ## v3
11
+ - Common `itertools` / `functools` / `collections` recipes
12
+
13
+ ## v4
14
+ - `pymethodbook --interactive` — an offline, browsable mode
15
+
16
+ ## v5
17
+ - Community-contributed "type packs" — a plugin format so others can add coverage (e.g. pandas) without touching core
18
+
19
+ ## Ongoing
20
+ - Expanding `str`, `set`, `tuple` coverage
21
+ - Improving example quality based on GitHub issues and requests.
@@ -0,0 +1,23 @@
1
+ """pymethodbook — a terminal-first reference tool for Python's built-in methods."""
2
+ from .core import get_method, load_type, search as _search
3
+ from .render import render_method, render_type_table, render_search_results
4
+
5
+ __version__ = "0.1.0"
6
+
7
+
8
+ def explain(type_name, method_name):
9
+ """Looks up one method and prints it."""
10
+ entry = get_method(type_name, method_name)
11
+ render_method(type_name, method_name, entry)
12
+
13
+
14
+ def enlist(type_name):
15
+ """Prints every curated method for a type."""
16
+ data = load_type(type_name)
17
+ render_type_table(type_name, data)
18
+
19
+
20
+ def search(keyword):
21
+ """Searches every type for a keyword and prints the results."""
22
+ results = _search(keyword)
23
+ render_search_results(keyword, results)
@@ -0,0 +1,40 @@
1
+ """Command-line interface for pymethodbook."""
2
+ import typer
3
+
4
+ from . import explain as _explain, enlist as _enlist, search as _search
5
+ from .core import TypeNotFoundError, MethodNotFoundError
6
+
7
+ app = typer.Typer(help="A terminal-first reference tool for Python's built-in methods.")
8
+
9
+
10
+ @app.command()
11
+ def explain(
12
+ type_name: str = typer.Argument(..., help="e.g. list, dict"),
13
+ method_name: str = typer.Argument(..., help="e.g. append, get"),
14
+ ):
15
+ """Show one method's description, signature, and example."""
16
+ try:
17
+ _explain(type_name, method_name)
18
+ except (TypeNotFoundError, MethodNotFoundError) as e:
19
+ typer.secho(str(e), fg=typer.colors.RED)
20
+ raise typer.Exit(code=1)
21
+
22
+
23
+ @app.command(name="list")
24
+ def list_methods(type_name: str = typer.Argument(..., help="e.g. list, dict")):
25
+ """List every curated method for a type."""
26
+ try:
27
+ _enlist(type_name)
28
+ except TypeNotFoundError as e:
29
+ typer.secho(str(e), fg=typer.colors.RED)
30
+ raise typer.Exit(code=1)
31
+
32
+
33
+ @app.command()
34
+ def search(keyword: str = typer.Argument(..., help="Keyword to search for")):
35
+ """Search every type for a keyword."""
36
+ _search(keyword)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ app()
@@ -0,0 +1,70 @@
1
+ """Core lookup and search logic for pymethodbook. No printing happens here —
2
+ this module only loads and finds data; pymethodbook/render.py displays it."""
3
+ import json
4
+ import difflib
5
+ from pathlib import Path
6
+
7
+ DATA_DIR = Path(__file__).parent / "data"
8
+
9
+
10
+ class TypeNotFoundError(Exception):
11
+ """Raised when a requested type has no data file."""
12
+
13
+
14
+ class MethodNotFoundError(Exception):
15
+ """Raised when a requested method isn't found for a type."""
16
+
17
+
18
+ def available_types():
19
+ """Returns a sorted list of supported type names, e.g. ['dict', 'list']."""
20
+ return sorted(p.stem for p in DATA_DIR.glob("*.json"))
21
+
22
+
23
+ def load_type(type_name):
24
+ """Loads and returns the full data dict for a type, e.g. load_type('list')."""
25
+ path = DATA_DIR / f"{type_name}.json"
26
+ if not path.exists():
27
+ types = ", ".join(available_types())
28
+ raise TypeNotFoundError(f"No data for type '{type_name}'. Available types: {types}")
29
+ with open(path, encoding="utf-8") as f:
30
+ return json.load(f)
31
+
32
+
33
+ def get_method(type_name, method_name):
34
+ """Returns the entry dict for one method. Raises MethodNotFoundError with
35
+ close-match suggestions if the name is misspelled."""
36
+ data = load_type(type_name)
37
+ if method_name in data:
38
+ return data[method_name]
39
+
40
+ close = difflib.get_close_matches(method_name, data.keys(), n=3, cutoff=0.5)
41
+ hint = f" Did you mean: {', '.join(close)}?" if close else ""
42
+ raise MethodNotFoundError(f"'{method_name}' not found on {type_name}.{hint}")
43
+
44
+
45
+ def search(keyword):
46
+ """Searches every type's methods for keyword matches in the name or
47
+ description. Returns a list of (type_name, method_name, entry) tuples —
48
+ exact name matches first, then substring matches, then fuzzy matches."""
49
+ keyword_lower = keyword.lower()
50
+ exact, substring, fuzzy = [], [], []
51
+
52
+ for type_name in available_types():
53
+ data = load_type(type_name)
54
+ already = set()
55
+
56
+ for method_name, entry in data.items():
57
+ if method_name.lower() == keyword_lower:
58
+ exact.append((type_name, method_name, entry))
59
+ already.add(method_name)
60
+ elif keyword_lower in method_name.lower() or keyword_lower in entry["description"].lower():
61
+ substring.append((type_name, method_name, entry))
62
+ already.add(method_name)
63
+
64
+ remaining = [m for m in data if m not in already]
65
+ close = difflib.get_close_matches(keyword_lower, [m.lower() for m in remaining], n=3, cutoff=0.6)
66
+ for method_name in remaining:
67
+ if method_name.lower() in close:
68
+ fuzzy.append((type_name, method_name, data[method_name]))
69
+
70
+ return exact + substring + fuzzy
@@ -0,0 +1,68 @@
1
+ {
2
+ "get": {
3
+ "signature": "dict.get(key, default=None)",
4
+ "description": "Returns the value for key if present, otherwise default, without raising an error.",
5
+ "example": "d = {\"a\": 1}\nd.get(\"b\", 0)\n# -> 0",
6
+ "category": "read-only"
7
+ },
8
+ "keys": {
9
+ "signature": "dict.keys()",
10
+ "description": "Returns a view of all keys in the dictionary.",
11
+ "example": "d = {\"a\": 1, \"b\": 2}\nlist(d.keys())\n# -> ['a', 'b']",
12
+ "category": "read-only"
13
+ },
14
+ "values": {
15
+ "signature": "dict.values()",
16
+ "description": "Returns a view of all values in the dictionary.",
17
+ "example": "d = {\"a\": 1, \"b\": 2}\nlist(d.values())\n# -> [1, 2]",
18
+ "category": "read-only"
19
+ },
20
+ "items": {
21
+ "signature": "dict.items()",
22
+ "description": "Returns a view of (key, value) pairs in the dictionary.",
23
+ "example": "d = {\"a\": 1}\nlist(d.items())\n# -> [('a', 1)]",
24
+ "category": "read-only"
25
+ },
26
+ "update": {
27
+ "signature": "dict.update(other)",
28
+ "description": "Adds key/value pairs from another dict or iterable, overwriting existing keys, in place.",
29
+ "example": "d = {\"a\": 1}\nd.update({\"b\": 2})\n# d -> {'a': 1, 'b': 2}",
30
+ "category": "mutating"
31
+ },
32
+ "pop": {
33
+ "signature": "dict.pop(key, default)",
34
+ "description": "Removes key and returns its value; returns default (or raises KeyError) if key is missing.",
35
+ "example": "d = {\"a\": 1, \"b\": 2}\nd.pop(\"a\")\n# -> 1, d -> {'b': 2}",
36
+ "category": "mutating"
37
+ },
38
+ "popitem": {
39
+ "signature": "dict.popitem()",
40
+ "description": "Removes and returns the most recently inserted (key, value) pair.",
41
+ "example": "d = {\"a\": 1, \"b\": 2}\nd.popitem()\n# -> ('b', 2)",
42
+ "category": "mutating"
43
+ },
44
+ "setdefault": {
45
+ "signature": "dict.setdefault(key, default=None)",
46
+ "description": "Returns the value for key if present; otherwise inserts key with default and returns it.",
47
+ "example": "d = {\"a\": 1}\nd.setdefault(\"b\", 2)\n# -> 2, d -> {'a': 1, 'b': 2}",
48
+ "category": "mutating"
49
+ },
50
+ "clear": {
51
+ "signature": "dict.clear()",
52
+ "description": "Removes all items from the dictionary, in place.",
53
+ "example": "d = {\"a\": 1}\nd.clear()\n# d -> {}",
54
+ "category": "mutating"
55
+ },
56
+ "copy": {
57
+ "signature": "dict.copy()",
58
+ "description": "Returns a shallow copy of the dictionary as a new, separate object.",
59
+ "example": "d = {\"a\": 1}\nd2 = d.copy()\n# d2 -> {'a': 1}",
60
+ "category": "read-only"
61
+ },
62
+ "fromkeys": {
63
+ "signature": "dict.fromkeys(iterable, value=None)",
64
+ "description": "Creates a new dict with keys from iterable, all set to the same value.",
65
+ "example": "dict.fromkeys(['a', 'b'], 0)\n# -> {'a': 0, 'b': 0}",
66
+ "category": "read-only"
67
+ }
68
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "append": {
3
+ "signature": "list.append(item)",
4
+ "description": "Adds a single item to the end of the list, in place.",
5
+ "example": "nums = [1, 2]\nnums.append(3)\n# nums -> [1, 2, 3]",
6
+ "category": "mutating"
7
+ },
8
+ "extend": {
9
+ "signature": "list.extend(iterable)",
10
+ "description": "Adds all items from an iterable to the end of the list, in place.",
11
+ "example": "nums = [1, 2]\nnums.extend([3, 4])\n# nums -> [1, 2, 3, 4]",
12
+ "category": "mutating"
13
+ },
14
+ "insert": {
15
+ "signature": "list.insert(index, item)",
16
+ "description": "Inserts an item at the given index, shifting later items to the right.",
17
+ "example": "nums = [1, 3]\nnums.insert(1, 2)\n# nums -> [1, 2, 3]",
18
+ "category": "mutating"
19
+ },
20
+ "remove": {
21
+ "signature": "list.remove(value)",
22
+ "description": "Removes the first item equal to value; raises ValueError if it isn't found.",
23
+ "example": "nums = [1, 2, 3, 2]\nnums.remove(2)\n# nums -> [1, 3, 2]",
24
+ "category": "mutating"
25
+ },
26
+ "pop": {
27
+ "signature": "list.pop(index=-1)",
28
+ "description": "Removes and returns the item at index (the last item by default).",
29
+ "example": "nums = [1, 2, 3]\nlast = nums.pop()\n# last -> 3, nums -> [1, 2]",
30
+ "category": "mutating"
31
+ },
32
+ "clear": {
33
+ "signature": "list.clear()",
34
+ "description": "Removes all items from the list, in place.",
35
+ "example": "nums = [1, 2, 3]\nnums.clear()\n# nums -> []",
36
+ "category": "mutating"
37
+ },
38
+ "index": {
39
+ "signature": "list.index(value, start=0, end=len(list))",
40
+ "description": "Returns the index of the first item equal to value; raises ValueError if not found.",
41
+ "example": "nums = [10, 20, 30]\nnums.index(20)\n# -> 1",
42
+ "category": "read-only"
43
+ },
44
+ "count": {
45
+ "signature": "list.count(value)",
46
+ "description": "Returns how many times value appears in the list.",
47
+ "example": "nums = [1, 2, 2, 3]\nnums.count(2)\n# -> 2",
48
+ "category": "read-only"
49
+ },
50
+ "sort": {
51
+ "signature": "list.sort(key=None, reverse=False)",
52
+ "description": "Sorts the list in place.",
53
+ "example": "nums = [3, 1, 2]\nnums.sort()\n# nums -> [1, 2, 3]",
54
+ "category": "mutating"
55
+ },
56
+ "reverse": {
57
+ "signature": "list.reverse()",
58
+ "description": "Reverses the order of the list, in place.",
59
+ "example": "nums = [1, 2, 3]\nnums.reverse()\n# nums -> [3, 2, 1]",
60
+ "category": "mutating"
61
+ },
62
+ "copy": {
63
+ "signature": "list.copy()",
64
+ "description": "Returns a shallow copy of the list as a new, separate object.",
65
+ "example": "nums = [1, 2, 3]\nnums2 = nums.copy()\n# nums2 -> [1, 2, 3]",
66
+ "category": "read-only"
67
+ }
68
+ }
@@ -0,0 +1,50 @@
1
+ """Terminal presentation layer for pymethodbook, built on `rich`.
2
+ A single shared Console auto-detects non-terminal output (e.g. piped to a
3
+ file or `| cat`) and drops ANSI color codes automatically — no extra
4
+ handling needed."""
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.syntax import Syntax
9
+
10
+ console = Console()
11
+
12
+
13
+ def render_method(type_name, method_name, entry):
14
+ """Prints one method's description, signature, and example."""
15
+ code = Syntax(entry["example"], "python", theme="ansi_dark", line_numbers=False)
16
+ panel = Panel(code, title=f"{type_name}.{method_name}", subtitle=entry["signature"])
17
+ console.print(f"[bold]{entry['description']}[/bold]")
18
+ console.print(panel)
19
+ console.print(f"[dim]category: {entry['category']}[/dim]")
20
+
21
+
22
+ def render_type_table(type_name, data):
23
+ """Prints every method for a type as a table, sorted by category then name."""
24
+ table = Table(title=f"{type_name} methods")
25
+ table.add_column("Method", style="bold")
26
+ table.add_column("Signature")
27
+ table.add_column("Category")
28
+ table.add_column("Description")
29
+
30
+ for method_name, entry in sorted(data.items(), key=lambda kv: (kv[1]["category"], kv[0])):
31
+ table.add_row(method_name, entry["signature"], entry["category"], entry["description"])
32
+
33
+ console.print(table)
34
+
35
+
36
+ def render_search_results(keyword, results):
37
+ """Prints search results across types as a table."""
38
+ if not results:
39
+ console.print(f"[yellow]No matches for '{keyword}'.[/yellow]")
40
+ return
41
+
42
+ table = Table(title=f"Search results for '{keyword}'")
43
+ table.add_column("Type", style="bold")
44
+ table.add_column("Method")
45
+ table.add_column("Description")
46
+
47
+ for type_name, method_name, entry in results:
48
+ table.add_row(type_name, method_name, entry["description"])
49
+
50
+ console.print(table)
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pymethodbook"
7
+ version = "0.1.0"
8
+ description = "A terminal-first reference tool for Python's built-in methods — curated descriptions, runnable examples, and a searchable CLI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Logesh Chandrasekar", email = "logeshwarchandrasekar@gmail.com" }
14
+ ]
15
+ keywords = ["python", "reference", "cli", "cheatsheet", "builtins", "documentation"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ "Intended Audience :: Developers",
27
+ "Environment :: Console",
28
+ "Topic :: Software Development :: Documentation",
29
+ ]
30
+ dependencies = ["rich>=13.0", "typer>=0.9"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/logeshchandrasekar/pymethodbook"
34
+ Repository = "https://github.com/logeshchandrasekar/pymethodbook"
35
+ Issues = "https://github.com/logeshchandrasekar/pymethodbook/issues"
36
+
37
+ [project.scripts]
38
+ pymethodbook = "pymethodbook.cli:app"
@@ -0,0 +1,52 @@
1
+ """Validates every JSON file in pymethodbook/data/.
2
+
3
+ Usage: python scripts/validate_data.py
4
+ Exits with status 1 if any entry is missing or has an empty required field.
5
+ """
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ REQUIRED_KEYS = ("signature", "description", "example", "category")
11
+ DATA_DIR = Path(__file__).parent.parent / "pymethodbook" / "data"
12
+
13
+
14
+ def validate_file(path):
15
+ errors = []
16
+ with open(path, encoding="utf-8") as f:
17
+ data = json.load(f)
18
+
19
+ for method_name, entry in data.items():
20
+ for key in REQUIRED_KEYS:
21
+ if key not in entry:
22
+ errors.append(f"{path.name}::{method_name} missing '{key}'")
23
+ elif not str(entry[key]).strip():
24
+ errors.append(f"{path.name}::{method_name} has an empty '{key}'")
25
+ return len(data), errors
26
+
27
+
28
+ def main():
29
+ json_files = sorted(DATA_DIR.glob("*.json"))
30
+ if not json_files:
31
+ print(f"No JSON files found in {DATA_DIR}")
32
+ return 1
33
+
34
+ all_errors = []
35
+ for path in json_files:
36
+ count, errors = validate_file(path)
37
+ status = "OK" if not errors else "FAILED"
38
+ print(f"{path.name}: {status} ({count} methods)")
39
+ all_errors.extend(errors)
40
+
41
+ if all_errors:
42
+ print("\nErrors found:")
43
+ for err in all_errors:
44
+ print(f" - {err}")
45
+ return 1
46
+
47
+ print("\nAll data files valid.")
48
+ return 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ sys.exit(main())
@@ -0,0 +1,35 @@
1
+ from typer.testing import CliRunner
2
+
3
+ from pymethodbook.cli import app
4
+
5
+ runner = CliRunner()
6
+
7
+
8
+ def test_explain_known_method_succeeds():
9
+ result = runner.invoke(app, ["explain", "list", "append"])
10
+ assert result.exit_code == 0
11
+
12
+
13
+ def test_explain_unknown_type_fails_cleanly():
14
+ result = runner.invoke(app, ["explain", "banana", "append"])
15
+ assert result.exit_code == 1
16
+
17
+
18
+ def test_explain_typo_method_fails_cleanly():
19
+ result = runner.invoke(app, ["explain", "list", "apend"])
20
+ assert result.exit_code == 1
21
+
22
+
23
+ def test_list_command_succeeds():
24
+ result = runner.invoke(app, ["list", "dict"])
25
+ assert result.exit_code == 0
26
+
27
+
28
+ def test_list_unknown_type_fails_cleanly():
29
+ result = runner.invoke(app, ["list", "banana"])
30
+ assert result.exit_code == 1
31
+
32
+
33
+ def test_search_command_succeeds():
34
+ result = runner.invoke(app, ["search", "copy"])
35
+ assert result.exit_code == 0
@@ -0,0 +1,45 @@
1
+ import pytest
2
+ from pymethodbook.core import (
3
+ available_types, load_type, get_method, search,
4
+ TypeNotFoundError, MethodNotFoundError,
5
+ )
6
+
7
+
8
+ def test_available_types_includes_list_and_dict():
9
+ types = available_types()
10
+ assert "list" in types
11
+ assert "dict" in types
12
+
13
+
14
+ def test_load_type_returns_dict_with_entries():
15
+ data = load_type("list")
16
+ assert "append" in data
17
+ assert "signature" in data["append"]
18
+
19
+
20
+ def test_load_type_unknown_type_raises():
21
+ with pytest.raises(TypeNotFoundError):
22
+ load_type("banana")
23
+
24
+
25
+ def test_get_method_returns_entry():
26
+ entry = get_method("list", "append")
27
+ assert entry["category"] == "mutating"
28
+
29
+
30
+ def test_get_method_typo_suggests_close_match():
31
+ with pytest.raises(MethodNotFoundError) as exc_info:
32
+ get_method("list", "apend")
33
+ assert "append" in str(exc_info.value)
34
+
35
+
36
+ def test_search_finds_exact_method_name():
37
+ results = search("append")
38
+ names = [method for _, method, _ in results]
39
+ assert "append" in names
40
+
41
+
42
+ def test_search_finds_by_description_keyword():
43
+ results = search("shallow copy")
44
+ names = [method for _, method, _ in results]
45
+ assert "copy" in names
@@ -0,0 +1,29 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ DATA_DIR = Path(__file__).parent.parent / "pymethodbook" / "data"
7
+ REQUIRED_KEYS = {"signature", "description", "example", "category"}
8
+
9
+
10
+ def load_all_entries():
11
+ entries = []
12
+ for path in DATA_DIR.glob("*.json"):
13
+ with open(path, encoding="utf-8") as f:
14
+ data = json.load(f)
15
+ for method_name, entry in data.items():
16
+ entries.append((path.name, method_name, entry))
17
+ return entries
18
+
19
+
20
+ @pytest.mark.parametrize("filename,method_name,entry", load_all_entries())
21
+ def test_entry_has_required_keys(filename, method_name, entry):
22
+ missing = REQUIRED_KEYS - entry.keys()
23
+ assert not missing, f"{filename}::{method_name} missing {missing}"
24
+
25
+
26
+ @pytest.mark.parametrize("filename,method_name,entry", load_all_entries())
27
+ def test_entry_fields_not_empty(filename, method_name, entry):
28
+ for key in REQUIRED_KEYS:
29
+ assert str(entry.get(key, "")).strip(), f"{filename}::{method_name} has an empty '{key}'"
@@ -0,0 +1,17 @@
1
+ from pymethodbook.render import render_method, render_type_table, render_search_results
2
+ from pymethodbook.core import get_method, load_type, search
3
+
4
+
5
+ def test_render_method_runs_without_error():
6
+ entry = get_method("list", "append")
7
+ render_method("list", "append", entry)
8
+
9
+
10
+ def test_render_type_table_runs_without_error():
11
+ data = load_type("dict")
12
+ render_type_table("dict", data)
13
+
14
+
15
+ def test_render_search_results_runs_without_error():
16
+ results = search("copy")
17
+ render_search_results("copy", results)