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.
- pymethodbook-0.1.0/.github/workflows/ci.yml +34 -0
- pymethodbook-0.1.0/.gitignore +218 -0
- pymethodbook-0.1.0/CONTRIBUTING.md +41 -0
- pymethodbook-0.1.0/LICENSE +21 -0
- pymethodbook-0.1.0/PKG-INFO +76 -0
- pymethodbook-0.1.0/README.md +48 -0
- pymethodbook-0.1.0/ROADMAP.md +21 -0
- pymethodbook-0.1.0/pymethodbook/__init__.py +23 -0
- pymethodbook-0.1.0/pymethodbook/cli.py +40 -0
- pymethodbook-0.1.0/pymethodbook/core.py +70 -0
- pymethodbook-0.1.0/pymethodbook/data/dict.json +68 -0
- pymethodbook-0.1.0/pymethodbook/data/list.json +68 -0
- pymethodbook-0.1.0/pymethodbook/render.py +50 -0
- pymethodbook-0.1.0/pyproject.toml +38 -0
- pymethodbook-0.1.0/scripts/validate_data.py +52 -0
- pymethodbook-0.1.0/tests/test_cli.py +35 -0
- pymethodbook-0.1.0/tests/test_core.py +45 -0
- pymethodbook-0.1.0/tests/test_data_schema.py +29 -0
- pymethodbook-0.1.0/tests/test_render.py +17 -0
|
@@ -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)
|