From: Vladimír Vondruš Date: Wed, 10 Jul 2019 20:45:15 +0000 (+0200) Subject: documentation/python: make it possible to configure how URLs look. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=66ace237992193954387183de471ce5220fc1e1a;p=blog.git documentation/python: make it possible to configure how URLs look. Well, this is a bit overdone preparation for links to actual class / module members, but can't hurt, no?? :D --- diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 777370b9..1bb3a401 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -197,8 +197,8 @@ Variable Description :py:`PAGE_HEADER: str` :abbr:`reST ` markup to put at the top of every page. If not set, nothing is added anywhere. The - ``{filename}`` placeholder is replaced with - current file name. + ``{url}`` placeholder is replaced with + current file URL. :py:`FINE_PRINT: str` :abbr:`reST ` markup to put into the footer. If not set, a default generic text is used. If empty, no footer @@ -250,6 +250,10 @@ Variable Description :py:`SEARCH_DISABLED` is not :py:`True`. :py:`DOCUTILS_SETTINGS: Dict[Any]` Additional docutils settings. Key/value pairs as described in `the docs `_. +:py:`URL_FORMATTER: Callable` Function for creating filenames and URLs + for modules, classes, pages and index + pages. See `Custom URL formatters`_ for + more information. =================================== =========================================== `Theme selection`_ @@ -303,12 +307,14 @@ CSS files. The :py:`LINKS_NAVBAR1` and :py:`LINKS_NAVBAR2` options define which links are shown on the top navbar, split into left and right column on small screen sizes. These options take a list of :py:`(title, path, sub)` tuples --- -``title`` is the link title, ``path`` is path to a particular page or -module/class (in the form of ``module.sub.ClassName``, for example) and ``sub`` -is an optional submenu, containing :py:`(title, path)` tuples. The ``path`` can -be also one of ``pages``, ``modules`` or ``classes``, linking to the page / -module / class index. When rendering, the path is converted to an actual URL to -the destination file. +``title`` is the link title; ``path`` is either one of :py:`'index'`, +:py:`'pages'`, :py:`'modules'` or :py:`'classes'` (linking to the main page or +page / module / class index path), a full URL (pasted as-is) or a *path* to a +particular page or module/class (in the form of +:py:`['module', 'sub', 'ClassName']` for :py:`module.sub.ClassName`, which then +gets formatted according to `URL formatting rules <#custom-url-formatters>`_); +and ``sub`` is an optional submenu, containing :py:`(title, path)` tuples, with +``path`` being interpreted the same way. By default the variables are defined like following --- there's just three items in the left column, with no submenus and the right column is empty: @@ -372,6 +378,30 @@ search to a subdomain: SEARCH_EXTERNAL_URL = 'https://google.com/search?q=site:doc.magnum.graphics+{query}' +`Custom URL formatters`_ +------------------------ + +The :py:`URL_FORMATTER` option allows you to control how *all* filenames and +generated URLs look like. It takes an entry type and a "path" as a list of +strings (so for example :py:`my.module.Class` is represented as +:py:`['my', 'module', 'Class']`), returning a tuple a filename and an URL. +Those can be the same, but also different (for example a file getting saved +into ``my/module/Class/index.html`` but the actual URL being +``https://docs.my.module/Class/``). The default implementation looks like this, +producing both filenames and URLs in the form of ``my.module.Class.html``: + +.. code:: py + + def default_url_formatter(type: EntryType, path: List[str]) -> Tuple[str, str]: + url = '.'.join(path) + '.html' + return url, url + +The ``type`` is an enum, if you don't want to fiddle with imports, compare +:py:`str(type)` against a string, which is one of :py:`'PAGE'`, :py:`'MODULE'`, +:py:`'CLASS'` or :py:`'SPECIAL'`. The :py:`'SPECIAL'` is for index pages and in +that case the ``path`` has always just one item, one of :py:`'pages'`, +:py:`'modules'` or :py:`'classes'`. + `Module inspection`_ ==================== @@ -824,10 +854,10 @@ Filename Use ======================= ======================================================= Each template gets passed all configuration values from the `Configuration`_ -table as-is, together with a :py:`FILENAME` variable with name of given output -file. In addition to builtin Jinja2 filters, the ``basename_or_url`` filter -returns either a basename of file path, if the path is relative; or a full URL, -if the argument is an absolute URL. It's useful in cases like this: +table as-is, together with a :py:`URL` variable with URL of given output file. +In addition to builtin Jinja2 filters, the ``basename_or_url`` filter returns +either a basename of file path, if the path is relative; or a full URL, if the +argument is an absolute URL. It's useful in cases like this: .. code:: html+jinja @@ -845,7 +875,8 @@ HTML markup without any additional escaping. Property Description ======================================= ======================================= :py:`page.summary` Doc summary -:py:`page.url` File URL +:py:`page.filename` File name [3]_ +:py:`page.url` File URL [3]_ :py:`page.breadcrumb` List of :py:`(title, URL)` tuples for breadcrumb navigation. :py:`page.content` Detailed documentation, if any @@ -1077,7 +1108,7 @@ Filename Use ======================= ======================================================= Each template is passed all configuration values from the `Configuration`_ -table as-is, together with a :py:`FILENAME`, as above. The navigation tree is +table as-is, together with an :py:`URL`, as above. The navigation tree is provided in an :py:`index` object, which has the following properties: .. class:: m-table m-fullwidth @@ -1117,3 +1148,5 @@ Module/class list is ordered in a way that all modules are before all classes. *documented* enum value listing that makes it worth to render the full description block. If :py:`False`, the member should be included only in the summary listing on top of the page to avoid unnecessary repetition. +.. [3] :py:`page.filename` and :py:`page.url` is generated by an URL formatter, + see `Custom URL formatters`_ for more information diff --git a/documentation/python.py b/documentation/python.py index 814e43ed..8d8d45ac 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -42,7 +42,7 @@ import shutil from enum import Enum from types import SimpleNamespace as Empty from importlib.machinery import SourceFileLoader -from typing import Tuple, Dict, Set, Any, List +from typing import Tuple, Dict, Set, Any, List, Callable from urllib.parse import urljoin from distutils.version import LooseVersion from docutils.transforms import Transform @@ -54,15 +54,24 @@ import m.htmlsanity default_templates = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates/python/') +special_pages = ['index', 'modules', 'classes', 'pages'] + class EntryType(Enum): - PAGE = 0 - MODULE = 1 - CLASS = 2 - ENUM = 3 - ENUM_VALUE = 4 - FUNCTION = 5 - PROPERTY = 6 - DATA = 7 + SPECIAL = 0 # one of files from special_pages + PAGE = 1 + MODULE = 2 + CLASS = 3 + ENUM = 4 + ENUM_VALUE = 5 + FUNCTION = 6 + PROPERTY = 7 + DATA = 8 + +def default_url_formatter(type: EntryType, path: List[str]) -> Tuple[str, str]: + # TODO: what about nested pages, how to format? + url = '.'.join(path) + '.html' + assert '/' not in url # TODO + return url, url default_config = { 'PROJECT_TITLE': 'My Python Project', @@ -115,6 +124,8 @@ default_config = { """, 'SEARCH_BASE_URL': None, 'SEARCH_EXTERNAL_URL': None, + + 'URL_FORMATTER': default_url_formatter } class State: @@ -190,9 +201,6 @@ def is_internal_or_imported_module_member(state: State, parent, path: str, name: def is_enum(state: State, object) -> bool: return (inspect.isclass(object) and issubclass(object, enum.Enum)) or (state.config['PYBIND11_COMPATIBILITY'] and hasattr(object, '__members__')) -def make_url(path: List[str]) -> str: - return '.'.join(path) + '.html' - def object_type(state: State, object) -> EntryType: if inspect.ismodule(object): return EntryType.MODULE if inspect.isclass(object): @@ -517,8 +525,8 @@ def extract_annotation(state: State, annotation) -> str: def render(config, template: str, page, env: jinja2.Environment): template = env.get_template(template) - rendered = template.render(page=page, FILENAME=page.url, **config) - with open(os.path.join(config['OUTPUT'], page.url), 'wb') as f: + rendered = template.render(page=page, URL=page.url, **config) + with open(os.path.join(config['OUTPUT'], page.filename), 'wb') as f: f.write(rendered.encode('utf-8')) # Add back a trailing newline so we don't need to bother with # patching test files to include a trailing newline to make Git @@ -530,7 +538,7 @@ def extract_module_doc(state: State, path: List[str], module): assert inspect.ismodule(module) out = Empty() - out.url = make_url(path) + out.url = state.config['URL_FORMATTER'](EntryType.MODULE, path)[1] out.name = path[-1] out.summary = extract_summary(state, state.class_docs, path, module.__doc__) return out @@ -539,7 +547,7 @@ def extract_class_doc(state: State, path: List[str], class_): assert inspect.isclass(class_) out = Empty() - out.url = make_url(path) + out.url = state.config['URL_FORMATTER'](EntryType.CLASS, path)[1] out.name = path[-1] out.summary = extract_summary(state, state.class_docs, path, class_.__doc__) return out @@ -783,20 +791,24 @@ def extract_data_doc(state: State, parent, path: List[str], data): return out def render_module(state: State, path, module, env): - logging.debug("generating %s.html", '.'.join(path)) + # Generate breadcrumb as the first thing as it generates the output + # filename as a side effect + breadcrumb = [] + filename: str + url: str + for i in range(len(path)): + filename, url = state.config['URL_FORMATTER'](EntryType.MODULE, path[:i + 1]) + breadcrumb += [(path[i], url)] + + logging.debug("generating %s", filename) # Call all registered page begin hooks for hook in state.hooks_pre_page: hook() - url_base = '' - breadcrumb = [] - for i in path: - url_base += i + '.' - breadcrumb += [(i, url_base + 'html')] - page = Empty() page.summary = extract_summary(state, state.module_docs, path, module.__doc__) - page.url = breadcrumb[-1][1] + page.filename = filename + page.url = url page.breadcrumb = breadcrumb page.prefix_wbr = '.'.join(path + ['']) page.modules = [] @@ -895,20 +907,26 @@ _filtered_builtin_properties = set([ ]) def render_class(state: State, path, class_, env): - logging.debug("generating %s.html", '.'.join(path)) + # Generate breadcrumb as the first thing as it generates the output + # filename as a side effect. It's a bit hairy because we need to figure out + # proper entry type for the URL formatter for each part of the breadcrumb. + breadcrumb = [] + filename: str + url: str + for i in range(len(path)): + type = state.name_map['.'.join(path[:i + 1])].type + filename, url = state.config['URL_FORMATTER'](type, path[:i + 1]) + breadcrumb += [(path[i], url)] + + logging.debug("generating %s", filename) # Call all registered page begin hooks for hook in state.hooks_pre_page: hook() - url_base = '' - breadcrumb = [] - for i in path: - url_base += i + '.' - breadcrumb += [(i, url_base + 'html')] - page = Empty() page.summary = extract_summary(state, state.class_docs, path, class_.__doc__) - page.url = breadcrumb[-1][1] + page.filename = filename + page.url = url page.breadcrumb = breadcrumb page.prefix_wbr = '.'.join(path + ['']) page.classes = [] @@ -1040,14 +1058,16 @@ def render_doc(state: State, filename): # discard the output afterwards. with open(filename, 'r') as f: publish_rst(state, f.read()) -def render_page(state: State, path, filename, env): - logging.debug("generating %s.html", '.'.join(path)) +def render_page(state: State, path, input_filename, env): + filename, url = state.config['URL_FORMATTER'](EntryType.PAGE, path) + + logging.debug("generating %s", filename) # Call all registered page begin hooks for hook in state.hooks_pre_page: hook() # Render the file - with open(filename, 'r') as f: pub = publish_rst(state, f.read(), source_path=filename) + with open(input_filename, 'r') as f: pub = publish_rst(state, f.read(), source_path=input_filename) # Extract metadata from the page metadata = {} @@ -1072,10 +1092,11 @@ def render_page(state: State, path, filename, env): # Breadcrumb, we don't do page hierarchy yet assert len(path) == 1 - breadcrumb = [(pub.writer.parts.get('title'), path[0] + '.html')] + breadcrumb = [(pub.writer.parts.get('title'), url)] page = Empty() - page.url = breadcrumb[-1][1] + page.filename = filename + page.url = url page.breadcrumb = breadcrumb page.prefix_wbr = path[0] @@ -1094,22 +1115,6 @@ def render_page(state: State, path, filename, env): render(state.config, 'page.html', page, env) def run(basedir, config, templates): - # Prepare Jinja environment - env = jinja2.Environment( - loader=jinja2.FileSystemLoader(templates), trim_blocks=True, - lstrip_blocks=True, enable_async=True) - # Filter to return file basename or the full URL, if absolute - def basename_or_url(path): - if urllib.parse.urlparse(path).netloc: return path - return os.path.basename(path) - # Filter to return URL for given symbol or the full URL, if absolute - def path_to_url(path): - if urllib.parse.urlparse(path).netloc: return path - return path + '.html' - env.filters['basename_or_url'] = basename_or_url - env.filters['path_to_url'] = path_to_url - env.filters['urljoin'] = urljoin - # Populate the INPUT, if not specified, make it absolute if config['INPUT'] is None: config['INPUT'] = basedir else: config['INPUT'] = os.path.join(basedir, config['INPUT']) @@ -1124,6 +1129,28 @@ def run(basedir, config, templates): state = State(config) + # Prepare Jinja environment + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(templates), trim_blocks=True, + lstrip_blocks=True, enable_async=True) + # Filter to return file basename or the full URL, if absolute + def basename_or_url(path): + if urllib.parse.urlparse(path).netloc: return path + return os.path.basename(path) + # Filter to return URL for given symbol. If the path is a string, first try + # to treat it as an URL. If that fails, turn it into a list and try to look + # it up in various dicts. + def path_to_url(path): + if isinstance(path, str): + if urllib.parse.urlparse(path).netloc: return path + path = [path] + entry = state.name_map['.'.join(path)] + return state.config['URL_FORMATTER'](entry.type, entry.path)[1] + + env.filters['basename_or_url'] = basename_or_url + env.filters['path_to_url'] = path_to_url + env.filters['urljoin'] = urljoin + # Set up extra plugin paths. The one for m.css plugins was added above. for path in config['PLUGIN_PATHS']: if path not in sys.path: sys.path.append(os.path.join(config['INPUT'], path)) @@ -1156,6 +1183,14 @@ def run(basedir, config, templates): crawl_module(state, [module_name], module) class_index += [module_name] + # Add special pages to the name map. The pages are done after so they can + # override these. + for page in special_pages: + entry = Empty() + entry.type = EntryType.SPECIAL + entry.path = [page] + state.name_map[page] = entry + # Do the same for pages # TODO: turn also into some crawl_page() function? once we have subpages? page_index = [] @@ -1205,7 +1240,7 @@ def run(basedir, config, templates): index_entry = Empty() index_entry.kind = 'module' if entry.type == EntryType.MODULE else 'class' index_entry.name = entry.path[-1] - index_entry.url = make_url(entry.path) + index_entry.url = state.config['URL_FORMATTER'](entry.type, entry.path)[1] index_entry.summary = entry.summary index_entry.has_nestable_children = False index_entry.children = [] @@ -1236,7 +1271,7 @@ def run(basedir, config, templates): index_entry = Empty() index_entry.kind = 'page' index_entry.name = entry.name - index_entry.url = make_url(entry.path) + index_entry.url = state.config['URL_FORMATTER'](entry.type, entry.path)[1] index_entry.summary = entry.summary index_entry.has_nestable_children = False index_entry.children = [] @@ -1246,10 +1281,11 @@ def run(basedir, config, templates): index = Empty() index.classes = class_index index.pages = page_index - for file in ['modules.html', 'classes.html', 'pages.html']: - template = env.get_template(file) - rendered = template.render(index=index, FILENAME=file, **config) - with open(os.path.join(config['OUTPUT'], file), 'wb') as f: + for file in special_pages[1:]: # exclude index + template = env.get_template(file + '.html') + filename, url = state.config['URL_FORMATTER'](EntryType.SPECIAL, [file]) + rendered = template.render(index=index, URL=url, **config) + with open(os.path.join(config['OUTPUT'], filename), 'wb') as f: f.write(rendered.encode('utf-8')) # Add back a trailing newline so we don't need to bother with # patching test files to include a trailing newline to make Git @@ -1261,9 +1297,12 @@ def run(basedir, config, templates): if 'index.rst' not in [os.path.basename(i) for i in config['INPUT_PAGES']]: logging.debug("writing index.html for an empty main page") + filename, url = state.config['URL_FORMATTER'](EntryType.SPECIAL, ['index']) + page = Empty() - page.breadcrumb = [(config['PROJECT_TITLE'], 'index.html')] - page.url = page.breadcrumb[-1][1] + page.filename = filename + page.url = url + page.breadcrumb = [(config['PROJECT_TITLE'], url)] render(config, 'page.html', page, env) # Copy referenced files diff --git a/documentation/templates/python/base.html b/documentation/templates/python/base.html index aaa3197f..84f37387 100644 --- a/documentation/templates/python/base.html +++ b/documentation/templates/python/base.html @@ -28,10 +28,10 @@
{% if MAIN_PROJECT_URL and PROJECT_TITLE %} - {{ PROJECT_TITLE }} | {{ PROJECT_SUBTITLE }} + {{ PROJECT_TITLE }} | {{ PROJECT_SUBTITLE }} {% else %} - {{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} {{ PROJECT_SUBTITLE }}{% endif %} + {{ PROJECT_TITLE }}{% if PROJECT_SUBTITLE %} {{ PROJECT_SUBTITLE }}{% endif %} {% endif %} {% if LINKS_NAVBAR1 or LINKS_NAVBAR2 or not SEARCH_DISABLED %}
@@ -96,7 +96,7 @@
{% if PAGE_HEADER %} - {{ PAGE_HEADER|render_rst|replace('{filename}', FILENAME) }} + {{ PAGE_HEADER|render_rst|replace('{url}', URL) }} {% endif %} {% block main %} {% endblock %} diff --git a/documentation/test_python/layout/about.rst b/documentation/test_python/layout/about.rst new file mode 100644 index 00000000..fed2083e --- /dev/null +++ b/documentation/test_python/layout/about.rst @@ -0,0 +1 @@ +.. this is here only to make the LINKS_NAVBAR work diff --git a/documentation/test_python/layout/getting-started.rst b/documentation/test_python/layout/getting-started.rst new file mode 100644 index 00000000..fed2083e --- /dev/null +++ b/documentation/test_python/layout/getting-started.rst @@ -0,0 +1 @@ +.. this is here only to make the LINKS_NAVBAR work diff --git a/documentation/test_python/layout/troubleshooting.rst b/documentation/test_python/layout/troubleshooting.rst new file mode 100644 index 00000000..fed2083e --- /dev/null +++ b/documentation/test_python/layout/troubleshooting.rst @@ -0,0 +1 @@ +.. this is here only to make the LINKS_NAVBAR work diff --git a/documentation/test_python/link_formatting/c.link_formatting.Class.Sub.html b/documentation/test_python/link_formatting/c.link_formatting.Class.Sub.html new file mode 100644 index 00000000..08334192 --- /dev/null +++ b/documentation/test_python/link_formatting/c.link_formatting.Class.Sub.html @@ -0,0 +1,49 @@ + + + + + link_formatting.Class.Sub | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/link_formatting/c.link_formatting.Class.html b/documentation/test_python/link_formatting/c.link_formatting.Class.html new file mode 100644 index 00000000..b54f5c28 --- /dev/null +++ b/documentation/test_python/link_formatting/c.link_formatting.Class.html @@ -0,0 +1,67 @@ + + + + + link_formatting.Class | My Python Project + + + + + +
+
+
+
+
+

+ link_formatting.Class class +

+

This is a nice class.

+
+

Contents

+ +
+
+

Classes

+
+
class Sub
+
And a nice subclass, oh.
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/link_formatting/link_formatting/__init__.py b/documentation/test_python/link_formatting/link_formatting/__init__.py new file mode 100644 index 00000000..5ccfb30b --- /dev/null +++ b/documentation/test_python/link_formatting/link_formatting/__init__.py @@ -0,0 +1,9 @@ +"""This is a module.""" + +from . import sub + +class Class: + """This is a nice class.""" + + class Sub: + """And a nice subclass, oh.""" diff --git a/documentation/test_python/link_formatting/link_formatting/sub.py b/documentation/test_python/link_formatting/link_formatting/sub.py new file mode 100644 index 00000000..0b4339b6 --- /dev/null +++ b/documentation/test_python/link_formatting/link_formatting/sub.py @@ -0,0 +1 @@ +"""This is a nice submodule.""" diff --git a/documentation/test_python/link_formatting/m.link_formatting.html b/documentation/test_python/link_formatting/m.link_formatting.html new file mode 100644 index 00000000..cb18ca8b --- /dev/null +++ b/documentation/test_python/link_formatting/m.link_formatting.html @@ -0,0 +1,75 @@ + + + + + link_formatting | My Python Project + + + + + +
+
+
+
+
+

+ link_formatting module +

+

This is a module.

+
+

Contents

+ +
+
+

Modules

+
+
module sub
+
This is a nice submodule.
+
+
+
+

Classes

+
+
class Class
+
This is a nice class.
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/link_formatting/m.link_formatting.sub.html b/documentation/test_python/link_formatting/m.link_formatting.sub.html new file mode 100644 index 00000000..9dcb9209 --- /dev/null +++ b/documentation/test_python/link_formatting/m.link_formatting.sub.html @@ -0,0 +1,49 @@ + + + + + link_formatting.sub | My Python Project + + + + + +
+
+
+
+
+

+ link_formatting.sub module +

+

This is a nice submodule.

+
+
+
+
+ + diff --git a/documentation/test_python/link_formatting/page.rst b/documentation/test_python/link_formatting/page.rst new file mode 100644 index 00000000..aae94066 --- /dev/null +++ b/documentation/test_python/link_formatting/page.rst @@ -0,0 +1,2 @@ +This is a page +############## diff --git a/documentation/test_python/link_formatting/s.classes.html b/documentation/test_python/link_formatting/s.classes.html new file mode 100644 index 00000000..7c6598b0 --- /dev/null +++ b/documentation/test_python/link_formatting/s.classes.html @@ -0,0 +1,76 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Classes

+ + +
+
+
+
+ + diff --git a/documentation/test_python/link_formatting/s.modules.html b/documentation/test_python/link_formatting/s.modules.html new file mode 100644 index 00000000..731ae5d6 --- /dev/null +++ b/documentation/test_python/link_formatting/s.modules.html @@ -0,0 +1,70 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Modules

+ + +
+
+
+
+ + diff --git a/documentation/test_python/link_formatting/s.pages.html b/documentation/test_python/link_formatting/s.pages.html new file mode 100644 index 00000000..408243fc --- /dev/null +++ b/documentation/test_python/link_formatting/s.pages.html @@ -0,0 +1,65 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Pages

+ + +
+
+
+
+ + diff --git a/documentation/test_python/test_layout.py b/documentation/test_python/test_layout.py index e74c9508..0dedeb4f 100644 --- a/documentation/test_python/test_layout.py +++ b/documentation/test_python/test_layout.py @@ -37,8 +37,9 @@ class Layout(BaseTestCase): 'THEME_COLOR': '#00ffff', 'FAVICON': 'favicon-light.png', - 'PAGE_HEADER': "`A self link <{filename}>`_", + 'PAGE_HEADER': "`A self link <{url}>`_", 'FINE_PRINT': "This beautiful thing is done thanks to\n`m.css `_.", + 'INPUT_PAGES': ['getting-started.rst', 'troubleshooting.rst', 'about.rst'], 'LINKS_NAVBAR1': [ ('Pages', 'pages', [ ('Getting started', 'getting-started'), diff --git a/documentation/test_python/test_link_formatting.py b/documentation/test_python/test_link_formatting.py new file mode 100644 index 00000000..093b6318 --- /dev/null +++ b/documentation/test_python/test_link_formatting.py @@ -0,0 +1,77 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import os +import unittest + +from typing import List + +from . import BaseInspectTestCase +from python import EntryType + +def custom_url_formatter(type: EntryType, path: List[str]) -> str: + if type == EntryType.CLASS: + filename = 'c.' + '.'.join(path) + '.html' + elif type == EntryType.MODULE: + filename = 'm.' + '.'.join(path) + '.html' + elif type == EntryType.PAGE: + filename = 'p.' + '.'.join(path) + '.html' + elif type == EntryType.SPECIAL: + filename = 's.' + '.'.join(path) + '.html' + else: assert False + + return filename, filename + "#this-is-an-url" + +class Test(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, '', *args, **kwargs) + + def test(self): + self.run_python({ + 'INPUT_PAGES': ['page.rst'], + 'URL_FORMATTER': custom_url_formatter, + 'LINKS_NAVBAR1': [ + ('Pages', 'pages', []), + ('Modules', 'modules', []), + ('Classes', 'classes', [])], + 'LINKS_NAVBAR2': [('A page', 'page', []), + ('A module', 'link_formatting', []), + ('The class', ['link_formatting', 'Class'], [])] + }) + self.assertEqual(*self.actual_expected_contents('m.link_formatting.html')) + self.assertEqual(*self.actual_expected_contents('m.link_formatting.sub.html')) + self.assertEqual(*self.actual_expected_contents('c.link_formatting.Class.html')) + self.assertEqual(*self.actual_expected_contents('c.link_formatting.Class.Sub.html')) + + # There's nothing inside p.page.html that wouldn't be already covered + # by others + self.assertTrue(os.path.exists(os.path.join(self.path, 'output/p.page.html'))) + + self.assertEqual(*self.actual_expected_contents('s.classes.html')) + self.assertEqual(*self.actual_expected_contents('s.modules.html')) + self.assertEqual(*self.actual_expected_contents('s.pages.html')) + + # There's nothing inside s.index.html that wouldn't be already covered + # by others + self.assertTrue(os.path.exists(os.path.join(self.path, 'output/s.index.html')))