diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 2dd7d75..9d34f19 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -14,7 +14,7 @@ jobs: - name: Setup and build 🔧 run: | sudo apt-get install python3-pip - pip3 install sphinx sphinx-vhdl + pip3 install sphinx sphinx-vhdl sphinx_rtd_theme sphinx-build -M html doc/source public touch public/html/.nojekyll - name: Deploy 🚀 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index b9f4abc..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,21 +0,0 @@ -image: python:3.7-alpine - -run: - script: - - python3 setup.py bdist_wheel - artifacts: - paths: - - dist/*.whl - -pages: - stage: deploy - script: - - pip install -U sphinx - - pip install -U sphinx-rtd-theme - - pip install dist/*.whl - - sphinx-build -b html doc/source/ public - artifacts: - paths: - - public - only: - - main diff --git a/README.md b/README.md index ac92e23..3389bbe 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# Sphinx-vhdl +# SPHINX-VHDL [![PyPI](https://img.shields.io/pypi/v/sphinx-vhdl)](https://pypi.org/project/sphinx-vhdl/) -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/CESNET/sphinx-vhdl/documentation?label=documentation)](https://cesnet.github.io/sphinx-vhdl/) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/CESNET/sphinx-vhdl/.github/workflows/doc.yml)](https://cesnet.github.io/sphinx-vhdl/) > A [sphinx](https://www.sphinx-doc.org/) domain for semi-automatically documenting VHDL This extension for Sphinx allows you to keep your documentation in code and automatically draw it out into your main documentation using just few simple directives. -You can see the detailed documentation at https://cesnet.github.io/sphinx-vhdl/, or build it yourself (running `make html` while in the `doc` directory and having `sphinx` installed should be sufficient) +You can see the detailed documentation at https://cesnet.github.io/sphinx-vhdl/, or build it yourself (running `make` while in the `doc` directory and having `sphinx` + `sphinx_rtd_theme` installed should be sufficient) ## Usage @@ -16,7 +16,9 @@ The python package must be installed with pip3 install sphinx-vhdl ``` -The usage of this extension requires Python >= 3.6 and Sphinx >= 4.0.0. +This extension requires Python >= 3.8 and Sphinx >= 6.0.0. + +*Note that your documentation may use multiple sphinx extensions or an alternative theme (such as `sphinx_rtd_theme`), which you must also have installed.* ## Configuration @@ -27,6 +29,12 @@ extensions = ['sphinxvhdl.vhdl'] vhdl_autodoc_source_path = 'path/to/your/vhdl/sources/root' ``` +## Where is the SPHINX-VHDL extension used? + +- [Open FPGA Modules (OFM) by CESNET](https://github.com/CESNET/ofm/) +- [NDK Minimal Application by CESNET](https://github.com/CESNET/ndk-app-minimal) +- *Do you use SPHINX-VHDL in your public VHDL repository? Please add a link to this list!* + ## Repository maintainer - Jakub Cabal, cabal@cesnet.cz diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..bf783b4 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,9 @@ +# Minimal makefile for Sphinx documentation + +.PHONY: all clean + +all: + sphinx-build -M html source build -v -E + +clean: + rm -rf ./build diff --git a/doc/source/conf.py b/doc/source/conf.py index 2110a7c..fed007e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = 'sphinx-vhdl' -copyright = '2021, Cesnet z.s.p.o.' +copyright = '2024, Cesnet z.s.p.o.' author = 'Cesnet z.s.p.o.' @@ -31,7 +31,7 @@ 'sphinxvhdl.vhdl' ] -vhdl_autodoc_source_path = 'doc/source' +vhdl_autodoc_source_path = './' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -47,7 +47,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/doc/source/example_built.rst b/doc/source/example_built.rst index b2568a3..a4fd609 100644 --- a/doc/source/example_built.rst +++ b/doc/source/example_built.rst @@ -5,7 +5,7 @@ Example Documentation .. vhdl:autopackage:: math_pack - .. vhdl:autofunction:: log2 +.. vhdl:autofunction:: log2 .. vhdl:autoentity:: counter .. vhdl:autoconstants:: counter diff --git a/setup.cfg b/setup.cfg index cb8b458..bb10b27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sphinx-vhdl -version = 0.1.6 +version = 0.2.0 author = CESNET z.s.p.o. author_email = cabal@cesnet.cz description = A Sphinx domain and autodocumenter for VHDL @@ -21,7 +21,9 @@ classifiers = package_dir = = src packages = find: -python_requires = >=3.6 +python_requires = >=3.8 +install_requires = + sphinx >= 6.0 [options.packages.find] where = src \ No newline at end of file diff --git a/src/sphinxvhdl/autodoc.py b/src/sphinxvhdl/autodoc.py index 96d8838..4b7b292 100644 --- a/src/sphinxvhdl/autodoc.py +++ b/src/sphinxvhdl/autodoc.py @@ -1,6 +1,7 @@ # autodoc.py: A basic VHDL parser and documentation extractor # Copyright (C) 2021 CESNET z.s.p.o. # Author(s): Jindrich Dite +# Jakub Cabal # # SPDX-License-Identifier: BSD-3-Clause @@ -10,9 +11,8 @@ from typing import Optional, List from enum import Enum, auto -import logging - -LOG = logging.getLogger('sphinxvhdl-autodoc') +from sphinx.util import logging +logger = logging.getLogger(__name__) entities = {} portsignals = defaultdict(dict) @@ -28,24 +28,14 @@ functions = {} # Function for parsing line comments -def parse_inline_doc_or_raise(line: str, current_doc: List[str]): +def parse_inline_doc_or_print_error(current_doc, filename, line, lineno): if '-- ' in line: if len(current_doc) > 0: - raise ValueError( - 'Documented entity has both a pre- and inline documentation; only one is allowed. Offending line:\n' + - line) + logger.warning(f"SPHINX-VHDL: Documented entity has both a pre- and inline documentation; only one is allowed!\n Offending line: {line}", location=f"{filename}:{lineno}") else: current_doc.append(line.split('-- ', 1)[1]) -def parse_inline_doc_or_print_error(current_doc, filename, line, lineno): - try: - parse_inline_doc_or_raise(line, current_doc) - except ValueError as ex: - LOG.warning(f'Error parsing file {filename} at line {lineno}:') - LOG.warning(ex.args) - - class ParseState(Enum): ENTITY_DECL = auto() ARCH_DECL = auto() @@ -79,24 +69,25 @@ def init(path: str) -> None: for line in source_code: lineno += 1 line = line.strip() + line_lowercase = line.lower() # Group parsing logic if state == ParseState.PORT and group_state == ParseState.GENERIC: current_group = "" # Line comments logic - if line.startswith('-- '): + if line_lowercase.startswith('-- '): # Logic for sampling names of groups of ports and generics - if (state == ParseState.PORT or state == ParseState.GENERIC) and '====' in line: + if (state == ParseState.PORT or state == ParseState.GENERIC) and '====' in line_lowercase: group_state = state state = ParseState.GROUPS current_group = "" current_doc = [] - elif state == ParseState.GROUPS and current_group != '' and '====' not in line: + elif state == ParseState.GROUPS and current_group != '' and '====' not in line_lowercase: current_doc.append(line[3:]) - elif state == ParseState.GROUPS and '====' not in line: + elif state == ParseState.GROUPS and '====' not in line_lowercase: current_group = current_entity + " " + line[3:].strip() current_doc = [] - elif state == ParseState.GROUPS and '====' in line: + elif state == ParseState.GROUPS and '====' in line_lowercase: group_definition = current_doc groups_desc[current_group] = group_definition state = group_state @@ -105,12 +96,12 @@ def init(path: str) -> None: current_doc.append(line[3:]) # If line start with keyword architecture then save name of architecture - elif line.lower().startswith('architecture'): + elif line_lowercase.startswith('architecture'): state = ParseState.ARCH_DECL current_constant = line.split()[3] # If line contains keyword constant and state is not generice then start to collecting constants - elif state == ParseState.ARCH_DECL and 'constant' in line: + elif state == ParseState.ARCH_DECL and 'constant' in line_lowercase: parse_inline_doc_or_print_error(current_doc, filename, line, lineno) definition = line.split('--')[0].split(';')[0] if ':=' not in definition: @@ -120,12 +111,12 @@ def init(path: str) -> None: current_doc = [] # If there is -- without gap, then ignore - elif line == '--': + elif line_lowercase == '--': current_doc.append('') # If there is word entity then try parse, save entity name and add description of entity to associative array # ID of ass. array is name of entity. At the end clear current description and change state to entity declaration - elif line.lower().startswith('entity ') and ' is' in line: + elif line_lowercase.startswith('entity ') and ' is' in line_lowercase: parse_inline_doc_or_print_error(current_doc, filename, line, lineno) current_entity = line.split()[1] entities[current_entity.lower()] = current_doc @@ -133,17 +124,17 @@ def init(path: str) -> None: state = ParseState.ENTITY_DECL # Check if there is any port declaration - elif state == ParseState.ENTITY_DECL and line.lower().startswith('port'): + elif state == ParseState.ENTITY_DECL and line_lowercase.startswith('port'): state = ParseState.PORT current_doc = [] # Check if there is any generic declaration - elif state == ParseState.ENTITY_DECL and line.lower().startswith('generic'): + elif state == ParseState.ENTITY_DECL and line_lowercase.startswith('generic'): state = ParseState.GENERIC current_doc = [] # If there is line which contains ":" then it's one of ports, parse it and save his definition - elif state == ParseState.PORT and ':' in line: + elif state == ParseState.PORT and ':' in line_lowercase: parse_inline_doc_or_print_error(current_doc, filename, line, lineno) definition = line.split('--')[0].split(';')[0].split(':=')[0].strip() if definition.lower().startswith('signal'): @@ -157,7 +148,7 @@ def init(path: str) -> None: current_doc = [] # If there is line which contains ":" then it's one of generic, parse it and save his definition - elif state == ParseState.GENERIC and ':' in line: + elif state == ParseState.GENERIC and ':' in line_lowercase: parse_inline_doc_or_print_error(current_doc, filename, line, lineno) definition = line.split('--')[0].split(';')[0].strip() if ':=' not in definition: @@ -173,13 +164,13 @@ def init(path: str) -> None: current_doc = [] # End of the entity was found - elif state == ParseState.ENTITY_DECL and line.lower().startswith('end'): + elif state == ParseState.ENTITY_DECL and line_lowercase.startswith('end'): state = None group_state = None current_doc = [] # If there is magic word package then parse package and save his definition - elif (state is None or state is ParseState.PACKAGE) and line.lower().startswith('package'): + elif (state is None or state is ParseState.PACKAGE) and line_lowercase.startswith('package'): parse_inline_doc_or_print_error(current_doc, filename, line, lineno) state = ParseState.PACKAGE current_package = ('' if current_package == '' else (current_package + '.')) + line.split()[1] @@ -187,13 +178,13 @@ def init(path: str) -> None: current_doc = [] # Signalization of end of the package - elif state is ParseState.PACKAGE and line.lower().startswith('end package'): + elif state is ParseState.PACKAGE and line_lowercase.startswith('end package'): current_package = '.'.join(current_package.split('.')[:-1]) state = None if current_package == '' else ParseState.PACKAGE current_doc = [] # Package contains type, parse it - elif (state is None or state is ParseState.PACKAGE) and line.lower().startswith('type'): + elif (state is None or state is ParseState.PACKAGE) and line_lowercase.startswith('type'): if ' record' in line.split('--')[0].lower().split(maxsplit=2)[-1]: parse_inline_doc_or_print_error(current_doc, filename, line, lineno) records[line.split()[1]] = current_doc @@ -212,7 +203,7 @@ def init(path: str) -> None: current_doc = [] # Signalization of the end of record - elif state is ParseState.RECORD and line.lower().startswith('end record'): + elif state is ParseState.RECORD and line_lowercase.startswith('end record'): if current_package != '': state = ParseState.PACKAGE else: @@ -228,16 +219,16 @@ def init(path: str) -> None: # Enumarate parsing elif state is ParseState.ENUM: - if not line.startswith(')'): + if not line_lowercase.startswith(')'): parse_inline_doc_or_print_error(current_doc, filename, line, lineno) enumvals[current_type_name][line.split(',')[0]] = current_doc current_doc = [] # Function parsing - elif line.lower().startswith('function') and line.split('--')[0].strip().endswith(';'): + elif line_lowercase.startswith('function') and line.split('--')[0].strip().endswith(';'): parse_inline_doc_or_print_error(current_doc, filename, line, lineno) return_type = '' if 'return' not in line else (line.split('return')[1].strip()[:-1] + '.') - functions[return_type + line.lower().split()[1]] = current_doc + functions[return_type + line_lowercase.split()[1]] = current_doc current_doc = [] # Ignore others diff --git a/src/sphinxvhdl/vhdl.py b/src/sphinxvhdl/vhdl.py index 5ce0e4d..df5caf9 100644 --- a/src/sphinxvhdl/vhdl.py +++ b/src/sphinxvhdl/vhdl.py @@ -1,6 +1,7 @@ # vhdl.py: A vhdl domain for the Sphinx documentation system # Copyright (C) 2021 CESNET z.s.p.o. # Author(s): Jindrich Dite +# Jakub Cabal # # SPDX-License-Identifier: BSD-3-Clause @@ -19,14 +20,17 @@ from sphinx.roles import XRefRole from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode +from sphinx.util import logging from . import autodoc +logger = logging.getLogger(__name__) def init_autodoc(domain: Domain): if not domain.data['autodoc_initialized']: domain.data['autodoc_initialized'] = True autodoc.init(domain.env.app.config.vhdl_autodoc_source_path) + logger.info('SPHINX-VHDL: Parsing of VHDL files completed.') class VHDLEnumTypeDirective(ObjectDescription): @@ -376,11 +380,17 @@ class VHDLAutoEntityDirective(VHDLEntityDirective): def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: init_autodoc(self.env.domains['vhdl']) - self.content = self.content + StringList(['', ''] + autodoc.entities[sig.lower()]) - if 'noautogenerics' not in self.options: - self.content = self.content + StringList(['', f'.. vhdl:autogenerics:: {sig}', '']) - if 'noautoports' not in self.options: - self.content = self.content + StringList(['', f'.. vhdl:autoports:: {sig}', '']) + try: + my_entity = autodoc.entities[sig.lower()] + self.content = self.content + StringList(['', ''] + autodoc.entities[sig.lower()]) + if 'noautogenerics' not in self.options: + self.content = self.content + StringList(['', f'.. vhdl:autogenerics:: {sig}', '']) + if 'noautoports' not in self.options: + self.content = self.content + StringList(['', f'.. vhdl:autoports:: {sig}', '']) + except: + logger.warning(f"SPHINX-VHDL: Entity {sig.lower()} was not found in parsed VHDL files!", location=self.get_location()) + self.content = self.content + StringList([f"SPHINX-VHDL: Entity was not found in parsed VHDL files!"]) + return super().handle_signature(sig, signode) @@ -398,9 +408,15 @@ class VHDLAutoFunctionDirective(VHDLFunctionDirective): def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: init_autodoc(self.env.domains['vhdl']) identifier = get_closest_identifier(sig.lower(), list(autodoc.functions.items())) - self.content = self.content + StringList(['', ''] + identifier[1]) - super().handle_signature(f'{identifier[0].split(".")[-1]} {identifier[0].split(".")[0]}', signode) - return sig + if identifier is None: + logger.warning(f"SPHINX-VHDL: Function {sig.lower()} was not found in parsed VHDL files!", location=self.get_location()) + self.content = StringList([f"SPHINX-VHDL: Function was not found in parsed VHDL files!"]) + self.content + sig = f'{sig.lower()} Unknown' + else: + self.content = self.content + StringList(['', ''] + identifier[1]) + sig = f'{identifier[0].split(".")[-1]} {identifier[0].split(".")[0]}' + + return super().handle_signature(sig, signode) class VHDLAutoEnumDirective(VHDLEnumTypeDirective): @@ -415,7 +431,12 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: class VHDLAutoPackageDirective(VHDLPackagesDirective): def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: init_autodoc(self.env.domains['vhdl']) - self.content = StringList(get_closest_identifier(sig, list(autodoc.packages.items()))[1] + ['', '']) + self.content + identifier = get_closest_identifier(sig.lower(), list(autodoc.packages.items())) + if identifier is None: + logger.warning(f"SPHINX-VHDL: Package {sig.lower()} was not found in parsed VHDL files!", location=self.get_location()) + self.content = StringList([f"SPHINX-VHDL: Package was not found in parsed VHDL files!"]) + self.content + else: + self.content = StringList(identifier[1] + ['', '']) + self.content return super().handle_signature(sig, signode) @@ -457,9 +478,14 @@ class VHDLAutoTypeDirective(VHDLGeneralTypeDirective): def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: init_autodoc(self.env.domains['vhdl']) - closest_identifier = get_closest_identifier(sig, autodoc.types.items()) - self.content = self.content + StringList(['', ''] + closest_identifier[1][1]) - return super().handle_signature(sig + " : " + closest_identifier[1][0], signode) + identifier = get_closest_identifier(sig.lower(), autodoc.types.items()) + if identifier is None: + logger.warning(f"SPHINX-VHDL: Type {sig.lower()} was not found in parsed VHDL files!", location=self.get_location()) + self.content = StringList([f"SPHINX-VHDL: Type was not found in parsed VHDL files!"]) + self.content + return super().handle_signature(sig + " : Unknown", signode) + else: + self.content = self.content + StringList(['', ''] + identifier[1][1]) + return super().handle_signature(sig + " : " + identifier[1][0], signode) class VHDLTypeIndex(Index): @@ -483,22 +509,28 @@ def generate(self, docnames: Iterable[str] = None) -> Tuple[List[Tuple[str, List return result, True -def get_closest_identifier(target_identifier: str, search_through: List[Tuple[str, ObjDescT]]) -> Tuple[str, ObjDescT]: +def get_closest_identifier(target_identifier: str, search_through: List[Tuple[str, ObjDescT]]): """ Finds the item with the closes matching identifier to a target one in a list :param target_identifier: an identifier to match against :param search_through: List of pairs of an identifier and any other bound data - :return: The tuple with closes match + :return: The tuple with closes match or None """ identifier_part = target_identifier.split('.') option_list = [] + match = False for x in search_through: a = 0 for y in x[0].split('.'): if y in identifier_part: + match = True a += 1 option_list.append((a, x)) - return max(option_list, key=lambda z: z[0])[1] + + if match: + return max(option_list, key=lambda z: z[0])[1] + else: + return None class VHDLDomain(Domain): @@ -566,18 +598,22 @@ def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Buil raise NotImplementedError simple_name = target.split('.')[-1].lower() if simple_name in index: - target_address = get_closest_identifier(target, index[simple_name]) - result = make_refnode(builder, fromdocname, - target_address[1][0], - target_address[1][1], - contnode) - return result + target_address = get_closest_identifier(target.lower(), index[simple_name]) + if target_address is None: + logger.warning(f"SPHINX-VHDL: Unknown reference {target} discovered by resolve_xref function!") + else: + result = make_refnode(builder, fromdocname, + target_address[1][0], + target_address[1][1], + contnode) + return result def setup(app: Sphinx): app.add_domain(VHDLDomain) app.add_config_value('vhdl_autodoc_source_path', '.', 'env', [str]) + logger.verbose('The sphinx-vhdl extension has been activated.') return { - 'version': '0.1' + 'version': '0.2' }