diff --git a/README.rst b/README.rst index 64b30e004..680679920 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,8 @@ in development ^^^^^^^^^^^^^^ * Drop Python 3.7 and support Python 3.13. +* Implement canonical HTML element (````) to help search engines reduce outdated content. + Enable this feature by passing the base URL of the API documentation with option ``--html-base-url``. * Improve collection of objects: - Document objects declared in the ``else`` block of 'if' statements (previously they were ignored). - Document objects declared in ``finalbody`` and ``else`` block of 'try' statements (previously they were ignored). @@ -85,6 +87,10 @@ in development * Replace the deprecated dependency appdirs with platformdirs. * Fix WinError caused by the failure of the symlink creation process. Pydoctor should now run on windows without the need to be administrator. +* Adjust the sphinx extension to support Sphinx 8.1. The entries dynamically added to the intersphinx config + from the ``pydoctor_url_path`` config option now includes a project name which defaults to 'main' (instead of putting None), + use mapping instead of a list to define your own project name. +* Improve the themes so the adds injected by ReadTheDocs are rendered with the correct width and do not overlap too much with the main content. pydoctor 24.3.3 ^^^^^^^^^^^^^^^ diff --git a/docs/source/conf.py b/docs/source/conf.py index 5043188c9..eeba964ff 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -99,6 +99,7 @@ pydoctor_args = { 'main': [ '--html-output={outdir}/api/', # Make sure to have a trailing delimiter for better usage coverage. + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/api', '--project-name=pydoctor', f'--project-version={version}', '--docformat=epytext', @@ -108,6 +109,7 @@ ] + _common_args, 'custom_template_demo': [ '--html-output={outdir}/custom_template_demo/', + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/custom_template_demo', f'--project-version={version}', f'--template-dir={_pydoctor_root}/docs/sample_template', f'{_pydoctor_root}/pydoctor', @@ -116,6 +118,7 @@ '-qqq' ], # we don't want to hear any warnings from this custom template demo. 'epydoc_demo': [ '--html-output={outdir}/docformat/epytext_demo', + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/docformat/epytext_demo', '--project-name=pydoctor-epytext-demo', '--project-version=1.3.0', '--docformat=epytext', @@ -126,6 +129,7 @@ ] + _common_args, 'restructuredtext_demo': [ '--html-output={outdir}/docformat/restructuredtext_demo', + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/docformat/restructuredtext_demo', '--project-name=pydoctor-restructuredtext-demo', '--project-version=1.0.0', '--docformat=restructuredtext', @@ -136,6 +140,7 @@ ] + _common_args, 'numpy_demo': [ # no need to pass --docformat here, we use __docformat__ '--html-output={outdir}/docformat/numpy_demo', + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/docformat/numpy_demo', '--project-name=pydoctor-numpy-style-demo', '--project-version=1.0.0', '--project-url=../google-numpy.html', @@ -145,6 +150,7 @@ ] + _common_args, 'google_demo': [ '--html-output={outdir}/docformat/google_demo', + '--html-base-url=https://pydoctor.readthedocs.io/en/latest/docformat/google_demo', '--project-name=pydoctor-google-style-demo', '--project-version=1.0.0', '--docformat=google', diff --git a/docs/source/publish-github-action.rst b/docs/source/publish-github-action.rst index 3e30ce751..17dc0fa2f 100644 --- a/docs/source/publish-github-action.rst +++ b/docs/source/publish-github-action.rst @@ -5,7 +5,7 @@ Simple GitHub Action to publish API docs Here is an example of a simple GitHub Action to automatically generate your documentation with Pydoctor -and publish it to your default GitHub Pages website. +and publish it to your default GitHub Pages website when there is a push on the ``main`` branch. Just substitute `(projectname)` and `(packagedirectory)` with the appropriate information. @@ -14,7 +14,8 @@ with the appropriate information. name: apidocs on: - - push + push: + branches: [main] jobs: deploy: @@ -22,15 +23,15 @@ with the appropriate information. steps: - uses: actions/checkout@master - - name: Set up Python 3.8 + - name: Set up Python 3.12 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.12 - name: Install requirements for documentation generation run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install docutils pydoctor + python -m pip install pydoctor - name: Generate API documentation with pydoctor run: | @@ -40,6 +41,7 @@ with the appropriate information. --project-name=(projectname) \ --project-url=https://github.com/$GITHUB_REPOSITORY \ --html-viewsource-base=https://github.com/$GITHUB_REPOSITORY/tree/$GITHUB_SHA \ + --html-base-url=https://$GITHUB_REPOSITORY_OWNER.github.io/${GITHUB_REPOSITORY#*/} \ --html-output=./apidocs \ --docformat=restructuredtext \ --intersphinx=https://docs.python.org/3/objects.inv \ diff --git a/docs/source/publish-readthedocs.rst b/docs/source/publish-readthedocs.rst new file mode 100644 index 000000000..01580f4aa --- /dev/null +++ b/docs/source/publish-readthedocs.rst @@ -0,0 +1,51 @@ +:orphan: + +Simple ReadTheDocs config to publish API docs +--------------------------------------------- + +Here is an example of a simple ReadTheDocs integration to automatically +generate your documentation with Pydoctor. + +.. note:: This kind of integration should + not be confused with `Sphinx support `_ that can also be used to run + pydoctor inside ReadTheDocs as part of the standard Sphinx build process. + + This page, on the other hand, documents **how to simply run pydoctor + and publish on ReadTheDocs** by using build customizations features. + +This example only includes a configuration file (``.readthedocs.yaml``), +but the repository must also have been +integrated to ReadTheDocs (by linking your Github account and importing your project for +instance or by `manual webhook configuration `_). + +The config file below assume you're cloning your repository with http(s) protocol +and that repository is a GitHub instance +(the value of ``--html-viewsource-base`` could vary depending on your git server). + +Though, a similar process can be applied to Gitea, GitLab, Bitbucket ot others git servers. + +Just substitute `(projectname)` and `(packagedirectory)` +with the appropriate information. + +.. code:: yaml + + version: 2 + build: + os: "ubuntu-22.04" + tools: + python: "3.10" + commands: + - pip install pydoctor + - | + pydoctor \ + --project-name=(projectname) \ + --project-version=${READTHEDOCS_GIT_IDENTIFIER} \ + --project-url=${READTHEDOCS_GIT_CLONE_URL%*.git} \ + --html-viewsource-base=${READTHEDOCS_GIT_CLONE_URL%*.git}/tree/${READTHEDOCS_GIT_COMMIT_HASH} \ + --html-base-url=${READTHEDOCS_CANONICAL_URL} \ + --html-output $READTHEDOCS_OUTPUT/html/ \ + --docformat=restructuredtext \ + --intersphinx=https://docs.python.org/3/objects.inv \ + ./(packagedirectory) + +`More on ReadTheDocs build customizations `_. diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 92d3e9b6d..f27274c03 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -26,9 +26,10 @@ The result looks like `this `_. pydoctor \ --project-name=pydoctor \ - --project-version=1.2.0 \ + --project-version=20.7.2 \ --project-url=https://github.com/twisted/pydoctor/ \ --html-viewsource-base=https://github.com/twisted/pydoctor/tree/20.7.2 \ + --html-base-url=https://pydoctor.readthedocs.io/en/latest/api \ --html-output=docs/api \ --docformat=epytext \ --intersphinx=https://docs.python.org/3/objects.inv \ @@ -53,6 +54,9 @@ Output files are static HTML pages which require no extra server-side support. Here is a `GitHub Action example `_ to automatically publish your API documentation to your default GitHub Pages website. +Here is a `ReadTheDocs configuration `_ to automatically +publish your API documentation to ReadTheDocs + Return codes ------------ diff --git a/docs/tests/test.py b/docs/tests/test.py index 398e14912..be6134047 100644 --- a/docs/tests/test.py +++ b/docs/tests/test.py @@ -93,6 +93,7 @@ def test_page_contains_infos(): - nav and links to modules, classes, names - js script source - pydoctor github link in the footer + - canonical link """ infos = (f'', - 'pydoctor',) + 'pydoctor', + 'pydoctor', 'Twisted', - '',) + '', + ' None: self._errors.append(ParseError(msg, linenum, is_fatal)) -class _DocumentPseudoWriter(Writer): +if TYPE_CHECKING: + _StrWriter: TypeAlias = Writer[str] +else: + _StrWriter = Writer + +class _DocumentPseudoWriter(_StrWriter): """ A pseudo-writer for the docutils framework, that can be used to access the document itself. The output of C{_DocumentPseudoWriter} diff --git a/pydoctor/options.py b/pydoctor/options.py index 1c26a9bb0..681aaf6c9 100644 --- a/pydoctor/options.py +++ b/pydoctor/options.py @@ -148,6 +148,11 @@ def get_parser() -> ArgumentParser: "The default behaviour auto detects most common providers like Github, Bitbucket, GitLab or SourceForge. " "But in some cases you might have to override the template string, for instance to make it work with git-web, use: " '--html-viewsource-template="{mod_source_href}#n{lineno}"'), metavar='SOURCETEMPLATE', default=Options.HTML_SOURCE_TEMPLATE_DEFAULT) + parser.add_argument( + '--html-base-url', dest='htmlbaseurl', + help=("A base URL used to include a canonical link in every html page. " + "This help search engine to link to the preferred version of " + "a web page to prevent duplicated or oudated content. "), default=None, metavar='BASEURL', ) parser.add_argument( '--buildtime', dest='buildtime', help=("Use the specified build time over the current time. " @@ -297,6 +302,10 @@ def _convert_htmlwriter(s: str) -> Type['IWriter']: error(str(e)) def _convert_privacy(l: List[str]) -> List[Tuple['model.PrivacyClass', str]]: return list(map(functools.partial(parse_privacy_tuple, opt='--privacy'), l)) +def _convert_htmlbaseurl(url:str | None) -> str | None: + if url and not url.endswith('/'): + url += '/' + return url _RECOGNIZED_SOURCE_HREF = { # Sourceforge @@ -361,6 +370,7 @@ class Options: htmlwriter: Type['IWriter'] = attr.ib(converter=_convert_htmlwriter) htmlsourcebase: Optional[str] = attr.ib() htmlsourcetemplate: str = attr.ib() + htmlbaseurl: str | None = attr.ib(converter=_convert_htmlbaseurl) buildtime: Optional[str] = attr.ib() warnings_as_errors: bool = attr.ib() verbosity: int = attr.ib() diff --git a/pydoctor/sphinx_ext/build_apidocs.py b/pydoctor/sphinx_ext/build_apidocs.py index 1247e0071..7463bbc85 100644 --- a/pydoctor/sphinx_ext/build_apidocs.py +++ b/pydoctor/sphinx_ext/build_apidocs.py @@ -113,7 +113,7 @@ def on_builder_inited(app: Sphinx) -> None: intersphinx_mapping = config.intersphinx_mapping url = url_path.format(**{'rtd_version': rtd_version}) inv = (str(temp_path / 'objects.inv'),) - intersphinx_mapping[f'{key}-api-docs'] = (None, (url, inv)) + intersphinx_mapping[f'{key}-api-docs'] = (key, (url, inv)) # Build the API docs in temporary path. shutil.rmtree(temp_path, ignore_errors=True) diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py index 68e013ec0..22dabe5f0 100644 --- a/pydoctor/templatewriter/pages/__init__.py +++ b/pydoctor/templatewriter/pages/__init__.py @@ -7,6 +7,7 @@ ) import ast import abc +from urllib.parse import urljoin from twisted.web.iweb import IRenderable, ITemplateLoader, IRequest from twisted.web.template import Element, Tag, renderer, tags @@ -146,9 +147,19 @@ class Head(TemplateElement): filename = 'head.html' - def __init__(self, title: str, loader: ITemplateLoader) -> None: + def __init__(self, title: str, baseurl: str | None, pageurl: str, + loader: ITemplateLoader) -> None: super().__init__(loader) self._title = title + self._baseurl = baseurl + self._pageurl = pageurl + + @renderer + def canonicalurl(self, request: IRequest, tag: Tag) -> Flattenable: + if not self._baseurl: + return '' + canonical_link = urljoin(self._baseurl, self._pageurl) + return tags.link(rel='canonical', href=canonical_link) @renderer def title(self, request: IRequest, tag: Tag) -> str: @@ -171,6 +182,14 @@ def __init__(self, system: model.System, if not loader: loader = self.lookup_loader(template_lookup) super().__init__(loader) + + @property + def page_url(self) -> str: + # This MUST be overriden in CommonPage + """ + The relative page url + """ + return self.filename def render(self, request: Optional[IRequest]) -> Tag: return tags.transparent(super().render(request)).fillSlots(**self.slot_map) @@ -197,7 +216,8 @@ def title(self) -> str: @renderer def head(self, request: IRequest, tag: Tag) -> IRenderable: - return Head(self.title(), Head.lookup_loader(self.template_lookup)) + return Head(self.title(), self.system.options.htmlbaseurl, self.page_url, + loader=Head.lookup_loader(self.template_lookup)) @renderer def nav(self, request: IRequest, tag: Tag) -> IRenderable: diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py index 007744365..aad63d3cd 100644 --- a/pydoctor/test/test_commandline.py +++ b/pydoctor/test/test_commandline.py @@ -304,6 +304,7 @@ def test_index_hardlink(tmp_path: Path) -> None: assert not (tmp_path / 'basic.html').is_symlink() assert (tmp_path / 'basic.html').is_file() + def test_apidocs_help(tmp_path: Path) -> None: """ Checks that the help page is weel generated. @@ -311,4 +312,22 @@ def test_apidocs_help(tmp_path: Path) -> None: exit_code = driver.main(args=['--html-output', str(tmp_path), 'pydoctor/test/testpackages/basic/']) assert exit_code == 0 help_page = (tmp_path / 'apidocs-help.html').read_text() - assert '

Search

' in help_page \ No newline at end of file + assert '

Search

' in help_page + +def test_htmlbaseurl_option_all_pages(tmp_path: Path) -> None: + """ + Check that the canonical link is included in all html pages, including summary pages. + """ + exit_code = driver.main(args=[ + '--html-base-url=https://example.com.abcde', + '--html-output', str(tmp_path), 'pydoctor/test/testpackages/basic/']) + assert exit_code == 0 + for t in tmp_path.iterdir(): + if not t.name.endswith('.html'): + continue + filename = t.name + if t.stem == 'basic': + filename = 'index.html' # since we have only one module it's linked as index.html + assert f' None: + src = ''' + var = True + class Cls: + foo = False + ''' + mod = fromText(src, modname='t', system=model.System(model.Options.from_args( + ['--html-base-url=https://example.org/t/docs'] + ))) + html1 = getHTMLOf(mod) + html2 = getHTMLOf(mod.contents['Cls']) + + assert ' None: + src = ''' + var = True + class Cls: + foo = False + ''' + mod = fromText(src, modname='t', system=model.System(model.Options.from_args( + ['--html-base-url=https://example.org/t/docs'] + ))) + mod2 = fromText(src, modname='t2', system=mod.system) + html1 = getHTMLOf(mod) + html2 = getHTMLOf(mod.contents['Cls']) + + assert ' - -
+ +
API Documentation for , generated by pydoctor at . diff --git a/pydoctor/themes/base/head.html b/pydoctor/themes/base/head.html index d6f6fdcbe..39a05dc1a 100644 --- a/pydoctor/themes/base/head.html +++ b/pydoctor/themes/base/head.html @@ -1,5 +1,5 @@ - + <t:transparent t:render="title"> The title of Something </t:transparent> @@ -10,4 +10,5 @@ + Canonical URL diff --git a/pydoctor/themes/classic/head.html b/pydoctor/themes/classic/head.html index 37b2af2a6..7615872c8 100644 --- a/pydoctor/themes/classic/head.html +++ b/pydoctor/themes/classic/head.html @@ -1,5 +1,5 @@ - + <t:transparent t:render="title"> The title of Something </t:transparent> @@ -11,4 +11,5 @@ + Canonical URL diff --git a/pydoctor/themes/readthedocs/footer.html b/pydoctor/themes/readthedocs/footer.html index 5b24be51b..ecbfd5342 100644 --- a/pydoctor/themes/readthedocs/footer.html +++ b/pydoctor/themes/readthedocs/footer.html @@ -1,8 +1,8 @@