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
2 changes: 1 addition & 1 deletion .mdl_style.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

all
exclude_rule 'MD024'
rule 'MD013', line_length: 100, ignore_code_blocks: true, tables: false
rule 'MD013', line_length: 120, ignore_code_blocks: true, tables: false
rule 'MD029', style: 'ordered'
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Graphworks

[![Python package](https://github.com/nathan-gilbert/graphworks/actions/workflows/python-package-ci.yml/badge.svg)](https://github.com/nathan-gilbert/graphworks/actions/workflows/python-package-ci.yml)
[![Python package](https://github.com/nathan-gilbert/graphworks/actions/workflows/python-package-ci.yml/badge.svg)][1]

## A Python module for efficient graph theoretic programming

Expand Down Expand Up @@ -128,3 +128,5 @@ Version is managed automatically via git tags using `hatchling-vcs`.
- [ ] Thread safety (immutable graph views or locking around mutations)
- [ ] Input validation hardening
- [ ] Performance benchmarks

[1]: https://github.com/nathan-gilbert/graphworks/actions/workflows/python-package-ci.yml
13 changes: 12 additions & 1 deletion src/graphworks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,22 @@

from ._version import __version__, __version_tuple__
from .edge import Edge
from .export.ascii_utils import to_ascii
from .export.text_utils import to_text
from .graph import Graph
from .types import AdjacencyMatrix
from .vertex import Vertex

__all__ = ["AdjacencyMatrix", "Edge", "Graph", "Vertex", "__version__", "__version_tuple__"]
__all__ = [
"AdjacencyMatrix",
"Edge",
"Graph",
"Vertex",
"__version__",
"__version_tuple__",
"to_ascii",
"to_text",
]

# ---- Package metadata (PEP 566 / importlib.metadata style) ----

Expand Down
3 changes: 3 additions & 0 deletions src/graphworks/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@

- :mod:`~graphworks.export.json_utils` — JSON file export
- :mod:`~graphworks.export.graphviz_utils` — Graphviz DOT export
- :mod:`~graphworks.export.text_utils` — zero-dependency ASCII/Unicode tree rendering
- :mod:`~graphworks.export.ascii_utils` — zero-dependency 2-D ASCII art rendering (Phase 2,
experimental)
"""
94 changes: 94 additions & 0 deletions src/graphworks/export/_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Shared label and weight formatting for the graph renderers.

Both :mod:`graphworks.export.text_utils` and :mod:`graphworks.export.ascii_utils` resolve what
text to show for a vertex and what annotation to show for an edge through the helpers here, so the
two styles can never drift. :class:`RenderStyle` bundles the three display toggles common to
``to_text`` and ``to_ascii``.

The toggles default to "show when present", which is backward-compatible: a graph with no custom
vertex/edge labels and ``weighted=False`` renders exactly as it did before these options existed.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ..graph import Graph


@dataclass(frozen=True, slots=True)
class RenderStyle:
"""Display toggles shared by the text and ASCII renderers.

:param vertex_labels: Show a vertex's :attr:`~graphworks.vertex.Vertex.label` when set,
falling back to its name.
:type vertex_labels: bool
:param edge_labels: Show an edge's :attr:`~graphworks.edge.Edge.label` when set.
:type edge_labels: bool
:param weights: Show edge weights. ``None`` (the default) follows the graph's
:attr:`~graphworks.graph.Graph.weighted` flag; ``True``/``False`` force the choice.
:type weights: bool | None
"""

vertex_labels: bool = True
edge_labels: bool = True
weights: bool | None = None


def vertex_text(graph: Graph, name: str, *, use_label: bool = True) -> str:
"""Return the display text for vertex *name*.

:param graph: The graph being rendered.
:type graph: Graph
:param name: The vertex name (its identity key).
:type name: str
:param use_label: When ``True``, prefer the vertex's label if one is set.
:type use_label: bool
:return: The vertex label if requested and present, otherwise the name.
:rtype: str
"""
vertex = graph.vertex(name)
if use_label and vertex is not None and vertex.label:
return vertex.label
return name


def edge_annotation(
graph: Graph,
source: str | None,
target: str,
*,
style: RenderStyle,
) -> str:
"""Return the bare annotation for the *source* → *target* edge (no surrounding punctuation).

Combines the edge label and/or weight into a comma-separated string such as ``"retry, w=3"``.
Returns ``""`` when there is no edge, no source, or nothing to show. Callers wrap the result
as they see fit — ``to_text`` brackets it, ``to_ascii`` stamps it at the edge midpoint.

:param graph: The graph being rendered.
:type graph: Graph
:param source: The edge's source vertex, or ``None`` for a root (no incoming edge).
:type source: str | None
:param target: The edge's target vertex.
:type target: str
:param style: The active display toggles.
:type style: RenderStyle
:return: The annotation content, or ``""``.
:rtype: str
"""
if source is None:
return ""
edge = graph.edge(source, target)
if edge is None:
return ""

parts: list[str] = []
if style.edge_labels and edge.label:
parts.append(edge.label)
show_weights = graph.weighted if style.weights is None else style.weights
if show_weights and edge.weight is not None:
parts.append(f"w={edge.weight}")
return ", ".join(parts)
Loading
Loading