remove time graph visualiser. More tests. Other fixes
This commit is contained in:
parent
0b3249c7ef
commit
985541a1d8
18 changed files with 4087 additions and 971 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
* Remove screenshot tool
|
||||
* Improve width of bug report dialog
|
||||
* Add Tag relationship visualisation tool
|
||||
* Improve size of checkboxes
|
||||
* Convert bullet - to actual unicode bullets
|
||||
* Add alarm option to set reminders
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -3,7 +3,11 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
Bouquin ("Book-ahn") is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
|
||||
Bouquin ("Book-ahn") is a notebook and planner application written in Python, PyQt and SQLCipher.
|
||||
|
||||
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
|
||||
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
||||
also how long we spent on them.
|
||||
|
||||
It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a drop-in replacement
|
||||
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
|
||||
|
|
@ -23,27 +27,22 @@ report from within the app.
|
|||

|
||||

|
||||
|
||||
### Tag relationship visualiser
|
||||

|
||||
|
||||
## Features
|
||||
## Some of the features
|
||||
|
||||
* Data is encrypted at rest
|
||||
* Encryption key is prompted for and never stored, unless user chooses to via Settings
|
||||
* Every 'page' is linked to the calendar day
|
||||
* All changes are version controlled, with ability to view/diff versions and revert
|
||||
* Text is Markdown with basic styling
|
||||
* Automatic rendering of basic Markdown syntax
|
||||
* Tabs are supported - right-click on a date from the calendar to open it in a new tab.
|
||||
* Images are supported
|
||||
* Search all pages, or find text on page (Ctrl+F)
|
||||
* Add tags to pages, find pages by tag in the Tag Browser, and customise tag names and colours
|
||||
* Search all pages, or find text on current page
|
||||
* Add and manage tags
|
||||
* Automatic periodic saving (or explicitly save)
|
||||
* Transparent integrity checking of the database when it opens
|
||||
* Automatic locking of the app after a period of inactivity (default 15 min)
|
||||
* Rekey the database (change the password)
|
||||
* Export the database to json, html, csv, markdown or .sql (for sqlite3)
|
||||
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
||||
* Dark and light themes
|
||||
* Dark and light theme support
|
||||
* Automatically generate checkboxes when typing 'TODO'
|
||||
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
|
||||
* English, French and Italian locales provided
|
||||
|
|
|
|||
|
|
@ -790,49 +790,6 @@ class DBManager:
|
|||
revisions_by_date,
|
||||
)
|
||||
|
||||
def get_tag_cooccurrences(self):
|
||||
"""
|
||||
Compute tag–tag co-occurrence across pages.
|
||||
|
||||
Returns:
|
||||
tags_by_id: dict[int, TagRow] # id -> (id, name, color)
|
||||
edges: list[(int, int, int)] # (tag_id1, tag_id2, page_count)
|
||||
tag_page_counts: dict[int, int] # tag_id -> number of pages it appears on
|
||||
"""
|
||||
cur = self.conn.cursor()
|
||||
|
||||
# 1) All tags (reuse existing helper)
|
||||
all_tags: list[TagRow] = self.list_tags()
|
||||
tags_by_id: dict[int, TagRow] = {t[0]: t for t in all_tags}
|
||||
|
||||
# 2) How many pages each tag appears on (for node sizing)
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT tag_id, COUNT(DISTINCT page_date) AS c
|
||||
FROM page_tags
|
||||
GROUP BY tag_id;
|
||||
"""
|
||||
).fetchall()
|
||||
tag_page_counts = {r["tag_id"]: r["c"] for r in rows}
|
||||
|
||||
# 3) Co-occurrence of tag pairs on the same page
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
pt1.tag_id AS tag1,
|
||||
pt2.tag_id AS tag2,
|
||||
COUNT(DISTINCT pt1.page_date) AS c
|
||||
FROM page_tags AS pt1
|
||||
JOIN page_tags AS pt2
|
||||
ON pt1.page_date = pt2.page_date
|
||||
AND pt1.tag_id < pt2.tag_id
|
||||
GROUP BY pt1.tag_id, pt2.tag_id;
|
||||
""",
|
||||
).fetchall()
|
||||
|
||||
edges = [(r["tag1"], r["tag2"], r["c"]) for r in rows]
|
||||
return tags_by_id, edges, tag_page_counts
|
||||
|
||||
# -------- Time logging: projects & activities ---------------------
|
||||
|
||||
def list_projects(self) -> list[ProjectRow]:
|
||||
|
|
@ -1037,7 +994,7 @@ class DBManager:
|
|||
AND t.page_date BETWEEN ? AND ?
|
||||
GROUP BY bucket, activity_name
|
||||
ORDER BY bucket, LOWER(activity_name);
|
||||
""", # nosec B608: bucket_expr comes from a fixed internal list
|
||||
""", # nosec
|
||||
(project_id, start_date_iso, end_date_iso),
|
||||
).fetchall()
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@
|
|||
"delete_tag": "Delete tag",
|
||||
"delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages.",
|
||||
"tag_already_exists_with_that_name": "A tag already exists with that name",
|
||||
"tag_graph": "Tag relationship graph",
|
||||
"statistics": "Statistics",
|
||||
"main_window_statistics_accessible_flag": "Stat&istics",
|
||||
"stats_pages_with_content": "Pages with content (current version)",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ from PySide6.QtWidgets import (
|
|||
)
|
||||
|
||||
from .db import DBManager
|
||||
from .tag_graph_dialog import TagGraphDialog
|
||||
from . import strings
|
||||
from sqlcipher3.dbapi2 import IntegrityError
|
||||
|
||||
|
|
@ -72,10 +71,6 @@ class TagBrowserDialog(QDialog):
|
|||
self.delete_btn.setEnabled(False)
|
||||
btn_row.addWidget(self.delete_btn)
|
||||
|
||||
self.tag_graph_btn = QPushButton(strings._("tag_graph"))
|
||||
self.tag_graph_btn.clicked.connect(self._open_tag_graph)
|
||||
btn_row.addWidget(self.tag_graph_btn)
|
||||
|
||||
btn_row.addStretch(1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
|
|
@ -256,9 +251,3 @@ class TagBrowserDialog(QDialog):
|
|||
self._db.delete_tag(tag_id)
|
||||
self._populate(None)
|
||||
self.tagsModified.emit()
|
||||
|
||||
# ------------ Tag graph handler --------------- #
|
||||
def _open_tag_graph(self):
|
||||
dlg = TagGraphDialog(self._db, self)
|
||||
dlg.resize(800, 600)
|
||||
dlg.exec()
|
||||
|
|
|
|||
|
|
@ -1,309 +0,0 @@
|
|||
import networkx as nx
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore
|
||||
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QToolTip
|
||||
from PySide6.QtGui import QFont, QCursor, QColor
|
||||
|
||||
from .db import DBManager
|
||||
from . import strings
|
||||
|
||||
|
||||
class DraggableGraphItem(pg.GraphItem):
|
||||
"""GraphItem where individual nodes can be dragged with the left mouse button,
|
||||
and hover events can be reported back to the owning dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, on_position_changed=None, on_hover=None, **kwds):
|
||||
# Our own fields MUST be set before super().__init__ because
|
||||
# GraphItem.__init__ will call self.setData(...)
|
||||
self._drag_index = None
|
||||
self._drag_offset = None
|
||||
self._on_position_changed = on_position_changed
|
||||
self._on_hover = on_hover
|
||||
self.pos = None
|
||||
self._data_kwargs = {} # cache of last setData kwargs
|
||||
|
||||
super().__init__(**kwds)
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
def setData(self, **kwds):
|
||||
"""Cache kwargs so we don't lose size/adj/brush on drag."""
|
||||
if "pos" in kwds:
|
||||
self.pos = kwds["pos"]
|
||||
self._data_kwargs.update(kwds)
|
||||
super().setData(**self._data_kwargs)
|
||||
|
||||
def mouseDragEvent(self, ev):
|
||||
# --- start of drag ---
|
||||
if ev.isStart():
|
||||
if ev.button() != QtCore.Qt.MouseButton.LeftButton:
|
||||
ev.ignore()
|
||||
return
|
||||
|
||||
pos = ev.buttonDownPos()
|
||||
pts = self.scatter.pointsAt(pos)
|
||||
|
||||
# pointsAt may return an empty list/array
|
||||
if pts is None or len(pts) == 0:
|
||||
ev.ignore()
|
||||
return
|
||||
|
||||
spot = pts[0]
|
||||
self._drag_index = spot.index()
|
||||
|
||||
node_pos = np.array(self.pos[self._drag_index], dtype=float)
|
||||
|
||||
if hasattr(pos, "x"):
|
||||
mouse = np.array([pos.x(), pos.y()], dtype=float)
|
||||
else:
|
||||
mouse = np.array(pos, dtype=float)
|
||||
|
||||
self._drag_offset = node_pos - mouse
|
||||
ev.accept()
|
||||
return
|
||||
|
||||
# --- end of drag ---
|
||||
if ev.isFinish():
|
||||
self._drag_index = None
|
||||
self._drag_offset = None
|
||||
ev.accept()
|
||||
return
|
||||
|
||||
# --- drag in progress ---
|
||||
if self._drag_index is None:
|
||||
ev.ignore()
|
||||
return
|
||||
|
||||
pos = ev.pos()
|
||||
if hasattr(pos, "x"):
|
||||
mouse = np.array([pos.x(), pos.y()], dtype=float)
|
||||
else:
|
||||
mouse = np.array(pos, dtype=float)
|
||||
|
||||
new_pos = mouse + self._drag_offset
|
||||
self.pos[self._drag_index] = new_pos # mutate in-place
|
||||
|
||||
# Repaint graph, preserving all the other kwargs (size, adj, colours, ...)
|
||||
self.setData(pos=self.pos)
|
||||
|
||||
if self._on_position_changed is not None:
|
||||
self._on_position_changed(self.pos)
|
||||
|
||||
ev.accept()
|
||||
|
||||
def hoverEvent(self, ev):
|
||||
"""Report which node (if any) is under the mouse while hovering."""
|
||||
# Leaving the item entirely
|
||||
if ev.isExit():
|
||||
if self._on_hover is not None:
|
||||
self._on_hover(None, ev)
|
||||
return
|
||||
|
||||
pos = ev.pos()
|
||||
pts = self.scatter.pointsAt(pos)
|
||||
|
||||
if pts is None or len(pts) == 0:
|
||||
if self._on_hover is not None:
|
||||
self._on_hover(None, ev)
|
||||
return
|
||||
|
||||
idx = pts[0].index()
|
||||
if self._on_hover is not None:
|
||||
self._on_hover(idx, ev)
|
||||
|
||||
|
||||
class TagGraphDialog(QDialog):
|
||||
def __init__(self, db: DBManager, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(strings._("tag_graph"))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.view = pg.GraphicsLayoutWidget()
|
||||
layout.addWidget(self.view)
|
||||
|
||||
self.plot = self.view.addPlot()
|
||||
self.plot.hideAxis("bottom")
|
||||
self.plot.hideAxis("left")
|
||||
|
||||
# Dark-ish background, Grafana / neon style
|
||||
self.view.setBackground("#050816")
|
||||
self.plot.setMouseEnabled(x=True, y=True)
|
||||
self.plot.getViewBox().setDefaultPadding(0.15)
|
||||
|
||||
# State for tags / edges / labels / halo
|
||||
self._label_items = []
|
||||
self._tag_ids = []
|
||||
self._tag_names = {}
|
||||
self._tag_page_counts = {}
|
||||
|
||||
self._halo_sizes = []
|
||||
self._halo_brushes = []
|
||||
|
||||
self.graph_item = DraggableGraphItem(
|
||||
on_position_changed=self._on_positions_changed,
|
||||
on_hover=self._on_hover_index,
|
||||
)
|
||||
self.plot.addItem(self.graph_item)
|
||||
|
||||
# Separate scatter for "halo" glow behind nodes
|
||||
self._halo_item = pg.ScatterPlotItem(pxMode=True)
|
||||
self._halo_item.setZValue(-1) # draw behind nodes/labels
|
||||
self.plot.addItem(self._halo_item)
|
||||
|
||||
self._populate_graph(db)
|
||||
|
||||
def _populate_graph(self, db: DBManager):
|
||||
tags_by_id, edges, tag_page_counts = db.get_tag_cooccurrences()
|
||||
|
||||
if not tags_by_id:
|
||||
return
|
||||
|
||||
# Map tag_id -> index
|
||||
tag_ids = list(tags_by_id.keys())
|
||||
self._tag_ids = tag_ids
|
||||
self._tag_page_counts = dict(tag_page_counts)
|
||||
self._tag_names = {tid: tags_by_id[tid][1] for tid in tag_ids}
|
||||
|
||||
idx_of = {tid: i for i, tid in enumerate(tag_ids)}
|
||||
N = len(tag_ids)
|
||||
|
||||
# ---- Layout: prefer a weighted spring layout via networkx (topic islands)
|
||||
if edges:
|
||||
G = nx.Graph()
|
||||
for tid in tag_ids:
|
||||
G.add_node(tid)
|
||||
for t1, t2, w in edges:
|
||||
G.add_edge(t1, t2, weight=w)
|
||||
|
||||
pos_dict = nx.spring_layout(G, weight="weight", k=1.2, iterations=80)
|
||||
pos = np.array([pos_dict[tid] for tid in tag_ids], dtype=float)
|
||||
else:
|
||||
# Fallback: random-ish blob
|
||||
pos = np.random.normal(size=(N, 2))
|
||||
|
||||
# Adjacency (edges)
|
||||
adj = np.array([[idx_of[t1], idx_of[t2]] for t1, t2, _ in edges], dtype=int)
|
||||
|
||||
# Node sizes: proportional to how often tag is used
|
||||
max_pages = max(tag_page_counts.values() or [1])
|
||||
sizes = np.array(
|
||||
[10 + 20 * (tag_page_counts.get(tid, 0) / max_pages) for tid in tag_ids],
|
||||
dtype=float,
|
||||
)
|
||||
|
||||
# ---- Neon-style nodes ----
|
||||
# Inner fill: dark; outline: tag hex colour
|
||||
node_brushes = []
|
||||
node_pens = []
|
||||
|
||||
dark_fill = (5, 8, 22, 230) # almost background, slightly lighter
|
||||
|
||||
# For halo
|
||||
halo_sizes = []
|
||||
halo_brushes = []
|
||||
|
||||
for i, tid in enumerate(tag_ids):
|
||||
_id, name, color = tags_by_id[tid]
|
||||
|
||||
# node interior (dark) + bright outline
|
||||
node_brushes.append(pg.mkBrush(dark_fill))
|
||||
node_pens.append(pg.mkPen(color, width=2.5))
|
||||
|
||||
# halo: semi-transparent version of DB colour, larger than node
|
||||
qcol = QColor(color)
|
||||
qcol.setAlpha(90)
|
||||
halo_brushes.append(pg.mkBrush(qcol))
|
||||
halo_sizes.append(sizes[i] * 1.8)
|
||||
|
||||
self._halo_sizes = halo_sizes
|
||||
self._halo_brushes = halo_brushes
|
||||
|
||||
# ---- Edges: softer neon-ish lines with opacity / width based on co-occurrence ----
|
||||
if edges:
|
||||
weights = np.array([w for _, _, w in edges], dtype=float)
|
||||
max_w = weights.max() if weights.size else 1.0
|
||||
weight_factors = (weights / max_w).clip(0.0, 1.0)
|
||||
|
||||
# bright cyan-ish neon
|
||||
base_color = (56, 189, 248) # tailwind-ish cyan-400
|
||||
edge_pens = []
|
||||
|
||||
for wf in weight_factors:
|
||||
alpha = int(40 + 160 * wf) # 40–200
|
||||
width = 0.7 + 2.3 * wf # 0.7–3.0
|
||||
edge_pens.append(pg.mkPen((*base_color, alpha), width=width))
|
||||
else:
|
||||
edge_pens = None
|
||||
|
||||
# Assign data to GraphItem (this will set self.graph_item.pos)
|
||||
self.graph_item.setData(
|
||||
pos=pos,
|
||||
adj=adj,
|
||||
size=sizes,
|
||||
symbolBrush=node_brushes,
|
||||
symbolPen=node_pens,
|
||||
edgePen=edge_pens,
|
||||
pxMode=True,
|
||||
)
|
||||
|
||||
# ---- Neon halo layer (behind nodes) ----
|
||||
xs = [p[0] for p in pos]
|
||||
ys = [p[1] for p in pos]
|
||||
self._halo_item.setData(
|
||||
x=xs,
|
||||
y=ys,
|
||||
size=self._halo_sizes,
|
||||
brush=self._halo_brushes,
|
||||
pen=None,
|
||||
)
|
||||
|
||||
# ---- Add text labels for each tag ----
|
||||
self._label_items = [] # reset
|
||||
font = QFont()
|
||||
font.setPointSize(8)
|
||||
|
||||
for i, tid in enumerate(tag_ids):
|
||||
_id, name, color = tags_by_id[tid]
|
||||
label = pg.TextItem(text=name, color=color, anchor=(0.5, 0.5))
|
||||
label.setFont(font)
|
||||
self.plot.addItem(label)
|
||||
self._label_items.append(label)
|
||||
|
||||
# Initial placement of labels
|
||||
self._on_positions_changed(pos)
|
||||
|
||||
def _on_positions_changed(self, pos):
|
||||
"""Called by DraggableGraphItem whenever node positions change."""
|
||||
if not self._label_items:
|
||||
return
|
||||
|
||||
# Update labels
|
||||
for i, label in enumerate(self._label_items):
|
||||
label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.15)
|
||||
|
||||
# Update halo positions to match nodes
|
||||
if self._halo_sizes and self._halo_brushes:
|
||||
xs = [p[0] for p in pos]
|
||||
ys = [p[1] for p in pos]
|
||||
self._halo_item.setData(
|
||||
x=xs,
|
||||
y=ys,
|
||||
size=self._halo_sizes,
|
||||
brush=self._halo_brushes,
|
||||
pen=None,
|
||||
)
|
||||
|
||||
def _on_hover_index(self, index, ev):
|
||||
"""Show '<tag>: N pages' when hovering a node."""
|
||||
if index is None or not self._tag_ids:
|
||||
QToolTip.hideText()
|
||||
return
|
||||
|
||||
tag_id = self._tag_ids[index]
|
||||
name = self._tag_names.get(tag_id, "")
|
||||
count = self._tag_page_counts.get(tag_id, 0)
|
||||
text = f"{name}: {count} page{'s' if count != 1 else ''}"
|
||||
|
||||
QToolTip.showText(QCursor.pos(), text, self)
|
||||
|
|
@ -161,7 +161,7 @@ class TimeLogWidget(QFrame):
|
|||
# Always refresh summary + header totals
|
||||
self._reload_summary()
|
||||
|
||||
if self.toggle_btn.isChecked():
|
||||
if not self.toggle_btn.isChecked():
|
||||
self.summary_label.setText(strings._("time_log_collapsed_hint"))
|
||||
|
||||
|
||||
|
|
|
|||
398
poetry.lock
generated
398
poetry.lock
generated
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
version = "2025.11.12"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"},
|
||||
{file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"},
|
||||
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
|
||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -146,133 +146,141 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.10.7"
|
||||
version = "7.12.0"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"},
|
||||
{file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"},
|
||||
{file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"},
|
||||
{file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"},
|
||||
{file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"},
|
||||
{file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"},
|
||||
{file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"},
|
||||
{file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"},
|
||||
{file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"},
|
||||
{file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"},
|
||||
{file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef"},
|
||||
{file = "coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4"},
|
||||
{file = "coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a"},
|
||||
{file = "coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07"},
|
||||
{file = "coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c"},
|
||||
{file = "coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe"},
|
||||
{file = "coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291"},
|
||||
{file = "coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384"},
|
||||
{file = "coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a"},
|
||||
{file = "coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "desktop-entry-lib"
|
||||
version = "3.2"
|
||||
version = "5.0"
|
||||
description = "A library for working with .desktop files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "desktop-entry-lib-3.2.tar.gz", hash = "sha256:12189249f86dde52d055ac28897ad1ed14ef965407a50fb3ad4ac6cb1a7e8cde"},
|
||||
{file = "desktop_entry_lib-3.2-py3-none-any.whl", hash = "sha256:600748b2aab2cafbb3ebc08b5c1ded024d66e0756868b6d26b5aff44d336c4b5"},
|
||||
{file = "desktop_entry_lib-5.0-py3-none-any.whl", hash = "sha256:e60a0c2c5e42492dbe5378e596b1de87d1b1c4dc74d1f41998a164ee27a1226f"},
|
||||
{file = "desktop_entry_lib-5.0.tar.gz", hash = "sha256:9a621bac1819fe21021356e41fec0ac096ed56e6eb5dcfe0639cd8654914b864"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pyfakefs", "pytest", "pytest-cov"]
|
||||
xdg-desktop-portal = ["jeepney"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
|
||||
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
|
|
@ -290,97 +298,13 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
|
|||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
|
||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.5"
|
||||
description = "Python package for creating and manipulating graphs and networks"
|
||||
optional = false
|
||||
python-versions = ">=3.11"
|
||||
files = [
|
||||
{file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"},
|
||||
{file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"]
|
||||
developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"]
|
||||
doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"]
|
||||
example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"]
|
||||
extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"]
|
||||
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"]
|
||||
test-extras = ["pytest-mpl", "pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"},
|
||||
{file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"},
|
||||
{file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"},
|
||||
{file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"},
|
||||
{file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"},
|
||||
{file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"},
|
||||
{file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"},
|
||||
{file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"},
|
||||
{file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"},
|
||||
{file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"},
|
||||
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -437,20 +361,7 @@ files = [
|
|||
[package.dependencies]
|
||||
desktop-entry-lib = "*"
|
||||
requests = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyqtgraph"
|
||||
version = "0.14.0"
|
||||
description = "Scientific Graphics and GUI Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = "*"
|
||||
numpy = ">=1.25.0"
|
||||
tomli = {version = "*", markers = "python_version < \"3.11\""}
|
||||
|
||||
[[package]]
|
||||
name = "pyside6"
|
||||
|
|
@ -519,10 +430,12 @@ files = [
|
|||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
iniconfig = ">=1"
|
||||
packaging = ">=20"
|
||||
pluggy = ">=1.5,<2"
|
||||
pygments = ">=2.7.2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||
|
|
@ -749,6 +662,57 @@ files = [
|
|||
{file = "sqlcipher3_wheels-0.5.5.post0.tar.gz", hash = "sha256:2c291ba05fa3e57c9b4d407d2751aa69266b5372468e7402daaa312b251aca7f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.3.0"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
|
||||
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
|
||||
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
|
|
@ -779,5 +743,5 @@ zstd = ["zstandard (>=0.18.0)"]
|
|||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.11,<3.14"
|
||||
content-hash = "02e508f6d5fc3843084d48a8e495af69555ce6163e281d62b2d3242378e68274"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "d5fd8ea759b6bd3f23336930bdce9241659256ed918ec31746787cc86e817235"
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ packages = [{ include = "bouquin" }]
|
|||
include = ["bouquin/locales/*.json"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.11,<3.14"
|
||||
python = ">=3.10,<3.14"
|
||||
pyside6 = ">=6.8.1,<7.0.0"
|
||||
sqlcipher3-wheels = "^0.5.5.post0"
|
||||
requests = "^2.32.5"
|
||||
pyqtgraph = "^0.14.0"
|
||||
networkx = "^3.5"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
bouquin = "bouquin.__main__:main"
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -1,5 +1,8 @@
|
|||
import bouquin.bug_report_dialog as bugmod
|
||||
from bouquin.bug_report_dialog import BugReportDialog
|
||||
from bouquin import strings
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtGui import QTextCursor
|
||||
|
||||
|
||||
def test_bug_report_truncates_text_to_max_chars(qtbot):
|
||||
|
|
@ -193,3 +196,129 @@ def test_bug_report_send_failure_non_201_shows_critical_and_not_accepted(
|
|||
|
||||
# Dialog should NOT be accepted on failure
|
||||
assert accepted.get("called") is not True
|
||||
|
||||
|
||||
def test_bug_report_dialog_text_limit_clamps_cursor(qtbot):
|
||||
"""Test that cursor position is clamped when text exceeds limit."""
|
||||
strings.load_strings("en")
|
||||
dialog = BugReportDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Set text that exceeds MAX_CHARS
|
||||
max_chars = dialog.MAX_CHARS
|
||||
long_text = "A" * (max_chars + 100)
|
||||
|
||||
# Set text and move cursor to end
|
||||
dialog.text_edit.setPlainText(long_text)
|
||||
dialog.text_edit.moveCursor(QTextCursor.MoveOperation.End)
|
||||
|
||||
# Text should be truncated
|
||||
assert len(dialog.text_edit.toPlainText()) == max_chars
|
||||
|
||||
# Cursor should be clamped to max position
|
||||
final_cursor = dialog.text_edit.textCursor()
|
||||
assert final_cursor.position() <= max_chars
|
||||
|
||||
|
||||
def test_bug_report_dialog_empty_text_shows_warning(qtbot, monkeypatch):
|
||||
"""Test that sending empty report shows warning."""
|
||||
strings.load_strings("en")
|
||||
dialog = BugReportDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Clear any text
|
||||
dialog.text_edit.clear()
|
||||
|
||||
warning_shown = {"shown": False}
|
||||
|
||||
def mock_warning(*args):
|
||||
warning_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "warning", mock_warning)
|
||||
|
||||
# Try to send empty report
|
||||
dialog._send()
|
||||
|
||||
assert warning_shown["shown"]
|
||||
|
||||
|
||||
def test_bug_report_dialog_whitespace_only_shows_warning(qtbot, monkeypatch):
|
||||
"""Test that sending whitespace-only report shows warning."""
|
||||
strings.load_strings("en")
|
||||
dialog = BugReportDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Set whitespace only
|
||||
dialog.text_edit.setPlainText(" \n\n \t\t ")
|
||||
|
||||
warning_shown = {"shown": False}
|
||||
|
||||
def mock_warning(*args):
|
||||
warning_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "warning", mock_warning)
|
||||
|
||||
dialog._send()
|
||||
|
||||
assert warning_shown["shown"]
|
||||
|
||||
|
||||
def test_bug_report_dialog_network_error(qtbot, monkeypatch):
|
||||
"""Test handling network error during send."""
|
||||
strings.load_strings("en")
|
||||
dialog = BugReportDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
dialog.text_edit.setPlainText("Test bug report")
|
||||
|
||||
# Mock requests.post to raise exception
|
||||
import requests
|
||||
|
||||
def mock_post(*args, **kwargs):
|
||||
raise requests.exceptions.ConnectionError("Network error")
|
||||
|
||||
monkeypatch.setattr(requests, "post", mock_post)
|
||||
|
||||
critical_shown = {"shown": False}
|
||||
|
||||
def mock_critical(*args):
|
||||
critical_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "critical", mock_critical)
|
||||
|
||||
dialog._send()
|
||||
|
||||
assert critical_shown["shown"]
|
||||
|
||||
|
||||
def test_bug_report_dialog_timeout_error(qtbot, monkeypatch):
|
||||
"""Test handling timeout error during send."""
|
||||
strings.load_strings("en")
|
||||
dialog = BugReportDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
dialog.text_edit.setPlainText("Test bug report")
|
||||
|
||||
# Mock requests.post to raise timeout
|
||||
import requests
|
||||
|
||||
def mock_post(*args, **kwargs):
|
||||
raise requests.exceptions.Timeout("Request timed out")
|
||||
|
||||
monkeypatch.setattr(requests, "post", mock_post)
|
||||
|
||||
critical_shown = {"shown": False}
|
||||
|
||||
def mock_critical(*args):
|
||||
critical_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "critical", mock_critical)
|
||||
|
||||
dialog._send()
|
||||
|
||||
assert critical_shown["shown"]
|
||||
|
|
|
|||
335
tests/test_db.py
335
tests/test_db.py
|
|
@ -3,6 +3,7 @@ import json, csv
|
|||
import datetime as dt
|
||||
from sqlcipher3 import dbapi2 as sqlite
|
||||
from bouquin.db import DBManager
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
def _today():
|
||||
|
|
@ -17,6 +18,10 @@ def _tomorrow():
|
|||
return (dt.date.today() + dt.timedelta(days=1)).isoformat()
|
||||
|
||||
|
||||
def _days_ago(n):
|
||||
return (date.today() - timedelta(days=n)).isoformat()
|
||||
|
||||
|
||||
def _entry(text, i=0):
|
||||
return f"{text} line {i}\nsecond line\n\n- [x] done\n- [ ] todo"
|
||||
|
||||
|
|
@ -201,3 +206,333 @@ def test_integrity_check_raises_without_details(tmp_db_cfg):
|
|||
db.conn = _Conn([(None,), (None,)])
|
||||
with pytest.raises(sqlite.IntegrityError):
|
||||
db._integrity_ok()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DB _strip_markdown and _count_words Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_db_strip_markdown_empty_text(fresh_db):
|
||||
"""Test strip_markdown with empty text."""
|
||||
result = fresh_db._strip_markdown("")
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_db_strip_markdown_none_text(fresh_db):
|
||||
"""Test strip_markdown with None."""
|
||||
result = fresh_db._strip_markdown(None)
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_db_strip_markdown_fenced_code_blocks(fresh_db):
|
||||
"""Test stripping fenced code blocks."""
|
||||
text = """
|
||||
Some text here
|
||||
```python
|
||||
def hello():
|
||||
print("world")
|
||||
```
|
||||
More text after
|
||||
"""
|
||||
result = fresh_db._strip_markdown(text)
|
||||
assert "def hello" not in result
|
||||
assert "Some text" in result
|
||||
assert "More text" in result
|
||||
|
||||
|
||||
def test_db_strip_markdown_inline_code(fresh_db):
|
||||
"""Test stripping inline code."""
|
||||
text = "Here is some `inline code` in text"
|
||||
result = fresh_db._strip_markdown(text)
|
||||
assert "`" not in result
|
||||
assert "inline code" not in result
|
||||
assert "Here is some" in result
|
||||
assert "in text" in result
|
||||
|
||||
|
||||
def test_db_strip_markdown_links(fresh_db):
|
||||
"""Test converting markdown links to plain text."""
|
||||
text = "Check out [this link](https://example.com) for more info"
|
||||
result = fresh_db._strip_markdown(text)
|
||||
assert "this link" in result
|
||||
assert "https://example.com" not in result
|
||||
assert "[" not in result
|
||||
assert "]" not in result
|
||||
|
||||
|
||||
def test_db_strip_markdown_emphasis_and_headers(fresh_db):
|
||||
"""Test stripping emphasis markers and headers."""
|
||||
text = """
|
||||
# Header 1
|
||||
## Header 2
|
||||
**bold text** and *italic text*
|
||||
> blockquote
|
||||
_underline_
|
||||
"""
|
||||
result = fresh_db._strip_markdown(text)
|
||||
assert "#" not in result
|
||||
assert "*" not in result
|
||||
assert "_" not in result
|
||||
assert ">" not in result
|
||||
assert "bold text" in result
|
||||
assert "italic text" in result
|
||||
|
||||
|
||||
def test_db_strip_markdown_html_tags(fresh_db):
|
||||
"""Test stripping HTML tags."""
|
||||
text = "Some <b>bold</b> and <i>italic</i> text with <div>divs</div>"
|
||||
result = fresh_db._strip_markdown(text)
|
||||
# The regex replaces tags with spaces, may leave some angle brackets from malformed HTML
|
||||
# The important thing is that the words are preserved
|
||||
assert "bold" in result
|
||||
assert "italic" in result
|
||||
assert "divs" in result
|
||||
|
||||
|
||||
def test_db_strip_markdown_complex_document(fresh_db):
|
||||
"""Test stripping complex markdown document."""
|
||||
text = """
|
||||
# My Document
|
||||
|
||||
This is a paragraph with **bold** and *italic* text.
|
||||
|
||||
```javascript
|
||||
const x = 10;
|
||||
console.log(x);
|
||||
```
|
||||
|
||||
Here's a [link](https://example.com) and some `code`.
|
||||
|
||||
> A blockquote
|
||||
|
||||
<p>HTML paragraph</p>
|
||||
"""
|
||||
result = fresh_db._strip_markdown(text)
|
||||
assert "My Document" in result
|
||||
assert "paragraph" in result
|
||||
assert "const x" not in result
|
||||
assert "https://example.com" not in result
|
||||
assert "<p>" not in result
|
||||
|
||||
|
||||
def test_db_count_words_simple(fresh_db):
|
||||
"""Test word counting on simple text."""
|
||||
text = "This is a simple test with seven words"
|
||||
count = fresh_db._count_words(text)
|
||||
assert count == 8
|
||||
|
||||
|
||||
def test_db_count_words_empty(fresh_db):
|
||||
"""Test word counting on empty text."""
|
||||
count = fresh_db._count_words("")
|
||||
assert count == 0
|
||||
|
||||
|
||||
def test_db_count_words_with_markdown(fresh_db):
|
||||
"""Test word counting strips markdown first."""
|
||||
text = "**Bold** and *italic* and `code` words"
|
||||
count = fresh_db._count_words(text)
|
||||
# Should count: Bold, and, italic, and, words (5 words, code is in backticks so stripped)
|
||||
assert count == 5
|
||||
|
||||
|
||||
def test_db_count_words_with_unicode(fresh_db):
|
||||
"""Test word counting with unicode characters."""
|
||||
text = "Hello 世界 café naïve résumé"
|
||||
count = fresh_db._count_words(text)
|
||||
# Should count all words including unicode
|
||||
assert count >= 5
|
||||
|
||||
|
||||
def test_db_count_words_with_numbers(fresh_db):
|
||||
"""Test word counting includes numbers."""
|
||||
text = "There are 123 apples and 456 oranges"
|
||||
count = fresh_db._count_words(text)
|
||||
assert count == 7
|
||||
|
||||
|
||||
def test_db_count_words_with_punctuation(fresh_db):
|
||||
"""Test word counting handles punctuation correctly."""
|
||||
text = "Hello, world! How are you? I'm fine, thanks."
|
||||
count = fresh_db._count_words(text)
|
||||
# Hello, world, How, are, you, I, m, fine, thanks = 9 words
|
||||
assert count == 9
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DB gather_stats Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_db_gather_stats_empty_database(fresh_db):
|
||||
"""Test gather_stats on empty database."""
|
||||
stats = fresh_db.gather_stats()
|
||||
|
||||
assert len(stats) == 10
|
||||
(
|
||||
pages_with_content,
|
||||
total_revisions,
|
||||
page_most_revisions,
|
||||
page_most_revisions_count,
|
||||
words_by_date,
|
||||
total_words,
|
||||
unique_tags,
|
||||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
) = stats
|
||||
|
||||
assert pages_with_content == 0
|
||||
assert total_revisions == 0
|
||||
assert page_most_revisions is None
|
||||
assert page_most_revisions_count == 0
|
||||
assert len(words_by_date) == 0
|
||||
assert total_words == 0
|
||||
assert unique_tags == 0
|
||||
assert page_most_tags is None
|
||||
assert page_most_tags_count == 0
|
||||
assert len(revisions_by_date) == 0
|
||||
|
||||
|
||||
def test_db_gather_stats_with_content(fresh_db):
|
||||
"""Test gather_stats with actual content."""
|
||||
# Add multiple pages with different content
|
||||
fresh_db.save_new_version("2024-01-01", "Hello world this is a test", "v1")
|
||||
fresh_db.save_new_version(
|
||||
"2024-01-01", "Hello world this is version two", "v2"
|
||||
) # 2nd revision
|
||||
fresh_db.save_new_version("2024-01-02", "Another page with more words here", "v1")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
|
||||
(
|
||||
pages_with_content,
|
||||
total_revisions,
|
||||
page_most_revisions,
|
||||
page_most_revisions_count,
|
||||
words_by_date,
|
||||
total_words,
|
||||
unique_tags,
|
||||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
) = stats
|
||||
|
||||
assert pages_with_content == 2
|
||||
assert total_revisions == 3
|
||||
assert page_most_revisions == "2024-01-01"
|
||||
assert page_most_revisions_count == 2
|
||||
assert total_words > 0
|
||||
assert len(words_by_date) == 2
|
||||
|
||||
|
||||
def test_db_gather_stats_word_counting(fresh_db):
|
||||
"""Test that gather_stats counts words correctly."""
|
||||
# Add page with known word count
|
||||
fresh_db.save_new_version("2024-01-01", "one two three four five", "test")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, _ = stats
|
||||
|
||||
assert total_words == 5
|
||||
|
||||
test_date = date(2024, 1, 1)
|
||||
assert test_date in words_by_date
|
||||
assert words_by_date[test_date] == 5
|
||||
|
||||
|
||||
def test_db_gather_stats_with_tags(fresh_db):
|
||||
"""Test gather_stats with tags."""
|
||||
# Add tags
|
||||
fresh_db.add_tag("tag1", "#ff0000")
|
||||
fresh_db.add_tag("tag2", "#00ff00")
|
||||
fresh_db.add_tag("tag3", "#0000ff")
|
||||
|
||||
# Add pages with tags
|
||||
fresh_db.save_new_version("2024-01-01", "Page 1", "test")
|
||||
fresh_db.save_new_version("2024-01-02", "Page 2", "test")
|
||||
|
||||
fresh_db.set_tags_for_page(
|
||||
"2024-01-01", ["tag1", "tag2", "tag3"]
|
||||
) # Page 1 has 3 tags
|
||||
fresh_db.set_tags_for_page("2024-01-02", ["tag1"]) # Page 2 has 1 tag
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, _ = stats
|
||||
|
||||
assert unique_tags == 3
|
||||
assert page_most_tags == "2024-01-01"
|
||||
assert page_most_tags_count == 3
|
||||
|
||||
|
||||
def test_db_gather_stats_revisions_by_date(fresh_db):
|
||||
"""Test revisions_by_date tracking."""
|
||||
# Add multiple revisions on different dates
|
||||
fresh_db.save_new_version("2024-01-01", "First", "v1")
|
||||
fresh_db.save_new_version("2024-01-01", "Second", "v2")
|
||||
fresh_db.save_new_version("2024-01-01", "Third", "v3")
|
||||
fresh_db.save_new_version("2024-01-02", "Fourth", "v1")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date = stats
|
||||
|
||||
assert date(2024, 1, 1) in revisions_by_date
|
||||
assert revisions_by_date[date(2024, 1, 1)] == 3
|
||||
assert date(2024, 1, 2) in revisions_by_date
|
||||
assert revisions_by_date[date(2024, 1, 2)] == 1
|
||||
|
||||
|
||||
def test_db_gather_stats_handles_malformed_dates(fresh_db):
|
||||
"""Test that gather_stats handles malformed dates gracefully."""
|
||||
# This is hard to test directly since the DB enforces date format
|
||||
# But we can test that normal dates work
|
||||
fresh_db.save_new_version("2024-01-15", "Test", "v1")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date = stats
|
||||
|
||||
# Should have parsed the date correctly
|
||||
assert date(2024, 1, 15) in revisions_by_date
|
||||
|
||||
|
||||
def test_db_gather_stats_current_version_only(fresh_db):
|
||||
"""Test that word counts use current version only, not all revisions."""
|
||||
# Add multiple revisions
|
||||
fresh_db.save_new_version("2024-01-01", "one two three", "v1")
|
||||
fresh_db.save_new_version("2024-01-01", "one two three four five", "v2")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, _ = stats
|
||||
|
||||
# Should count words from current version (5 words), not old version
|
||||
assert total_words == 5
|
||||
assert words_by_date[date(2024, 1, 1)] == 5
|
||||
|
||||
|
||||
def test_db_gather_stats_no_tags(fresh_db):
|
||||
"""Test gather_stats when there are no tags."""
|
||||
fresh_db.save_new_version("2024-01-01", "No tags here", "test")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, _ = stats
|
||||
|
||||
assert unique_tags == 0
|
||||
assert page_most_tags is None
|
||||
assert page_most_tags_count == 0
|
||||
|
||||
|
||||
def test_db_gather_stats_exception_in_dates_with_content(fresh_db, monkeypatch):
|
||||
"""Test that gather_stats handles exception in dates_with_content."""
|
||||
|
||||
def bad_dates():
|
||||
raise RuntimeError("Simulated error")
|
||||
|
||||
monkeypatch.setattr(fresh_db, "dates_with_content", bad_dates)
|
||||
|
||||
# Should still return stats without crashing
|
||||
stats = fresh_db.gather_stats()
|
||||
pages_with_content = stats[0]
|
||||
|
||||
# Should default to 0 when exception occurs
|
||||
assert pages_with_content == 0
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ from bouquin.markdown_highlighter import MarkdownHighlighter
|
|||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||
|
||||
|
||||
def _today():
|
||||
from datetime import date
|
||||
|
||||
return date.today().isoformat()
|
||||
|
||||
|
||||
def text(editor) -> str:
|
||||
return editor.toPlainText()
|
||||
|
||||
|
|
@ -1464,3 +1470,192 @@ def test_markdown_highlighter_switch_dark_mode(app):
|
|||
both_valid = light_bg.isValid() and dark_bg.isValid()
|
||||
|
||||
assert is_light_lighter or both_valid # At least colors are being set
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MarkdownHighlighter Tests - Missing Coverage
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_markdown_highlighter_code_block_detection(qtbot, app):
|
||||
"""Test code block detection and highlighting."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
# Set text with code block
|
||||
text = """
|
||||
Some text
|
||||
```python
|
||||
def hello():
|
||||
pass
|
||||
```
|
||||
More text
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
# The highlighter should process the text
|
||||
# Just ensure no crash
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_headers(qtbot, app):
|
||||
"""Test header highlighting."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = """
|
||||
# Header 1
|
||||
## Header 2
|
||||
### Header 3
|
||||
Normal text
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_emphasis(qtbot, app):
|
||||
"""Test emphasis highlighting."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = "**bold** and *italic* and ***both***"
|
||||
doc.setPlainText(text)
|
||||
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_horizontal_rule(qtbot, app):
|
||||
"""Test horizontal rule highlighting."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = """
|
||||
Text above
|
||||
---
|
||||
Text below
|
||||
***
|
||||
More text
|
||||
___
|
||||
End
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_complex_document(qtbot, app):
|
||||
"""Test highlighting a complex document with mixed elements."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = """
|
||||
# Main Title
|
||||
|
||||
This is a paragraph with **bold** and *italic* text.
|
||||
|
||||
## Code Example
|
||||
|
||||
Here's some `inline code` and a block:
|
||||
|
||||
```python
|
||||
def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
||||
- Item with *emphasis*
|
||||
- Another item with **bold**
|
||||
- [A link](https://example.com)
|
||||
|
||||
> A blockquote with **formatted** text
|
||||
> Second line
|
||||
|
||||
---
|
||||
|
||||
### Final Section
|
||||
|
||||
~~Strikethrough~~ and normal text.
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
# Should handle complex document
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_empty_document(qtbot, app):
|
||||
"""Test highlighting empty document."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
doc.setPlainText("")
|
||||
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_update_on_text_change(qtbot, app):
|
||||
"""Test that highlighter updates when text changes."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
doc.setPlainText("Initial text")
|
||||
doc.setPlainText("# Header text")
|
||||
doc.setPlainText("**Bold text**")
|
||||
|
||||
# Should handle updates
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_nested_emphasis(qtbot, app):
|
||||
"""Test nested emphasis patterns."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = "This has **bold with *italic* inside** and more"
|
||||
doc.setPlainText(text)
|
||||
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_unclosed_code_block(qtbot, app):
|
||||
"""Test handling of unclosed code block."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = """
|
||||
```python
|
||||
def hello():
|
||||
print("world")
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
# Should handle gracefully
|
||||
assert highlighter is not None
|
||||
|
||||
|
||||
def test_markdown_highlighter_special_characters(qtbot, app):
|
||||
"""Test handling special characters in markdown."""
|
||||
theme_manager = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
doc = QTextDocument()
|
||||
highlighter = MarkdownHighlighter(doc, theme_manager)
|
||||
|
||||
text = """
|
||||
Special chars: < > & " '
|
||||
Escaped: \\* \\_ \\`
|
||||
Unicode: 你好 café résumé
|
||||
"""
|
||||
doc.setPlainText(text)
|
||||
|
||||
assert highlighter is not None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import datetime as _dt
|
||||
|
||||
from PySide6.QtWidgets import QLabel
|
||||
|
||||
from bouquin.statistics_dialog import StatisticsDialog
|
||||
from bouquin import strings
|
||||
|
||||
from datetime import date
|
||||
from PySide6.QtCore import Qt, QPoint
|
||||
from PySide6.QtWidgets import QLabel
|
||||
from PySide6.QtTest import QTest
|
||||
|
||||
from bouquin.statistics_dialog import DateHeatmap, StatisticsDialog
|
||||
|
||||
|
||||
class FakeStatsDB:
|
||||
"""Minimal stub that returns a fixed stats payload."""
|
||||
|
|
@ -104,3 +108,312 @@ def test_statistics_dialog_no_data_shows_placeholder(qtbot):
|
|||
# When there's no data, the heatmap and metric combo shouldn't exist
|
||||
assert not hasattr(dlg, "metric_combo")
|
||||
assert not hasattr(dlg, "_heatmap")
|
||||
|
||||
|
||||
def _date(year, month, day):
|
||||
return date(year, month, day)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DateHeatmapTests - Missing Coverage
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_activity_heatmap_empty_data(qtbot):
|
||||
"""Test heatmap with empty data dict."""
|
||||
strings.load_strings("en")
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Set empty data
|
||||
heatmap.set_data({})
|
||||
|
||||
# Should handle empty data gracefully
|
||||
assert heatmap._start is None
|
||||
assert heatmap._end is None
|
||||
assert heatmap._max_value == 0
|
||||
|
||||
# Size hint should return default dimensions
|
||||
size = heatmap.sizeHint()
|
||||
assert size.width() > 0
|
||||
assert size.height() > 0
|
||||
|
||||
# Paint should not crash
|
||||
heatmap.update()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_activity_heatmap_none_data(qtbot):
|
||||
"""Test heatmap with None data."""
|
||||
strings.load_strings("en")
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Set None data
|
||||
heatmap.set_data(None)
|
||||
|
||||
assert heatmap._start is None
|
||||
assert heatmap._end is None
|
||||
|
||||
# Paint event should return early
|
||||
heatmap.update()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_activity_heatmap_click_when_no_data(qtbot):
|
||||
"""Test clicking heatmap when there's no data."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
heatmap.set_data({})
|
||||
|
||||
# Simulate click - should not crash or emit signal
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Click in the middle of widget
|
||||
pos = QPoint(100, 100)
|
||||
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
|
||||
|
||||
# Should not have clicked any date
|
||||
assert len(clicked_dates) == 0
|
||||
|
||||
|
||||
def test_activity_heatmap_click_outside_grid(qtbot):
|
||||
"""Test clicking outside the grid area."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Set some data
|
||||
data = {
|
||||
date(2024, 1, 1): 5,
|
||||
date(2024, 1, 2): 10,
|
||||
}
|
||||
heatmap.set_data(data)
|
||||
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Click in top-left margin (before grid starts)
|
||||
pos = QPoint(5, 5)
|
||||
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
|
||||
|
||||
assert len(clicked_dates) == 0
|
||||
|
||||
|
||||
def test_activity_heatmap_click_beyond_end_date(qtbot):
|
||||
"""Test clicking on trailing empty cells beyond the last date."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Set data that doesn't fill a complete week
|
||||
data = {
|
||||
date(2024, 1, 1): 5, # Monday
|
||||
date(2024, 1, 2): 10, # Tuesday
|
||||
}
|
||||
heatmap.set_data(data)
|
||||
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Try clicking far to the right (beyond end date)
|
||||
# This is tricky to target precisely, but we can simulate
|
||||
pos = QPoint(1000, 50) # Far right
|
||||
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
|
||||
|
||||
# Should either not click or only click valid dates
|
||||
# If it did click, it should be a valid date within range
|
||||
if clicked_dates:
|
||||
assert clicked_dates[0] <= date(2024, 1, 2)
|
||||
|
||||
|
||||
def test_activity_heatmap_click_invalid_row(qtbot):
|
||||
"""Test clicking below the 7 weekday rows."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
data = {
|
||||
date(2024, 1, 1): 5,
|
||||
date(2024, 1, 8): 10,
|
||||
}
|
||||
heatmap.set_data(data)
|
||||
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Click below the grid (row 8 or higher)
|
||||
pos = QPoint(100, 500) # Very low Y
|
||||
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
|
||||
|
||||
assert len(clicked_dates) == 0
|
||||
|
||||
|
||||
def test_activity_heatmap_right_click_ignored(qtbot):
|
||||
"""Test that right-click is ignored."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
data = {date(2024, 1, 1): 5}
|
||||
heatmap.set_data(data)
|
||||
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Right click should be ignored
|
||||
pos = QPoint(100, 100)
|
||||
QTest.mouseClick(heatmap, Qt.RightButton, pos=pos)
|
||||
|
||||
assert len(clicked_dates) == 0
|
||||
|
||||
|
||||
def test_activity_heatmap_month_label_rendering(qtbot):
|
||||
"""Test heatmap spanning multiple months renders month labels."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Data spanning multiple months
|
||||
data = {
|
||||
date(2024, 1, 1): 5,
|
||||
date(2024, 1, 15): 10,
|
||||
date(2024, 2, 1): 8,
|
||||
date(2024, 2, 15): 12,
|
||||
date(2024, 3, 1): 6,
|
||||
}
|
||||
heatmap.set_data(data)
|
||||
|
||||
# Should calculate proper size
|
||||
size = heatmap.sizeHint()
|
||||
assert size.width() > 0
|
||||
assert size.height() > 0
|
||||
|
||||
# Paint should work without crashing
|
||||
heatmap.update()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_activity_heatmap_same_month_continues(qtbot):
|
||||
"""Test that month labels skip weeks in the same month."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
# Multiple dates in same month
|
||||
data = {}
|
||||
for day in range(1, 29): # January 1-28
|
||||
data[date(2024, 1, day)] = day
|
||||
|
||||
heatmap.set_data(data)
|
||||
|
||||
# Should render without issues
|
||||
heatmap.update()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_activity_heatmap_data_with_zero_values(qtbot):
|
||||
"""Test heatmap with zero values in data."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
data = {
|
||||
date(2024, 1, 1): 0,
|
||||
date(2024, 1, 2): 5,
|
||||
date(2024, 1, 3): 0,
|
||||
}
|
||||
heatmap.set_data(data)
|
||||
|
||||
assert heatmap._max_value == 5
|
||||
|
||||
heatmap.update()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_activity_heatmap_single_day(qtbot):
|
||||
"""Test heatmap with just one day of data."""
|
||||
heatmap = DateHeatmap()
|
||||
qtbot.addWidget(heatmap)
|
||||
heatmap.show()
|
||||
|
||||
data = {date(2024, 1, 15): 10}
|
||||
heatmap.set_data(data)
|
||||
|
||||
# Should handle single day
|
||||
assert heatmap._start is not None
|
||||
assert heatmap._end is not None
|
||||
|
||||
clicked_dates = []
|
||||
heatmap.date_clicked.connect(clicked_dates.append)
|
||||
|
||||
# Click should work
|
||||
pos = QPoint(100, 100)
|
||||
QTest.mouseClick(heatmap, Qt.LeftButton, pos=pos)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# StatisticsDialog Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_statistics_dialog_with_empty_database(qtbot, fresh_db):
|
||||
"""Test statistics dialog with an empty database."""
|
||||
strings.load_strings("en")
|
||||
|
||||
dialog = StatisticsDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Should handle empty database gracefully
|
||||
assert dialog.isVisible()
|
||||
|
||||
# Heatmap should be empty
|
||||
heatmap = dialog.findChild(DateHeatmap)
|
||||
if heatmap:
|
||||
# No crash when displaying empty heatmap
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_statistics_dialog_with_data(qtbot, fresh_db):
|
||||
"""Test statistics dialog with actual data."""
|
||||
strings.load_strings("en")
|
||||
|
||||
# Add some content
|
||||
fresh_db.save_new_version("2024-01-01", "Hello world", "test")
|
||||
fresh_db.save_new_version("2024-01-02", "More content here", "test")
|
||||
fresh_db.save_new_version("2024-01-03", "Even more text", "test")
|
||||
|
||||
dialog = StatisticsDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Should display statistics
|
||||
assert dialog.isVisible()
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_statistics_dialog_gather_stats_exception_handling(
|
||||
qtbot, fresh_db, monkeypatch
|
||||
):
|
||||
"""Test that gather_stats handles exceptions gracefully."""
|
||||
strings.load_strings("en")
|
||||
|
||||
# Make dates_with_content raise an exception
|
||||
def bad_dates_with_content():
|
||||
raise RuntimeError("Simulated DB error")
|
||||
|
||||
monkeypatch.setattr(fresh_db, "dates_with_content", bad_dates_with_content)
|
||||
|
||||
# Should still create dialog without crashing
|
||||
dialog = StatisticsDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
dialog.show()
|
||||
|
||||
# Should handle error gracefully
|
||||
assert dialog.isVisible()
|
||||
|
|
|
|||
|
|
@ -1,365 +0,0 @@
|
|||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from pyqtgraph.Qt import QtCore
|
||||
|
||||
from bouquin.tag_graph_dialog import TagGraphDialog, DraggableGraphItem
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for DraggableGraphItem tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DummyPos:
|
||||
"""Simple object with x()/y() so it looks like a QPointF to the code."""
|
||||
|
||||
def __init__(self, x, y):
|
||||
self._x = float(x)
|
||||
self._y = float(y)
|
||||
|
||||
def x(self):
|
||||
return self._x
|
||||
|
||||
def y(self):
|
||||
return self._y
|
||||
|
||||
|
||||
class FakePoint:
|
||||
"""Minimal object returned from scatter.pointsAt()."""
|
||||
|
||||
def __init__(self, idx):
|
||||
self._idx = idx
|
||||
|
||||
def index(self):
|
||||
return self._idx
|
||||
|
||||
|
||||
class FakeDragEvent:
|
||||
"""Stub object that looks like pyqtgraph's mouse drag event."""
|
||||
|
||||
def __init__(self, stage, button, start_pos, move_pos):
|
||||
# stage is one of: "start", "move", "finish"
|
||||
self._stage = stage
|
||||
self._button = button
|
||||
self._start_pos = start_pos
|
||||
self._pos = move_pos
|
||||
self.accepted = False
|
||||
self.ignored = False
|
||||
|
||||
# Life-cycle ---------------------------------------------------------
|
||||
def isStart(self):
|
||||
return self._stage == "start"
|
||||
|
||||
def isFinish(self):
|
||||
return self._stage == "finish"
|
||||
|
||||
# Buttons / positions ------------------------------------------------
|
||||
def button(self):
|
||||
return self._button
|
||||
|
||||
def buttonDownPos(self):
|
||||
return self._start_pos
|
||||
|
||||
def pos(self):
|
||||
return self._pos
|
||||
|
||||
# Accept / ignore ----------------------------------------------------
|
||||
def accept(self):
|
||||
self.accepted = True
|
||||
|
||||
def ignore(self):
|
||||
self.ignored = True
|
||||
|
||||
|
||||
class FakeHoverEvent:
|
||||
"""Stub for hoverEvent tests."""
|
||||
|
||||
def __init__(self, pos=None, exit=False):
|
||||
self._pos = pos
|
||||
self._exit = exit
|
||||
|
||||
def isExit(self):
|
||||
return self._exit
|
||||
|
||||
def pos(self):
|
||||
return self._pos
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DraggableGraphItem
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_draggable_graph_item_setdata_caches_kwargs(app):
|
||||
item = DraggableGraphItem()
|
||||
pos1 = np.array([[0.0, 0.0]], dtype=float)
|
||||
adj = np.zeros((0, 2), dtype=int)
|
||||
sizes = np.array([5.0], dtype=float)
|
||||
|
||||
# First call sets all kwargs
|
||||
item.setData(pos=pos1, adj=adj, size=sizes)
|
||||
|
||||
assert item.pos is pos1
|
||||
assert "adj" in item._data_kwargs
|
||||
assert "size" in item._data_kwargs
|
||||
|
||||
# Second call only passes pos; cached kwargs should keep size/adj
|
||||
pos2 = np.array([[1.0, 1.0]], dtype=float)
|
||||
item.setData(pos=pos2)
|
||||
|
||||
assert item.pos is pos2
|
||||
assert item._data_kwargs["adj"] is adj
|
||||
# size should still be present and unchanged
|
||||
assert np.all(item._data_kwargs["size"] == sizes)
|
||||
|
||||
|
||||
def test_draggable_graph_item_drag_updates_position_and_calls_callback(app):
|
||||
moved = []
|
||||
|
||||
def on_pos_changed(pos):
|
||||
# Store a copy so later mutations don't affect our assertion
|
||||
moved.append(np.array(pos, copy=True))
|
||||
|
||||
item = DraggableGraphItem(on_position_changed=on_pos_changed)
|
||||
|
||||
# Simple 2-node graph
|
||||
pos = np.array([[0.0, 0.0], [5.0, 5.0]], dtype=float)
|
||||
adj = np.array([[0, 1]], dtype=int)
|
||||
item.setData(pos=pos, adj=adj, size=np.array([5.0, 5.0], dtype=float))
|
||||
|
||||
# Make pointsAt always return the first node
|
||||
item.scatter.pointsAt = lambda p: [FakePoint(0)]
|
||||
|
||||
# Start drag on node 0 at (0, 0)
|
||||
start_ev = FakeDragEvent(
|
||||
stage="start",
|
||||
button=QtCore.Qt.MouseButton.LeftButton,
|
||||
start_pos=DummyPos(0.0, 0.0),
|
||||
move_pos=None,
|
||||
)
|
||||
item.mouseDragEvent(start_ev)
|
||||
assert item._drag_index == 0
|
||||
assert start_ev.accepted
|
||||
assert not start_ev.ignored
|
||||
|
||||
# Move mouse to (2, 3) – node 0 should follow exactly
|
||||
move_ev = FakeDragEvent(
|
||||
stage="move",
|
||||
button=QtCore.Qt.MouseButton.LeftButton,
|
||||
start_pos=DummyPos(0.0, 0.0),
|
||||
move_pos=DummyPos(2.0, 3.0),
|
||||
)
|
||||
item.mouseDragEvent(move_ev)
|
||||
assert move_ev.accepted
|
||||
assert not move_ev.ignored
|
||||
|
||||
assert item.pos.shape == (2, 2)
|
||||
assert item.pos[0, 0] == pytest.approx(2.0)
|
||||
assert item.pos[0, 1] == pytest.approx(3.0)
|
||||
|
||||
# Callback should have been invoked with the updated positions
|
||||
assert moved, "on_position_changed should be called at least once"
|
||||
np.testing.assert_allclose(moved[-1][0], [2.0, 3.0], atol=1e-6)
|
||||
|
||||
# Finish drag: internal state should reset
|
||||
finish_ev = FakeDragEvent(
|
||||
stage="finish",
|
||||
button=QtCore.Qt.MouseButton.LeftButton,
|
||||
start_pos=DummyPos(0.0, 0.0),
|
||||
move_pos=DummyPos(2.0, 3.0),
|
||||
)
|
||||
item.mouseDragEvent(finish_ev)
|
||||
assert finish_ev.accepted
|
||||
assert item._drag_index is None
|
||||
assert item._drag_offset is None
|
||||
|
||||
|
||||
def test_draggable_graph_item_ignores_non_left_button(app):
|
||||
item = DraggableGraphItem()
|
||||
|
||||
pos = np.array([[0.0, 0.0]], dtype=float)
|
||||
adj = np.zeros((0, 2), dtype=int)
|
||||
item.setData(pos=pos, adj=adj, size=np.array([5.0], dtype=float))
|
||||
|
||||
# pointsAt would return something, but the button is not left,
|
||||
# so the event should be ignored.
|
||||
item.scatter.pointsAt = lambda p: [FakePoint(0)]
|
||||
|
||||
ev = FakeDragEvent(
|
||||
stage="start",
|
||||
button=QtCore.Qt.MouseButton.RightButton,
|
||||
start_pos=DummyPos(0.0, 0.0),
|
||||
move_pos=None,
|
||||
)
|
||||
item.mouseDragEvent(ev)
|
||||
assert ev.ignored
|
||||
assert not ev.accepted
|
||||
assert item._drag_index is None
|
||||
|
||||
|
||||
def test_draggable_graph_item_hover_reports_index_and_exit(app):
|
||||
hovered = []
|
||||
|
||||
def on_hover(idx, ev):
|
||||
hovered.append(idx)
|
||||
|
||||
item = DraggableGraphItem(on_hover=on_hover)
|
||||
|
||||
# Case 1: exit event should report None
|
||||
ev_exit = FakeHoverEvent(exit=True)
|
||||
item.hoverEvent(ev_exit)
|
||||
assert hovered[-1] is None
|
||||
|
||||
# Case 2: no points under mouse -> None
|
||||
item.scatter.pointsAt = lambda p: []
|
||||
ev_none = FakeHoverEvent(pos=DummyPos(0.0, 0.0))
|
||||
item.hoverEvent(ev_none)
|
||||
assert hovered[-1] is None
|
||||
|
||||
# Case 3: one point under mouse -> its index
|
||||
item.scatter.pointsAt = lambda p: [FakePoint(3)]
|
||||
ev_hit = FakeHoverEvent(pos=DummyPos(1.0, 2.0))
|
||||
item.hoverEvent(ev_hit)
|
||||
assert hovered[-1] == 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TagGraphDialog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EmptyTagDB:
|
||||
"""DB stub that returns no tag data."""
|
||||
|
||||
def get_tag_cooccurrences(self):
|
||||
return {}, [], {}
|
||||
|
||||
|
||||
class SimpleTagDB:
|
||||
"""Deterministic stub for tag co-occurrence data."""
|
||||
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def get_tag_cooccurrences(self):
|
||||
self.called = True
|
||||
tags_by_id = {
|
||||
1: (1, "alpha", "#ff0000"),
|
||||
2: (2, "beta", "#00ff00"),
|
||||
3: (3, "gamma", "#0000ff"),
|
||||
}
|
||||
edges = [
|
||||
(1, 2, 3),
|
||||
(2, 3, 1),
|
||||
]
|
||||
tag_page_counts = {1: 5, 2: 3, 3: 1}
|
||||
return tags_by_id, edges, tag_page_counts
|
||||
|
||||
|
||||
def test_tag_graph_dialog_handles_empty_db(app, qtbot):
|
||||
dlg = TagGraphDialog(EmptyTagDB())
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
# When there are no tags, nothing should be populated
|
||||
assert dlg._tag_ids == []
|
||||
assert dlg._label_items == []
|
||||
assert dlg._tag_names == {}
|
||||
assert dlg._tag_page_counts == {}
|
||||
|
||||
|
||||
def test_tag_graph_dialog_populates_graph_from_db(app, qtbot):
|
||||
db = SimpleTagDB()
|
||||
dlg = TagGraphDialog(db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
assert db.called
|
||||
|
||||
# Basic invariants about the populated state
|
||||
assert dlg._tag_ids == [1, 2, 3]
|
||||
assert dlg._tag_names[1] == "alpha"
|
||||
assert dlg._tag_page_counts[1] == 5
|
||||
|
||||
# GraphItem should have one position per node
|
||||
assert dlg.graph_item.pos.shape == (3, 2)
|
||||
|
||||
# Labels and halo state should match number of tags
|
||||
assert len(dlg._label_items) == 3
|
||||
assert len(dlg._halo_sizes) == 3
|
||||
assert len(dlg._halo_brushes) == 3
|
||||
|
||||
|
||||
def test_tag_graph_dialog_on_positions_changed_updates_labels_and_halo(
|
||||
app, qtbot, monkeypatch
|
||||
):
|
||||
db = SimpleTagDB()
|
||||
dlg = TagGraphDialog(db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
assert len(dlg._label_items) == 3
|
||||
|
||||
# Set up fake halo sizes/brushes so the halo branch runs
|
||||
dlg._halo_sizes = [10.0, 20.0, 30.0]
|
||||
dlg._halo_brushes = ["a", "b", "c"]
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_set_data(*, x, y, size, brush, pen):
|
||||
captured["x"] = x
|
||||
captured["y"] = y
|
||||
captured["size"] = size
|
||||
captured["brush"] = brush
|
||||
captured["pen"] = pen
|
||||
|
||||
monkeypatch.setattr(dlg._halo_item, "setData", fake_set_data)
|
||||
|
||||
# New layout positions
|
||||
pos = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], dtype=float)
|
||||
dlg._on_positions_changed(pos)
|
||||
|
||||
# Each label should be slightly below its node (y + 0.15)
|
||||
for i, label in enumerate(dlg._label_items):
|
||||
assert label.pos().x() == pytest.approx(pos[i, 0])
|
||||
assert label.pos().y() == pytest.approx(pos[i, 1] + 0.15)
|
||||
|
||||
# Halo layer should receive the updated coordinates and our sizes/brushes
|
||||
assert captured["x"] == [1.0, 3.0, 5.0]
|
||||
assert captured["y"] == [2.0, 4.0, 6.0]
|
||||
assert captured["size"] == dlg._halo_sizes
|
||||
assert captured["brush"] == dlg._halo_brushes
|
||||
assert captured["pen"] is None
|
||||
|
||||
|
||||
def test_tag_graph_dialog_hover_index_shows_and_hides_tooltip(app, qtbot, monkeypatch):
|
||||
db = SimpleTagDB()
|
||||
dlg = TagGraphDialog(db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
shown = {}
|
||||
hidden = {"called": False}
|
||||
|
||||
def fake_show_text(pos, text, widget):
|
||||
shown["pos"] = pos
|
||||
shown["text"] = text
|
||||
shown["widget"] = widget
|
||||
|
||||
def fake_hide_text():
|
||||
hidden["called"] = True
|
||||
|
||||
# Patch the module-level QToolTip used by TagGraphDialog
|
||||
monkeypatch.setattr("bouquin.tag_graph_dialog.QToolTip.showText", fake_show_text)
|
||||
monkeypatch.setattr("bouquin.tag_graph_dialog.QToolTip.hideText", fake_hide_text)
|
||||
|
||||
# Hover over first node (index 0)
|
||||
dlg._on_hover_index(0, ev=None)
|
||||
assert "alpha" in shown["text"]
|
||||
assert "page" in shown["text"]
|
||||
assert shown["widget"] is dlg
|
||||
|
||||
# Now simulate leaving the item entirely
|
||||
dlg._on_hover_index(None, ev=None)
|
||||
assert hidden["called"]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
from PySide6.QtCore import Qt, QPoint, QEvent
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtGui import QMouseEvent, QColor
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox, QInputDialog, QColorDialog
|
||||
from bouquin.db import DBManager
|
||||
from bouquin.strings import load_strings
|
||||
|
|
@ -9,6 +9,8 @@ from bouquin.tag_browser import TagBrowserDialog
|
|||
from bouquin.flow_layout import FlowLayout
|
||||
from sqlcipher3.dbapi2 import IntegrityError
|
||||
|
||||
import bouquin.strings as strings
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DB Layer Tag Tests
|
||||
|
|
@ -1798,3 +1800,360 @@ def test_multiple_widgets_same_database(app, fresh_db):
|
|||
widget2._on_toggle(True)
|
||||
|
||||
assert widget2.chip_layout.count() == 1
|
||||
|
||||
|
||||
def test_tag_browser_add_tag_with_color(qtbot, fresh_db, monkeypatch):
|
||||
"""Test adding a new tag with color selection."""
|
||||
strings.load_strings("en")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
# Mock input dialog and color dialog
|
||||
def mock_get_text(*args, **kwargs):
|
||||
return "NewTag", True
|
||||
|
||||
def mock_get_color(initial, parent):
|
||||
return QColor("#ff0000")
|
||||
|
||||
monkeypatch.setattr(QInputDialog, "getText", mock_get_text)
|
||||
monkeypatch.setattr(QColorDialog, "getColor", mock_get_color)
|
||||
|
||||
tags_before = len(fresh_db.list_tags())
|
||||
|
||||
# Trigger add tag
|
||||
browser._add_a_tag()
|
||||
|
||||
# Should have added tag
|
||||
tags_after = len(fresh_db.list_tags())
|
||||
assert tags_after == tags_before + 1
|
||||
|
||||
|
||||
def test_tag_browser_add_tag_cancelled_at_name(qtbot, fresh_db, monkeypatch):
|
||||
"""Test cancelling tag addition at name input."""
|
||||
strings.load_strings("en")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
# Mock cancelled input
|
||||
def mock_get_text(*args, **kwargs):
|
||||
return "", False
|
||||
|
||||
monkeypatch.setattr(QInputDialog, "getText", mock_get_text)
|
||||
|
||||
tags_before = len(fresh_db.list_tags())
|
||||
|
||||
browser._add_a_tag()
|
||||
|
||||
# Should not have added tag
|
||||
tags_after = len(fresh_db.list_tags())
|
||||
assert tags_after == tags_before
|
||||
|
||||
|
||||
def test_tag_browser_add_tag_cancelled_at_color(qtbot, fresh_db, monkeypatch):
|
||||
"""Test cancelling tag addition at color selection."""
|
||||
strings.load_strings("en")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
# Name input succeeds, color cancelled
|
||||
def mock_get_text(*args, **kwargs):
|
||||
return "NewTag", True
|
||||
|
||||
def mock_get_color(initial, parent):
|
||||
return QColor() # Invalid color = cancelled
|
||||
|
||||
monkeypatch.setattr(QInputDialog, "getText", mock_get_text)
|
||||
monkeypatch.setattr(QColorDialog, "getColor", mock_get_color)
|
||||
|
||||
tags_before = len(fresh_db.list_tags())
|
||||
|
||||
browser._add_a_tag()
|
||||
|
||||
# Should not have added tag
|
||||
tags_after = len(fresh_db.list_tags())
|
||||
assert tags_after == tags_before
|
||||
|
||||
|
||||
def test_tag_browser_add_duplicate_tag_shows_error(qtbot, fresh_db, monkeypatch):
|
||||
"""Test adding duplicate tag shows error."""
|
||||
strings.load_strings("en")
|
||||
|
||||
# Add existing tag
|
||||
fresh_db.add_tag("Existing", "#ff0000")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
# Try to add same tag
|
||||
def mock_get_text(*args, **kwargs):
|
||||
return "Existing", True
|
||||
|
||||
def mock_get_color(initial, parent):
|
||||
return QColor("#00ff00")
|
||||
|
||||
monkeypatch.setattr(QInputDialog, "getText", mock_get_text)
|
||||
monkeypatch.setattr(QColorDialog, "getColor", mock_get_color)
|
||||
|
||||
critical_shown = {"shown": False}
|
||||
|
||||
def mock_critical(*args):
|
||||
critical_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "critical", mock_critical)
|
||||
|
||||
browser._add_a_tag()
|
||||
|
||||
# Should show error
|
||||
assert critical_shown["shown"]
|
||||
|
||||
|
||||
def test_tag_browser_edit_tag_integrity_error(qtbot, fresh_db, monkeypatch):
|
||||
"""Test editing tag to duplicate name shows error."""
|
||||
strings.load_strings("en")
|
||||
|
||||
# Add two tags
|
||||
fresh_db.add_tag("Tag1", "#ff0000")
|
||||
fresh_db.add_tag("Tag2", "#00ff00")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
# Select first tag
|
||||
browser._populate(None)
|
||||
tree = browser.tree
|
||||
if tree.topLevelItemCount() > 0:
|
||||
tree.setCurrentItem(tree.topLevelItem(0))
|
||||
|
||||
# Try to rename to Tag2 (duplicate)
|
||||
def mock_get_text(*args, **kwargs):
|
||||
return "Tag2", True
|
||||
|
||||
monkeypatch.setattr(QInputDialog, "getText", mock_get_text)
|
||||
|
||||
critical_shown = {"shown": False}
|
||||
|
||||
def mock_critical(*args):
|
||||
critical_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "critical", mock_critical)
|
||||
|
||||
browser._edit_tag_name()
|
||||
|
||||
# Should show error
|
||||
assert critical_shown["shown"]
|
||||
|
||||
|
||||
def test_tag_browser_change_tag_color_integrity_error(qtbot, fresh_db, monkeypatch):
|
||||
"""Test changing tag color with integrity error."""
|
||||
strings.load_strings("en")
|
||||
|
||||
fresh_db.add_tag("TestTag", "#ff0000")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
browser._populate(None)
|
||||
tree = browser.tree
|
||||
if tree.topLevelItemCount() > 0:
|
||||
tree.setCurrentItem(tree.topLevelItem(0))
|
||||
|
||||
# Mock color dialog
|
||||
def mock_get_color(initial, parent):
|
||||
return QColor("#00ff00")
|
||||
|
||||
monkeypatch.setattr(QColorDialog, "getColor", mock_get_color)
|
||||
|
||||
# Mock update_tag to raise IntegrityError
|
||||
fresh_db.update_tag
|
||||
|
||||
def bad_update(*args):
|
||||
raise IntegrityError("Simulated error")
|
||||
|
||||
monkeypatch.setattr(fresh_db, "update_tag", bad_update)
|
||||
|
||||
critical_shown = {"shown": False}
|
||||
|
||||
def mock_critical(*args):
|
||||
critical_shown["shown"] = True
|
||||
|
||||
monkeypatch.setattr(QMessageBox, "critical", mock_critical)
|
||||
|
||||
browser._change_tag_color()
|
||||
|
||||
# Should show error
|
||||
assert critical_shown["shown"]
|
||||
|
||||
|
||||
def test_tag_browser_change_tag_color_cancelled(qtbot, fresh_db, monkeypatch):
|
||||
"""Test cancelling color change."""
|
||||
strings.load_strings("en")
|
||||
|
||||
fresh_db.add_tag("TestTag", "#ff0000")
|
||||
|
||||
browser = TagBrowserDialog(fresh_db)
|
||||
qtbot.addWidget(browser)
|
||||
browser.show()
|
||||
|
||||
browser._populate(None)
|
||||
tree = browser.tree
|
||||
if tree.topLevelItemCount() > 0:
|
||||
tree.setCurrentItem(tree.topLevelItem(0))
|
||||
|
||||
# Mock cancelled color dialog
|
||||
def mock_get_color(initial, parent):
|
||||
return QColor() # Invalid = cancelled
|
||||
|
||||
monkeypatch.setattr(QColorDialog, "getColor", mock_get_color)
|
||||
|
||||
# Should not crash
|
||||
browser._change_tag_color()
|
||||
|
||||
|
||||
def test_tag_chip_runtime_error_on_mouse_release(qtbot, monkeypatch):
|
||||
"""Test TagChip handles RuntimeError on mouseReleaseEvent."""
|
||||
chip = TagChip(1, "test", "#ff0000")
|
||||
qtbot.addWidget(chip)
|
||||
chip.show()
|
||||
|
||||
# Mock super().mouseReleaseEvent to raise RuntimeError
|
||||
from PySide6.QtWidgets import QFrame
|
||||
|
||||
original_mouse_release = QFrame.mouseReleaseEvent
|
||||
|
||||
def bad_mouse_release(self, event):
|
||||
raise RuntimeError("Widget deleted")
|
||||
|
||||
monkeypatch.setattr(QFrame, "mouseReleaseEvent", bad_mouse_release)
|
||||
|
||||
clicked_names = []
|
||||
chip.clicked.connect(clicked_names.append)
|
||||
|
||||
# Simulate left click
|
||||
from PySide6.QtTest import QTest
|
||||
|
||||
QTest.mouseClick(chip, Qt.LeftButton)
|
||||
|
||||
# Should have emitted signal despite RuntimeError
|
||||
assert "test" in clicked_names
|
||||
|
||||
# Restore original
|
||||
monkeypatch.setattr(QFrame, "mouseReleaseEvent", original_mouse_release)
|
||||
|
||||
|
||||
def test_page_tags_widget_many_tags(qtbot, fresh_db):
|
||||
"""Test page tags widget with many tags."""
|
||||
strings.load_strings("en")
|
||||
|
||||
# Add many tags
|
||||
for i in range(20):
|
||||
fresh_db.add_tag(f"Tag{i}", f"#{i:02x}0000")
|
||||
|
||||
fresh_db.save_new_version("2024-01-01", "Content", "test")
|
||||
|
||||
# Add all tags to page
|
||||
tag_names = [f"Tag{i}" for i in range(20)]
|
||||
fresh_db.set_tags_for_page("2024-01-01", tag_names)
|
||||
|
||||
widget = PageTagsWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
|
||||
# Set current date
|
||||
widget.set_current_date("2024-01-01")
|
||||
|
||||
# Should display all tags
|
||||
qtbot.wait(50)
|
||||
|
||||
|
||||
def test_page_tags_widget_tag_click(qtbot, fresh_db):
|
||||
"""Test clicking on a tag in PageTagsWidget."""
|
||||
strings.load_strings("en")
|
||||
|
||||
fresh_db.add_tag("Clickable", "#ff0000")
|
||||
fresh_db.save_new_version("2024-01-01", "Content", "test")
|
||||
fresh_db.set_tags_for_page("2024-01-01", ["Clickable"])
|
||||
|
||||
widget = PageTagsWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
|
||||
widget.set_current_date("2024-01-01")
|
||||
widget.toggle_btn.setChecked(True)
|
||||
widget._on_toggle(True)
|
||||
|
||||
# Find the tag chip
|
||||
chips = widget.findChildren(TagChip)
|
||||
assert len(chips) > 0
|
||||
|
||||
# Click it - shouldn't crash
|
||||
from PySide6.QtTest import QTest
|
||||
|
||||
QTest.mouseClick(chips[0], Qt.LeftButton)
|
||||
|
||||
|
||||
def test_page_tags_widget_no_date_set(qtbot, fresh_db):
|
||||
"""Test PageTagsWidget with no date set."""
|
||||
strings.load_strings("en")
|
||||
|
||||
widget = PageTagsWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
|
||||
# Should handle no date gracefully
|
||||
widget.set_current_date(None)
|
||||
qtbot.wait(10)
|
||||
|
||||
|
||||
def test_page_tags_widget_date_with_no_tags(qtbot, fresh_db):
|
||||
"""Test PageTagsWidget for date with no tags."""
|
||||
strings.load_strings("en")
|
||||
|
||||
fresh_db.save_new_version("2024-01-01", "Content", "test")
|
||||
|
||||
widget = PageTagsWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
|
||||
widget.set_current_date("2024-01-01")
|
||||
|
||||
# Should show no tags
|
||||
pills = widget.findChildren(TagChip)
|
||||
assert len(pills) == 0
|
||||
|
||||
|
||||
def test_page_tags_widget_updates_on_tag_change(qtbot, fresh_db):
|
||||
"""Test PageTagsWidget updates when tags change."""
|
||||
strings.load_strings("en")
|
||||
|
||||
fresh_db.add_tag("Initial", "#ff0000")
|
||||
fresh_db.save_new_version("2024-01-01", "Content", "test")
|
||||
fresh_db.set_tags_for_page("2024-01-01", ["Initial"])
|
||||
|
||||
widget = PageTagsWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
widget.show()
|
||||
|
||||
widget.set_current_date("2024-01-01")
|
||||
widget.toggle_btn.setChecked(True)
|
||||
widget._on_toggle(True)
|
||||
|
||||
assert widget.chip_layout.count() == 1
|
||||
|
||||
# Add another tag
|
||||
fresh_db.add_tag("Second", "#00ff00")
|
||||
fresh_db.set_tags_for_page("2024-01-01", ["Initial", "Second"])
|
||||
|
||||
# Reload
|
||||
widget.set_current_date("2024-01-01")
|
||||
qtbot.wait(100)
|
||||
|
||||
assert widget.chip_layout.count() == 2
|
||||
|
|
|
|||
2558
tests/test_time_log.py
Normal file
2558
tests/test_time_log.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,15 +3,11 @@ from bouquin.flow_layout import FlowLayout
|
|||
from bouquin.markdown_editor import MarkdownEditor
|
||||
from bouquin.markdown_highlighter import MarkdownHighlighter
|
||||
from bouquin.statistics_dialog import DateHeatMap
|
||||
from bouquin.tag_graph_dialog import DraggableGraphItem
|
||||
|
||||
DBManager.row_factory
|
||||
|
||||
DateHeatMap.minimumSizeHint
|
||||
|
||||
DraggableGraphItem.hoverEvent
|
||||
DraggableGraphItem.mouseDragEvent
|
||||
|
||||
FlowLayout.itemAt
|
||||
FlowLayout.expandingDirections
|
||||
FlowLayout.hasHeightForWidth
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue