Skip to content

Commit

Permalink
♻️ Make matplotlib optional
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Nov 8, 2023
1 parent 40856b2 commit 2217df5
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 59 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ jobs:
run: |
python -m pytest -v --ignore=tests/benchmarks -m "jstest" tests
docs-no-mpl:
# the docs should build without matplotlib (just issuing warnings)
name: Docs no matplotlib
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set Up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Update pip
run: python -m pip install --upgrade pip
- name: Install Dependencies
run: |
python -m pip install -e .[docs]
python -m pip uninstall -y matplotlib numpy
python -m pip freeze
- name: Run HTML build
run: sphinx-build -b html . _build
working-directory: docs

check:

# This job does nothing and is only used for the branch protection
Expand All @@ -89,6 +110,7 @@ jobs:
- lint
- tests-core
- tests-js
- docs-no-mpl

runs-on: ubuntu-latest

Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"sphinxcontrib.plantuml",
"sphinx_needs",
"sphinx.ext.autodoc",
"matplotlib.sphinxext.plot_directive",
"sphinx_copybutton",
"sphinxcontrib.programoutput",
"sphinx_design",
Expand Down
33 changes: 17 additions & 16 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ packages = [
[tool.poetry.dependencies]
python = ">=3.8,<4"
sphinx = ">=5.0,<8"
matplotlib = ">=3.3.0" # needpie
requests-file = "^1.5.1" # external links
requests = "^2.25.1" # external_links
jsonschema = ">=3.2.0" # needsimport schema validation
sphinx-data-viewer = "^0.1.1" # needservice debug output
sphinxcontrib-jquery = "^4" # needed for datatables in sphinx>=6

# [project.optional-dependencies.matplotlib]
# for needpie / needbar
matplotlib = { version = ">=3.3.0", optional = true }

# [project.optional-dependencies.test]
pytest = { version = "^7", optional = true }
lxml = { version = "^4.6.5", optional = true }
Expand All @@ -64,10 +67,12 @@ sphinx-design = { version="^0.5", optional = true }
sphinx-immaterial = { version="0.11.7", optional = true }

[tool.poetry.extras]
test = ["pytest", "syrupy", "sphinxcontrib-plantuml", "requests-mock", "lxml", "responses", "pytest-xprocess"]
matplotlib = ["matplotlib"]
test = ["matplotlib", "pytest", "syrupy", "sphinxcontrib-plantuml", "requests-mock", "lxml", "responses", "pytest-xprocess"]
test-parallel = ["pytest-xdist"]
benchmark = ["pytest-benchmark", "memray"]
docs = [
"matplotlib",
"sphinxcontrib-plantuml",
"sphinx-copybutton",
"sphinxcontrib-programoutput",
Expand Down
48 changes: 25 additions & 23 deletions sphinx_needs/directives/needbar.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import hashlib
import math
import os
from typing import List, Sequence

import matplotlib
import numpy
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.application import Sphinx

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list
from sphinx_needs.utils import add_doc, remove_node_from_tree, save_matplotlib_figure

if not os.environ.get("DISPLAY"):
matplotlib.use("Agg")
import hashlib

from docutils.parsers.rst import directives

from sphinx_needs.logging import get_logger
from sphinx_needs.utils import add_doc, import_matplotlib, remove_node_from_tree, save_matplotlib_figure

logger = get_logger(__name__)

Expand Down Expand Up @@ -82,7 +74,8 @@ def run(self) -> Sequence[nodes.Node]:
text_color = text_color.strip()

style = self.options.get("style")
style = style.strip() if style else matplotlib.style.use("default")
matplotlib = import_matplotlib()
style = style.strip() if style else (matplotlib.style.use("default") if matplotlib else "default")

legend = "legend" in self.options

Expand Down Expand Up @@ -169,17 +162,29 @@ def run(self) -> Sequence[nodes.Node]:
# 10. cleanup matplotlib
def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, found_nodes: List[nodes.Element]) -> None:
env = app.env
needs_data = SphinxNeedsData(env)
needs_config = NeedsSphinxConfig(env.config)

matplotlib = import_matplotlib()

if matplotlib is None and found_nodes and needs_config.include_needs:
logger.warning(
"Matplotlib is not installed and required by needbar. "
"Install with `sphinx-needs[matplotlib]` to use. [needs.mpl]",
once=True,
type="needs",
subtype="mpl",
)

# NEEDFLOW
# for node in doctree.findall(Needbar):
for node in found_nodes:
if not needs_config.include_needs:
if matplotlib is None or not needs_config.include_needs:
remove_node_from_tree(node)
continue

id = node.attributes["ids"][0]
current_needbar = SphinxNeedsData(env).get_or_create_bars()[id]
current_needbar = needs_data.get_or_create_bars()[id]

# 1. define constants
error_id = current_needbar["error_id"]
Expand Down Expand Up @@ -253,9 +258,7 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun

# 5. process content
local_data_number = []
need_list = list(
prepare_need_list(SphinxNeedsData(env).get_or_create_needs().values())
) # adds parts to need_list
need_list = list(prepare_need_list(needs_data.get_or_create_needs().values())) # adds parts to need_list

for line in local_data:
line_number = []
Expand Down Expand Up @@ -322,7 +325,7 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
colors = colors * multi
colors = colors[: len(local_data)]

y_offset = numpy.zeros(len(local_data_number[0]))
y_offset = [0.0 for _ in range(len(local_data_number[0]))]

# 8. create figure
bar_labels = []
Expand All @@ -347,7 +350,7 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun

if current_needbar["stacked"]:
# handle stacked bar
y_offset = y_offset + numpy.array(local_data_number[x])
y_offset = [i + j for i, j in zip(y_offset, local_data_number[x])]

if current_needbar["show_sum"]:
try:
Expand Down Expand Up @@ -375,15 +378,14 @@ def process_needbar(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
if sum_rotation.isdigit():
matplotlib.pyplot.setp(bar_labels, rotation=int(sum_rotation))

centers = [(i + j) / 2.0 for i, j in zip(index[0], index[len(local_data_number) - 1])]
if not current_needbar["horizontal"]:
# We want to support even older version of matplotlib, which do not support axes.set_xticks(labels)
x_pos = (numpy.array(index[0]) + numpy.array(index[len(local_data_number) - 1])) / 2
axes.set_xticks(x_pos)
axes.set_xticks(centers)
axes.set_xticklabels(labels=xlabels)
else:
# We want to support even older version of matplotlib, which do not support axes.set_yticks(labels)
y_pos = (numpy.array(index[0]) + numpy.array(index[len(local_data_number) - 1])) / 2
axes.set_yticks(y_pos)
axes.set_yticks(centers)
axes.set_yticklabels(labels=xlabels)
axes.invert_yaxis() # labels read top-to-bottom

Expand Down
30 changes: 16 additions & 14 deletions sphinx_needs/directives/needpie.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
import os
import hashlib
from typing import Iterable, List, Sequence

import matplotlib
import numpy as np
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.application import Sphinx

from sphinx_needs.config import NeedsSphinxConfig
from sphinx_needs.data import SphinxNeedsData
from sphinx_needs.debug import measure_time
from sphinx_needs.filter_common import FilterBase, filter_needs, prepare_need_list

if not os.environ.get("DISPLAY"):
matplotlib.use("Agg")
import hashlib

import matplotlib.pyplot
from docutils.parsers.rst import directives

from sphinx_needs.logging import get_logger
from sphinx_needs.utils import (
add_doc,
check_and_get_external_filter_func,
remove_node_from_tree,
import_matplotlib,
save_matplotlib_figure,
)

Expand Down Expand Up @@ -114,11 +106,21 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
needs_data = SphinxNeedsData(env)
needs_config = NeedsSphinxConfig(env.config)

matplotlib = import_matplotlib()

if matplotlib is None and found_nodes and needs_config.include_needs:
logger.warning(
"Matplotlib is not installed and required by needpie. "
"Install with `sphinx-needs[matplotlib]` to use. [needs.mpl]",
once=True,
type="needs",
subtype="mpl",
)

# NEEDFLOW
include_needs = needs_config.include_needs
# for node in doctree.findall(Needpie):
for node in found_nodes:
if not include_needs:
if matplotlib is None or not needs_config.include_needs:
remove_node_from_tree(node)
continue

Expand Down Expand Up @@ -215,7 +217,7 @@ def process_needpie(app: Sphinx, doctree: nodes.document, fromdocname: str, foun
if text_color:
pie_kwargs["textprops"] = {"color": text_color}

wedges, _texts, autotexts = axes.pie(sizes, normalize=np.asarray(sizes, np.float32).sum() >= 1, **pie_kwargs)
wedges, _texts, autotexts = axes.pie(sizes, normalize=sum(float(s) for s in sizes) >= 1, **pie_kwargs)

ratio = 20 # we will remove all labels with size smaller 5%
legend_enforced = False
Expand Down
Loading

0 comments on commit 2217df5

Please sign in to comment.