Skip to content

refactor: Change extension entrypoint to sphinxnotes.snippet (2nd) #34

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

Merged
merged 3 commits into from
Oct 14, 2024
Merged
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 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