diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml
index 8060e9d35..4b25a8531 100644
--- a/.github/workflows/unit.yaml
+++ b/.github/workflows/unit.yaml
@@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
python-version: ['pypy-3.8', 'pypy-3.9', 'pypy-3.10',
- '3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-rc.2']
+ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
diff --git a/README.rst b/README.rst
index 680679920..85a0b56e0 100644
--- a/README.rst
+++ b/README.rst
@@ -91,6 +91,10 @@ in development
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.
+* Fix an issue in the readthedocs theme that prevented to use the search bar from the summary pages (like the class hierarchy).
+* The generated documentation now includes a help page under the path ``/apidocs-help.html``.
+ This page is accessible by clicking on the information icon in the navbar (``ℹ``).
+* Improve the javascript searching code to better understand terms that contains a dot (``.``).
pydoctor 24.3.3
^^^^^^^^^^^^^^^
diff --git a/pydoctor/linker.py b/pydoctor/linker.py
index a36949339..a569107b2 100644
--- a/pydoctor/linker.py
+++ b/pydoctor/linker.py
@@ -284,3 +284,16 @@ def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag:
def switch_context(self, ob:Optional['model.Documentable']) -> Iterator[None]:
with self._scope_linker.switch_context(ob):
yield
+
+class NotFoundLinker(DocstringLinker):
+ """A DocstringLinker implementation that cannot find any links."""
+
+ def link_to(self, target: str, label: "Flattenable") -> Tag:
+ return tags.transparent(label)
+
+ def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag:
+ return tags.code(label)
+
+ @contextlib.contextmanager
+ def switch_context(self, ob: Optional[model.Documentable]) -> Iterator[None]:
+ yield
diff --git a/pydoctor/templatewriter/summary.py b/pydoctor/templatewriter/summary.py
index 36bd5adea..d24992905 100644
--- a/pydoctor/templatewriter/summary.py
+++ b/pydoctor/templatewriter/summary.py
@@ -2,6 +2,8 @@
from __future__ import annotations
from collections import defaultdict
+from string import Template
+from textwrap import dedent
from typing import (
TYPE_CHECKING, DefaultDict, Dict, Iterable, List, Mapping, MutableSet,
Sequence, Tuple, Type, Union, cast
@@ -358,12 +360,141 @@ def stuff(self, request: object, tag: Tag) -> Tag:
))
return tag
+# TODO: The help page should dynamically include notes about the (source) code links.
+class HelpPage(Page):
+
+ filename = 'apidocs-help.html'
+
+ RST_SOURCE_TEMPLATE = Template('''
+ Navigation
+ ----------
+
+ There is one page per class, module and package.
+ Each page present summary table(s) which feature the members of the object.
+
+ Package or Module page
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ Each of these pages has two main sections consisting of:
+
+ - summary tables submodules and subpackages and the members of the module or in the ``__init__.py`` file.
+ - detailed descriptions of function and attribute members.
+
+ Class page
+ ~~~~~~~~~~
+
+ Each class has its own separate page.
+ Each of these pages has three main sections consisting of:
+
+ - declaration, constructors, know subclasses and description
+ - summary tables of members, including inherited
+ - detailed descriptions of method and attribute members
+
+ Entries in each of these sections are omitted if they are empty or not applicable.
+
+ Module Index
+ ~~~~~~~~~~~~
+
+ Provides a high level overview of the packages and modules structure.
+
+ Class Hierarchy
+ ~~~~~~~~~~~~~~~
+
+ Provides a list of classes organized by inheritance structure. Note that ``object`` is ommited.
+
+ Index Of Names
+ ~~~~~~~~~~~~~~
+
+ The Index contains an alphabetic index of all objects in the documentation.
+
+
+ Search
+ ------
+
+ You can search for definitions of modules, packages, classes, functions, methods and attributes.
+
+ These items can be searched using part or all of the name and/or from their docstrings if "search in docstrings" is enabled.
+ Multiple search terms can be provided separated by whitespace.
+
+ The search is powered by `lunrjs `_.
+
+ Indexing
+ ~~~~~~~~
+
+ By default the search only matches on the name of the object.
+ Enable the full text search in the docstrings with the checkbox option.
+
+ You can instruct the search to look only in specific fields by passing the field name in the search like ``docstring:term``.
+
+ **Possible fields are**:
+
+ - ``name``, the name of the object (example: "MyClassAdapter" or "my_fmin_opti").
+ - ``qname``, the fully qualified name of the object (example: "lib.classses.MyClassAdapter").
+ - ``names``, the name splitted on camel case or snake case (example: "My Class Adapter" or "my fmin opti")
+ - ``docstring``, the docstring of the object (example: "This is an adapter for HTTP json requests that logs into a file...")
+ - ``kind``, can be one of: $kind_names
+
+ Last two fields are only applicable if "search in docstrings" is enabled.
+
+ Other search features
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Term presence.
+ The default behaviour is to give a better ranking to object matching multiple terms of your query,
+ but still show entries that matches only one of the two terms.
+ To change this behavour, you can use the sign ``+``.
+
+ - To indicate a term must exactly match use the plus sing: ``+``.
+ - To indicate a term must not match use the minus sing: ``-``.
+
+
+ Wildcards
+ A trailling wildcard is automatically added to each term of your query if they don't contain an explicit term presence (``+`` or ``-``).
+ Searching for ``foo`` is the same as searching for ``foo*``.
+
+ If the query include a dot (``.``), a leading wildcard will to also added,
+ searching for ``model.`` is the same as ``*model.*`` and ``.model`` is the same as ``*.model*``.
+
+ In addition to this automatic feature, you can manually add a wildcard anywhere else in the query.
+
+
+ Query examples
+ ~~~~~~~~~~~~~~
+
+ - "doc" matches "pydoctor.model.Documentable" and "pydoctor.model.DocLocation".
+ - "+doc" matches "pydoctor.model.DocLocation" but won't match "pydoctor.model.Documentable".
+ - "ensure doc" matches "pydoctor.epydoc2stan.ensure_parsed_docstring" and other object whose matches either "doc" or "ensure".
+ - "inp str" matches "java.io.InputStream" and other object whose matches either "in" or "str".
+ - "model." matches everything in the pydoctor.model module.
+ - ".web.*tag" matches "twisted.web.teplate.Tag" and related.
+ - "docstring:ansi" matches object whose docstring matches "ansi".
+ ''')
+
+ def title(self) -> str:
+ return 'Help'
+
+ @renderer
+ def heading(self, request: object, tag: Tag) -> Tag:
+ return tag.clear()("Help")
+
+ @renderer
+ def helpcontent(self, request: object, tag: Tag) -> Tag:
+ from pydoctor.epydoc.markup import restructuredtext, ParseError
+ from pydoctor.linker import NotFoundLinker
+ errs: list[ParseError] = []
+ parsed = restructuredtext.parse_docstring(dedent(self.RST_SOURCE_TEMPLATE.substitute(
+ kind_names=', '.join(f'"{k.name}"' for k in model.DocumentableKind)
+ )), errs)
+ assert not errs
+ return parsed.to_stan(NotFoundLinker())
+
def summaryPages(system: model.System) -> Iterable[Type[Page]]:
- pages = [
+ pages: list[type[Page]] = [
ModuleIndexPage,
ClassIndexPage,
NameIndexPage,
UndocumentedSummaryPage,
+ HelpPage,
]
if len(system.root_names) > 1:
pages.append(IndexPage)
diff --git a/pydoctor/test/__init__.py b/pydoctor/test/__init__.py
index 45e8cf406..d6a07e9f9 100644
--- a/pydoctor/test/__init__.py
+++ b/pydoctor/test/__init__.py
@@ -1,24 +1,18 @@
"""PyDoctor's test suite."""
-import contextlib
from logging import LogRecord
-from typing import Iterable, TYPE_CHECKING, Iterator, Optional, Sequence
+from typing import Iterable, TYPE_CHECKING, Sequence
import sys
import pytest
from pathlib import Path
-from twisted.web.template import Tag, tags
-
from pydoctor import epydoc2stan, model
from pydoctor.templatewriter import IWriter, TemplateLookup
-from pydoctor.epydoc.markup import DocstringLinker
-
-if TYPE_CHECKING:
- from twisted.web.template import Flattenable
+from pydoctor.linker import NotFoundLinker
posonlyargs = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8")
typecomment = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8")
-
+NotFoundLinker = NotFoundLinker
# Because pytest 6.1 does not yet export types for fixtures, we define
# approximations that are good enough for our test cases:
@@ -87,18 +81,4 @@ def _writeDocsFor(self, ob: model.Documentable) -> None:
for o in ob.contents.values():
self._writeDocsFor(o)
-
-
-class NotFoundLinker(DocstringLinker):
- """A DocstringLinker implementation that cannot find any links."""
-
- def link_to(self, target: str, label: "Flattenable") -> Tag:
- return tags.transparent(label)
-
- def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag:
- return tags.code(label)
-
- @contextlib.contextmanager
- def switch_context(self, ob: Optional[model.Documentable]) -> Iterator[None]:
- yield
\ No newline at end of file
diff --git a/pydoctor/test/test_commandline.py b/pydoctor/test/test_commandline.py
index 01e06c97e..63c39c90a 100644
--- a/pydoctor/test/test_commandline.py
+++ b/pydoctor/test/test_commandline.py
@@ -304,6 +304,16 @@ 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 well generated.
+ """
+ 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
+
def test_htmlbaseurl_option_all_pages(tmp_path: Path) -> None:
"""
Check that the canonical link is included in all html pages, including summary pages.
@@ -319,4 +329,5 @@ def test_htmlbaseurl_option_all_pages(tmp_path: Path) -> None:
if t.stem == 'basic':
filename = 'index.html' # since we have only one module it's linked as index.html
assert f'
-
+
Cannot search: JavaScript is not supported/enabled in your browser.
-
-
-
- Search bar offers the following options:
-
-
- Term presence. The below example searches for documents that
- must contain “foo”, might contain “bar” and must not contain “baz”: +foo bar -baz
-
-
-
- Wildcards. The below example searches for documents with words beginning with “foo”: foo*
-
-
-
- Search in specific fields. The following search matches all objects
- in "twisted.mail" that matches “search”: +qname:twisted.mail.* +search
-
-
- Possible fields: 'name', 'qname' (fully qualified name), 'docstring', and 'kind'.
- Last two fields are only applicable if "search in docstrings" is enabled.
-
-
-
-
- Fuzzy matches. The following search matches all documents
- that have a word within 1 edit distance of “foo”: foo~1
-
-
-
-
-
-
@@ -96,8 +63,8 @@
Cannot search: JavaScript is not supported/enabled in your browser.
For more information on the search, visit the help page.
diff --git a/pydoctor/themes/base/search.js b/pydoctor/themes/base/search.js
index 7fb34200c..f76b5dd86 100644
--- a/pydoctor/themes/base/search.js
+++ b/pydoctor/themes/base/search.js
@@ -113,16 +113,6 @@ function showResultContainer(){
updateClearSearchBtn();
}
-function toggleSearchHelpText() {
- document.body.classList.toggle("search-help-hidden");
- if (document.body.classList.contains("search-help-hidden") && input.value.length==0){
- hideResultContainer();
- }
- else{
- showResultContainer();
- }
-}
-
function resetResultList(){
resetLongSearchTimerInfo();
results_list.innerHTML = '';
@@ -168,20 +158,19 @@ function _stopSearchingProcess(){
//////// SEARCH WARPPER FUNCTIONS /////////
// Values configuring the search-as-you-type feature.
-var SEARCH_DEFAULT_DELAY = 100; // in miliseconds
-var SEARCH_INCREASED_DELAY = 200;
-var SEARCH_INDEX_SIZE_TRESH_INCREASE_DELAY = 10; // in MB
-var SEARCH_INDEX_SIZE_TRESH_DISABLE_SEARCH_AS_YOU_TYPE = 20;
-var SEARCH_AUTO_WILDCARD = true;
+var SEARCH_DEFAULT_DELAY = 150; // in miliseconds
+var SEARCH_INCREASED_DELAY = 300; // in miliseconds
+var SEARCH_INDEX_SIZE_TRESH_INCREASE_DELAY = 15; // in MB
+var SEARCH_INDEX_SIZE_TRESH_DISABLE_SEARCH_AS_YOU_TYPE = 25; // in MB
-// Search delay depends on index size.
+// Search delay depends on index size in MB
function _getIndexSizePromise(indexURL){
return httpGetPromise(indexURL).then((responseText) => {
if (responseText==null){
return 0;
}
- let indexSizeApprox = responseText.length / 1000000; // in MB
- return indexSizeApprox;
+ let indexSizeApprox = responseText.length / 1000000;
+ return indexSizeApprox; // in MB
});
}
function _getSearchDelayPromise(indexURL){ // -> Promise of a Search delay number.
@@ -293,8 +282,8 @@ function launchSearch(noDelay){
if (isSearchReadyPromise==null){
isSearchReadyPromise = _getIsSearchReadyPromise()
}
- return isSearchReadyPromise.then((r)=>{
- return lunrSearch(_query, indexURL, _fields, "lunr.js", !noDelay?searchDelay:0, SEARCH_AUTO_WILDCARD).then((lunrResults) => {
+ return isSearchReadyPromise.then((r)=>{
+ return lunrSearch(_query, indexURL, _fields, "lunr.js", !noDelay?searchDelay:0).then((lunrResults) => {
// outdated query results
if (_searchStartTime != _lastSearchStartTime){return;}
@@ -431,7 +420,6 @@ document.addEventListener('keyup', (evt) => {
// we don't want to show it if the browser do not support JS.
window.addEventListener('load', (event) => {
document.getElementById('search-box-container').style.display = 'block';
- document.getElementById('search-help-box').style.display = 'block';
hideResultContainer();
});
@@ -440,8 +428,7 @@ window.addEventListener("click", (event) => {
if (event){
// 1. Hide the dropdown if the user clicks outside of it
if (!event.target.closest('#search-results-container')
- && !event.target.closest('#search-box')
- && !event.target.closest('#search-help-button')){
+ && !event.target.closest('#search-box')){
hideResultContainer();
return;
}
@@ -456,7 +443,7 @@ window.addEventListener("click", (event) => {
// 3.Hide the dropdown if the user clicks on a link that brings them to the same page.
// This includes links in summaries.
- link = event.target.closest('#search-results a')
+ link = event.target.closest('#search-results-container a')
if (link){
page_parts = document.location.pathname.split('/')
current_page = page_parts[page_parts.length-1]
diff --git a/pydoctor/themes/base/searchlib.js b/pydoctor/themes/base/searchlib.js
index 882d77153..c1a3995f0 100644
--- a/pydoctor/themes/base/searchlib.js
+++ b/pydoctor/themes/base/searchlib.js
@@ -7,14 +7,13 @@
// Other required ressources like lunr.js, searchindex.json and all-documents.html are passed as URL
// to functions. This makes the code reusable outside of pydoctor build directory.
// Implementation note: Searches are designed to be launched synchronously, if lunrSearch() is called sucessively (while already running),
-// old promise will never resolves and the searhc worker will be restarted.
+// old promise will never resolves and the search worker will be restarted.
// Hacky way to make the worker code inline with the rest of the source file handling the search.
// Worker message params are the following:
// - query: string
// - indexJSONData: dict
// - defaultFields: list of strings
-// - autoWildcard: boolean
let _lunrWorkerCode = `
// The lunr.js code will be inserted here.
@@ -29,19 +28,17 @@ onmessage = (message) => {
if (!message.data.defaultFields) {
throw new Error('No default fields provided.');
}
- if (!message.data.hasOwnProperty('autoWildcard')){
- throw new Error('No value for auto wildcard provided.');
- }
+
// Create index
let index = lunr.Index.load(message.data.indexJSONData);
-
+
// Declare query function building
function _queryfn(_query){ // _query is the Query object
// Edit the parsed query clauses that are applicable for all fields (default) in order
// to remove the field 'kind' from the clause since this it's only useful when specifically requested.
var parser = new lunr.QueryParser(message.data.query, _query)
parser.parse()
- var hasTraillingWildcard = false;
+
_query.clauses.forEach(clause => {
if (clause.fields == _query.allFields){
// we change the query fields when they are applicable to all fields
@@ -49,27 +46,44 @@ onmessage = (message) => {
// which should not be matched by default.
clause.fields = message.data.defaultFields;
}
- // clause.wildcard is actually always NONE due to https://github.com/olivernn/lunr.js/issues/495
- // But this works...
- if (clause.term.slice(-1) == '*'){
- // we want to avoid the auto wildcard system only if a trailling wildcard is already added
- // not if a leading wildcard exists
- hasTraillingWildcard = true
- }
+
});
// Auto wilcard feature, see issue https://github.com/twisted/pydoctor/issues/648
var new_clauses = [];
- if ((message.data.autoWildcard == true) && (hasTraillingWildcard == false)){
- _query.clauses.forEach(clause => {
- // Setting clause.wildcard is useless.
+
+ _query.clauses.forEach(clause => {
+ if (clause.presence === 1) { // ignore clauses that have explicit presence (+/-)
+ // Setting clause.wildcard is useless, and clause.wildcard is actually always NONE
+ // due to https://github.com/olivernn/lunr.js/issues/495
// But this works...
- let new_clause = {...clause}
- new_clause.term = new_clause.term + '*'
- clause.boost = 2
- new_clause.boost = 0
- new_clauses.push(new_clause)
- });
- }
+ if (clause.term.slice(-1) != '*'){
+ let new_clause = {...clause}
+ new_clause.term = new_clause.term + '*'
+ clause.boost = 2
+ new_clause.boost = 1
+ new_clauses.push(new_clause)
+ }
+
+ // Adding a leading wildcard if the dot is included as well.
+ // This should only apply to terms that are applicable to name-like fields.
+ // so we refer to the default fields
+ if (clause.term.indexOf('.') != -1) {
+ if (clause.term.slice(0,1) != '*'){
+ let second_new_clause = {...clause}
+ second_new_clause.boost = 1
+ if (clause.term.slice(0,1) != '.'){
+ second_new_clause.term = '.' + second_new_clause.term
+ }
+ second_new_clause.term = '*' + second_new_clause.term
+ if (clause.term.slice(-1) != '*'){
+ second_new_clause.term = second_new_clause.term + '*'
+ }
+ new_clauses.push(second_new_clause)
+ }
+ }
+ }
+ });
+
new_clauses.forEach(clause => {
_query.clauses.push(clause)
});
@@ -78,8 +92,8 @@ onmessage = (message) => {
}
// Launch the search
- let results = index.query(_queryfn)
-
+ var results = index.query(_queryfn)
+
// Post message with results
postMessage({'results':results});
};
@@ -174,9 +188,8 @@ function _getWorkerPromise(lunJsSourceCode){ // -> Promise of a fresh worker to
* @param lunrJsURL: URL pointing to a copy of lunr.js.
* @param searchDelay: Number of miliseconds to wait before actually launching the query. This is useful to set for "search as you type" kind of search box
* because it let a chance to users to continue typing without triggering useless searches (because previous search is aborted on launching a new one).
- * @param autoWildcard: Whether to automatically append wildcards to all query clauses when no wildcard is already specified. boolean.
- */
-function lunrSearch(query, indexURL, defaultFields, lunrJsURL, searchDelay, autoWildcard){
+*/
+function lunrSearch(query, indexURL, defaultFields, lunrJsURL, searchDelay){
// Abort ongoing search
abortSearch();
@@ -187,7 +200,7 @@ function lunrSearch(query, indexURL, defaultFields, lunrJsURL, searchDelay, auto
searchEventsEnv.removeEventListener('abortSearch', this);
});
- // Pref:
+ // Perf:
// Because this function can be called a lot of times in a very few moments,
// Actually launch search after a delay to let a chance to users to continue typing,
// which would trigger a search abort event, which would avoid wasting a worker
@@ -220,7 +233,6 @@ function lunrSearch(query, indexURL, defaultFields, lunrJsURL, searchDelay, auto
'query': query,
'indexJSONData': lunrIndexData,
'defaultFields': defaultFields,
- 'autoWildcard': autoWildcard,
}
if (!_aborted){
diff --git a/pydoctor/themes/readthedocs/common.html b/pydoctor/themes/readthedocs/common.html
index e30fab52e..2514f634e 100644
--- a/pydoctor/themes/readthedocs/common.html
+++ b/pydoctor/themes/readthedocs/common.html
@@ -28,70 +28,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Search bar offers the following options:
-
-
- Term presence. The below example searches for documents that
- must contain “foo”, might contain “bar” and must not contain “baz”: +foo bar -baz
-
-
-
- Wildcards. The below example searches for documents with words beginning with “foo”: foo*
-
-
-
- Search in specific fields. The following search matches all objects
- in "twisted.mail" that matches “search”: +qname:twisted.mail.* +search
-
-
- Possible fields: 'name', 'qname' (fully qualified name), 'docstring', and 'kind'.
- Last two fields are only applicable if "search in docstrings" is enabled.
-
-
-
-
- Fuzzy matches. The following search matches all documents
- that have a word within 1 edit distance of “foo”: foo~1
-