pyrefdev 2025.6__tar.gz → 2025.8__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.
- {pyrefdev-2025.6 → pyrefdev-2025.8}/CHANGELOG.md +15 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/CLAUDE.md +4 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/PKG-INFO +14 -2
- {pyrefdev-2025.6 → pyrefdev-2025.8}/README.md +13 -1
- pyrefdev-2025.8/src/pyrefdev/_version.py +1 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/config.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/__main__.py +4 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/add_docs.py +20 -12
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/crawl_docs.py +18 -29
- pyrefdev-2025.8/src/pyrefdev/indexer/crawl_pypi.py +27 -0
- pyrefdev-2025.8/src/pyrefdev/indexer/index.py +171 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/parse_docs.py +35 -21
- pyrefdev-2025.8/src/pyrefdev/indexer/parse_pypi.py +14 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/update_docs.py +6 -8
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/indexer/update_landing_page.py +1 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/__init__.py +17 -4
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/__python__.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/aiohttp.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/aioitertools.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/aiosignal.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/anyio.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/apache-beam.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/arrow.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/attrs.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/azure-identity.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/boto3.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/botocore.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/cachetools.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/cattrs.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/charset-normalizer.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/click.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/coverage.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/cryptography.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/cyclopts.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/distlib.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/fastapi.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/fastjsonschema.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/filelock.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/flask.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/frozenlist.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/fsspec.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/google-api-core.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/google-auth.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/grpcio.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/h11.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/h5py.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/httplib2.py +2 -1
- pyrefdev-2025.8/src/pyrefdev/mapping/ipython.py +1480 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/jinja2.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/jsonschema.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/jupyter-core.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/libcst.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/markupsafe.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/matplotlib.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/more-itertools.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/multidict.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/nbformat.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/networkx.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/numba.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/numpy.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/oauthlib.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/packaging.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pandas.py +2 -1
- pyrefdev-2025.8/src/pyrefdev/mapping/passlib.py +411 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pathspec.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pillow.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/platformdirs.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pluggy.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/propcache.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/protobuf.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/psutil.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pyOpenSSL.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pyarrow.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pyasn1.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pydantic-core.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pydantic.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pygments.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pyjwt.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pyparsing.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/pytest.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/python-dateutil.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/requests-oauthlib.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/requests-toolbelt.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/requests.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/rich.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/rpds-py.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/s3fs.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/scikit-learn.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/scipy.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/semver.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/setuptools.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/six.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/sniffio.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/sqlalchemy.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/tomlkit.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/torch.py +1182 -1181
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/tox.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/trio.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/typing-extensions.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/urllib3.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/werkzeug.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/wrapt.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/yarl.py +2 -1
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/mapping/yib.py +2 -1
- pyrefdev-2025.8/src/pyrefdev/server.py +126 -0
- pyrefdev-2025.8/static/styles.css +693 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/templates/index.html +30 -415
- pyrefdev-2025.8/templates/search.html +152 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/tests/test_pyrefdev.py +3 -1
- pyrefdev-2025.6/src/pyrefdev/_version.py +0 -1
- pyrefdev-2025.6/src/pyrefdev/indexer/requests.py +0 -54
- pyrefdev-2025.6/src/pyrefdev/indexer/schema.py +0 -16
- pyrefdev-2025.6/src/pyrefdev/mapping/ipython.py +0 -5
- pyrefdev-2025.6/src/pyrefdev/server.py +0 -35
- {pyrefdev-2025.6 → pyrefdev-2025.8}/.github/workflows/ci.yml +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/.github/workflows/pypi_upload.yml +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/.gitignore +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/.pre-commit-config.yaml +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/LICENSE +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/pyproject.toml +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/pyrefdev.service +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/__init__.py +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/src/pyrefdev/__main__.py +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/static/logo128.png +0 -0
- {pyrefdev-2025.6 → pyrefdev-2025.8}/uv.lock +0 -0
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
Nothing noticeable unreleased.
|
|
6
6
|
|
|
7
|
+
## v2025.8
|
|
8
|
+
|
|
9
|
+
- When no symbol is specified, the lucky button now redirects to a random API reference URL.
|
|
10
|
+
- Support [passlib](https://passlib.readthedocs.io/en/stable/).
|
|
11
|
+
- Updated certain torch URLs with more detailed docs.
|
|
12
|
+
- Fixed symbols for [ipython](https://ipython.readthedocs.io/en/stable/).
|
|
13
|
+
- Indexer: in the `add-docs` command, guess the API docs based on PyPI package information when `--index-url` is not specified.
|
|
14
|
+
- Indexer: add a `crawl-pypi` command to cache the PyPI package information.
|
|
15
|
+
- Indexer: add a `parse-pypi` command to parse cached PyPI package information and update the config when API docs are found.
|
|
16
|
+
|
|
17
|
+
## v2025.7
|
|
18
|
+
|
|
19
|
+
- Added search to [pyref.dev](https://pyref.dev).
|
|
20
|
+
- When the symbol is not found, redirect to the search page.
|
|
21
|
+
|
|
7
22
|
## v2025.6
|
|
8
23
|
|
|
9
24
|
- Added a `-p/--print` flag to the `pyrefdev` CLI to print the URL instead of opening it in the browser.
|
|
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
6
6
|
|
|
7
7
|
The project has several components:
|
|
8
8
|
|
|
9
|
-
1. **Web Server** (`src/pyrefdev/server.py`): FastAPI application that serves redirects based on symbol lookup. `index.html` is the landing page of the server
|
|
9
|
+
1. **Web Server** (`src/pyrefdev/server.py`): FastAPI application that serves redirects based on symbol lookup. `templates/index.html` is the landing page of the server, `static/styles.css` contains CSS styles
|
|
10
10
|
2. **CLI Tool** (`src/pyrefdev/__main__.py`): Command-line interface that opens documentation in browser
|
|
11
11
|
3. **Indexer** (`src/pyrefdev/indexer/`): Tools for crawling, parsing, and managing documentation mappings
|
|
12
12
|
4. **Mapping System** (`src/pyrefdev/mapping/`): Individual Python files per package containing symbol-to-URL mappings
|
|
@@ -48,4 +48,7 @@ pyrefdev-indexer update-landing-page
|
|
|
48
48
|
|
|
49
49
|
## Important Notes
|
|
50
50
|
|
|
51
|
+
- Do not add tests unless explictly asked to.
|
|
52
|
+
- When creating a new file with content, ensure the file has an extra new line at the end.
|
|
53
|
+
- Do NOT add redundant comments describe what the cod does. When needed, DO add comments that explains "why".
|
|
51
54
|
- Server deployment uses systemd and is configured in `pyrefdev.service`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyrefdev
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.8
|
|
4
4
|
Summary: The entry point to Python reference docs
|
|
5
5
|
Project-URL: Homepage, https://pyref.dev
|
|
6
6
|
Project-URL: Source, https://github.com/mangoumbrella/pyref.dev
|
|
@@ -51,7 +51,19 @@ It allows you to quickly jump to the official documentation for Python standard
|
|
|
51
51
|
https://pyref.dev/<fully.qualified.symbol.name>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
You can also search for symbols using:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
https://pyref.dev/is?symbol=<SYMBOL>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
And if you are feeling lucky, ask it to redirect to the first result:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
https://pyref.dev/is?lucky=true&symbol=<SYMBOL>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Lastly, you can `pip install pyrefdev` and run the `pyrefdev` CLI tool.
|
|
55
67
|
|
|
56
68
|
## Examples
|
|
57
69
|
|
|
@@ -14,7 +14,19 @@ It allows you to quickly jump to the official documentation for Python standard
|
|
|
14
14
|
https://pyref.dev/<fully.qualified.symbol.name>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
You can also search for symbols using:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
https://pyref.dev/is?symbol=<SYMBOL>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And if you are feeling lucky, ask it to redirect to the first result:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
https://pyref.dev/is?lucky=true&symbol=<SYMBOL>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Lastly, you can `pip install pyrefdev` and run the `pyrefdev` CLI tool.
|
|
18
30
|
|
|
19
31
|
## Examples
|
|
20
32
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "2025.8"
|
|
@@ -113,10 +113,11 @@ _packages = [
|
|
|
113
113
|
Package(pypi="coverage", index_url="https://coverage.readthedocs.io/en/latest/"),
|
|
114
114
|
Package(pypi="matplotlib", index_url="https://matplotlib.org/stable/api/index.html"),
|
|
115
115
|
Package(pypi="networkx", index_url="https://networkx.org/documentation/stable/reference/index.html"),
|
|
116
|
-
Package(pypi="ipython", index_url="https://ipython.readthedocs.io/en/stable/"),
|
|
116
|
+
Package(pypi="ipython", index_url="https://ipython.readthedocs.io/en/stable/", namespaces=["IPython"]),
|
|
117
117
|
Package(pypi="httplib2", index_url="https://httplib2.readthedocs.io/en/latest/"),
|
|
118
118
|
Package(pypi="cyclopts", index_url="https://cyclopts.readthedocs.io/en/latest/"),
|
|
119
119
|
Package(pypi="yib", index_url="https://yib.readthedocs.io/en/latest/"),
|
|
120
|
+
Package(pypi="passlib", index_url="https://passlib.readthedocs.io/en/stable/"),
|
|
120
121
|
# ENTRY-LINE-MARKER
|
|
121
122
|
]
|
|
122
123
|
# fmt: on
|
|
@@ -3,7 +3,9 @@ import cyclopts
|
|
|
3
3
|
import pyrefdev
|
|
4
4
|
from pyrefdev.indexer.add_docs import add_docs
|
|
5
5
|
from pyrefdev.indexer.crawl_docs import crawl_docs
|
|
6
|
+
from pyrefdev.indexer.crawl_pypi import crawl_pypi
|
|
6
7
|
from pyrefdev.indexer.parse_docs import parse_docs
|
|
8
|
+
from pyrefdev.indexer.parse_pypi import parse_pypi
|
|
7
9
|
from pyrefdev.indexer.update_docs import update_docs
|
|
8
10
|
from pyrefdev.indexer.update_landing_page import update_landing_page
|
|
9
11
|
from pyrefdev.config import console
|
|
@@ -17,7 +19,9 @@ app = cyclopts.App(
|
|
|
17
19
|
)
|
|
18
20
|
app.command(add_docs)
|
|
19
21
|
app.command(crawl_docs)
|
|
22
|
+
app.command(crawl_pypi)
|
|
20
23
|
app.command(parse_docs)
|
|
24
|
+
app.command(parse_pypi)
|
|
21
25
|
app.command(update_docs)
|
|
22
26
|
app.command(update_landing_page)
|
|
23
27
|
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from pyrefdev import config
|
|
6
6
|
from pyrefdev.config import console
|
|
7
|
+
from pyrefdev.indexer.index import Index
|
|
7
8
|
from pyrefdev.indexer.update_docs import update_docs
|
|
8
9
|
from pyrefdev.indexer.update_landing_page import update_landing_page_with_packages
|
|
9
10
|
|
|
@@ -14,15 +15,31 @@ _MARKER = re.compile("(\n.*ENTRY-LINE-MARKER.*\n)")
|
|
|
14
15
|
def add_docs(
|
|
15
16
|
*,
|
|
16
17
|
package: str,
|
|
17
|
-
|
|
18
|
-
url: str,
|
|
18
|
+
url: str | None = None,
|
|
19
19
|
namespaces: list[str] | None = None,
|
|
20
20
|
crawl: bool = True,
|
|
21
|
-
|
|
21
|
+
index: Index = Index(),
|
|
22
|
+
num_threads_per_package: int = 1,
|
|
22
23
|
) -> None:
|
|
24
|
+
"""Add a new package."""
|
|
23
25
|
if package in config.SUPPORTED_PACKAGES:
|
|
24
26
|
console.fatal(f"Package exists: {package}")
|
|
25
27
|
|
|
28
|
+
if url is None:
|
|
29
|
+
url = index.guess_index_url(package, should_die_if_not_found=True)
|
|
30
|
+
update_config(package, url, namespaces)
|
|
31
|
+
|
|
32
|
+
if crawl:
|
|
33
|
+
importlib.reload(config)
|
|
34
|
+
update_docs(
|
|
35
|
+
index=index,
|
|
36
|
+
package=package,
|
|
37
|
+
num_threads_per_package=num_threads_per_package,
|
|
38
|
+
)
|
|
39
|
+
update_landing_page_with_packages(config.SUPPORTED_PACKAGES)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def update_config(package: str, url: str, namespaces: list[str] | None = None) -> None:
|
|
26
43
|
if namespaces:
|
|
27
44
|
ns_content = ", ".join(f'"{ns}"' for ns in namespaces)
|
|
28
45
|
ns_str = f", namespaces=[{ns_content}]"
|
|
@@ -33,12 +50,3 @@ def add_docs(
|
|
|
33
50
|
config_content = config_file.read_text()
|
|
34
51
|
config_content = _MARKER.sub(config_entry + r"\g<1>", config_content)
|
|
35
52
|
config_file.write_text(config_content)
|
|
36
|
-
|
|
37
|
-
if crawl:
|
|
38
|
-
importlib.reload(config)
|
|
39
|
-
update_docs(
|
|
40
|
-
docs_directory=docs_directory,
|
|
41
|
-
package=package,
|
|
42
|
-
num_threads_per_package=num_threads_per_package,
|
|
43
|
-
)
|
|
44
|
-
update_landing_page_with_packages(config.SUPPORTED_PACKAGES)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from concurrent import futures
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import queue
|
|
4
|
-
import tempfile
|
|
5
4
|
import threading
|
|
6
5
|
from urllib import error, parse
|
|
7
6
|
|
|
@@ -10,53 +9,43 @@ from packaging import version
|
|
|
10
9
|
from rich.progress import Progress, TaskID
|
|
11
10
|
|
|
12
11
|
from pyrefdev.config import console, get_packages, Package
|
|
13
|
-
from pyrefdev.indexer.
|
|
14
|
-
from pyrefdev.indexer.schema import CrawlState
|
|
12
|
+
from pyrefdev.indexer.index import Index, IndexState, urlopen
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def crawl_docs(
|
|
18
16
|
*,
|
|
19
17
|
package: str | None = None,
|
|
20
|
-
docs_directory: Path | None = None,
|
|
21
18
|
force: bool = False,
|
|
19
|
+
index: Index = Index(),
|
|
22
20
|
num_parallel_packages: int = 2,
|
|
23
|
-
num_threads_per_package: int
|
|
21
|
+
num_threads_per_package: int = 1,
|
|
24
22
|
) -> None:
|
|
25
23
|
"""Crawl the docs into a local directory."""
|
|
26
24
|
if num_parallel_packages <= 0:
|
|
27
25
|
raise ValueError(
|
|
28
26
|
f"--num-parallel-packages must be > 0, found {num_parallel_packages}"
|
|
29
27
|
)
|
|
30
|
-
if num_threads_per_package is None:
|
|
31
|
-
num_threads_per_package = 2
|
|
32
28
|
if num_threads_per_package <= 0:
|
|
33
29
|
raise ValueError(
|
|
34
30
|
f"--num-threads-per-package must be > 0, found {num_threads_per_package}"
|
|
35
31
|
)
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
if docs_directory.exists():
|
|
39
|
-
if not docs_directory.is_dir():
|
|
40
|
-
console.fatal(f"{docs_directory} is not a directory")
|
|
41
|
-
else:
|
|
42
|
-
docs_directory = Path(tempfile.mkdtemp(prefix="pyref.dev."))
|
|
43
|
-
|
|
44
|
-
console.print(f"Crawling documents into {docs_directory}")
|
|
33
|
+
console.print(f"Crawling documents into {index.docs_directory}")
|
|
45
34
|
packages = get_packages(package)
|
|
46
35
|
with Progress(console=console) as progress:
|
|
47
36
|
task = progress.add_task(
|
|
48
37
|
f"Crawling {len(packages)} packages", total=len(packages)
|
|
49
38
|
)
|
|
39
|
+
|
|
50
40
|
def crawl_package(pkg: Package):
|
|
51
41
|
try:
|
|
52
|
-
package_version = fetch_package_version(pkg)
|
|
42
|
+
package_version = index.fetch_package_version(pkg)
|
|
53
43
|
if package_version is None:
|
|
54
44
|
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
crawl_state = CrawlState.loads(crawl_state_file.read_text())
|
|
45
|
+
if (
|
|
46
|
+
not force
|
|
47
|
+
and (crawl_state := index.load_crawl_state(pkg.pypi)) is not None
|
|
48
|
+
):
|
|
60
49
|
crawled_version = version.parse(crawl_state.package_version)
|
|
61
50
|
if package_version > crawled_version:
|
|
62
51
|
console.print(
|
|
@@ -72,12 +61,12 @@ def crawl_docs(
|
|
|
72
61
|
crawler = _Crawler(
|
|
73
62
|
pkg,
|
|
74
63
|
progress,
|
|
75
|
-
|
|
64
|
+
index,
|
|
76
65
|
pkg.index_url,
|
|
77
66
|
crawl_state,
|
|
78
67
|
)
|
|
79
68
|
crawler.crawl(num_threads=num_threads_per_package)
|
|
80
|
-
crawler.save_crawl_state(package_version,
|
|
69
|
+
crawler.save_crawl_state(package_version, index)
|
|
81
70
|
finally:
|
|
82
71
|
progress.advance(task)
|
|
83
72
|
|
|
@@ -92,13 +81,13 @@ class _Crawler:
|
|
|
92
81
|
self,
|
|
93
82
|
package: Package,
|
|
94
83
|
progress: Progress,
|
|
95
|
-
|
|
84
|
+
index: Index,
|
|
96
85
|
root_url: str,
|
|
97
|
-
crawl_state:
|
|
86
|
+
crawl_state: IndexState | None,
|
|
98
87
|
):
|
|
99
88
|
self._package = package
|
|
100
89
|
self._progress = progress
|
|
101
|
-
self._docs_directory = docs_directory
|
|
90
|
+
self._docs_directory = index.docs_directory / package.pypi
|
|
102
91
|
self._root_url = root_url
|
|
103
92
|
self._prefix = (
|
|
104
93
|
root_url
|
|
@@ -162,18 +151,18 @@ class _Crawler:
|
|
|
162
151
|
] = url
|
|
163
152
|
self._crawl_state.failed_urls = failed_urls
|
|
164
153
|
|
|
165
|
-
def save_crawl_state(self, package_version: version.Version,
|
|
154
|
+
def save_crawl_state(self, package_version: version.Version, index: Index) -> None:
|
|
166
155
|
if (state := self._crawl_state) is None:
|
|
167
156
|
file_to_urls = {
|
|
168
157
|
str(file.relative_to(self._docs_directory)): url
|
|
169
158
|
for url, file in self._crawled_url_to_files.items()
|
|
170
159
|
}
|
|
171
|
-
state =
|
|
160
|
+
state = IndexState(
|
|
172
161
|
package_version=str(package_version),
|
|
173
162
|
file_to_urls=file_to_urls,
|
|
174
163
|
failed_urls=self._failed_urls,
|
|
175
164
|
)
|
|
176
|
-
|
|
165
|
+
index.save_crawl_state(self._package.pypi, state)
|
|
177
166
|
|
|
178
167
|
def _crawl_thread(self, task: TaskID) -> None:
|
|
179
168
|
while True:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from urllib.error import HTTPError
|
|
3
|
+
|
|
4
|
+
from rich.progress import track
|
|
5
|
+
|
|
6
|
+
from pyrefdev.config import console
|
|
7
|
+
from pyrefdev.indexer.index import Index, urlopen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def crawl_pypi(*, refresh: bool = True, index: Index = Index()) -> None:
|
|
11
|
+
"""Crawl top 15000 PyPI packages."""
|
|
12
|
+
with urlopen(
|
|
13
|
+
"https://hugovk.github.io/top-pypi-packages/top-pypi-packages.min.json"
|
|
14
|
+
) as f:
|
|
15
|
+
top_packages_data = json.load(f)
|
|
16
|
+
rows = top_packages_data["rows"]
|
|
17
|
+
for row in track(
|
|
18
|
+
rows, description=f"Reading {len(rows)} packages", console=console
|
|
19
|
+
):
|
|
20
|
+
package_name = row["project"]
|
|
21
|
+
try:
|
|
22
|
+
index.fetch_pypi_data(package_name, refresh=refresh)
|
|
23
|
+
except HTTPError as e:
|
|
24
|
+
if e.code == 404:
|
|
25
|
+
console.warning(f"Package {package_name} not found")
|
|
26
|
+
continue
|
|
27
|
+
raise
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import random
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal, overload
|
|
10
|
+
from urllib import error, request
|
|
11
|
+
|
|
12
|
+
from packaging import version
|
|
13
|
+
from cyclopts import Parameter
|
|
14
|
+
|
|
15
|
+
from pyrefdev import __version__
|
|
16
|
+
from pyrefdev.config import Package, console
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_RTD_URL_PATTERN = re.compile(r"https?://([^\s/]+\.readthedocs\.io)\b")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def urlopen(url: str):
|
|
23
|
+
req = request.Request(
|
|
24
|
+
url,
|
|
25
|
+
method="GET",
|
|
26
|
+
headers={"User-Agent": f"pyrefdev/{__version__} (+https://pyref.dev)"},
|
|
27
|
+
)
|
|
28
|
+
backoffs = [1, 2, 5, 15, 30, 60]
|
|
29
|
+
while True:
|
|
30
|
+
try:
|
|
31
|
+
return request.urlopen(req, timeout=60)
|
|
32
|
+
except error.HTTPError as e:
|
|
33
|
+
if e.code == 429: # Too Many Request
|
|
34
|
+
if not backoffs:
|
|
35
|
+
raise
|
|
36
|
+
backoff = backoffs.pop(0) * (0.9 + random.random() / 5.0)
|
|
37
|
+
time.sleep(backoff)
|
|
38
|
+
else:
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass
|
|
43
|
+
class IndexState:
|
|
44
|
+
package_version: str
|
|
45
|
+
file_to_urls: dict[str, str]
|
|
46
|
+
failed_urls: list[str]
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def loads(cls, content: str) -> "IndexState":
|
|
50
|
+
return cls(**json.loads(content))
|
|
51
|
+
|
|
52
|
+
def dumps(self) -> str:
|
|
53
|
+
return json.dumps(dataclasses.asdict(self))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_default_api_docs_directory() -> Path:
|
|
57
|
+
cwd = Path(__file__).parent
|
|
58
|
+
git_root = subprocess.check_output(
|
|
59
|
+
["git", "rev-parse", "--show-toplevel"], cwd=cwd, text=True
|
|
60
|
+
).strip()
|
|
61
|
+
git_remote = subprocess.check_output(
|
|
62
|
+
["git", "remote", "get-url", "origin"], cwd=git_root, text=True
|
|
63
|
+
).strip()
|
|
64
|
+
if "/pyref.dev" in git_remote:
|
|
65
|
+
return Path(git_root) / "api-docs"
|
|
66
|
+
else:
|
|
67
|
+
directory = Path(tempfile.mkdtemp(prefix="pyref.dev."))
|
|
68
|
+
console.print(f"Using temporary directory for API docs: {directory}")
|
|
69
|
+
return directory
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@Parameter(name="*")
|
|
73
|
+
@dataclasses.dataclass
|
|
74
|
+
class Index:
|
|
75
|
+
docs_directory: Path = dataclasses.field(
|
|
76
|
+
default_factory=_get_default_api_docs_directory
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _ensure_directory(self) -> None:
|
|
80
|
+
self.docs_directory.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
(self.docs_directory / "__pypi__").mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
def load_crawl_state(self, package: str) -> IndexState | None:
|
|
84
|
+
self._ensure_directory()
|
|
85
|
+
crawl_state_file = self.docs_directory / f"{package}.json"
|
|
86
|
+
if not crawl_state_file.exists():
|
|
87
|
+
return None
|
|
88
|
+
return IndexState.loads(crawl_state_file.read_text())
|
|
89
|
+
|
|
90
|
+
def save_crawl_state(self, package: str, crawl_state: IndexState) -> None:
|
|
91
|
+
self._ensure_directory()
|
|
92
|
+
crawl_state_file = self.docs_directory / f"{package}.json"
|
|
93
|
+
crawl_state_file.write_text(crawl_state.dumps())
|
|
94
|
+
|
|
95
|
+
def fetch_pypi_data(self, package: str, *, refresh: bool) -> bytes:
|
|
96
|
+
pypi_data_file = self.docs_directory / "__pypi__" / f"{package}.json"
|
|
97
|
+
if not refresh and pypi_data_file.exists():
|
|
98
|
+
return pypi_data_file.read_bytes()
|
|
99
|
+
with urlopen(f"https://pypi.org/pypi/{package}/json") as f:
|
|
100
|
+
data = f.read()
|
|
101
|
+
self._ensure_directory()
|
|
102
|
+
pypi_data_file.write_bytes(data)
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def get_pypi_packages(self) -> list[str]:
|
|
106
|
+
return list(f.stem for f in self.docs_directory.glob("__pypi__/*.json"))
|
|
107
|
+
|
|
108
|
+
def fetch_package_version(self, package: Package) -> version.Version | None:
|
|
109
|
+
if package.is_cpython():
|
|
110
|
+
return _fetch_latest_cpython_version()
|
|
111
|
+
try:
|
|
112
|
+
data = self.fetch_pypi_data(package.pypi, refresh=True)
|
|
113
|
+
pypi_info = json.loads(data)
|
|
114
|
+
return version.parse(pypi_info["info"]["version"])
|
|
115
|
+
except error.URLError as e:
|
|
116
|
+
console.warning(
|
|
117
|
+
f"Failed to fetch pypi version for {package.pypi}, error: {e}"
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
@overload
|
|
122
|
+
def guess_index_url(
|
|
123
|
+
self, package: str, *, should_die_if_not_found: Literal[True]
|
|
124
|
+
) -> str: ...
|
|
125
|
+
def guess_index_url(
|
|
126
|
+
self, package: str, *, should_die_if_not_found: bool
|
|
127
|
+
) -> str | None:
|
|
128
|
+
data = self.fetch_pypi_data(package, refresh=False)
|
|
129
|
+
pypi_info = json.loads(data).get("info", {})
|
|
130
|
+
candidates = list((pypi_info.get("project_urls") or {}).values())
|
|
131
|
+
candidates.append(pypi_info.get("description", ""))
|
|
132
|
+
|
|
133
|
+
readthedocs_urls = set()
|
|
134
|
+
|
|
135
|
+
for url in candidates:
|
|
136
|
+
for match in _RTD_URL_PATTERN.findall(url):
|
|
137
|
+
readthedocs_urls.add(f"https://{match}")
|
|
138
|
+
|
|
139
|
+
if len(readthedocs_urls) == 1:
|
|
140
|
+
url = next(iter(readthedocs_urls))
|
|
141
|
+
try:
|
|
142
|
+
with urlopen(url) as f:
|
|
143
|
+
url = f.url # Maybe redirected URL.
|
|
144
|
+
return url
|
|
145
|
+
except error.URLError as e:
|
|
146
|
+
console.warning(f"Failed to fetch {url}, error: {e}")
|
|
147
|
+
readthedocs_urls = []
|
|
148
|
+
|
|
149
|
+
msg_fn = console.fatal if should_die_if_not_found else console.warning
|
|
150
|
+
if len(readthedocs_urls) == 0:
|
|
151
|
+
msg_fn(f"No readthedocs.io URLs found for package: {package}")
|
|
152
|
+
else:
|
|
153
|
+
msg_fn(
|
|
154
|
+
f"Multiple readthedocs.io URLs found for package: {package}. URLs:\n"
|
|
155
|
+
+ "\n".join(readthedocs_urls)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _fetch_latest_cpython_version() -> version.Version | None:
|
|
160
|
+
try:
|
|
161
|
+
with urlopen("https://endoflife.date/api/python.json") as f:
|
|
162
|
+
content = f.read().decode("utf-8")
|
|
163
|
+
latest_version = version.parse("3.13.5") # Known version as of 2025-06-28
|
|
164
|
+
cycles = json.loads(content)
|
|
165
|
+
for cycle in cycles:
|
|
166
|
+
if (latest := version.parse(cycle["latest"])) > latest_version:
|
|
167
|
+
latest_version = latest
|
|
168
|
+
return latest_version
|
|
169
|
+
except error.URLError as e:
|
|
170
|
+
console.warning(f"Failed to fetch latest CPython version, error: {e}")
|
|
171
|
+
return None
|
|
@@ -8,14 +8,14 @@ from pathlib import Path
|
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
10
|
import threading
|
|
11
|
-
from urllib import
|
|
11
|
+
from urllib.parse import urlparse, urlunparse
|
|
12
12
|
|
|
13
13
|
import bs4
|
|
14
14
|
from rich.progress import Progress
|
|
15
15
|
|
|
16
16
|
from pyrefdev import mapping
|
|
17
17
|
from pyrefdev.config import console, get_packages, Package
|
|
18
|
-
from pyrefdev.indexer.
|
|
18
|
+
from pyrefdev.indexer.index import Index
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
_STDLIB_MODULES_NAMES = frozenset({*sys.stdlib_module_names, "test"})
|
|
@@ -65,11 +65,12 @@ class ProgressExecutor(futures.ThreadPoolExecutor):
|
|
|
65
65
|
def parse_docs(
|
|
66
66
|
*,
|
|
67
67
|
package: str | None = None,
|
|
68
|
-
docs_directory: Path,
|
|
69
68
|
in_place: bool = False,
|
|
69
|
+
index: Index = Index(),
|
|
70
70
|
num_parallel_packages: int = multiprocessing.cpu_count(),
|
|
71
71
|
num_threads_per_package: int = multiprocessing.cpu_count(),
|
|
72
72
|
) -> None:
|
|
73
|
+
"""Parse crawled docs and update the mapping files."""
|
|
73
74
|
if sys.version_info[:2] != (3, 13):
|
|
74
75
|
console.fatal("pyrefdev-indexer parse_docs must be run on Python 3.13.")
|
|
75
76
|
|
|
@@ -88,7 +89,7 @@ def parse_docs(
|
|
|
88
89
|
_parse_package,
|
|
89
90
|
executor.progress,
|
|
90
91
|
pkg,
|
|
91
|
-
|
|
92
|
+
index,
|
|
92
93
|
in_place=in_place,
|
|
93
94
|
num_threads_per_package=num_threads_per_package,
|
|
94
95
|
)
|
|
@@ -101,13 +102,13 @@ def parse_docs(
|
|
|
101
102
|
def _parse_package(
|
|
102
103
|
progress: Progress,
|
|
103
104
|
package: Package,
|
|
104
|
-
|
|
105
|
+
index: Index,
|
|
105
106
|
*,
|
|
106
107
|
in_place: bool,
|
|
107
108
|
num_threads_per_package: int,
|
|
108
109
|
) -> None:
|
|
109
|
-
|
|
110
|
-
crawl_state
|
|
110
|
+
crawl_state = index.load_crawl_state(package.pypi)
|
|
111
|
+
assert crawl_state is not None, f"No crawl state for {package.pypi}"
|
|
111
112
|
file_and_urls: list[tuple[str, str]] = list(crawl_state.file_to_urls.items())
|
|
112
113
|
if package.is_cpython():
|
|
113
114
|
symbol_to_urls: dict[str, str] = _SPECIAL_SYMBOLS.copy()
|
|
@@ -117,6 +118,7 @@ def _parse_package(
|
|
|
117
118
|
}
|
|
118
119
|
parser = _Parser(package)
|
|
119
120
|
lock = threading.RLock()
|
|
121
|
+
package_docs = index.docs_directory / package.pypi
|
|
120
122
|
|
|
121
123
|
def parse(file, url):
|
|
122
124
|
if package.is_cpython:
|
|
@@ -133,15 +135,16 @@ def _parse_package(
|
|
|
133
135
|
)
|
|
134
136
|
for symbol, fragment in symbols.items():
|
|
135
137
|
with lock:
|
|
136
|
-
if symbol
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
if (existing_url := symbol_to_urls.get(symbol)) and (
|
|
139
|
+
# More components take precedence.
|
|
140
|
+
existing_url.count("/") >= url.count("/")
|
|
141
|
+
):
|
|
142
|
+
continue
|
|
143
|
+
if fragment.startswith(_MODULE_FRAGMENT_PREFIX) and module_count == 1:
|
|
144
|
+
# This page contains a single module, let's redirect to this page without the anchor.
|
|
145
|
+
symbol_to_urls[symbol] = f"{url}"
|
|
146
|
+
else:
|
|
147
|
+
symbol_to_urls[symbol] = f"{url}#{fragment}"
|
|
145
148
|
|
|
146
149
|
with ProgressExecutor(
|
|
147
150
|
f"Parsing {len(file_and_urls)} files for {package_docs}",
|
|
@@ -156,7 +159,12 @@ def _parse_package(
|
|
|
156
159
|
|
|
157
160
|
console.print(f"Found {len(symbol_to_urls)} symbols in {package.pypi}")
|
|
158
161
|
_heuristically_fillin_modules(package, symbol_to_urls)
|
|
159
|
-
|
|
162
|
+
|
|
163
|
+
lines = [
|
|
164
|
+
f'VERSION = "{crawl_state.package_version}"',
|
|
165
|
+
"",
|
|
166
|
+
]
|
|
167
|
+
lines.extend(_create_symbols_map(symbol_to_urls))
|
|
160
168
|
mapping_file = Path(mapping.__file__).parent / f"{package.pypi}.py"
|
|
161
169
|
if in_place:
|
|
162
170
|
mapping_file.write_text("\n".join(itertools.chain(lines, [""])))
|
|
@@ -165,8 +173,13 @@ def _parse_package(
|
|
|
165
173
|
before = mapping_file.read_text().splitlines(keepends=True)
|
|
166
174
|
else:
|
|
167
175
|
before = []
|
|
168
|
-
diffs =
|
|
169
|
-
|
|
176
|
+
diffs = list(
|
|
177
|
+
difflib.unified_diff(
|
|
178
|
+
before,
|
|
179
|
+
[line + "\n" for line in lines],
|
|
180
|
+
fromfile="before",
|
|
181
|
+
tofile="after",
|
|
182
|
+
)
|
|
170
183
|
)
|
|
171
184
|
if diffs:
|
|
172
185
|
console.print("".join(diffs))
|
|
@@ -175,6 +188,8 @@ def _parse_package(
|
|
|
175
188
|
def _heuristically_fillin_modules(
|
|
176
189
|
package: Package, symbol_to_urls: dict[str, str]
|
|
177
190
|
) -> None:
|
|
191
|
+
if package.is_cpython():
|
|
192
|
+
return
|
|
178
193
|
extra_module_to_urls: dict[str, str] = {}
|
|
179
194
|
for symbol in symbol_to_urls:
|
|
180
195
|
if symbol in package.namespaces:
|
|
@@ -200,7 +215,7 @@ def _heuristically_fillin_modules(
|
|
|
200
215
|
|
|
201
216
|
|
|
202
217
|
def _remove_fragment(url: str) -> str:
|
|
203
|
-
return
|
|
218
|
+
return urlunparse(urlparse(url)._replace(fragment=""))
|
|
204
219
|
|
|
205
220
|
|
|
206
221
|
def _create_symbols_map(symbol_to_urls: dict[str, str]) -> list[str]:
|
|
@@ -210,7 +225,6 @@ def _create_symbols_map(symbol_to_urls: dict[str, str]) -> list[str]:
|
|
|
210
225
|
|
|
211
226
|
lines = [
|
|
212
227
|
"# fmt: off",
|
|
213
|
-
"",
|
|
214
228
|
"MAPPING = {",
|
|
215
229
|
]
|
|
216
230
|
previous_symbol = None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pyrefdev.indexer.index import Index
|
|
2
|
+
from pyrefdev.indexer.add_docs import update_config
|
|
3
|
+
from pyrefdev.config import SUPPORTED_PACKAGES
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_pypi(*, index: Index = Index()) -> None:
|
|
7
|
+
"""Parse PyPI data and add new packages to the config."""
|
|
8
|
+
for pypi in index.get_pypi_packages():
|
|
9
|
+
if pypi in SUPPORTED_PACKAGES:
|
|
10
|
+
continue
|
|
11
|
+
url = index.guess_index_url(pypi, should_die_if_not_found=False)
|
|
12
|
+
if url is None:
|
|
13
|
+
continue
|
|
14
|
+
update_config(pypi, url, None)
|