Skip to content

Commit b7c3fa4

Browse files
authored
Merge pull request #12 from sphinx-notes/section
Section snippet
2 parents acbf0c4 + dbdbdea commit b7c3fa4

File tree

14 files changed

+385
-533
lines changed

14 files changed

+385
-533
lines changed

sphinxnotes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
99
:copyright: Copyright 2020 by the Shengyu Zhang.
1010
"""
11+
12+
# NOTE: Don't use ``__import__('pkg_resources').declare_namespace(__name__)``
13+
# here, it causes the application startup process to be slower

sphinxnotes/snippet/__init__.py

Lines changed: 95 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -7,194 +7,143 @@
77
"""
88

99
from __future__ import annotations
10-
from typing import List, Tuple, Optional, Any, Dict
11-
from dataclasses import dataclass, field
12-
from abc import ABC, abstractclassmethod
10+
from typing import List, Tuple, Optional
1311
import itertools
1412

1513
from docutils import nodes
1614

1715

1816
__title__= 'sphinxnotes-snippet'
19-
__license__ = 'BSD',
17+
__license__ = 'BSD'
2018
__version__ = '1.0b6'
2119
__author__ = 'Shengyu Zhang'
2220
__url__ = 'https://sphinx-notes.github.io/snippet'
2321
__description__ = 'Non-intrusive snippet manager for Sphinx documentation'
2422
__keywords__ = 'documentation, sphinx, extension, utility'
2523

26-
@dataclass
27-
class Snippet(ABC):
24+
class Snippet(object):
2825
"""
29-
Snippet is a {abstract,data}class represents a snippet of reStructuredText
30-
documentation. Note that it is not always continuous fragment at text (rst)
31-
level.
32-
"""
33-
_scope:Tuple[int,int] = field(init=False)
34-
_refid:Optional[str] = field(init=False)
35-
36-
def __post_init__(self) -> None:
37-
"""Post-init processing routine of dataclass"""
38-
39-
# Calcuate scope before deepcopy
40-
scope = [float('inf'), -float('inf')]
41-
for node in self.nodes():
42-
if not node.line:
43-
continue # Skip node that have None line, I dont know why :'(
44-
scope[0] = min(scope[0], line_of_start(node))
45-
scope[1] = max(scope[1], line_of_end(node))
46-
self._scope = scope
47-
48-
# Find exactly one id attr in nodes
49-
self._refid = None
50-
for node in self.nodes():
51-
if node['ids']:
52-
self._refid = node['ids'][0]
53-
break
54-
# If no node has id, use parent's
55-
if not self._refid:
56-
for node in self.nodes():
57-
if node.parent['ids']:
58-
self._refid = node.parent['ids'][0]
59-
break
60-
61-
62-
@abstractclassmethod
63-
def nodes(self) -> List[nodes.Node]:
64-
"""Return the out of tree nodes that make up this snippet."""
65-
pass
66-
67-
68-
@abstractclassmethod
69-
def excerpt(self) -> str:
70-
"""Return excerpt of snippet (for preview)."""
71-
pass
72-
73-
74-
@abstractclassmethod
75-
def kind(self) -> str:
76-
"""Return kind of snippet (for filtering)."""
77-
pass
78-
79-
80-
def file(self) -> str:
81-
"""Return source file path of snippet"""
82-
# All nodes should have same source file
83-
return self.nodes()[0].source
84-
85-
86-
def scope(self) -> Tuple[int,int]:
87-
"""
88-
Return the scope of snippet, which corresponding to the line
89-
number in the source file.
90-
91-
A scope is a left closed and right open interval of the line number
92-
``[left, right)``.
93-
"""
94-
return self._scope
95-
96-
97-
def text(self) -> List[str]:
98-
"""Return the original reStructuredText text of snippet."""
99-
return read_partial_file(self.file(), self.scope())
26+
Snippet is base class of reStructuredText snippet.
10027
28+
:param nodes: Document nodes that make up this snippet
29+
"""
10130

102-
def refid(self) -> Optional[str]:
103-
"""
104-
Return the possible identifier key of snippet.
105-
It is picked from nodes' (or nodes' parent's) `ids attr`_.
31+
#: Source file path of snippet
32+
file:str
10633

