Skip to content

Commit e84b905

Browse files
committed
Merge branch 'main' into refactor-search-serializer-context
2 parents 2297b54 + 55b5624 commit e84b905

File tree

65 files changed

+1649
-1201
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1649
-1201
lines changed

.github/workflows/pip-tools.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ on:
1111
# Run weekly on day 0 at 00:00 UTC
1212
- cron: "0 0 * * 0"
1313

14+
permissions:
15+
contents: read
16+
1417
jobs:
1518
update-dependencies:
1619
permissions:

CHANGELOG.rst

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
Version 8.6.0
2+
-------------
3+
4+
:Date: September 28, 2022
5+
6+
* `@github-actions[bot] <https://github.com/github-actions[bot]>`__: Dependencies: all packages updated via pip-tools (`#9621 <https://github.com/readthedocs/readthedocs.org/pull/9621>`__)
7+
* `@evildmp <https://github.com/evildmp>`__: Made some small changes to the MyST migration how-to (`#9620 <https://github.com/readthedocs/readthedocs.org/pull/9620>`__)
8+
* `@boahc077 <https://github.com/boahc077>`__: ci: add minimum GitHub at the workflow level for pip-tools.yaml (`#9617 <https://github.com/readthedocs/readthedocs.org/pull/9617>`__)
9+
* `@stsewd <https://github.com/stsewd>`__: Search: refactor API view (`#9613 <https://github.com/readthedocs/readthedocs.org/pull/9613>`__)
10+
* `@sashashura <https://github.com/sashashura>`__: GitHub Workflows security hardening (`#9609 <https://github.com/readthedocs/readthedocs.org/pull/9609>`__)
11+
* `@stsewd <https://github.com/stsewd>`__: Redirects: test with/without organizations (`#9605 <https://github.com/readthedocs/readthedocs.org/pull/9605>`__)
12+
* `@humitos <https://github.com/humitos>`__: Builds: concurrency small optimization (`#9602 <https://github.com/readthedocs/readthedocs.org/pull/9602>`__)
13+
* `@uvidyadharan <https://github.com/uvidyadharan>`__: Update intersphinx.rst (`#9601 <https://github.com/readthedocs/readthedocs.org/pull/9601>`__)
14+
* `@ericholscher <https://github.com/ericholscher>`__: Release 8.5.0 (`#9600 <https://github.com/readthedocs/readthedocs.org/pull/9600>`__)
15+
* `@github-actions[bot] <https://github.com/github-actions[bot]>`__: Dependencies: all packages updated via pip-tools (`#9596 <https://github.com/readthedocs/readthedocs.org/pull/9596>`__)
16+
* `@stsewd <https://github.com/stsewd>`__: OAuth: save refresh token (`#9594 <https://github.com/readthedocs/readthedocs.org/pull/9594>`__)
17+
* `@stsewd <https://github.com/stsewd>`__: Redirects: allow update (`#9593 <https://github.com/readthedocs/readthedocs.org/pull/9593>`__)
18+
* `@stsewd <https://github.com/stsewd>`__: Unresolver: strict validation for external versions and other fixes (`#9534 <https://github.com/readthedocs/readthedocs.org/pull/9534>`__)
19+
* `@stsewd <https://github.com/stsewd>`__: New unresolver implementation (`#9500 <https://github.com/readthedocs/readthedocs.org/pull/9500>`__)
20+
* `@stsewd <https://github.com/stsewd>`__: API v3: fix organizations permissions (`#8771 <https://github.com/readthedocs/readthedocs.org/pull/8771>`__)
21+
122
Version 8.5.0
223
-------------
324

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
master_doc = "index"
7171
copyright = "2010, Read the Docs, Inc & contributors"
72-
version = "8.5.0"
72+
version = "8.6.0"
7373
release = version
7474
exclude_patterns = ["_build"]
7575
default_role = "obj"

docs/user/guides/authors.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ and :doc:`/intro/getting-started-with-mkdocs`.
1616
cross-referencing-with-sphinx
1717
intersphinx
1818
jupyter
19-
migrate-rest-myst
19+
Migrate from rST to MyST <migrate-rest-myst>

docs/user/guides/intersphinx.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ Result:
8383
provided by Intersphinx:
8484

8585
.. prompt:: bash $
86-
87-
python -msphinx.ext.intersphinx https://www.sphinx-doc.org/en/master/objects.inv
86+
87+
python -m sphinx.ext.intersphinx https://www.sphinx-doc.org/en/master/objects.inv
8888

