diff --git a/src/docc/__init__.py b/src/docc/__init__.py index bd3546f..9336b28 100644 --- a/src/docc/__init__.py +++ b/src/docc/__init__.py @@ -17,5 +17,5 @@ The documentation compiler. """ -__version__ = "0.5.1" +__version__ = "0.6.0" "Current version of docc" diff --git a/src/docc/plugins/files.py b/src/docc/plugins/files.py index c52b9b3..32400a2 100644 --- a/src/docc/plugins/files.py +++ b/src/docc/plugins/files.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Ethereum Foundation +# Copyright (C) 2023,2026 Ethereum Foundation # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -131,6 +131,11 @@ class FilesDiscover(Discover): sources: Sequence[FileSource] + def _file_source( + self, relative_path: PurePath, absolute_path: PurePath + ) -> FileSource: + return FileSource(relative_path, absolute_path) + def __init__(self, config: PluginSettings) -> None: """ Construct a new instance with the given configuration. @@ -140,13 +145,12 @@ def __init__(self, config: PluginSettings) -> None: self.sources = [] else: sources = [] + self.sources = sources for item in files: absolute_path = config.resolve_path(PurePath(item)) relative_path = config.unresolve_path(absolute_path) - sources.append(FileSource(relative_path, absolute_path)) - - self.sources = sources + sources.append(self._file_source(relative_path, absolute_path)) def discover(self, known: FrozenSet[T]) -> Iterator[Source]: """ diff --git a/src/docc/plugins/html/__init__.py b/src/docc/plugins/html/__init__.py index 4e7b694..bb12986 100644 --- a/src/docc/plugins/html/__init__.py +++ b/src/docc/plugins/html/__init__.py @@ -17,7 +17,6 @@ Plugin that renders to HTML. """ - import html.parser import sys import xml.etree.ElementTree as ET @@ -144,26 +143,31 @@ def __init__(self, config: PluginSettings) -> None: Construct a new instance with the given configuration. """ + def _resource_source_with_path( + self, mod: str, input_path: PurePath, output_path: PurePath + ) -> ResourceSource: + return ResourceSource.with_path(mod, input_path, output_path) + def discover(self, known: FrozenSet[T]) -> Iterator[Source]: """ Find sources. """ - yield ResourceSource.with_path( + yield self._resource_source_with_path( "docc.plugins.html", PurePath("static") / "chota" / "dist" / "chota.min.css", PurePath("static") / "chota", ) - yield ResourceSource.with_path( + yield self._resource_source_with_path( "docc.plugins.html", PurePath("static") / "docc.css", PurePath("static") / "docc", ) - yield ResourceSource.with_path( + yield self._resource_source_with_path( "docc.plugins.html", PurePath("static") / "fuse" / "dist" / "fuse.min.js", PurePath("static") / "fuse", ) - yield ResourceSource.with_path( + yield self._resource_source_with_path( "docc.plugins.html", PurePath("static") / "search.js", PurePath("static") / "search", diff --git a/src/docc/plugins/listing/__init__.py b/src/docc/plugins/listing/__init__.py index f41adf5..b9f732c 100644 --- a/src/docc/plugins/listing/__init__.py +++ b/src/docc/plugins/listing/__init__.py @@ -17,7 +17,6 @@ Plugin that renders directory listings. """ -from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable from os.path import commonpath @@ -29,6 +28,7 @@ Final, FrozenSet, Iterator, + Optional, Set, Tuple, Type, @@ -50,7 +50,7 @@ from docc.transform import Transform -class Listable(ABC): +class Listable: """ Mixin to change visibility of a Source in a directory listing. """ @@ -63,7 +63,7 @@ def _sorting_key( return thing.listing_order_key() elif isinstance(thing, Source): path = thing.relative_path or thing.output_path - return (True, path, None) + return (Listable._index_dir(thing) is None, path, None) return (True, None, thing) @staticmethod @@ -76,18 +76,27 @@ def _show_source(source: Source) -> bool: return True + @staticmethod + def _index_dir(source: Source) -> Optional[PurePath]: + if isinstance(source, Listable): + return source.index_dir + return None + @property - @abstractmethod - def show_in_listing(self) -> bool: + def index_dir(self) -> Optional[PurePath]: """ - `True` if this `Source` should be shown in directory listings. + For index sources, the directory the source indexes. For other sources, + `None`. + + For example, for an output path of `./foo/index`, this should return + `./foo`. """ - raise NotImplementedError() + return None @property - def is_leaf(self) -> bool: + def show_in_listing(self) -> bool: """ - `True` if this `Source` cannot contain child sources (eg. a file). + `True` if this `Source` should be shown in directory listings. """ return True @@ -99,8 +108,8 @@ def listing_order_key( """ if isinstance(self, Source): path = self.relative_path or self.output_path - return (self.is_leaf, path, None) - return (self.is_leaf, None, self) + return (self.index_dir is None, path, None) + return (True, None, self) class ListingDiscover(Discover): @@ -111,15 +120,20 @@ class ListingDiscover(Discover): def __init__(self, config: PluginSettings) -> None: pass + def _index_path(self, parent: PurePath) -> PurePath: + return parent / "index" + def _listing_source( self, source: Source, parent: PurePath ) -> "ListingSource": - return ListingSource(parent, parent / "index", set()) + return ListingSource(parent, self._index_path(parent)) def discover(self, known: FrozenSet[T]) -> Iterator["ListingSource"]: """ Find sources. """ + output_paths = {s.output_path: s for s in known} + listings = {} for source in known: @@ -134,11 +148,14 @@ def discover(self, known: FrozenSet[T]) -> Iterator["ListingSource"]: try: listing = listings[parent] except KeyError: - listing = self._listing_source(source, parent) - listings[parent] = listing - yield listing + index_path = self._index_path(parent) + try: + listing = output_paths[index_path] + except KeyError: + listing = self._listing_source(source, parent) + listings[parent] = listing + yield listing - listing.sources.add(source) source = listing @@ -158,8 +175,13 @@ def add_source(self, source: Source) -> None: """ Register a source. """ - path = source.relative_path or source.output_path - self.sources[path.parent].add(source) + index_dir = Listable._index_dir(source) + if index_dir is None: + path = source.relative_path or source.output_path + self.sources[path.parent].add(source) + else: + self.sources[index_dir].add(source) + self.sources[index_dir.parent].add(source) def descendants(self, source: Source) -> Iterable[Source]: """ @@ -215,19 +237,13 @@ class ListingSource(Source, Listable): _relative_path: Final[PurePath] _output_path: Final[PurePath] - sources: Final[Set[Source]] - - show_in_listing: bool = True - is_leaf: bool = False def __init__( self, relative_path: PurePath, output_path: PurePath, - sources: Set[Source], ) -> None: self._relative_path = relative_path - self.sources = sources self._output_path = output_path @property @@ -244,6 +260,17 @@ def relative_path(self) -> PurePath: """ return self._relative_path + @property + def index_dir(self) -> PurePath: + """ + For index sources, the directory the source indexes. For other sources, + `None`. + + For example, for an output path of `./foo/index`, this should return + `./foo`. + """ + return self.output_path.parent + class ListingBuilder(Builder): """ @@ -339,6 +366,8 @@ def render_html( sources = sorted(sources, key=Listable._sorting_key) output_path = context[Source].output_path + output_parent = output_path.parent + entries = [] for source in sources: @@ -360,13 +389,20 @@ def render_html( ) # TODO: Don't hardcode extension. active = source is context[Source] - path = source.relative_path or source.output_path - - if node.leaf: - path = path.name - - if isinstance(source, Listable) and not source.is_leaf: - path = str(path) + "/" + entry_index = Listable._index_dir(source) + relative = source.relative_path or source.output_path + + if entry_index is None or ( + entry_index == output_parent and relative != entry_index + ): + # Regular source, or an index source (like `__init__.py`) appearing + # in its own listing. Show as ``. + path = relative.name if node.leaf else str(relative) + else: + # Synthetic listing, or a file-based index (like `__init__.py`) + # appearing in its parent directory's listing. Show as `/`. + path = entry_index.name if node.leaf else str(entry_index) + path = path + "/" entries.append((path, relative_path, active)) diff --git a/src/docc/plugins/python/cst.py b/src/docc/plugins/python/cst.py index 7f7ee20..e70c076 100644 --- a/src/docc/plugins/python/cst.py +++ b/src/docc/plugins/python/cst.py @@ -48,7 +48,7 @@ from docc.context import Context from docc.discover import Discover, T from docc.document import BlankNode, Document, ListNode, Node, Visit, Visitor -from docc.plugins.listing import ListingNode +from docc.plugins.listing import Listable, ListingNode from docc.plugins.references import Definition, Reference from docc.plugins.verbatim import Fragment, Pos, Verbatim from docc.settings import PluginSettings @@ -87,6 +87,14 @@ class PythonDiscover(Discover): settings: PluginSettings + def _python_source( + self, + root_path: PurePath, + relative_path: PurePath, + absolute_path: PurePath, + ) -> "PythonSource": + return PythonSource(root_path, relative_path, absolute_path) + def __init__(self, config: PluginSettings) -> None: self.settings = config @@ -127,10 +135,12 @@ def discover(self, known: FrozenSet[T]) -> Iterator[Source]: parents = relative_path.parents if not any(p in parents for p in self.excluded_paths): - yield PythonSource(root_path, relative_path, absolute_path) + yield self._python_source( + root_path, relative_path, absolute_path + ) -class PythonSource(TextSource): +class PythonSource(TextSource, Listable): """ A Source representing a Python file. """ @@ -149,6 +159,9 @@ def __init__( self._relative_path = relative_path self.absolute_path = absolute_path + def _is_init(self) -> bool: + return self._relative_path.name == "__init__.py" + @property def relative_path(self) -> Optional[PurePath]: """ @@ -161,6 +174,9 @@ def output_path(self) -> PurePath: """ Where to put the output derived from this source. """ + if self._is_init(): + return self._relative_path.with_name("index") + return self._relative_path def open(self) -> TextIO: @@ -169,6 +185,19 @@ def open(self) -> TextIO: """ return open(self.absolute_path, "r") + @property + def index_dir(self) -> Optional[PurePath]: + """ + For index sources, the directory the source indexes. For other sources, + `None`. + + For example, for an output path of `./foo/index`, this should return + `./foo`. + """ + if self._is_init(): + return self.output_path.parent + return None + class PythonBuilder(Builder): """