107-
.. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids
108-
"""
109-
return self._refid
34+
#: Line number range of snippet, in the source file which is left closed
35+
#: and right opened.
36+
lineno:Tuple[int,int]
11037

38+
#: The original reStructuredText of snippet
39+
rst:List[str]
11140

112-
def __getstate__(self) -> Dict[str,Any]:
113-
"""Implement :py:meth:`pickle.object.__getstate__`."""
114-
return self.__dict__.copy()
41+
#: The possible identifier key of snippet, which is picked from nodes'
42+
#: (or nodes' parent's) `ids attr`_.
43+
#:
44+
#: .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids
45+
refid:Optional[str]
11546

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

117-
@dataclass
118-
class Headline(Snippet):
119-
"""Documentation title and possible subtitle."""
120-
title:nodes.title
121-
subtitle:Optional[nodes.title]
50+
self.file = nodes[0].source
12251

123-
def nodes(self) -> List[nodes.Node]:
124-
if not self.subtitle:
125-
return [self.title]
126-
return [self.title, self.subtitle]
52+
lineno = [float('inf'), -float('inf')]
53+
for node in nodes:
54+
if not node.line:
55+
continue # Skip node that have None line, I dont know why
56+
lineno[0] = min(lineno[0], _line_of_start(node))
57+
lineno[1] = max(lineno[1], _line_of_end(node))
58+
self.lineno = lineno
59+
60+
lines = []
61+
with open(self.file, "r") as f:
62+
start = self.lineno[0] - 1
63+
stop = self.lineno[1] - 1
64+
for line in itertools.islice(f, start, stop):
65+
lines.append(line.strip('\n'))
66+
self.rst = lines
67+
68+
# Find exactly one ID attr in nodes
69+
self.refid = None
70+
for node in nodes:
71+
if node['ids']:
72+
self.refid = node['ids'][0]
73+
break
12774

75+
# If no ID found, try parent
76+
if not self.refid:
77+
for node in nodes:
78+
if node.parent['ids']:
79+
self.refid = node.parent['ids'][0]
80+
break
12881

129-
def excerpt(self) -> str:
130-
if not self.subtitle:
131-
return '<%s>' % self.title.astext()
132-
return '<%s ~%s~>' % (self.title.astext(), self.subtitle.astext())
13382

13483

135-
@classmethod
136-
def kind(cls) -> str:
137-
return 'd'
84+
class Text(Snippet):
85+
#: Text of snippet
86+
text:str
13887

88+
def __init__(self, node:nodes.Node) -> None:
89+
super().__init__(node)
90+
self.text = node.astext()
13991

140-
def text(self) -> List[str]:
141-
"""
142-
Headline represents a reStructuredText document,
143-
so return the whole source file.
144-
"""
145-
with open(self.file()) as f:
146-
return f.read().splitlines()
14792

93+
class CodeBlock(Text):
94+
#: Language of code block
95+
language:str
96+
#: Caption of code block
97+
caption:Optional[str]
14898

149-
def __getstate__(self) -> Dict[str,Any]:
150-
self.title = self.title.deepcopy()
151-
if self.subtitle:
152-
self.subtitle = self.subtitle.deepcopy()
153-
return super().__getstate__()
99+
def __init__(self, node:nodes.literal_block) -> None:
100+
assert isinstance(node, nodes.literal_block)
101+
super().__init__(node)
102+
self.language = node['language']
103+
self.caption = node.get('caption')
154104

155105

156-
@dataclass
157-
class Code(Snippet):
158-
"""A code block with description."""
159-
description:List[nodes.Body]
160-
block:nodes.literal_block
106+
class WithCodeBlock(object):
107+
code_blocks:List[CodeBlock]
161108

162-
def nodes(self) -> List[nodes.Node]:
163-
return self.description.copy() + [self.block]
109+
def __init__(self, nodes:nodes.Nodes) -> None:
110+
self.code_blocks = []
111+
for n in nodes.traverse(nodes.literal_block):
112+
self.code_blocks.append(self.CodeBlock(n))
164113

165114

166-
def excerpt(self) -> str:
167-
return '/%s/ ' % self.language() + \
168-
self.description[0].astext().replace('\n', '')
115+
class Title(Text):
116+
def __init__(self, node:nodes.title) -> None:
117+
assert isinstance(node, nodes.title)
118+
super().__init__(node)
169119

170120

171-
@classmethod
172-
def kind(cls) -> str:
173-
return 'c'
121+
class WithTitle(object):
122+
title:Optional[Title]
174123

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

176-
def language(self) -> str:
177-
"""Return the (programing) language that appears in code."""
178-
return self.block['language']
179128

129+
class Section(Snippet, WithTitle):
130+
def __init__(self, node:nodes.section) -> None:
131+
assert isinstance(node, nodes.section)
132+
Snippet.__init__(self, node)
133+
WithTitle.__init__(self, node)
180134

181-
def __getstate__(self) -> Dict[str,Any]:
182-
self.description = [x.deepcopy() for x in self.description]
183-
self.block = self.block.deepcopy()
184-
return super().__getstate__()
185135

136+
class Document(Section):
137+
def __init__(self, node:nodes.document) -> None:
138+
assert isinstance(node, nodes.document)
139+
super().__init__(node.next_node(nodes.section))
186140

187-
def read_partial_file(filename:str, scope:Tuple[int,Optional[int]]) -> List[str]:
188-
lines = []
189-
with open(filename, "r") as f:
190-
start = scope[0] - 1
191-
stop = scope[1] - 1 if scope[1] else None
192-
for line in itertools.islice(f, start, stop):
193-
lines.append(line.strip('\n'))
194-
return lines
195141

142+
################
143+
# Nodes helper #
144+
################
196145

197-
def line_of_start(node:nodes.Node) -> int:
146+
def _line_of_start(node:nodes.Node) -> int:
198147
assert node.line
199148
if isinstance(node, nodes.title):
200149
if isinstance(node.parent.parent, nodes.document):
@@ -213,11 +162,11 @@ def line_of_start(node:nodes.Node) -> int:
213162
return node.line
214163

215164

216-
def line_of_end(node:nodes.Node) -> Optional[int]:
165+
def _line_of_end(node:nodes.Node) -> Optional[int]:
217166
next_node = node.next_node(descend=False, siblings=True, ascend=True)
218167
while next_node:
219168
if next_node.line:
220-
return line_of_start(next_node)
169+
return _line_of_start(next_node)
221170
next_node = next_node.next_node(
222171
# Some nodes' line attr is always None, but their children has
223172
# valid line attr

sphinxnotes/snippet/cache.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
class Item(object):
1717
""" Item of snippet cache. """
1818
snippet:Snippet
19+
tags:List[str]
20+
excerpt:str
1921
titlepath:List[str]
2022
keywords:List[str]
2123

2224

2325
DocID = Tuple[str,str] # (project, docname)
2426
IndexID = str # UUID
25-
Index = Tuple[str,str,List[str],List[str]] # (kind, excerpt, titlepath, keywords)
27+
Index = Tuple[str,str,List[str],List[str]] # (tags, excerpt, titlepath, keywords)
2628

2729
class Cache(PDict):
2830
"""A DocID -> List[Item] Cache."""
@@ -52,8 +54,8 @@ def post_dump(self, key:DocID, items:List[Item]) -> None:
5254
# Add new index to every where
5355
for i, item in enumerate(items):
5456
index_id = self.gen_index_id()
55-
self.indexes[index_id] = (item.snippet.kind(),
56-
item.snippet.excerpt(),
57+
self.indexes[index_id] = (item.tags,
58+
item.excerpt,
5759
item.titlepath,
5860
item.keywords)
5961
self.index_id_to_doc_id[index_id] = (key, i)

0 commit comments

Comments
 (0)