Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sphinxnotes-snippet v2 #30

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cruft.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"template": "https://github.com/sphinx-notes/template",
"commit": "80a61fa9abcd9474d8cfbc36d0bf5d41f99c916c",
"commit": "c4f14dab2840eeff6352647a923642b6377d1f49",
"checkout": null,
"context": {
"cookiecutter": {
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ view:

.PHONY: clean
clean:
$(MAKE) -C docs/ clean
$(RM) dist/
$(MAKE) -C docs/ clean | true
$(RM) dist/ | true

.PHONY: clean
fmt:
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('../src/sphinxnotes'))
extensions.append('snippet.ext')
extensions.append('snippet')

# DOG FOOD CONFIGURATION START

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
"Topic :: Utilities",
]

requires-python = ">=3.8"
requires-python = ">=3.12"
dependencies = [
"Sphinx >= 4",
"langid",
Expand Down
206 changes: 28 additions & 178 deletions src/sphinxnotes/snippet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,185 +2,35 @@
sphinxnotes.snippet
~~~~~~~~~~~~~~~~~~~

:copyright: Copyright 2020 Shengyu Zhang
Sphinx extension entrypoint.

:copyright: Copyright 2024 Shengyu Zhang
:license: BSD, see LICENSE for details.
"""

from __future__ import annotations
from typing import List, Tuple, Optional, TYPE_CHECKING
import itertools

from docutils import nodes

if TYPE_CHECKING:
from sphinx.environment import BuildEnvironment

__version__ = '1.1.1'


class Snippet(object):
"""
Snippet is base class of reStructuredText snippet.

:param nodes: Document nodes that make up this snippet
"""

#: docname where the snippet is located, can be referenced by
# :rst:role:`doc`.
docname: str

#: Source file path of snippet
file: str

#: Line number range of snippet, in the source file which is left closed
#: and right opened.
lineno: Tuple[int, int]

#: The original reStructuredText of snippet
rst: List[str]

#: The possible identifier key of snippet, which is picked from nodes'
#: (or nodes' parent's) `ids attr`_.
#:
#: .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids
refid: Optional[str]

def __init__(self, *nodes: nodes.Node) -> None:
assert len(nodes) != 0

env: BuildEnvironment = nodes[0].document.settings.env
self.file = nodes[0].source
self.docname = env.path2doc(self.file)

lineno = [float('inf'), -float('inf')]
for node in nodes:
if not node.line:
continue # Skip node that have None line, I dont know why
lineno[0] = min(lineno[0], _line_of_start(node))
lineno[1] = max(lineno[1], _line_of_end(node))
self.lineno = lineno

lines = []
with open(self.file, 'r') as f:
start = self.lineno[0] - 1
stop = self.lineno[1] - 1
for line in itertools.islice(f, start, stop):
lines.append(line.strip('\n'))
self.rst = lines

# Find exactly one ID attr in nodes
self.refid = None
for node in nodes:
if node['ids']:
self.refid = node['ids'][0]
break

# If no ID found, try parent
if not self.refid:
for node in nodes:
if node.parent['ids']:
self.refid = node.parent['ids'][0]
break


class Text(Snippet):
#: Text of snippet
text: str

def __init__(self, node: nodes.Node) -> None:
super().__init__(node)
self.text = node.astext()


class CodeBlock(Text):
#: Language of code block
language: str
#: Caption of code block
caption: Optional[str]

def __init__(self, node: nodes.literal_block) -> None:
assert isinstance(node, nodes.literal_block)
super().__init__(node)
self.language = node['language']
self.caption = node.get('caption')


class WithCodeBlock(object):
code_blocks: List[CodeBlock]

def __init__(self, nodes: nodes.Nodes) -> None:
self.code_blocks = []
for n in nodes.traverse(nodes.literal_block):
self.code_blocks.append(self.CodeBlock(n))


class Title(Text):
def __init__(self, node: nodes.title) -> None:
assert isinstance(node, nodes.title)
super().__init__(node)


class WithTitle(object):
title: Optional[Title]

def __init__(self, node: nodes.Node) -> None:
title_node = node.next_node(nodes.title)
self.title = Title(title_node) if title_node else None


class Section(Snippet, WithTitle):
def __init__(self, node: nodes.section) -> None:
assert isinstance(node, nodes.section)
Snippet.__init__(self, node)
WithTitle.__init__(self, node)


class Document(Section):
def __init__(self, node: nodes.document) -> None:
assert isinstance(node, nodes.document)
super().__init__(node.next_node(nodes.section))


################
# Nodes helper #
################


def _line_of_start(node: nodes.Node) -> int:
assert node.line
if isinstance(node, nodes.title):
if isinstance(node.parent.parent, nodes.document):
# Spceial case for Document Title / Subtitle
return 1
else:
# Spceial case for section title
return node.line - 1
elif isinstance(node, nodes.section):
if isinstance(node.parent, nodes.document):
# Spceial case for top level section
return 1
else:
# Spceial case for section
return node.line - 1
return node.line


def _line_of_end(node: nodes.Node) -> Optional[int]:
next_node = node.next_node(descend=False, siblings=True, ascend=True)
while next_node:
if next_node.line:
return _line_of_start(next_node)
next_node = next_node.next_node(
# Some nodes' line attr is always None, but their children has
# valid line attr
descend=True,
# If node and its children have not valid line attr, try use line
# of next node
ascend=True,
siblings=True,
)
# No line found, return the max line of source file
if node.source:
with open(node.source) as f:
return sum(1 for line in f)
raise AttributeError('None source attr of node %s' % node)
def setup(app):
# **WARNING**: We don't import these packages globally, because the current
# package (sphinxnotes.snippet) is always resloved when importing
# sphinxnotes.snippet.*. If we import packages here, eventually we will
# load a lot of packages from the Sphinx. It will seriously **SLOW DOWN**
# the startup time of our CLI tool (sphinxnotes.snippet.cli).
#
# .. seealso:: https://github.com/sphinx-notes/snippet/pull/31
from .ext import (
SnippetBuilder,
on_config_inited,
on_env_get_outdated,
on_doctree_resolved,
on_builder_finished,
)

app.add_builder(SnippetBuilder)

app.add_config_value('snippet_config', {}, '')
app.add_config_value('snippet_patterns', {'*': ['.*']}, '')

app.connect('config-inited', on_config_inited)
app.connect('env-get-outdated', on_env_get_outdated)
app.connect('doctree-resolved', on_doctree_resolved)
app.connect('build-finished', on_builder_finished)
52 changes: 26 additions & 26 deletions src/sphinxnotes/snippet/cache.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""sphinxnotes.snippet.cache
"""
sphinxnotes.snippet.cache
~~~~~~~~~~~~~~~~~~~~~~~~~

:copyright: Copyright 2021 Shengyu Zhang
:license: BSD, see LICENSE for details.
"""

from __future__ import annotations
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass

from . import Snippet
from .snippets import Snippet
from .utils.pdict import PDict


Expand All @@ -18,25 +18,25 @@ class Item(object):
"""Item of snippet cache."""

snippet: Snippet
tags: List[str]
tags: str
excerpt: str
titlepath: List[str]
keywords: List[str]
titlepath: list[str]
keywords: list[str]


DocID = Tuple[str, str] # (project, docname)
DocID = tuple[str, str] # (project, docname)
IndexID = str # UUID
Index = Tuple[str, str, List[str], List[str]] # (tags, excerpt, titlepath, keywords)
Index = tuple[str, str, list[str], list[str]] # (tags, excerpt, titlepath, keywords)


class Cache(PDict):
"""A DocID -> List[Item] Cache."""
class Cache(PDict[DocID, list[Item]]):
"""A DocID -> list[Item] Cache."""

indexes: Dict[IndexID, Index]
index_id_to_doc_id: Dict[IndexID, Tuple[DocID, int]]
doc_id_to_index_ids: Dict[DocID, List[IndexID]]
num_snippets_by_project: Dict[str, int]
num_snippets_by_docid: Dict[DocID, int]
indexes: dict[IndexID, Index]
index_id_to_doc_id: dict[IndexID, tuple[DocID, int]]
doc_id_to_index_ids: dict[DocID, list[IndexID]]
num_snippets_by_project: dict[str, int]
num_snippets_by_docid: dict[DocID, int]

def __init__(self, dirname: str) -> None:
self.indexes = {}
Expand All @@ -46,7 +46,7 @@ def __init__(self, dirname: str) -> None:
self.num_snippets_by_docid = {}
super().__init__(dirname)

def post_dump(self, key: DocID, items: List[Item]) -> None:
def post_dump(self, key: DocID, value: list[Item]) -> None:
"""Overwrite PDict.post_dump."""

# Remove old indexes and index IDs if exists
Expand All @@ -55,7 +55,7 @@ def post_dump(self, key: DocID, items: List[Item]) -> None:
del self.indexes[old_index_id]

# Add new index to every where
for i, item in enumerate(items):
for i, item in enumerate(value):
index_id = self.gen_index_id()
self.indexes[index_id] = (
item.tags,
Expand All @@ -69,12 +69,12 @@ def post_dump(self, key: DocID, items: List[Item]) -> None:
# Update statistic
if key[0] not in self.num_snippets_by_project:
self.num_snippets_by_project[key[0]] = 0
self.num_snippets_by_project[key[0]] += len(items)
self.num_snippets_by_project[key[0]] += len(value)
if key not in self.num_snippets_by_docid:
self.num_snippets_by_docid[key] = 0
self.num_snippets_by_docid[key] += len(items)
self.num_snippets_by_docid[key] += len(value)

def post_purge(self, key: DocID, items: List[Item]) -> None:
def post_purge(self, key: DocID, value: list[Item]) -> None:
"""Overwrite PDict.post_purge."""

# Purge indexes
Expand All @@ -83,17 +83,17 @@ def post_purge(self, key: DocID, items: List[Item]) -> None:
del self.indexes[index_id]

# Update statistic
self.num_snippets_by_project[key[0]] -= len(items)
self.num_snippets_by_project[key[0]] -= len(value)
if self.num_snippets_by_project[key[0]] == 0:
del self.num_snippets_by_project[key[0]]
self.num_snippets_by_docid[key] -= len(items)
self.num_snippets_by_docid[key] -= len(value)
if self.num_snippets_by_docid[key] == 0:
del self.num_snippets_by_docid[key]

def get_by_index_id(self, key: IndexID) -> Optional[Item]:
def get_by_index_id(self, key: IndexID) -> Item | None:
"""Like get(), but use IndexID as key."""
doc_id, item_index = self.index_id_to_doc_id.get(key, (None, None))
if not doc_id:
if not doc_id or item_index is None:
return None
return self[doc_id][item_index]

Expand All @@ -103,6 +103,6 @@ def gen_index_id(self) -> str:

return uuid.uuid4().hex[:7]

def stringify(self, key: DocID, items: List[Item]) -> str:
def stringify(self, key: DocID, value: list[Item]) -> str:
"""Overwrite PDict.stringify."""
return key[1]
return key[1] # docname
Loading