Skip to content

Commit

Permalink
refactor!: generalize STG to MutableTransition (#156)
Browse files Browse the repository at this point in the history
* chore: rewrite StateTransitionGraph with attrs
  **WARNING**: this removes the `__post_init__()` check, but this
  didn't seem to be doing anything anyway. Initially, I simply renamed
  the check `__attrs_post_init__()`, but this actually does perform the
  check and then the system crashes. This seems to be an issue that
  needs to be addressed separately.
* chore: simplify (over)defined_assert functions
* docs: hide TypeVars from topology API
* docs: improve docstrings of topology module
* ci: disable pylint line-too-long
* ci: do not fast-fail test jobs
* ci: ignore logo.svg in sphinx-autobuild
* ci: ignore tmp files in sphinx-autobuild
* docs: hide `dict.keys()` methods etc. from API
* docs: improve API rendering with autodoc_type_aliases
* feat: define FrozenTransition class (from StateTransition)
* feat: define Transition interface class
* feat: implement initial_states etc in FrozenTransition
* fix: return NewNodeType in FrozenTransition convert()
* refactor: change initial_states etc into mixin methods
* refactor: convert StateTransition into type alias
* refactor: make MutableTopology public
* refactor: make NodeType of StateTransitionGraph generic
* refactor: move organize_edge_ids to MutableTopology
* refactor: remove GraphSettings, GraphElementProperties, QuantumNumberSolution, InitialFacts
* refactor: remove ReactionInfo.from/to_graphs()
* refactor: remove get_edge/node_props()
* refactor: remove kw_only from MutableTopology
* refactor: rename GraphSettings attrs to states/interactions
* refactor: rename QuantumNumberSolution attrs to states/interactions
* refactor: rename StateTransitionGraph to MutableTransition
* refactor: rename edge/node_props to states/interactions
* refactor: sort output of create_isobar_topologies
  • Loading branch information
redeboer authored Feb 21, 2022
1 parent c6c35de commit 9e23b7a
Show file tree
Hide file tree
Showing 31 changed files with 1,302 additions and 1,110 deletions.
5 changes: 5 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ ignore =
W503
extend-select =
TI100
per-file-ignores =
# casts with generics
src/qrules/topology.py:E731
rst-roles =
attr
cite
Expand All @@ -44,6 +47,8 @@ rst-roles =
mod
ref
rst-directives =
autolink-preface
automethod
deprecated
envvar
exception
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
name: Unit tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- macos-11
Expand Down
2 changes: 2 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ ignore-patterns=
disable=
duplicate-code, # https://github.com/PyCQA/pylint/issues/214
invalid-unary-operand-type, # conflicts with attrs.field
line-too-long, # automatically fixed with black
logging-fstring-interpolation,
missing-class-docstring, # pydocstyle
missing-function-docstring, # pydocstyle
missing-module-docstring, # pydocstyle
no-member, # conflicts with attrs.field
no-name-in-module, # already checked by mypy
not-an-iterable, # conflicts with attrs.field
not-callable, # conflicts with attrs.field
redefined-builtin, # flake8-built
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.doctree
*.inv
*build/
_images/*
api/

!_static/*
Expand Down
130 changes: 130 additions & 0 deletions docs/_extend_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# flake8: noqa
# pylint: disable=import-error,import-outside-toplevel,invalid-name,protected-access
# pyright: reportMissingImports=false
"""Extend docstrings of the API.
This small script is used by ``conf.py`` to dynamically modify docstrings.
"""

import inspect
import logging
import textwrap
from typing import Callable, Dict, Optional, Type, Union

import qrules

logging.getLogger().setLevel(logging.ERROR)


def extend_docstrings() -> None:
script_name = __file__.rsplit("/", maxsplit=1)[-1]
script_name = ".".join(script_name.split(".")[:-1])
definitions = dict(globals())
for name, definition in definitions.items():
module = inspect.getmodule(definition)
if module is None:
continue
if module.__name__ not in {"__main__", script_name}:
continue
if not inspect.isfunction(definition):
continue
if not name.startswith("extend_"):
continue
if name == "extend_docstrings":
continue
function_arguments = inspect.signature(definition).parameters
if len(function_arguments):
raise ValueError(
f"Local function {name} should not have a signature"
)
definition()


def extend_create_isobar_topologies() -> None:
from qrules.topology import create_isobar_topologies

topologies = qrules.topology.create_isobar_topologies(4)
dot_renderings = map(
lambda t: qrules.io.asdot(t, render_resonance_id=True),
topologies,
)
images = [_graphviz_to_image(dot, indent=6) for dot in dot_renderings]
_append_to_docstring(
create_isobar_topologies,
f"""
.. panels::
:body: text-center
{images[0]}
---
{images[1]}
""",
)


def extend_create_n_body_topology() -> None:
from qrules.topology import create_n_body_topology

topology = create_n_body_topology(
number_of_initial_states=2,
number_of_final_states=5,
)
dot = qrules.io.asdot(topology, render_initial_state_id=True)
_append_to_docstring(
create_n_body_topology,
_graphviz_to_image(dot, indent=4),
)


def extend_Topology() -> None:
from qrules.topology import Topology, create_isobar_topologies

topologies = create_isobar_topologies(number_of_final_states=3)
dot = qrules.io.asdot(
topologies[0],
render_initial_state_id=True,
render_resonance_id=True,
)
_append_to_docstring(
Topology,
_graphviz_to_image(dot, indent=4),
)


def _append_to_docstring(
class_type: Union[Callable, Type], appended_text: str
) -> None:
assert class_type.__doc__ is not None
class_type.__doc__ += appended_text


_GRAPHVIZ_COUNTER = 0
_IMAGE_DIR = "_images"


def _graphviz_to_image( # pylint: disable=too-many-arguments
dot: str,
options: Optional[Dict[str, str]] = None,
format: str = "svg",
indent: int = 0,
caption: str = "",
label: str = "",
) -> str:
import graphviz # type: ignore[import]

if options is None:
options = {}
global _GRAPHVIZ_COUNTER # pylint: disable=global-statement
output_file = f"graphviz_{_GRAPHVIZ_COUNTER}"
_GRAPHVIZ_COUNTER += 1
graphviz.Source(dot).render(f"{_IMAGE_DIR}/{output_file}", format=format)
restructuredtext = "\n"
if label:
restructuredtext += f".. _{label}:\n"
restructuredtext += f".. figure:: /{_IMAGE_DIR}/{output_file}.{format}\n"
for option, value in options.items():
restructuredtext += f" :{option}: {value}\n"
if caption:
restructuredtext += f"\n {caption}\n"
return textwrap.indent(restructuredtext, indent * " ")
102 changes: 83 additions & 19 deletions docs/_relink_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,51 @@
See also https://github.com/sphinx-doc/sphinx/issues/5868.
"""

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
from docutils import nodes
from sphinx.addnodes import pending_xref
from sphinx.environment import BuildEnvironment


__TARGET_SUBSTITUTIONS = {
"a set-like object providing a view on D's items": "typing.ItemsView",
"a set-like object providing a view on D's keys": "typing.KeysView",
"an object providing a view on D's values": "typing.ValuesView",
"typing_extensions.Protocol": "typing.Protocol",
"typing_extensions.TypeAlias": "typing.TypeAlias",
}
__REF_TYPE_SUBSTITUTIONS = {
"None": "obj",
"qrules.combinatorics.InitialFacts": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.baryon_number": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.bottomness": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.c_parity": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.charge": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.charmness": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.electron_lepton_number": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.g_parity": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.isospin_magnitude": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.isospin_projection": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.mass": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.muon_lepton_number": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.parity": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.pid": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.spin_magnitude": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.spin_projection": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.strangeness": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.tau_lepton_number": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.topness": "obj",
"qrules.quantum_numbers.EdgeQuantumNumbers.width": "obj",
"qrules.quantum_numbers.NodeQuantumNumbers.l_magnitude": "obj",
"qrules.quantum_numbers.NodeQuantumNumbers.l_projection": "obj",
"qrules.quantum_numbers.NodeQuantumNumbers.parity_prefactor": "obj",
"qrules.quantum_numbers.NodeQuantumNumbers.s_magnitude": "obj",
"qrules.quantum_numbers.NodeQuantumNumbers.s_projection": "obj",
"qrules.solving.GraphElementProperties": "obj",
"qrules.solving.GraphSettings": "obj",
"qrules.transition.StateTransition": "obj",
"typing.TypeAlias": "obj",
}


Expand All @@ -31,34 +61,68 @@ def _new_type_to_xref(
env: "BuildEnvironment" = None,
suppress_prefix: bool = False,
) -> "pending_xref":
if env:
kwargs = {
"py:module": env.ref_context.get("py:module"),
"py:class": env.ref_context.get("py:class"),
}
else:
kwargs = {}
import sphinx
from sphinx.addnodes import pending_xref

target = __TARGET_SUBSTITUTIONS.get(target, target)
reftype = __REF_TYPE_SUBSTITUTIONS.get(target, "class")
if suppress_prefix:
short_text = target.split(".")[-1]
else:
short_text = target
if sphinx.version_info >= (4, 4):
# https://github.com/sphinx-doc/sphinx/blob/v4.4.0/sphinx/domains/python.py#L110-L133
from sphinx.domains.python import ( # type: ignore[attr-defined]
parse_reftarget,
)

from docutils.nodes import Text
from sphinx.addnodes import pending_xref
reftype, target, title, refspecific = parse_reftarget(
target, suppress_prefix
)
target = __TARGET_SUBSTITUTIONS.get(target, target)
reftype = __REF_TYPE_SUBSTITUTIONS.get(target, reftype)
assert env is not None
return pending_xref(
"",
*__create_nodes(env, title),
refdomain="py",
reftype=reftype,
reftarget=target,
refspecific=refspecific,
**__get_env_kwargs(env),
)

# Sphinx <4.4.0
# https://github.com/sphinx-doc/sphinx/blob/v4.3.2/sphinx/domains/python.py#L83-L107
target = __TARGET_SUBSTITUTIONS.get(target, target)
reftype = __REF_TYPE_SUBSTITUTIONS.get(target, "class")
assert env is not None
return pending_xref(
"",
Text(short_text),
*__create_nodes(env, target),
refdomain="py",
reftype=reftype,
reftarget=target,
**kwargs,
**__get_env_kwargs(env),
)


def __get_env_kwargs(env: "BuildEnvironment") -> dict:
if env:
return {
"py:module": env.ref_context.get("py:module"),
"py:class": env.ref_context.get("py:class"),
}
return {}


def __create_nodes(env: "BuildEnvironment", title: str) -> "List[nodes.Node]":
from docutils import nodes
from sphinx.addnodes import pending_xref_condition

short_name = title.split(".")[-1]
if env.config.python_use_unqualified_type_names:
return [
pending_xref_condition("", short_name, condition="resolved"),
pending_xref_condition("", title, condition="*"),
]
return [nodes.Text(short_name)]


def relink_references() -> None:
import sphinx.domains.python

Expand Down
34 changes: 24 additions & 10 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ def fetch_logo(url: str, output_path: str) -> None:

# -- Generate API ------------------------------------------------------------
sys.path.insert(0, os.path.abspath("."))
from _extend_docstrings import extend_docstrings # noqa: E402
from _relink_references import relink_references # noqa: E402

extend_docstrings()
relink_references()

shutil.rmtree("api", ignore_errors=True)
subprocess.call(
" ".join(
Expand Down Expand Up @@ -164,17 +167,29 @@ def fetch_logo(url: str, output_path: str) -> None:
# General sphinx settings
add_module_names = False
autodoc_default_options = {
"exclude-members": ", ".join(
[
"items",
"keys",
"values",
]
),
"members": True,
"undoc-members": True,
"show-inheritance": True,
"special-members": ", ".join(
[
"__call__",
"__getitem__",
]
),
}
autodoc_member_order = "bysource"
autodoc_type_aliases = {
"GraphElementProperties": "qrules.solving.GraphElementProperties",
"GraphSettings": "qrules.solving.GraphSettings",
"InitialFacts": "qrules.combinatorics.InitialFacts",
"StateTransition": "qrules.transition.StateTransition",
}
autodoc_typehints_format = "short"
codeautolink_concat_default = True
AUTODOC_INSERT_SIGNATURE_LINEBREAKS = True
Expand Down Expand Up @@ -220,15 +235,14 @@ def fetch_logo(url: str, output_path: str) -> None:
default_role = "py:obj"
primary_domain = "py"
nitpicky = True # warn if cross-references are missing
nitpick_ignore = [
("py:class", "EdgeType"),
("py:class", "NoneType"),
("py:class", "StateTransitionGraph"),
("py:class", "ValueType"),
("py:class", "json.encoder.JSONEncoder"),
("py:class", "typing_extensions.Protocol"),
("py:obj", "qrules.topology._K"),
("py:obj", "qrules.topology._V"),
nitpick_ignore_regex = [
(r"py:(class|obj)", "json.encoder.JSONEncoder"),
(r"py:(class|obj)", r"(qrules\.topology\.)?EdgeType"),
(r"py:(class|obj)", r"(qrules\.topology\.)?KT"),
(r"py:(class|obj)", r"(qrules\.topology\.)?NewEdgeType"),
(r"py:(class|obj)", r"(qrules\.topology\.)?NewNodeType"),
(r"py:(class|obj)", r"(qrules\.topology\.)?NodeType"),
(r"py:(class|obj)", r"(qrules\.topology\.)?VT"),
]


Expand Down
Loading

0 comments on commit 9e23b7a

Please sign in to comment.