diff --git a/.all-contributorsrc b/.all-contributorsrc
index 31755537..54751c1d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -181,6 +181,15 @@
"contributions": [
"infra"
]
+ },
+ {
+ "login": "mateuszkasprowicz",
+ "name": "Mateusz Kasprowicz",
+ "avatar_url": "https://avatars.githubusercontent.com/mateuszkasprowicz",
+ "profile": "https://github.com/mateuszkasprowicz",
+ "contributions": [
+ "code"
+ ]
}
],
"projectName": "skbase",
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 003aefcc..c34bc94e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -70,7 +70,7 @@ repos:
additional_dependencies: [black>=22.3.0]
- id: nbqa-flake8
args: [--nbqa-dont-skip-bad-cells, "--extend-ignore=E402,E203"]
- additional_dependencies: [flake8==3.8.3]
+ additional_dependencies: [flake8==7.3.0]
- repo: https://github.com/PyCQA/bandit
rev: 1.9.4
diff --git a/pyproject.toml b/pyproject.toml
index d18d3a08..ef29e9d8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -144,6 +144,10 @@ exclude_dirs = ["*/tests/*", "*/testing/*"]
zip-safe = true
[tool.setuptools.package-data]
+skbase = [
+ "base/_pretty_printing/*.css",
+ "base/_pretty_printing/*.js",
+]
sktime = [
"*.csv",
"*.csv.gz",
diff --git a/skbase/base/_meta.py b/skbase/base/_meta.py
index cb3021c5..9209d751 100644
--- a/skbase/base/_meta.py
+++ b/skbase/base/_meta.py
@@ -39,7 +39,7 @@ class has values that follow the named object specification. For example,
# _steps_attr points to the attribute of self
# which contains the heterogeneous set of estimators
# this must be an iterable of (name: str, estimator) pairs for the default
- _tags = {"named_object_parameters": "steps"}
+ _tags = {"named_object_parameters": "steps", "visual_block_kind": "serial"}
def is_composite(self):
"""Check if the object is composite.
@@ -776,7 +776,7 @@ def _get_name(name, obj):
names = [_get_name(name, est) for name, est in named_objects]
name_details = [str(obj) for obj in objs]
return _VisualBlock(
- "serial",
+ self.get_tag("visual_block_kind", tag_value_default="serial"),
objs,
names=names,
name_details=name_details,
diff --git a/skbase/base/_pretty_printing/_object_html_repr.css b/skbase/base/_pretty_printing/_object_html_repr.css
new file mode 100644
index 00000000..58f479a1
--- /dev/null
+++ b/skbase/base/_pretty_printing/_object_html_repr.css
@@ -0,0 +1,430 @@
+#$id {
+ --skbase-color-text: #111;
+ --skbase-color-text-muted: #666;
+ --skbase-color-line: #777;
+ --skbase-color-background: #fff;
+ --skbase-color-border-box: #111;
+ --skbase-color-icon: #696969;
+ --skbase-color-table-row-even: rgba(15, 23, 42, 0.04);
+ --skbase-color-table-row-hover: rgba(37, 99, 235, 0.14);
+ --skbase-color-param-user-set: #0f5ed7;
+ --skbase-color-param-icon: #334155;
+ --skbase-color-level-0: #f0f8ff;
+ --skbase-color-level-1: #d4ebff;
+ --skbase-color-level-2: #b3dbfd;
+ --skbase-color-level-3: #2563eb;
+ --skbase-color-fitted-level-0: #eff6ff;
+ --skbase-color-fitted-level-1: #dbeafe;
+ --skbase-color-fitted-level-2: #bfdbfe;
+ --skbase-color-fitted-level-3: #2563eb;
+}
+
+#$id.dark {
+ --skbase-color-text: #f5f5f5;
+ --skbase-color-text-muted: #bbb;
+ --skbase-color-background: #111;
+ --skbase-color-border-box: #f5f5f5;
+ --skbase-color-icon: #aaa;
+ --skbase-color-table-row-even: rgba(255, 255, 255, 0.08);
+ --skbase-color-table-row-hover: rgba(147, 197, 253, 0.20);
+ --skbase-color-param-user-set: #93c5fd;
+ --skbase-color-param-icon: #dbeafe;
+ --skbase-color-level-0: #0f172a;
+ --skbase-color-level-1: #172554;
+ --skbase-color-level-2: #1e3a8a;
+ --skbase-color-level-3: #93c5fd;
+ --skbase-color-fitted-level-0: #0f172a;
+ --skbase-color-fitted-level-1: #172554;
+ --skbase-color-fitted-level-2: #1e3a8a;
+ --skbase-color-fitted-level-3: #93c5fd;
+}
+
+#$id {
+ color: var(--skbase-color-text);
+}
+
+#$id pre {
+ padding: 0;
+}
+
+#$id input.sk-hidden--visually {
+ border: 0;
+ clip: rect(1px 1px 1px 1px);
+ clip: rect(1px, 1px, 1px, 1px);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+}
+
+#$id div.sk-container {
+ display: inline-block !important;
+ position: relative;
+}
+
+#$id div.sk-text-repr-fallback {
+ display: none;
+}
+
+#$id div.sk-item,
+#$id div.sk-serial,
+#$id div.sk-parallel-item {
+ background-image: linear-gradient(var(--skbase-color-line), var(--skbase-color-line));
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 2px 100%;
+}
+
+#$id div.sk-item {
+ z-index: 1;
+}
+
+#$id div.sk-dashed-wrapped {
+ background-color: var(--skbase-color-background);
+ border: 1px dashed var(--skbase-color-line);
+ box-sizing: border-box;
+ margin: 0 0.4em 0.5em 0.4em;
+ padding-bottom: 0.4em;
+}
+
+#$id div.sk-serial {
+ align-items: center;
+ background-color: var(--skbase-color-background);
+ display: flex;
+ flex-direction: column;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+#$id div.sk-parallel {
+ align-items: stretch;
+ background-color: var(--skbase-color-background);
+ display: flex;
+ justify-content: center;
+ position: relative;
+}
+
+#$id div.sk-parallel-item {
+ display: flex;
+ flex-direction: column;
+}
+
+#$id div.sk-parallel-item::after {
+ border-bottom: 2px solid var(--skbase-color-line);
+ content: "";
+ flex-grow: 1;
+ width: 100%;
+}
+
+#$id div.sk-parallel-item:first-child::after {
+ align-self: flex-end;
+ width: 50%;
+}
+
+#$id div.sk-parallel-item:last-child::after {
+ align-self: flex-start;
+ width: 50%;
+}
+
+#$id div.sk-parallel-item:only-child::after {
+ width: 0;
+}
+
+#$id div.sk-toggleable {
+ background-color: var(--skbase-color-background);
+}
+
+#$id label.sk-toggleable__label {
+ align-items: center;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: flex;
+ gap: 0.5em;
+ justify-content: center;
+ margin-bottom: 0;
+ padding: 0.5em;
+ text-align: center;
+ width: 100%;
+}
+
+#$id label.sk-toggleable__label .caption {
+ color: var(--skbase-color-text-muted);
+ font-size: 0.6rem;
+ font-weight: lighter;
+}
+
+#$id label.sk-toggleable__label-arrow::before {
+ color: var(--skbase-color-icon);
+ content: "▸";
+ float: left;
+ margin-right: 0.25em;
+}
+
+#$id label.sk-toggleable__label-arrow:hover::before {
+ color: var(--skbase-color-text);
+}
+
+#$id div.sk-toggleable__content {
+ background-color: var(--skbase-color-level-0);
+ display: none;
+ text-align: left;
+}
+
+#$id div.sk-toggleable__content.fitted {
+ background-color: var(--skbase-color-fitted-level-0);
+}
+
+#$id div.sk-toggleable__content pre {
+ background-color: var(--skbase-color-level-0);
+ border-radius: 0.25em;
+ color: var(--skbase-color-text);
+ margin: 0.2em;
+}
+
+#$id div.sk-toggleable__content.fitted pre {
+ background-color: var(--skbase-color-fitted-level-0);
+}
+
+#$id input.sk-toggleable__control:checked ~ div.sk-toggleable__content {
+ display: block;
+ overflow: visible;
+ width: 100%;
+}
+
+#$id input.sk-toggleable__control:checked ~ label.sk-toggleable__label-arrow::before {
+ content: "▾";
+}
+
+#$id div.sk-label input.sk-toggleable__control:checked ~ label.sk-toggleable__label,
+#$id div.sk-estimator input.sk-toggleable__control:checked ~ label.sk-toggleable__label,
+#$id div.sk-label:hover label.sk-toggleable__label,
+#$id div.sk-estimator:hover {
+ background-color: var(--skbase-color-level-2);
+ color: var(--skbase-color-text);
+}
+
+#$id div.sk-label.fitted input.sk-toggleable__control:checked ~ label.sk-toggleable__label,
+#$id div.sk-estimator.fitted input.sk-toggleable__control:checked ~ label.sk-toggleable__label,
+#$id div.sk-label.fitted:hover label.sk-toggleable__label.fitted,
+#$id div.sk-estimator.fitted:hover {
+ background-color: var(--skbase-color-fitted-level-2);
+}
+
+#$id div.sk-label label {
+ color: var(--skbase-color-text);
+ display: inline-block;
+ font-family: monospace;
+ font-weight: bold;
+ line-height: 1.2em;
+}
+
+#$id div.sk-label-container {
+ text-align: center;
+}
+
+#$id div.sk-estimator {
+ background-color: var(--skbase-color-level-0);
+ border: 1px dotted var(--skbase-color-border-box);
+ border-radius: 0.25em;
+ box-sizing: border-box;
+ font-family: monospace;
+ margin-bottom: 0.5em;
+}
+
+#$id div.sk-estimator.fitted {
+ background-color: var(--skbase-color-fitted-level-0);
+}
+
+#$id .sk-estimator-doc-link,
+#$id a:link.sk-estimator-doc-link,
+#$id a:visited.sk-estimator-doc-link {
+ background-color: var(--skbase-color-level-0);
+ border: var(--skbase-color-level-3) 1pt solid;
+ border-radius: 1em;
+ color: var(--skbase-color-level-3);
+ float: right;
+ font-family: monospace;
+ font-size: smaller;
+ height: 1em;
+ line-height: 1em;
+ margin-left: 0.5em;
+ text-align: center;
+ text-decoration: none !important;
+ width: 1em;
+}
+
+#$id .sk-estimator-doc-link.fitted,
+#$id a:link.sk-estimator-doc-link.fitted,
+#$id a:visited.sk-estimator-doc-link.fitted {
+ background-color: var(--skbase-color-fitted-level-0);
+ border: var(--skbase-color-fitted-level-3) 1pt solid;
+ color: var(--skbase-color-fitted-level-3);
+}
+
+#$id .sk-estimator-doc-link:hover {
+ background-color: var(--skbase-color-level-3);
+ border-color: var(--skbase-color-level-0);
+ color: var(--skbase-color-level-0);
+ text-decoration: none;
+}
+
+#$id .sk-estimator-doc-link.fitted:hover {
+ background-color: var(--skbase-color-fitted-level-3);
+ border-color: var(--skbase-color-fitted-level-0);
+ color: var(--skbase-color-fitted-level-0);
+}
+
+#$id .sk-estimator-doc-link span {
+ background: var(--skbase-color-level-0);
+ border: 0.5pt solid var(--skbase-color-level-3);
+ box-shadow: 2pt 2pt 4pt #999;
+ color: var(--skbase-color-text);
+ display: none;
+ font-weight: normal;
+ margin: 0.5ex;
+ max-width: 50ex;
+ min-width: 20ex;
+ padding: 0.5ex;
+ position: relative;
+ right: 0.2ex;
+ width: min-content;
+ z-index: 9999;
+}
+
+#$id .sk-estimator-doc-link.fitted span {
+ background: var(--skbase-color-fitted-level-0);
+ border-color: var(--skbase-color-fitted-level-3);
+}
+
+#$id .sk-estimator-doc-link:hover span {
+ display: block;
+}
+
+#$id .estimator-table {
+ font-family: monospace;
+}
+
+#$id .estimator-table summary {
+ cursor: pointer;
+ padding: 0.5rem;
+}
+
+#$id .estimator-table summary::marker {
+ font-size: 0.7rem;
+}
+
+#$id .estimator-table details[open] {
+ padding: 0 0.1rem 0.3rem 0.1rem;
+}
+
+#$id .estimator-table .parameters-table {
+ border-collapse: collapse;
+ margin-left: auto !important;
+ margin-right: auto !important;
+ margin-top: 0;
+ width: 100%;
+}
+
+#$id .estimator-table .parameters-table tr:nth-child(odd) {
+ background-color: var(--skbase-color-background);
+}
+
+#$id .estimator-table .parameters-table tr:nth-child(even) {
+ background-color: var(--skbase-color-table-row-even);
+}
+
+#$id .estimator-table .parameters-table tr:hover {
+ background-color: var(--skbase-color-table-row-hover);
+}
+
+#$id .estimator-table table td {
+ border: 1px solid rgba(106, 105, 104, 0.23);
+}
+
+#$id .estimator-table table td.param {
+ padding: 0;
+ position: relative;
+ text-align: left;
+}
+
+#$id .estimator-table table td.value {
+ max-width: 32em;
+ overflow-wrap: anywhere;
+}
+
+#$id .user-set td,
+#$id .user-set td.value {
+ color: var(--skbase-color-param-user-set);
+ text-align: left !important;
+}
+
+#$id .default td {
+ color: var(--skbase-color-text);
+ text-align: left !important;
+}
+
+#$id .user-set td i,
+#$id .default td i {
+ color: var(--skbase-color-param-icon);
+}
+
+#$id a.param-doc-link,
+#$id a.param-doc-link:link,
+#$id a.param-doc-link:visited {
+ color: inherit;
+ display: block;
+ padding: 0.5em;
+ text-decoration: underline dashed;
+ text-underline-offset: 0.3em;
+}
+
+#$id a.param-doc-link::before {
+ content: "";
+ inset: 0;
+ position: absolute;
+}
+
+#$id .param-doc-description {
+ background: var(--skbase-color-level-0);
+ border: thin solid var(--skbase-color-level-3);
+ box-shadow: 0.3em 0.3em 0.4em #999;
+ color: var(--skbase-color-text);
+ display: none;
+ left: 0;
+ margin-left: 1.5em;
+ max-height: 10em;
+ max-width: 36em;
+ overflow-y: auto;
+ padding: 0.5ex;
+ position: absolute;
+ text-align: left;
+ width: max-content;
+ z-index: 9999;
+}
+
+#$id .fitted .param-doc-description {
+ background: var(--skbase-color-fitted-level-0);
+ border-color: var(--skbase-color-fitted-level-3);
+}
+
+#$id .param-doc-link:hover .param-doc-description {
+ display: block;
+}
+
+#$id .copy-paste-icon {
+ background-color: currentColor;
+ cursor: pointer;
+ display: inline-block;
+ height: 14px;
+ mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48cGF0aCBkPSJNMjA4IDBMMzMyLjEgMGMxMi43IDAgMjQuOSA1LjEgMzMuOSAxNC4xbDY3LjkgNjcuOWM5IDkgMTQuMSAyMS4yIDE0LjEgMzMuOUw0NDggMzM2YzAgMjYuNS0yMS41IDQ4LTQ4IDQ4bC0xOTIgMGMtMjYuNSAwLTQ4LTIxLjUtNDgtNDhsMC0yODhjMC0yNi41IDIxLjUtNDggNDgtNDh6TTQ4IDEyOGw4MCAwIDAgNjQtNjQgMCAwIDI1NiAxOTIgMCAwLTMyIDY0IDAgMCA0OGMwIDI2LjUtMjEuNSA0OC00OCA0OEw0OCA1MTJjLTI2LjUgMC00OC0yMS41LTQ4LTQ4TDAgMTc2YzAtMjYuNSAyMS41LTQ4IDQ4LTQ4eiIvPjwvc3ZnPg==);
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-size: 14px 14px;
+ -webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48cGF0aCBkPSJNMjA4IDBMMzMyLjEgMGMxMi43IDAgMjQuOSA1LjEgMzMuOSAxNC4xbDY3LjkgNjcuOWM5IDkgMTQuMSAyMS4yIDE0LjEgMzMuOUw0NDggMzM2YzAgMjYuNS0yMS41IDQ4LTQ4IDQ4bC0xOTIgMGMtMjYuNSAwLTQ4LTIxLjUtNDgtNDhsMC0yODhjMC0yNi41IDIxLjUtNDggNDgtNDh6TTQ4IDEyOGw4MCAwIDAgNjQtNjQgMCAwIDI1NiAxOTIgMCAwLTMyIDY0IDAgMCA0OGMwIDI2LjUtMjEuNSA0OC00OCA0OEw0OCA1MTJjLTI2LjUgMC00OC0yMS41LTQ4LTQ4TDAgMTc2YzAtMjYuNSAyMS41LTQ4IDQ4LTQ4eiIvPjwvc3ZnPg==);
+ -webkit-mask-position: center;
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-size: 14px 14px;
+ width: 14px;
+}
diff --git a/skbase/base/_pretty_printing/_object_html_repr.js b/skbase/base/_pretty_printing/_object_html_repr.js
new file mode 100644
index 00000000..66d5f0b6
--- /dev/null
+++ b/skbase/base/_pretty_printing/_object_html_repr.js
@@ -0,0 +1,105 @@
+function skbaseCopyToClipboard(text, element) {
+ const toggleableContent = element.closest('.sk-toggleable__content');
+ const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';
+ const fullParamName = paramPrefix ? `${paramPrefix}${text}` : text;
+ const originalHTML = element.innerHTML.replace('Copied!', '').replace('Failed!', '');
+ const originalWidth = window.getComputedStyle(element).width;
+ const restore = () => {
+ element.innerHTML = originalHTML;
+ element.style.width = '';
+ element.style.color = '';
+ };
+ const markCopied = () => {
+ element.style.width = originalWidth;
+ element.style.color = 'green';
+ element.innerHTML = 'Copied!';
+ setTimeout(restore, 2000);
+ };
+ const markFailed = () => {
+ element.style.color = 'red';
+ element.innerHTML = 'Failed!';
+ setTimeout(restore, 2000);
+ };
+
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(fullParamName).then(markCopied).catch(markFailed);
+ } else {
+ const textArea = document.createElement('textarea');
+ textArea.value = fullParamName;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-9999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy') ? markCopied() : markFailed();
+ } catch (error) {
+ markFailed();
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ }
+ return false;
+}
+
+function skbaseDetectTheme(element) {
+ const body = document.querySelector('body');
+
+ if (body !== null) {
+ const themeKindAttr = body.getAttribute('data-vscode-theme-kind');
+ const themeNameAttr = body.getAttribute('data-vscode-theme-name');
+
+ if (themeKindAttr && themeNameAttr) {
+ const themeKind = themeKindAttr.toLowerCase();
+ const themeName = themeNameAttr.toLowerCase();
+
+ if (themeKind.includes('dark') || themeName.includes('dark')) {
+ return 'dark';
+ }
+ if (themeKind.includes('light') || themeName.includes('light')) {
+ return 'light';
+ }
+ }
+
+ if (body.getAttribute('data-jp-theme-light') === 'false') {
+ return 'dark';
+ }
+ if (body.getAttribute('data-jp-theme-light') === 'true') {
+ return 'light';
+ }
+ }
+
+ const color = window.getComputedStyle(element.parentNode, null).getPropertyValue('color');
+ const match = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/i);
+ if (match) {
+ const red = parseFloat(match[1]);
+ const green = parseFloat(match[2]);
+ const blue = parseFloat(match[3]);
+ const luma = 0.299 * red + 0.587 * green + 0.114 * blue;
+
+ if (luma > 180) {
+ return 'dark';
+ }
+ if (luma < 75) {
+ return 'light';
+ }
+ }
+
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+}
+
+function skbaseForceTheme(elementId) {
+ const estimatorElement = document.getElementById(elementId);
+ if (estimatorElement !== null) {
+ estimatorElement.classList.add(skbaseDetectTheme(estimatorElement));
+ }
+
+ document.querySelectorAll(`#${elementId} .copy-paste-icon`).forEach(function (element) {
+ const toggleableContent = element.closest('.sk-toggleable__content');
+ const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';
+ const paramCell = element.parentElement.nextElementSibling;
+ const paramName = paramCell ? paramCell.textContent.trim().split(' ')[0] : '';
+ const fullParamName = paramPrefix ? `${paramPrefix}${paramName}` : paramName;
+ element.setAttribute('title', fullParamName);
+ });
+}
diff --git a/skbase/base/_pretty_printing/_object_html_repr.py b/skbase/base/_pretty_printing/_object_html_repr.py
index bb5448fe..6806eb18 100644
--- a/skbase/base/_pretty_printing/_object_html_repr.py
+++ b/skbase/base/_pretty_printing/_object_html_repr.py
@@ -3,18 +3,50 @@
# Many elements of this code were developed in scikit-learn. These elements
# are copyrighted by the scikit-learn developers, BSD-3-Clause License. For
# conditions see https://github.com/scikit-learn/scikit-learn/blob/main/COPYING
-"""Functionality to represent instance of BaseObject as html."""
+"""Functionality to represent instances of BaseObject as HTML."""
import html
+import inspect
+import re
+import reprlib
import uuid
from contextlib import closing
+from functools import lru_cache
+from importlib import resources
from inspect import isclass
from io import StringIO
from string import Template
+from urllib.parse import quote
__author__ = ["RNKuhns"]
+class _HTMLDocumentationLinkMixin:
+ """Mixin for generating API documentation links in object HTML diagrams.
+
+ Classes can opt in by inheriting from this mixin or by defining compatible
+ class/instance attributes:
+
+ - ``_doc_link_module``: root module that is allowed to receive doc links.
+ Defaults to ``"skbase"``. Set to ``None`` to allow any root module.
+ - ``_doc_link_template``: format string used to build the link. The default
+ is a generic skbase ReadTheDocs API reference URL.
+ - ``_doc_link_url_param_generator``: optional callable returning a dict of
+ additional format parameters for custom templates.
+ """
+
+ _doc_link_module = "skbase"
+ _doc_link_template = (
+ "https://skbase.readthedocs.io/en/latest/api_reference/auto_generated/"
+ "{object_module}.{object_name}.html"
+ )
+ _doc_link_url_param_generator = None
+
+ def _get_doc_link(self):
+ """Generate a documentation URL for this object, or ``""`` if disabled."""
+ return _get_doc_link(self)
+
+
class _VisualBlock:
"""HTML Representation of BaseObject.
@@ -23,7 +55,7 @@ class _VisualBlock:
kind : {'serial', 'parallel', 'single'}
kind of HTML block
- estimators : list of ``BaseObject``s or ``_VisualBlock`s or a single ``BaseObject``
+ estimators : list of ``BaseObject``s or ``_VisualBlock``s or a single ``BaseObject``
If ``kind != 'single'``, then ``estimators`` is a list of ``BaseObjects``.
If ``kind == 'single'``, then ``estimators`` is a single ``BaseObject``.
@@ -37,17 +69,36 @@ class _VisualBlock:
If ``kind == 'single'``, then ``name_details`` is a single string
corresponding to the single ``BaseObject``.
+ name_caption : str, default=None
+ The caption below the name. ``None`` stands for no caption.
+ Only active when ``kind == 'single'``.
+
+ doc_link_label : str, default=None
+ The label for the documentation link. If provided, the label is
+ "Documentation for {doc_link_label}". Otherwise it uses ``names``.
+ Only active when ``kind == 'single'``.
+
dash_wrapped : bool, default=True
If true, wrapped HTML element will be wrapped with a dashed border.
Only active when ``kind != 'single'``.
"""
def __init__(
- self, kind, estimators, *, names=None, name_details=None, dash_wrapped=True
+ self,
+ kind,
+ estimators,
+ *,
+ names=None,
+ name_details=None,
+ name_caption=None,
+ doc_link_label=None,
+ dash_wrapped=True,
):
self.kind = kind
self.estimators = estimators
self.dash_wrapped = dash_wrapped
+ self.name_caption = name_caption
+ self.doc_link_label = doc_link_label
if self.kind in ("parallel", "serial"):
if names is None:
@@ -62,31 +113,324 @@ def _sk_visual_block_(self):
return self
+@lru_cache
+def _get_resource(name):
+ """Read a packaged pretty-printing resource."""
+ return (
+ resources.files("skbase.base._pretty_printing")
+ .joinpath(name)
+ .read_text(encoding="utf-8")
+ )
+
+
+def _get_css_style():
+ """Return CSS used by the HTML representation."""
+ return _get_resource("_object_html_repr.css")
+
+
+def _get_js():
+ """Return JavaScript used by the HTML representation."""
+ return _get_resource("_object_html_repr.js")
+
+
+@lru_cache
+def _get_param_doc_descriptions(docstring):
+ """Parse a small subset of NumPy-style parameter docs.
+
+ The parser intentionally stays lightweight to avoid a hard numpydoc
+ dependency in skbase. It extracts parameter type lines and indented
+ description text from a ``Parameters`` section.
+ """
+ if not docstring:
+ return {}
+
+ lines = inspect.cleandoc(docstring).splitlines()
+ in_parameters = False
+ params = {}
+ current = None
+
+ for idx, line in enumerate(lines):
+ stripped = line.strip()
+ next_line = lines[idx + 1].strip() if idx + 1 < len(lines) else ""
+
+ if not in_parameters:
+ if stripped == "Parameters" and set(next_line) <= {"-"} and next_line:
+ in_parameters = True
+ continue
+
+ if stripped and set(stripped) <= {"-"}:
+ continue
+
+ # A non-indented heading followed by an underline starts the next section.
+ if line == stripped and stripped and set(next_line) <= {"-"} and next_line:
+ break
+
+ match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$", stripped)
+ if match:
+ current = match.group(1)
+ params[current] = {"type": match.group(2), "desc": []}
+ continue
+
+ if current is not None:
+ if not stripped:
+ continue
+ params[current]["desc"].append(stripped)
+
+ return params
+
+
+def _generate_link_to_param_doc(object_class, param_name, doc_link):
+ """Generate a text-fragment URL to a parameter docstring entry."""
+ docstring = inspect.getdoc(object_class)
+ param_map = _get_param_doc_descriptions(docstring)
+ param_doc = param_map.get(param_name)
+
+ if param_doc is None:
+ return None
+
+ text_fragment = f"{quote(param_name)},-{quote(param_doc['type'])}"
+ return f"{doc_link}#:~:text={text_fragment}"
+
+
+def _get_doc_link(base_object):
+ """Return the configured documentation link for ``base_object``."""
+ if base_object is None or isinstance(base_object, str) or isclass(base_object):
+ return ""
+
+ direct_link = getattr(base_object, "_html_repr_doc_link", None)
+ if direct_link:
+ return direct_link
+
+ get_doc_link = getattr(base_object, "_get_doc_link", None)
+ mixin_get_doc_link = _HTMLDocumentationLinkMixin._get_doc_link
+ is_mixin_doc_link = getattr(get_doc_link, "__func__", None) is mixin_get_doc_link
+ if get_doc_link is not None and not is_mixin_doc_link:
+ try:
+ return get_doc_link()
+ except Exception:
+ return ""
+
+ template = getattr(base_object, "_doc_link_template", None)
+ if not template:
+ return ""
+
+ module_name = base_object.__class__.__module__
+ root_module = module_name.split(".")[0]
+ doc_link_module = getattr(base_object, "_doc_link_module", None)
+ if doc_link_module is not None and root_module != doc_link_module:
+ return ""
+
+ generator = getattr(base_object, "_doc_link_url_param_generator", None)
+ if generator is None:
+ object_name = base_object.__class__.__name__
+ params = {
+ "object_module": module_name,
+ "object_name": object_name,
+ "estimator_module": module_name,
+ "estimator_name": object_name,
+ }
+ else:
+ try:
+ params = generator()
+ except Exception:
+ return ""
+
+ try:
+ return template.format(**params)
+ except Exception:
+ return ""
+
+
+def _get_fitted_status(base_object):
+ """Return CSS/status icon for BaseEstimator-like fitted state."""
+ if not hasattr(base_object, "is_fitted"):
+ return "", ""
+
+ try:
+ is_fitted = bool(base_object.is_fitted)
+ except Exception:
+ is_fitted = False
+
+ status_label = "Fitted" if is_fitted else "Not fitted"
+ css_class = "fitted" if is_fitted else ""
+ status_icon = (
+ f''
+ f"i{status_label}"
+ )
+
+ return css_class, status_icon
+
+
+def _changed_param_names(base_object):
+ """Return shallow parameter names with non-default values."""
+ try:
+ from skbase.base._pretty_printing._pprint import _changed_params
+
+ return tuple(_changed_params(base_object))
+ except Exception:
+ return ()
+
+
+def _read_param(name, value, non_default_params):
+ """Categorize and format a parameter value for HTML display."""
+ repr_instance = reprlib.Repr()
+ repr_instance.maxdict = 2
+ repr_instance.maxlist = 2
+ repr_instance.maxset = 2
+ repr_instance.maxstring = 50
+ repr_instance.maxtuple = 1
+ repr_instance.maxother = 80
+
+ return {
+ "param_type": "user-set" if name in non_default_params else "default",
+ "param_type_extra": (
+ "sk-param-row--changed"
+ if name in non_default_params
+ else "sk-param-row--default"
+ ),
+ "param_name": html.escape(name),
+ "param_value": html.escape(repr_instance.repr(value)),
+ }
+
+
+def _params_html_repr(base_object, doc_link=""):
+ """Generate HTML table with shallow parameters for ``base_object``."""
+ if not hasattr(base_object, "get_params") or isclass(base_object):
+ return ""
+
+ try:
+ params = base_object.get_params(deep=False)
+ except Exception:
+ return ""
+
+ if not isinstance(params, dict) or not params:
+ return ""
+
+ non_default_params = _changed_param_names(base_object)
+ object_class = base_object.__class__
+ param_doc_map = _get_param_doc_descriptions(inspect.getdoc(object_class))
+
+ rows = []
+ for param_name, value in params.items():
+ param = _read_param(param_name, value, non_default_params)
+ param_display = param["param_name"]
+
+ param_doc = param_doc_map.get(param_name)
+ param_link = (
+ _generate_link_to_param_doc(object_class, param_name, doc_link)
+ if doc_link
+ else None
+ )
+ if param_link and param_doc:
+ param_link = html.escape(param_link, quote=True)
+ description = (
+ f"{html.escape(param_name)}: {html.escape(param_doc['type'])}"
+ "
"
+ f"{'
'.join(html.escape(x) for x in param_doc['desc'])}"
+ )
+ param_display = (
+ '{}{}'
+ ).format(param_link, param["param_name"], description)
+
+ rows.append(
+ '
{name_details}'
- "{html.escape(str(name_details))}")
+ out.write("{html.escape(base_object_str)}{fallback_msg}"
- "{}{}"
+ "