From 69b5cb4b4a18e5cd57bc1b067fcf1c040dbad3f8 Mon Sep 17 00:00:00 2001 From: Spill-Tea Date: Sat, 12 Jul 2025 16:37:59 -0700 Subject: [PATCH 1/4] feat(docs._ext): Update extensions with new updates to PyTemplate for custom syntax highlighting for code snippets. --- docs/source/_ext/lexer.py | 209 +++++++++++++++++++++++++++++++++++++ docs/source/_ext/styles.py | 122 +++++++++++++++++----- docs/source/_ext/utils.py | 57 ++++++++++ 3 files changed, 359 insertions(+), 29 deletions(-) create mode 100644 docs/source/_ext/lexer.py create mode 100644 docs/source/_ext/utils.py diff --git a/docs/source/_ext/lexer.py b/docs/source/_ext/lexer.py new file mode 100644 index 0000000..9421ff3 --- /dev/null +++ b/docs/source/_ext/lexer.py @@ -0,0 +1,209 @@ +# BSD 3-Clause License +# +# Copyright (c) 2025, Spill-Tea +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Customized python lexer.""" + +from collections import deque +from collections.abc import Iterator +from typing import ClassVar + +from pygments.lexer import bygroups, include +from pygments.lexers.python import PythonLexer +from pygments.token import ( + Comment, + Keyword, + Name, + Number, + Punctuation, + String, + Text, + Whitespace, + _TokenType, +) +from utils import get_bracket_level + + +def _find(it, obj, key=lambda a, b: a == b) -> int: + for n, j in enumerate(it): + if key(j, obj): + return n + raise IndexError("Unable to find object.") + + +def _get_index(n: int): + def inner(a, b) -> bool: + return a[n] == b + + return inner + + +root: list = [ + (r"\n", Whitespace), + ( # single line docstrings (edge case) + r'^(\s*)([rRuUbB]{,2})("""(?:.)*?""")', + bygroups(Whitespace, String.Affix, String.Doc), + ), + ( # Modfied triple double quote docstrings to highlight docstring titles + r'^(\s*)([rRuUbB]{,2})(""")', + bygroups(Whitespace, String.Affix, String.Doc), + "docstring-double", + ), + ( # Intentionally treat text encapsulated within single triple quotes as String + r"^(\s*)([rRuUbB]{,2})('''(?:.|\n)*?''')", + bygroups(Whitespace, String.Affix, String), + ), + (r"\A#!.+$", Comment.Hashbang), + ( + # Format Special Common Keyword Comments + # NOTE: Must come before Comment.Single token in order to be matched. + r"(#\s*)(TODO|FIXME|NOTE|BUG|HACK|XXX)(:?)(.*$)", + bygroups(Comment.Single, Comment.Special, Comment.Special, Comment.Single), + ), + (r"#.*$", Comment.Single), + (r"\\\n", Text), + (r"\\", Text), + include("keywords"), + include("soft-keywords"), + ( + r"(def)((?:\s|\\\s)+)", + bygroups(Keyword.Declare, Whitespace), + "funcname", + ), + ( + r"(class)((?:\s|\\\s)+)", + bygroups(Keyword.Declare, Whitespace), + "classname", + ), + ( + r"(from)((?:\s|\\\s)+)", + bygroups(Keyword.Namespace, Whitespace), + "fromimport", + ), + ( + r"(import)((?:\s|\\\s)+)", + bygroups(Keyword.Namespace, Whitespace), + "import", + ), + include("expr"), +] + + +python_tokens: dict[str, list] = PythonLexer.tokens.copy() +python_tokens["root"] = root +python_tokens["docstring-double"] = [ + ( + r"(?<=\n)(\s*)(Args|Attributes|Returns|Raises|" + r"Examples|Yields|References|Notes|Equations)(:)(\s*)", + bygroups(Whitespace, String.Doc.Title, String.Doc, Whitespace), + ), + (r'^\s*(?:""")', String.Doc, "#pop"), + (r".+[\r\n]*", String.Doc), +] + +# Tokenize function names when used (i.e. function calls) +# NOTE: Must be inserted before general `Name` token but after `Name.Builtins` token +# NOTE: Implementation limitations -> we cannot distinguish between class and function +# calls using regex based parsing alone (i.e without semantic analysis). +python_tokens["name"].insert( + _find(python_tokens["name"], Name, _get_index(1)), + (r"\b([a-zA-Z_]\w*)(?=\s*\()", Name.Function), +) + +python_tokens["numbers"] = [ + ( + r"(\d(?:_?\d)*\.(?:\d(?:_?\d)*)?|(?:\d(?:_?\d)*)?\.\d(?:_?\d)*)" + r"([eE][+-]?\d(?:_?\d)*)?([jJ]?)", + bygroups(Number.Float, Number.Float, Number.Other), + ), + (r"(\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*)([jJ]?)", bygroups(Number.Float, Number.Other)), + (r"(0[oO])((?:_?[0-7])+)", bygroups(Number.Other, Number.Oct)), + (r"(0[bB])((?:_?[01])+)", bygroups(Number.Other, Number.Bin)), + (r"(0[xX])((?:_?[a-fA-F0-9])+)", bygroups(Number.Other, Number.Hex)), + (r"(\d(?:_?\d)*)([jJ]?)", bygroups(Number.Integer, Number.Other)), +] + + +class CustomPythonLexer(PythonLexer): + """Enhanced regex-based python Lexer. + + Notes: + 1. Implemented a simple stack based rainbow bracket colorizer. + * limitation: Only detects errors that close more brackets than opens. + 2. Highlight Docstring titles (assumes google docstring format) + 3. Improved highlighting function calls (with limitations) + 4. Modify display of number components which indicate a different base number. + + """ + + n_brackets: int + _stack: deque[int] + tokens: ClassVar[dict[str, list]] = python_tokens + + def __init__(self, **options) -> None: + super().__init__(**options) + self._stack = deque[int]() + self.n_brackets = int(options.get("n_brackets", 4)) + + def _enter(self) -> _TokenType: + """Retrieve next token in cycle.""" + idx = len(self._stack) % self.n_brackets + self._stack.append(idx) + + return get_bracket_level(idx) + + def _exit(self) -> _TokenType: + """Remove element from stack and return token.""" + try: + idx: int = self._stack.pop() + return get_bracket_level(idx) + + except IndexError: + return Punctuation.Error + + def get_tokens_unprocessed( + self, + text, + stack=("root",), + ) -> Iterator[tuple[int, _TokenType, str]]: + _token: _TokenType + for idx, token, value in super().get_tokens_unprocessed(text, stack): + _token = token + if token is Name and value.isupper(): + _token = Name.Constant + + elif token is Punctuation: + match value: + case "(" | "[" | "{": + _token = self._enter() + case "}" | "]" | ")": + _token = self._exit() + case _: + ... + + yield idx, _token, value diff --git a/docs/source/_ext/styles.py b/docs/source/_ext/styles.py index 9aa5b76..1b66028 100644 --- a/docs/source/_ext/styles.py +++ b/docs/source/_ext/styles.py @@ -27,7 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Custom Pygment styles.""" +"""Custom Pygment syntax highlighting style.""" from typing import ClassVar @@ -39,43 +39,107 @@ Name, Number, Operator, + Other, Punctuation, String, + Text, _TokenType, ) +from utils import get_brackets + + +def bold(color: str) -> str: + """Embolden color.""" + return f"bold {color}" + + +def italic(color: str) -> str: + """Italicize color.""" + return f"italic {color}" + + +def underline(color: str) -> str: + """Underline text with color.""" + return f"underline {color}" + + +class Colors: + """Define colors used more than once.""" + + datatype: str = "#61C8B0" + variable: str = "#9CDCFE" + function: str = "#DCDCAA" + reserved: str = "#639BD4" + default: str = "#D4D4D4" + control: str = "#C586C0" + builtin: str = "#4EC9B0" + declare: str = "#569CD6" + problem: str = "#C3726A" + comment: str = "#6A9955" + bracket: str = "#F9C922" class VSCodeDarkPlus(Style): - """VSCode Dark+ Style.""" + """Custom theme deeply inspired by VSCode Dark+ as a pygments style.""" background_color: str = "#1E1E1E" - styles: ClassVar[dict[_TokenType, str]] = { - Number: "#B6CEA9", - Operator: "#D4D4D4", - Operator.Word: "#C586C0", - Comment: "#6D9957", - Comment.Preproc: "#639BD4", - Keyword.Namespace: "#C287A0", - # Keyword.Reserved: "#C287A0", - Keyword.Reserved: "#639BD4", - Keyword.Type: "#61C8B0", - Keyword.Constant: "#4FC1FF", - # Keyword: "#639BD4", - Keyword: "#C586C0", - Name: "#7FD0FD", - Name.Class: "#61C8B0", - Name.Namespace: "#61C8B0", - Name.Function: "#DCDCAA", - # Name.Builtin: "#DCDCAA", - Name.Builtin: "#4EC9B0", - Name.Type: "#4EC9B0", - Name.Builtin.Pseudo: "#9CDCFE", - Name.Variable: "#9CDCFE", - Name.Variable.Class: "#61C8B0", - Name.Variable.Magic: "#DCDCAA", - Name.Exception: "#61C8B0", - Error: "#61C8B0", + styles: ClassVar[dict[_TokenType, str]] = { # pyright: ignore + # Comments + Comment: Colors.comment, + Comment.Single: Colors.comment, + Comment.Preproc: Colors.reserved, + Comment.Special: bold(Colors.declare), + Comment.Hashbang: italic("#7C7046"), + Comment.Multiline: italic("#525252"), + # Keywords + Keyword: Colors.control, + Keyword.Type: Colors.datatype, + Keyword.Declare: bold(Colors.declare), + Keyword.Constant: bold(Colors.declare), + Keyword.Reserved: bold(Colors.reserved), + Keyword.Namespace: Colors.control, + # Variable Names + Name: Colors.variable, + Name.Type: Colors.builtin, + Name.Class: bold(Colors.datatype), + Name.Builtin: Colors.builtin, + Name.Builtin.Pseudo: italic(Colors.variable), + Name.Constant: "#4FC1FF", + Name.Function: Colors.function, + Name.Function.Magic: italic(Colors.function), + Name.Variable: Colors.variable, + Name.Variable.Class: Colors.datatype, + Name.Variable.Magic: Colors.function, + Name.Namespace: Colors.datatype, + Name.Exception: Colors.problem, + # (Doc)Strings + Text: Colors.default, String: "#C9937A", - Punctuation: "#F9C922", + String.Doc: italic(Colors.comment), + String.Doc.Title: bold("#80AE6B"), + String.Affix: Colors.declare, + String.Regex: "#D16969", + String.Escape: "#D7BA7D", + String.Interpol: Colors.declare, + # Numbers + Number: "#B6CEA9", + Number.Other: Colors.declare, + # Operators + Operator: Colors.default, + Operator.Word: Colors.control, + # Punctuation + Punctuation: Colors.default, + **get_brackets( + [ + Colors.bracket, + "#EA2EEA", + "#5DCD4C", + "#3B9ADE", + ] + ), + Punctuation.Error: underline("#F92222"), + # Miscellaneous + Error: underline(bold(Colors.problem)), + Other: Colors.default, } diff --git a/docs/source/_ext/utils.py b/docs/source/_ext/utils.py new file mode 100644 index 0000000..65f3688 --- /dev/null +++ b/docs/source/_ext/utils.py @@ -0,0 +1,57 @@ +# BSD 3-Clause License +# +# Copyright (c) 2025, Spill-Tea +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Common shared utility functions.""" + +from typing import Iterator + +from pygments.token import _TokenType, string_to_tokentype + + +def get_bracket_level(n: int) -> _TokenType: + """Retrieve the bracket depth level token.""" + name: str = f"Punctuation.Level{n}" + + return string_to_tokentype(name) + + +def nbrackets(n: int) -> Iterator[_TokenType]: + """Dynamically generate tokentype to identify a variable number of brackets.""" + for j in range(n): + yield get_bracket_level(j) + + +def dynamic_brackets(colors: list[str]) -> list[tuple[_TokenType, str]]: + """Dynamically generate bracket color options from a list of colors.""" + return [(key, colors[idx]) for idx, key in enumerate(nbrackets(len(colors)))] + + +def get_brackets(colors: list[str]) -> dict[_TokenType, str]: + """Get brackets in dictionary form.""" + return dict(dynamic_brackets(colors)) From 1919b750dd36cf5412b9143c4ba97c1db5c65748 Mon Sep 17 00:00:00 2001 From: Spill-Tea Date: Sat, 12 Jul 2025 16:39:26 -0700 Subject: [PATCH 2/4] feat(docs.conf): Update sphinx configuration to use and setup custom lexer. Toggle autosummary and napoleon extensions. --- docs/source/conf.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5f4aa50..0c788d7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,6 +32,9 @@ import os import sys +from sphinx.application import Sphinx +from sphinx.highlighting import lexer_classes + sys.path.insert(0, os.path.abspath("../src/")) sys.path.append(os.path.abspath("./_ext")) # Required for custom extensions @@ -51,6 +54,8 @@ "sphinx.ext.autosummary", "sphinx.ext.napoleon", ] +autosummary_generate = True # Turn on sphinx.ext.autosummary +napoleon_google_docstring = True templates_path = ["_templates"] exclude_patterns = [] @@ -63,3 +68,14 @@ html_static_path = ["_static"] html_css_files = ["custom.css"] pygments_style = "styles.VSCodeDarkPlus" + + +def setup(app: Sphinx) -> None: + """Custom sphinx application startup setup.""" + from lexer import CustomPythonLexer + + app.add_lexer("python", CustomPythonLexer) + assert "python" in lexer_classes, "python language not found in registry" + assert lexer_classes["python"] == CustomPythonLexer, ( + "custom Lexer not found in registry." + ) From 98bdd51e97d86c0b3fbf0bba89bad0b4f98f0b3b Mon Sep 17 00:00:00 2001 From: Spill-Tea Date: Sat, 12 Jul 2025 16:40:47 -0700 Subject: [PATCH 3/4] feat(docs.api): Update documentation of public api. --- docs/source/api/designer_dna.rst | 21 --------------------- docs/source/api/index.rst | 10 ++++++++++ docs/source/api/modules.rst | 7 ------- docs/source/api/oligos.rst | 28 ++++++++++++++++++++++++++++ docs/source/index.rst | 7 +++---- 5 files changed, 41 insertions(+), 32 deletions(-) delete mode 100644 docs/source/api/designer_dna.rst create mode 100644 docs/source/api/index.rst delete mode 100644 docs/source/api/modules.rst create mode 100644 docs/source/api/oligos.rst diff --git a/docs/source/api/designer_dna.rst b/docs/source/api/designer_dna.rst deleted file mode 100644 index 8006cb6..0000000 --- a/docs/source/api/designer_dna.rst +++ /dev/null @@ -1,21 +0,0 @@ -designer\_dna package -===================== - -Submodules ----------- - -designer\_dna.oligos module ---------------------------- - -.. automodule:: designer_dna.oligos - :members: - :show-inheritance: - :undoc-members: - -Module contents ---------------- - -.. automodule:: designer_dna - :members: - :show-inheritance: - :undoc-members: diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..cd167c3 --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,10 @@ +DesignerDNA API Documentation +============================= + +DesignerDNA API documentation. + + +.. toctree:: + :caption: Submodules + + Oligos diff --git a/docs/source/api/modules.rst b/docs/source/api/modules.rst deleted file mode 100644 index f7691a3..0000000 --- a/docs/source/api/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -designer_dna -============ - -.. toctree:: - :maxdepth: 4 - - designer_dna diff --git a/docs/source/api/oligos.rst b/docs/source/api/oligos.rst new file mode 100644 index 0000000..86be89e --- /dev/null +++ b/docs/source/api/oligos.rst @@ -0,0 +1,28 @@ +designer\_dna.oligos +==================== +.. toctree:: + :hidden: + + +.. automodule:: designer_dna.oligos + :members: + :show-inheritance: + :undoc-members: + + .. rubric:: Functions + + .. autosummary:: + + complement + complement_py + manacher + nrepeats + nrepeats_py + palindrome + palindrome_py + reverse + reverse_py + reverse_complement + reverse_complement_py + stretch + stretch_py diff --git a/docs/source/index.rst b/docs/source/index.rst index fb8a224..8498c2e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,11 +4,10 @@ DesignerDNA documentation Design, fiddle, and optimize DNA sequences. .. toctree:: - :maxdepth: 2 - :caption: Contents: - - api/modules + :hidden: + Home page + API reference Indices and tables ================== From 6223a9aac652eae20712b75ea8f5d9e6caaa7a12 Mon Sep 17 00:00:00 2001 From: Spill-Tea Date: Sat, 12 Jul 2025 17:33:08 -0700 Subject: [PATCH 4/4] chore(_oligos): remove unused variable in nrepeats function. --- src/designer_dna/_oligos.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/designer_dna/_oligos.pyx b/src/designer_dna/_oligos.pyx index 0ab3deb..c9e2adb 100644 --- a/src/designer_dna/_oligos.pyx +++ b/src/designer_dna/_oligos.pyx @@ -369,7 +369,7 @@ cpdef int nrepeats(str sequence, int n): StringView view = str_to_view(sequence) Py_ssize_t t = n Py_ssize_t v = view.size // t - Py_ssize_t i, j, k + Py_ssize_t j, k int current, max_val = 0 char* previous = malloc((t + 1) * sizeof(char))