Skip to content

Commit 1fd489c

Browse files
authored
Add modeling for intersphinx data (#5289)
* Remove old test references to intersphinx * Initial commit of domains app * Add initial domaindata modeling and API integration * Add indexing of DomainData to the builds. * Cleanup of some domain stuff * Add search to Domains 🎉 * Add initial search implementation to DomainData * Move project search to a subset of site search This removes the second entry point to site search, and unifies all our searching to use fancy facets and be nice. * Small cleanup around faceting * Limit resuts by DomainData type * Remove old domains app * Remove search changes * Missed a few * Missed a bit more * remove random bits that got merged in * Undo url fix * Fix linting issues * Fix tests and linting * Review feedback * Fix lint * Move sphinx to a runtime requirement. * Use timestamped model for standardized class names * Address review feedback * Fix API name * Fix test too * Update readthedocs/projects/tasks.py Co-Authored-By: ericholscher <[email protected]>
1 parent 2c479ec commit 1fd489c

File tree

10 files changed

+204
-1
lines changed

10 files changed

+204
-1
lines changed

readthedocs/projects/tasks.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from django.utils import timezone
2525
from django.utils.translation import ugettext_lazy as _
2626
from slumber.exceptions import HttpClientError
27+
from sphinx.ext import intersphinx
28+
2729

2830
from readthedocs.builds.constants import (
2931
BUILD_STATE_BUILDING,
@@ -58,6 +60,7 @@
5860
)
5961
from readthedocs.doc_builder.loader import get_builder_class
6062
from readthedocs.doc_builder.python_environments import Conda, Virtualenv
63+
from readthedocs.sphinx_domains.models import SphinxDomain
6164
from readthedocs.projects.models import APIProject
6265
from readthedocs.restapi.client import api as api_v2
6366
from readthedocs.vcs_support import utils as vcs_support_utils
@@ -1232,6 +1235,7 @@ def fileify(version_pk, commit):
12321235
),
12331236
)
12341237
_manage_imported_files(version, path, commit)
1238+
_update_intersphinx_data(version, path, commit)
12351239
else:
12361240
log.info(
12371241
LOG_TEMPLATE.format(
@@ -1242,6 +1246,59 @@ def fileify(version_pk, commit):
12421246
)
12431247

12441248

1249+
def _update_intersphinx_data(version, path, commit):
1250+
"""
1251+
Update intersphinx data for this version
1252+
1253+
:param version: Version instance
1254+
:param path: Path to search
1255+
:param commit: Commit that updated path
1256+
"""
1257+
object_file = os.path.join(path, 'objects.inv')
1258+
1259+
# These classes are copied from Sphinx
1260+
# https://git.io/fhFbI
1261+
class MockConfig:
1262+
intersphinx_timeout = None
1263+
tls_verify = False
1264+
1265+
class MockApp:
1266+
srcdir = ''
1267+
config = MockConfig()
1268+
1269+
def warn(self, msg):
1270+
log.warning('Sphinx MockApp: %s', msg)
1271+
1272+
invdata = intersphinx.fetch_inventory(MockApp(), '', object_file)
1273+
for key, value in sorted(invdata.items() or {}):
1274+
domain, _type = key.split(':')
1275+
for name, einfo in sorted(value.items()):
1276+
# project, version, url, display_name
1277+
# ('Sphinx', '1.7.9', 'faq.html#epub-faq', 'Epub info')
1278+
url = einfo[2]
1279+
if '#' in url:
1280+
doc_name, anchor = url.split('#')
1281+
else:
1282+
doc_name, anchor = url, ''
1283+
display_name = einfo[3]
1284+
obj, _ = SphinxDomain.objects.get_or_create(
1285+
project=version.project,
1286+
version=version,
1287+
domain=domain,
1288+
name=name,
1289+
display_name=display_name,
1290+
type=_type,
1291+
doc_name=doc_name,
1292+
anchor=anchor,
1293+
)
1294+
if obj.commit != commit:
1295+
obj.commit = commit
1296+
obj.save()
1297+
SphinxDomain.objects.filter(project=version.project,
1298+
version=version
1299+
).exclude(commit=commit).delete()
1300+
1301+
12451302
def _manage_imported_files(version, path, commit):
12461303
"""
12471304
Update imported files for version.

readthedocs/restapi/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
SocialAccountViewSet,
2626
VersionViewSet,
2727
)
28+
from readthedocs.sphinx_domains.api import SphinxDomainAPIView
2829

2930

3031
router = routers.DefaultRouter()
@@ -34,6 +35,7 @@
3435
router.register(r'project', ProjectViewSet, basename='project')
3536
router.register(r'notification', NotificationViewSet, basename='emailhook')
3637
router.register(r'domain', DomainViewSet, basename='domain')
38+
router.register(r'sphinx_domains', SphinxDomainAPIView, base_name='sphinxdomain')
3739
router.register(
3840
r'remote/org',
3941
RemoteOrganizationViewSet,

readthedocs/rtd_tests/tests/test_privacy_urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ def setUp(self):
364364
'api_webhook_gitlab': {'status_code': 405},
365365
'api_webhook_bitbucket': {'status_code': 405},
366366
'api_webhook_generic': {'status_code': 403},
367+
'sphinxdomain-detail': {'status_code': 404},
367368
'remoteorganization-detail': {'status_code': 404},
368369
'remoterepository-detail': {'status_code': 404},
369370
'remoteaccount-detail': {'status_code': 404},

readthedocs/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def INSTALLED_APPS(self): # noqa
103103
'readthedocs.notifications',
104104
'readthedocs.integrations',
105105
'readthedocs.analytics',
106+
'readthedocs.sphinx_domains',
106107
'readthedocs.search',
107108

108109

readthedocs/sphinx_domains/__init__.py

Whitespace-only changes.

readthedocs/sphinx_domains/admin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Domain Admin classes."""
2+
from django.contrib import admin
3+
4+
from .models import SphinxDomain
5+
6+
7+
class SphinxDomainAdmin(admin.ModelAdmin):
8+
list_filter = ('type', 'project')
9+
raw_id_fields = ('project', 'version')
10+
search_fields = ('doc_name', 'name')
11+
12+
13+
admin.site.register(SphinxDomain, SphinxDomainAdmin)

readthedocs/sphinx_domains/api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Domain API classes."""
2+
3+
from rest_framework import serializers
4+
5+
from readthedocs.restapi.views.model_views import UserSelectViewSet
6+
7+
from .models import SphinxDomain
8+
9+
10+
class SphinxDomainSerializer(serializers.ModelSerializer):
11+
project = serializers.SlugRelatedField(slug_field='slug', read_only=True)
12+
version = serializers.SlugRelatedField(slug_field='slug', read_only=True)
13+
14+
class Meta:
15+
model = SphinxDomain
16+
fields = (
17+
'project',
18+
'version',
19+
'name',
20+
'display_name',
21+
'role_name',
22+
'docs_url',
23+
)
24+
25+
26+
class SphinxDomainAdminSerializer(SphinxDomainSerializer):
27+
28+
class Meta(SphinxDomainSerializer.Meta):
29+
fields = '__all__'
30+
31+
32+
class SphinxDomainAPIView(UserSelectViewSet): # pylint: disable=too-many-ancestors
33+
model = SphinxDomain
34+
serializer_class = SphinxDomainSerializer
35+
admin_serializer_class = SphinxDomainAdminSerializer
36+
filter_fields = (
37+
'project__slug',
38+
'version__slug',
39+
'domain',
40+
'type',
41+
'doc_name',
42+
'name',
43+
)

readthedocs/sphinx_domains/models.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Sphinx Domain modeling.
3+
4+
http://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html
5+
"""
6+
7+
from django.db import models
8+
from django.utils.translation import ugettext_lazy as _
9+
10+
from django_extensions.db.models import TimeStampedModel
11+
12+
from readthedocs.builds.models import Version
13+
from readthedocs.core.resolver import resolve
14+
from readthedocs.projects.models import Project
15+
from readthedocs.projects.querysets import RelatedProjectQuerySet
16+
17+
18+
class SphinxDomain(TimeStampedModel):
19+
20+
"""
21+
Information from a project about it's Sphinx domains.
22+
23+
This captures data about API objects that exist in that codebase.
24+
"""
25+
26+
project = models.ForeignKey(
27+
Project,
28+
related_name='sphinx_domains',
29+
)
30+
version = models.ForeignKey(
31+
Version,
32+
verbose_name=_('Version'),
33+
related_name='sphinx_domains',
34+
)
35+
commit = models.CharField(_('Commit'), max_length=255, null=True)
36+
37+
domain = models.CharField(
38+
_('Domain'),
39+
max_length=255,
40+
)
41+
name = models.CharField(
42+
_('Name'),
43+
max_length=255,
44+
)
45+
display_name = models.CharField(
46+
_('Display Name'),
47+
max_length=255,
48+
)
49+
type = models.CharField(
50+
_('Type'),
51+
max_length=255,
52+
)
53+
doc_name = models.CharField(
54+
_('Doc Name'),
55+
max_length=255,
56+
)
57+
anchor = models.CharField(
58+
_('Anchor'),
59+
max_length=255,
60+
)
61+
objects = RelatedProjectQuerySet.as_manager()
62+
63+
def __str__(self):
64+
return f'''
65+
SphinxDomain [{self.project.slug}:{self.version.slug}]
66+
[{self.domain}:{self.type}] {self.name} ->
67+
{self.doc_name}#{self.anchor}
68+
'''
69+
70+
@property
71+
def role_name(self):
72+
return f'{self.domain}:{self.type}'
73+
74+
@property
75+
def docs_url(self):
76+
path = self.doc_name
77+
if self.anchor:
78+
path += f'#{self.anchor}'
79+
full_url = resolve(
80+
project=self.project,
81+
version_slug=self.version.slug,
82+
filename=path,
83+
)
84+
return full_url

requirements/local-docs-build.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
# Base packages
44
docutils==0.14
5-
Sphinx==1.8.4
65
sphinx_rtd_theme==0.4.3
76
sphinx-tabs==1.1.10
87
# Required to avoid Transifex error with reserved slug

requirements/pip.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ django-extensions==2.1.6
1010

1111
djangorestframework==3.9.1
1212

13+
# For intersphinx during builds
14+
Sphinx==1.8.4
15+
1316
# Filtering for the REST API
1417
django-filter==2.1.0
1518

0 commit comments

Comments
 (0)