Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions datamint/_repr_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Shared plain-text / Jupyter HTML repr rendering for entities, trainers, and datasets.

Any class that can produce a ``(label, value)`` field list gets a consistent
`print()` block and a consistent HTML card in Jupyter for free.
"""

# ---------------------------------------------------------------------------
# Jinja2 HTML template for the Jupyter card repr
# ---------------------------------------------------------------------------
_CARD_HTML_TEMPLATE = """\
<div style="max-width: 720px; margin: 10px 0; overflow: hidden; border-radius: 18px;
border: 1px solid var(--vscode-panel-border, #d0d7de);
background: var(--vscode-editor-background, #ffffff);
color: var(--vscode-foreground, #1f2328);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.10);">

{# ---- Header ---- #}
<div style="padding: 18px 20px;
border-bottom: 1px solid var(--vscode-panel-border, #d0d7de);
background: linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(16, 185, 129, 0.08));">
<div style="font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--vscode-descriptionForeground, #57606a);">{{ kind }}</div>
<div style="display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap; margin-top: 8px;">
<h4 style="margin: 0; font-size: 22px; font-weight: 700; color: inherit;">{{ name }}</h4>
</div>
</div>

{# ---- Fields table ---- #}
{%- if fields %}
<div style="padding: 12px 20px 18px;">
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
{%- for label, value in fields %}
<tr>
<th style="padding: 10px 12px 10px 0; width: 30%; text-align: left; vertical-align: top;
font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
color: var(--vscode-descriptionForeground, #57606a); white-space: nowrap;">{{ label }}</th>
<td style="padding: 10px 0; border-bottom: 1px solid var(--vscode-panel-border, #d0d7de);">
<span style="display: inline-block; padding: 2px 8px; border-radius: 999px;
background: var(--vscode-textCodeBlock-background, #f6f8fa);
color: var(--vscode-textPreformat-foreground, var(--vscode-foreground, #1f2328));
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 13px;"
>{{ value }}</span>
</td>
</tr>
{%- endfor %}
</table>
</div>
{%- else %}
<div style="padding: 18px 20px; font-size: 14px;
color: var(--vscode-descriptionForeground, #57606a);">No non-empty fields to display.</div>
{%- endif %}

</div>
"""

_card_template = None


def _get_card_template():
"""Lazily compile and cache the Jinja2 card template."""
global _card_template
if _card_template is None:
from jinja2 import Environment
_card_template = Environment(autoescape=True).from_string(_CARD_HTML_TEMPLATE)
return _card_template


def render_text_block(header: str, fields: list[tuple[str, str]], empty_message: str = "(no non-empty fields)") -> str:
"""Plain-text ``Header\\n Label: value`` block, used by ``__str__``/``__repr__``."""
if not fields:
return f"{header}\n {empty_message}"
lines = [header] + [f" {label}: {value}" for label, value in fields]
return "\n".join(lines)


def render_html_card(kind: str, name: str, fields: list[tuple[str, str]]) -> str:
"""Styled HTML card for Jupyter's ``_repr_html_`` display hook."""
return _get_card_template().render(kind=kind, name=name, fields=fields)
36 changes: 23 additions & 13 deletions datamint/dataset/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import numpy as np
from datamint.entities.annotation_worklist import AnnotationWorklist
from datamint.exceptions import DatamintException, ItemNotFoundError
from datamint._repr_utils import render_text_block, render_html_card
from .annotation_processor import AnnotationProcessor, MergeStrategy
from datamint.entities.annotations.annotation_spec import AnnotationSpec, CategoryAnnotationSpec
from datamint.entities.annotations import AnnotationType
Expand Down Expand Up @@ -1225,19 +1226,22 @@ def subset(self, indices: list[int]) -> 'DatamintBaseDataset':
raise IndexError(f"Subset indices out of bounds for dataset of length {len(self)}.") from e
return new_ds

def __repr__(self) -> str:
def _extra_repr_fields(self) -> list[tuple[str, str]]:
"""Hook for subclasses to insert extra ``(label, value)`` lines into the repr."""
return []

def _repr_fields(self) -> list[tuple[str, str]]:
name = self.project.name if self.project else "<Custom>"
head = f"Dataset {name}"
body = [f"Number of datapoints: {len(self)}"]
fields = [
("Project", name),
("Number of datapoints", str(len(self))),
]
if self.split_name is not None:
body.append(f"Split: {self.split_name}")
fields.append(("Split", str(self.split_name)))
if self.split_source is not None:
body.append(f"Split source: {self.split_source}")
fields.append(("Split source", str(self.split_source)))
if self.split_as_of_timestamp is not None:
body.append(f"Split as of: {self.split_as_of_timestamp}")

# if self.manager.root is not None:
# body.append(f"Location: {self.manager.dataset_dir}")
fields.append(("Split as of", str(self.split_as_of_timestamp)))

filters = [
(self.include_annotators, "Including annotators"),
Expand All @@ -1249,13 +1253,19 @@ def __repr__(self) -> str:
(self.include_frame_label_names, "Including frame labels"),
(self.exclude_frame_label_names, "Excluding frame labels"),
]

for value, desc in filters:
if value is not None:
body.append(f"{desc}: {value}")
fields.append((desc, str(value)))

fields.extend(self._extra_repr_fields())
return fields

def __repr__(self) -> str:
return render_text_block(self.__class__.__name__, self._repr_fields())

lines = [head] + [" " + line for line in body]
return "\n".join(lines)
def _repr_html_(self) -> str:
"""HTML representation for Jupyter Notebooks."""
return render_html_card(kind="Dataset", name=self.__class__.__name__, fields=self._repr_fields())

def split(
self,
Expand Down
5 changes: 0 additions & 5 deletions datamint/dataset/image_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,6 @@ def apply_alb_transform(
result['image'] = aug_img
return result

@override
def __repr__(self) -> str:
base = super(VolumeDataset, self).__repr__()
return f"ImageDataset\n{base}"


def detection_collate_fn(batch: list[dict]) -> dict:
"""Collate a list of detection items into a batch.
Expand Down
5 changes: 2 additions & 3 deletions datamint/dataset/sliced_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,5 @@ def apply_alb_transform(
result['masks'] = aug_segmentations
return result

def __repr__(self) -> str:
base = super().__repr__()
return f"SlicedVolumeDataset (axis={self._slice_axis})\n{base}"
def _extra_repr_fields(self) -> list[tuple[str, str]]:
return [*super()._extra_repr_fields(), ("Slice axis", str(self._slice_axis))]
4 changes: 0 additions & 4 deletions datamint/dataset/sliced_video_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,3 @@ def apply_alb_transform(
'image': aug_img,
'segmentations': aug_segmentations,
}

def __repr__(self) -> str:
base = super().__repr__()
return f"SlicedVideoDataset\n{base}"
4 changes: 0 additions & 4 deletions datamint/dataset/video_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ class VideoDataset(MultiFrameDataset):
print(frame_ds[0]['image'].shape) # (C, H, W)
"""

def __repr__(self) -> str:
base = super().__repr__()
return f"VideoDataset\n{base}"

def frame_by_frame(self) -> 'SlicedVideoDataset':
"""Create a 2D dataset iterating over individual video frames.

Expand Down
4 changes: 0 additions & 4 deletions datamint/dataset/volume_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ class VolumeDataset(MultiFrameDataset):
Inherits multi-frame loading and augmentation from :class:`MultiFrameDataset`.
"""

def __repr__(self) -> str:
base = super().__repr__()
return f"VolumeDataset\n{base}"

def slice(self, axis: str | int = 'axial') -> 'SlicedVolumeDataset':
"""Create a 2D dataset by slicing this volume along an axis.

Expand Down
82 changes: 3 additions & 79 deletions datamint/entities/base_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic import BaseModel, ConfigDict, PrivateAttr

from datamint.types import CacheMode
from datamint._repr_utils import render_text_block, render_html_card

if TYPE_CHECKING:
from datamint.api.entity_base_api import EntityBaseApi
Expand All @@ -21,68 +22,6 @@
# Track logged warnings to avoid duplicates
_LOGGED_WARNINGS: set[tuple[str, str]] = set()

# ---------------------------------------------------------------------------
# Jinja2 HTML template for BaseEntity Jupyter repr
# ---------------------------------------------------------------------------
_ENTITY_HTML_TEMPLATE = """\
<div style="max-width: 720px; margin: 10px 0; overflow: hidden; border-radius: 18px;
border: 1px solid var(--vscode-panel-border, #d0d7de);
background: var(--vscode-editor-background, #ffffff);
color: var(--vscode-foreground, #1f2328);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.10);">

{# ---- Header ---- #}
<div style="padding: 18px 20px;
border-bottom: 1px solid var(--vscode-panel-border, #d0d7de);
background: linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(16, 185, 129, 0.08));">
<div style="font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--vscode-descriptionForeground, #57606a);">Entity</div>
<div style="display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap; margin-top: 8px;">
<h4 style="margin: 0; font-size: 22px; font-weight: 700; color: inherit;">{{ entity_name }}</h4>
</div>
</div>

{# ---- Fields table ---- #}
{%- if fields %}
<div style="padding: 12px 20px 18px;">
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
{%- for name, value in fields %}
<tr>
<th style="padding: 10px 12px 10px 0; width: 30%; text-align: left; vertical-align: top;
font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
color: var(--vscode-descriptionForeground, #57606a); white-space: nowrap;">{{ name }}</th>
<td style="padding: 10px 0; border-bottom: 1px solid var(--vscode-panel-border, #d0d7de);">
<span style="display: inline-block; padding: 2px 8px; border-radius: 999px;
background: var(--vscode-textCodeBlock-background, #f6f8fa);
color: var(--vscode-textPreformat-foreground, var(--vscode-foreground, #1f2328));
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-size: 13px;"
>{{ value }}</span>
</td>
</tr>
{%- endfor %}
</table>
</div>
{%- else %}
<div style="padding: 18px 20px; font-size: 14px;
color: var(--vscode-descriptionForeground, #57606a);">No non-empty fields to display.</div>
{%- endif %}

</div>
"""

_entity_template = None


def _get_entity_template():
"""Lazily compile and cache the Jinja2 entity template."""
global _entity_template
if _entity_template is None:
from jinja2 import Environment
_entity_template = Environment(autoescape=True).from_string(_ENTITY_HTML_TEMPLATE)
return _entity_template


class BaseEntityModel(BaseModel):
"""Shared lightweight Pydantic base for Datamint entities and DTOs."""
Expand Down Expand Up @@ -128,25 +67,10 @@ def _get_display_fields(self, max_value_len: int = 120) -> list[tuple[str, str]]

def _repr_html_(self) -> str:
"""HTML representation for Jupyter Notebooks."""
entity_id = getattr(self, 'id', None)
fields = self._get_display_fields()

return _get_entity_template().render(
entity_name=self.__class__.__name__,
entity_id=str(entity_id) if entity_id else None,
fields=fields,
)
return render_html_card(kind='Entity', name=self.__class__.__name__, fields=self._get_display_fields())

def __str__(self) -> str:
fields = self._get_display_fields()

header = self.__class__.__name__

if not fields:
return f"{header}\n (no non-empty fields)"

lines = [header] + [f" {name}: {value}" for name, value in fields]
return "\n".join(lines)
return render_text_block(self.__class__.__name__, self._get_display_fields())

def __init__(self, **data):
super().__init__(**data)
Expand Down
33 changes: 33 additions & 0 deletions datamint/lightning/trainers/base_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from datamint.mlflow import set_project
from datamint.mlflow.flavors.model import BaseDatamintModel
from datamint.lightning.trainers.lightning_modules.base import DatamintLightningModule
from datamint._repr_utils import render_text_block, render_html_card

if TYPE_CHECKING:
from albumentations import BaseCompose
Expand Down Expand Up @@ -137,6 +138,38 @@ def _project_name(self) -> str:
def experiment_name(self) -> str:
return self.mlflow_experiment_name or f"{self._project_name}_training"

def _model_description(self) -> str:
"""Short human-readable model description for :meth:`__repr__`. Override per architecture."""
if self._user_model is not None:
model = self._user_model
cls = model if isinstance(model, type) else model.__class__
return f"Custom ({cls.__name__})"
return self.__class__.__name__.removesuffix("Trainer")

def _extra_repr_fields(self) -> list[tuple[str, str]]:
"""Architecture-specific ``(label, value)`` lines inserted between Batch size and Early stopping patience."""
return []

def _repr_fields(self) -> list[tuple[str, str]]:
"""Fields shown by :meth:`__repr__`/:meth:`_repr_html_`. Cheap and side-effect free: never resolves the dataset or builds the model."""
return [
("Project", self._project_name),
("Model", self._model_description()),
("Max epochs", str(self.max_epochs)),
("Batch size", str(self.batch_size)),
*self._extra_repr_fields(),
("Early stopping patience", str(self.early_stopping_patience) if self.early_stopping_patience else "disabled"),
("MLflow experiment", self.experiment_name),
("Auto-deploy adapter", "enabled" if self.auto_deploy_adapter else "disabled"),
]

def __repr__(self) -> str:
return render_text_block(self.__class__.__name__, self._repr_fields())

def _repr_html_(self) -> str:
"""HTML representation for Jupyter Notebooks."""
return render_html_card(kind="Trainer", name=self.__class__.__name__, fields=self._repr_fields())

def _with_project(self):
set_project(self._project_name)

Expand Down
10 changes: 10 additions & 0 deletions datamint/lightning/trainers/classification_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def __init__(
else:
self.image_size = image_size

def _model_description(self) -> str:
if self._user_model is not None:
return super()._model_description()
pretrained_str = "pretrained" if self.pretrained else "random init"
return f"{self.architecture} ({pretrained_str})"

def _extra_repr_fields(self) -> list[tuple[str, str]]:
image_size = f"{self.image_size[0]}×{self.image_size[1]}" if self.image_size else "auto (no resize)"
return [*super()._extra_repr_fields(), ("Image size", image_size)]

# ── Template hooks ──────────────────────────────────────────

def _build_dataset(self, project: 'str | Project', **kwargs: Any) -> ImageDataset:
Expand Down
5 changes: 5 additions & 0 deletions datamint/lightning/trainers/seg2d_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def __init__(
else:
self.image_size = image_size

@override
def _extra_repr_fields(self) -> list[tuple[str, str]]:
image_size = f"{self.image_size[0]}×{self.image_size[1]}" if self.image_size else "auto (no resize)"
return [*super()._extra_repr_fields(), ("Image size", image_size)]

def _build_dataset(self, project: 'str | Project', **kwargs: Any) -> ImageDataset | SlicedVolumeDataset:
default_params = dict(
return_as_semantic_segmentation=True,
Expand Down
13 changes: 13 additions & 0 deletions datamint/lightning/trainers/seg3d_trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ def __init__(
else:
self.image_size = image_size

def _model_description(self) -> str:
if self._user_model is not None:
return super()._model_description()
return f"{self.encoder_name} encoder"

def _extra_repr_fields(self) -> list[tuple[str, str]]:
image_size = f"{self.image_size[0]}×{self.image_size[1]}" if self.image_size else "auto (original slice size)"
return [
*super()._extra_repr_fields(),
("Slice axis", str(self.slice_axis)),
("Image size", image_size),
]

# ── Template hooks ──────────────────────────────────────────

def _build_dataset(self, project: 'str | Project', **kwargs: Any):
Expand Down
Loading
Loading