8989
Intersphinx in Read the Docs
9090
----------------------------
@@ -167,7 +167,7 @@ You can use it like this:
167167
The inventory file is by default located at ``objects.inv``, for example ``https://readthedocs-docs.readthedocs-hosted.com/en/latest/objects.inv``.
168168

169169
.. code:: python
170-
170+
171171
# conf.py file
172172
173173
intersphinx_mapping = {

docs/user/guides/migrate-rest-myst.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# Migrating from reStructuredText to MyST Markdown
1+
# How to migrate from reStructuredText to MyST Markdown
2+
3+
In this guide, you will find
4+
how you can start writing Markdown in your existing reStructuredText project,
5+
or migrate it completely.
26

37
Sphinx is usually associated with reStructuredText, the markup language
48
{pep}`designed for the CPython project in the early '00s <287>`.
@@ -9,15 +13,11 @@ The most powerful of such extensions is {doc}`MyST-Parser <myst-parser:index>`,
913
which implements a CommonMark-compliant, extensible Markdown dialect
1014
with support for the Sphinx roles and directives that make it so useful.
1115

12-
In this guide, you will find
13-
how you can start writing Markdown in your existing reStructuredText project,
14-
or migrate it completely.
15-
1616
If, **instead of migrating**, you are starting a new project from scratch,
1717
have a look at {doc}`myst-parser:intro`.
1818
If you are starting a **project for Jupyter**, you can start with Jupyter Book, which uses ``MyST-Parser``, see the official Jupyter Book tutorial: {doc}`jupyterbook:start/your-first-book`
1919

20-
## Writing your content both in reStructuredText and MyST
20+
## How to write your content both in reStructuredText and MyST
2121

2222
It is useful to ask whether a migration is necessary in the first place.
2323
Doing bulk migrations of large projects with lots of work in progress
@@ -54,7 +54,7 @@ If you want to use a different suffix, you can do so by changing your
5454
`source_suffix` configuration value in `conf.py`.
5555
```
5656

57-
## Converting existing reStructuredText documentation to MyST
57+
## How to convert existing reStructuredText documentation to MyST
5858

5959
To convert existing reST documents to MyST, you can use
6060
the `rst2myst` CLI script shipped by {doc}`rst-to-myst:index`.
@@ -71,7 +71,7 @@ $ rst2myst convert docs/**/*.rst # Convert every .rst file under the docs direc
7171

7272
This will create a `.md` MyST file for every `.rst` source file converted.
7373

74-
### Advanced usage of `rst2myst`
74+
### How to modify the behaviour of `rst2myst`
7575

7676
The `rst2myst` accepts several flags to modify its behavior.
7777
All of them have sensible defaults, so you don't have to specify them
@@ -90,7 +90,7 @@ These are a few options you might find useful:
9090
You can read the full list of options in
9191
{doc}``the `rst2myst` documentation <rst-to-myst:cli>``.
9292

93-
## Enabling optional syntax
93+
## How to enable optional syntax
9494

9595
Some reStructuredText syntax will require you to enable certain MyST plugins.
9696
For example, to write [reST definition lists], you need to add a
@@ -108,7 +108,7 @@ in their documentation.
108108

109109
[reST definition lists]: https://docutils.sourceforge.io/docs/user/rst/quickref.html#definition-lists
110110

111-
## Writing reStructuredText syntax within MyST
111+
## How to write reStructuredText syntax within MyST
112112

113113
There is a small chance that `rst2myst` does not properly understand a piece of reST syntax,
114114
either because there is a bug in the tool

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "readthedocs",
3-
"version": "8.5.0",
3+
"version": "8.6.0",
44
"description": "Read the Docs build dependencies",
55
"author": "Read the Docs, Inc <[email protected]>",
66
"scripts": {

readthedocs/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Read the Docs."""
22

33

4-
__version__ = "8.5.0"
4+
__version__ = "8.6.0"

readthedocs/core/unresolver.py

+93-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
from dataclasses import dataclass
3-
from urllib.parse import urlparse
3+
from urllib.parse import ParseResult, urlparse
44

55
import structlog
66
from django.conf import settings
@@ -27,8 +27,7 @@ class UnresolvedURL:
2727

2828
version: Version = None
2929
filename: str = None
30-
query: str = None
31-
fragment: str = None
30+
parsed_url: ParseResult = None
3231
domain: Domain = None
3332
external: bool = False
3433

@@ -41,59 +40,92 @@ class Unresolver:
4140
# - /en/latest/
4241
# - /en/latest/file/name/
4342
multiversion_pattern = re.compile(
44-
r"^/(?P<language>{lang_slug})(/((?P<version>{version_slug})(/(?P<file>{filename_slug}))?)?)?$".format( # noqa
43+
r"""
44+
^/(?P<language>{lang_slug}) # Must have the language slug.
45+
(/((?P<version>{version_slug})(/(?P<file>{filename_slug}))?)?)?$ # Optionally a version followed by a file. # noqa
46+
""".format(
4547
**pattern_opts
46-
)
48+
),
49+
re.VERBOSE,
4750
)
4851

4952
# This pattern matches:
5053
# - /projects/subproject
5154
# - /projects/subproject/
5255
# - /projects/subproject/file/name/
5356
subproject_pattern = re.compile(
54-
r"^/projects/(?P<project>{project_slug}+)(/(?P<file>{filename_slug}))?$".format(
57+
r"""
58+
^/projects/ # Must have the `projects` prefix.
59+
(?P<project>{project_slug}+) # Followed by the subproject alias.
60+
(/(?P<file>{filename_slug}))?$ # Optionally a filename, which will be recursively resolved.
61+
""".format(
5562
**pattern_opts
56-
)
63+
),
64+
re.VERBOSE,
5765
)
5866

59-
def unresolve(self, url, add_index=True):
67+
def unresolve(self, url, append_indexhtml=True):
6068
"""
6169
Turn a URL into the component parts that our views would use to process them.
6270
6371
This is useful for lots of places,
6472
like where we want to figure out exactly what file a URL maps to.
6573
6674
:param url: Full URL to unresolve (including the protocol and domain part).
67-
:param add_index: If `True` the filename will be normalized
75+
:param append_indexhtml: If `True` directories will be normalized
6876
to end with ``/index.html``.
6977
"""
7078
parsed = urlparse(url)
7179
domain = self.get_domain_from_host(parsed.netloc)
72-
project_slug, domain_object, external = self.unresolve_domain(domain)
73-
if not project_slug:
80+
(
81+
parent_project_slug,
82+
domain_object,
83+
external_version_slug,
84+
) = self.unresolve_domain(domain)
85+
if not parent_project_slug:
7486
return None
7587

76-
parent_project = Project.objects.filter(slug=project_slug).first()
88+
parent_project = Project.objects.filter(slug=parent_project_slug).first()
7789
if not parent_project:
7890
return None
7991

80-
project, version, filename = self._unresolve_path(
92+
current_project, version, filename = self._unresolve_path(
8193
parent_project=parent_project,
8294
path=parsed.path,
8395
)
8496

85-
if add_index and filename and filename.endswith("/"):
97+
# Make sure we are serving the external version from the subdomain.
98+
if external_version_slug and version:
99+
if external_version_slug != version.slug:
100+
log.warning(
101+
"Invalid version for external domain.",
102+
domain=domain,
103+
version_slug=version.slug,
104+
)
105+
version = None
106+
filename = None
107+
elif not version.is_external:
108+
log.warning(
109+
"Attempt of serving a non-external version from RTD_EXTERNAL_VERSION_DOMAIN.",
110+
domain=domain,
111+
version_slug=version.slug,
112+
version_type=version.type,
113+
url=url,
114+
)
115+
version = None
116+
filename = None
117+
118+
if append_indexhtml and filename and filename.endswith("/"):
86119
filename += "index.html"
87120

88121
return UnresolvedURL(
89122
parent_project=parent_project,
90-
project=project or parent_project,
123+
project=current_project or parent_project,
91124
version=version,
92125
filename=filename,
93-
query=parsed.query,
94-
fragment=parsed.fragment,
126+
parsed_url=parsed,
95127
domain=domain_object,
96-
external=external,
128+
external=bool(external_version_slug),
97129
)
98130

99131
@staticmethod
@@ -109,7 +141,11 @@ def _match_multiversion_project(self, parent_project, path):
109141
Try to match a multiversion project.
110142
111143
If the translation exists, we return a result even if the version doesn't,
112-
so the translation is taken as the canonical project (useful for 404 pages).
144+
so the translation is taken as the current project (useful for 404 pages).
145+
146+
:returns: None or a tuple with the current project, version and file.
147+
A tuple with only the project means we weren't able to find a version,
148+
but the translation was correct.
113149
"""
114150
match = self.multiversion_pattern.match(path)
115151
if not match:
@@ -138,24 +174,40 @@ def _match_subproject(self, parent_project, path):
138174
139175
If the subproject exists, we try to resolve the rest of the path
140176
with the subproject as the canonical project.
177+
178+
If the subproject exists, we return a result even if version doesn't,
179+
so the subproject is taken as the current project (useful for 404 pages).
180+
181+
:returns: None or a tuple with the current project, version and file.
182+
A tuple with only the project means we were able to find the subproject,
183+
but we weren't able to resolve the rest of the path.
141184
"""
142185
match = self.subproject_pattern.match(path)
143186
if not match:
144187
return None
145188

146-
project_slug = match.group("project")
189+
subproject_alias = match.group("project")
147190
file = self._normalize_filename(match.group("file"))
148191
project_relationship = (
149-
parent_project.subprojects.filter(alias=project_slug)
192+
parent_project.subprojects.filter(alias=subproject_alias)
150193
.prefetch_related("child")
151194
.first()
152195
)
153196
if project_relationship:
154-
return self._unresolve_path(
155-
parent_project=project_relationship.child,
197+
# We use the subproject as the new parent project
198+
# to resolve the rest of the path relative to it.
199+
subproject = project_relationship.child
200+
response = self._unresolve_path(
201+
parent_project=subproject,
156202
path=file,
157203
check_subprojects=False,
158204
)
205+
# If we got a valid response, return that,
206+
# otherwise return the current subproject
207+
# as the current project without a valid version or path.
208+
if response:
209+
return response
210+
return subproject, None, None
159211
return None
160212

161213
def _match_single_version_project(self, parent_project, path):
@@ -182,10 +234,19 @@ def _unresolve_path(self, parent_project, path, check_subprojects=True):
182234
If the returned version is `None`, then we weren't able to
183235
unresolve the path into a valid version of the project.
184236
237+
The checks are done in the following order:
238+
239+
- Check for multiple versions if the parent project
240+
isn't a single version project.
241+
- Check for subprojects.
242+
- Check for single versions if the parent project isn’t
243+
a multi version project.
244+
185245
:param parent_project: The project that owns the path.
186246
:param path: The path to unresolve.
187247
:param check_subprojects: If we should check for subprojects,
188-
this is used to call this function recursively.
248+
this is used to call this function recursively when
249+
resolving the path from a subproject (we don't support subprojects of subprojects).
189250
190251
:returns: A tuple with: project, version, and file name.
191252
"""
@@ -216,7 +277,7 @@ def _unresolve_path(self, parent_project, path, check_subprojects=True):
216277
if response:
217278
return response
218279

219-
return None, None, None
280+
return parent_project, None, None
220281

221282
@staticmethod
222283
def get_domain_from_host(host):
@@ -234,8 +295,8 @@ def unresolve_domain(self, domain):
234295
Unresolve domain by extracting relevant information from it.
235296
236297
:param str domain: Domain to extract the information from.
237-
:returns: A tuple with: the project slug, domain object, and if the domain
238-
is from an external version.
298+
:returns: A tuple with: the project slug, domain object, and the
299+
external version slug if the domain is from an external version.
239300
"""
240301
public_domain = self.get_domain_from_host(settings.PUBLIC_DOMAIN)
241302
external_domain = self.get_domain_from_host(
@@ -250,22 +311,22 @@ def unresolve_domain(self, domain):
250311
if public_domain == root_domain:
251312
project_slug = subdomain
252313
log.debug("Public domain.", domain=domain)
253-
return project_slug, None, False
314+
return project_slug, None, None
254315

255316
# TODO: This can catch some possibly valid domains (docs.readthedocs.io.com)
256317
# for example, but these might be phishing, so let's ignore them for now.
257318
log.warning("Weird variation of our domain.", domain=domain)
258-
return None, None, False
319+
return None, None, None
259320

260321
# Serve PR builds on external_domain host.
261322
if external_domain == root_domain:
262323
try:
324+
project_slug, version_slug = subdomain.rsplit("--", maxsplit=1)
263325
log.debug("External versions domain.", domain=domain)
264-
project_slug, _ = subdomain.rsplit("--", maxsplit=1)
265-
return project_slug, None, True
326+
return project_slug, None, version_slug
266327
except ValueError:
267328
log.info("Invalid format of external versions domain.", domain=domain)
268-
return None, None, False
329+
return None, None, None
269330

270331
# Custom domain.
271332
domain_object = (

0 commit comments

Comments
 (0)