Skip to content

Commit

Permalink
Merge pull request #34 from sphinx-notes/refactor/ext-entrypoint2
Browse files Browse the repository at this point in the history
refactor: Change extension entrypoint to sphinxnotes.snippet (2nd)
  • Loading branch information
SilverRainZ authored Oct 14, 2024
2 parents 264be70 + 31a46cb commit 4dc6f74
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 214 deletions.
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
218 changes: 28 additions & 190 deletions src/sphinxnotes/snippet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,197 +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 os import path

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

#: Absolute path of the source file.
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):
#: A set of absolute paths of dependent files for document.
#: Obtained from :attr:`BuildEnvironment.dependencies`.
deps: set[str]

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

# Record document's dependent files
self.deps = set()
env: BuildEnvironment = node.settings.env
for dep in env.dependencies[self.docname]:
# Relative to documentation root -> Absolute path of file system.
self.deps.add(path.join(env.srcdir, dep))


################
# 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)
2 changes: 1 addition & 1 deletion src/sphinxnotes/snippet/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import List, Tuple, Dict, Optional
from dataclasses import dataclass

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


Expand Down
31 changes: 26 additions & 5 deletions src/sphinxnotes/snippet/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
sphinxnotes.snippet.cli
~~~~~~~~~~~~~~~~~~~~~~~
:copyright: Copyright 2020 Shengyu Zhang
Command line entrypoint.
:copyright: Copyright 2024 Shengyu Zhang
:license: BSD, see LICENSE for details.
"""

# **NOTE**: Import new packages with caution:
# Importing complex packages (like sphinx.*) will directly slow down the
# startup of the CLI tool.
from __future__ import annotations
import sys
import argparse
Expand All @@ -16,9 +21,8 @@
import posixpath

from xdg.BaseDirectory import xdg_config_home
from sphinx.util.matching import patmatch

from . import __version__, Document
from .snippets import Document
from .config import Config
from .cache import Cache, IndexID, Index
from .table import tablify, COLUMNS
Expand All @@ -39,7 +43,7 @@ def get_integration_file(fn: str) -> str:
.. seealso::
see ``[tool.setuptools.package-data]`` section of pyproject.toml to know
how files are included.
how files are included.
"""
# TODO: use https://docs.python.org/3/library/importlib.resources.html#importlib.resources.files
prefix = path.abspath(path.dirname(__file__))
Expand All @@ -61,7 +65,11 @@ def main(argv: List[str] = sys.argv[1:]):
* (any) wildcard for any snippet"""),
)
parser.add_argument(
'-v', '--version', action='version', version='%(prog)s ' + __version__
'--version',
# add_argument provides action='version', but it requires a version
# literal and doesn't support lazily obtaining version.
action='store_true',
help="show program's version number and exit",
)
parser.add_argument(
'-c', '--config', default=DEFAULT_CONFIG_FILE, help='path to configuration file'
Expand Down Expand Up @@ -176,6 +184,16 @@ def main(argv: List[str] = sys.argv[1:]):
# Parse command line arguments
args = parser.parse_args(argv)

# Print version message.
# See parser.add_argument('--version', ...) for more detais.
if args.version:
# NOTE: Importing is slow, do it on demand.
from importlib.metadata import version

pkgname = 'sphinxnotes.snippet'
print(pkgname, version(pkgname))
parser.exit()

# Load config from file
if args.config == DEFAULT_CONFIG_FILE and not path.isfile(DEFAULT_CONFIG_FILE):
print(
Expand Down Expand Up @@ -219,6 +237,9 @@ def _on_command_stat(args: argparse.Namespace):
def _filter_list_items(
cache: Cache, tags: str, docname_glob: str
) -> Iterable[Tuple[IndexID, Index]]:
# NOTE: Importing is slow, do it on demand.
from sphinx.util.matching import patmatch

for index_id, index in cache.indexes.items():
# Filter by tags.
if index[0] not in tags and '*' not in tags:
Expand Down
20 changes: 4 additions & 16 deletions src/sphinxnotes/snippet/ext.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
sphinxnotes.ext.snippet
sphinxnotes.snippet.ext
~~~~~~~~~~~~~~~~~~~~~~~
Sphinx extension for sphinxnotes.snippet.
Sphinx extension implementation, but the entrypoint is located at __init__.py.
:copyright: Copyright 2021 Shengyu Zhang
:copyright: Copyright 2024 Shengyu Zhang
:license: BSD, see LICENSE for details.
"""

Expand All @@ -26,7 +26,7 @@
from collections.abc import Iterator

from .config import Config
from . import Snippet, WithTitle, Document, Section
from .snippets import Snippet, WithTitle, Document, Section
from .picker import pick
from .cache import Cache, Item
from .keyword import Extractor
Expand Down Expand Up @@ -206,15 +206,3 @@ def _format_modified_time(timestamp: float) -> str:
"""Return an RFC 3339 formatted string representing the given timestamp."""
seconds, fraction = divmod(timestamp, 1)
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(seconds)) + f'.{fraction:.3f}'


def setup(app: Sphinx):
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)
2 changes: 1 addition & 1 deletion src/sphinxnotes/snippet/picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from sphinx.util import logging

from . import Snippet, Section, Document
from .snippets import Snippet, Section, Document

if TYPE_CHECKING:
from sphinx.application import Sphinx
Expand Down
Loading

0 comments on commit 4dc6f74

Please sign in to comment.