You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
220 lines
7.6 KiB
220 lines
7.6 KiB
8 months ago
|
from .exception import BreatheError
|
||
|
|
||
|
from sphinx.application import Sphinx
|
||
|
|
||
|
import os
|
||
|
import fnmatch
|
||
|
|
||
|
|
||
|
from typing import Dict
|
||
|
|
||
|
|
||
|
class ProjectError(BreatheError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class NoDefaultProjectError(ProjectError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class AutoProjectInfo:
|
||
|
"""Created as a temporary step in the automatic xml generation process"""
|
||
|
|
||
|
def __init__(self, app: Sphinx, name: str, source_path: str, build_dir: str, reference: str):
|
||
|
self.app = app
|
||
|
|
||
|
self._name = name
|
||
|
self._source_path = source_path
|
||
|
self._build_dir = build_dir
|
||
|
self._reference = reference
|
||
|
|
||
|
def name(self):
|
||
|
return self._name
|
||
|
|
||
|
def build_dir(self):
|
||
|
return self._build_dir
|
||
|
|
||
|
def abs_path_to_source_file(self, file_):
|
||
|
"""
|
||
|
Returns full path to the provide file assuming that the provided path is relative to the
|
||
|
projects conf.py directory as specified in the breathe_projects_source config variable.
|
||
|
"""
|
||
|
|
||
|
# os.path.join does the appropriate handling if _source_path is an absolute path
|
||
|
return os.path.join(self.app.confdir, self._source_path, file_)
|
||
|
|
||
|
def create_project_info(self, project_path):
|
||
|
"""Creates a proper ProjectInfo object based on the information in this AutoProjectInfo"""
|
||
|
|
||
|
return ProjectInfo(self.app, self._name, project_path, self._source_path, self._reference)
|
||
|
|
||
|
|
||
|
class ProjectInfo:
|
||
|
def __init__(self, app: Sphinx, name: str, path: str, source_path: str, reference: str):
|
||
|
self.app = app
|
||
|
|
||
|
self._name = name
|
||
|
self._project_path = path
|
||
|
self._source_path = source_path
|
||
|
self._reference = reference
|
||
|
|
||
|
def name(self) -> str:
|
||
|
return self._name
|
||
|
|
||
|
def project_path(self):
|
||
|
return self._project_path
|
||
|
|
||
|
def source_path(self):
|
||
|
return self._source_path
|
||
|
|
||
|
def relative_path_to_xml_file(self, file_):
|
||
|
"""
|
||
|
Returns relative path from Sphinx documentation top-level source directory to the specified
|
||
|
file assuming that the specified file is a path relative to the doxygen xml output
|
||
|
directory.
|
||
|
"""
|
||
|
|
||
|
# os.path.join does the appropriate handling if _project_path is an absolute path
|
||
|
full_xml_project_path = os.path.join(self.app.confdir, self._project_path, file_)
|
||
|
return os.path.relpath(full_xml_project_path, self.app.srcdir)
|
||
|
|
||
|
def sphinx_abs_path_to_file(self, file_):
|
||
|
"""
|
||
|
Prepends os.path.sep to the value returned by relative_path_to_file.
|
||
|
|
||
|
This is to match Sphinx's concept of an absolute path which starts from the top-level source
|
||
|
directory of the project.
|
||
|
"""
|
||
|
return os.path.sep + self.relative_path_to_xml_file(file_)
|
||
|
|
||
|
def reference(self):
|
||
|
return self._reference
|
||
|
|
||
|
def domain_for_file(self, file_: str) -> str:
|
||
|
extension = file_.split(".")[-1]
|
||
|
try:
|
||
|
domain = self.app.config.breathe_domain_by_extension[extension]
|
||
|
except KeyError:
|
||
|
domain = ""
|
||
|
|
||
|
domainFromFilePattern = self.app.config.breathe_domain_by_file_pattern
|
||
|
for pattern, pattern_domain in domainFromFilePattern.items():
|
||
|
if fnmatch.fnmatch(file_, pattern):
|
||
|
domain = pattern_domain
|
||
|
|
||
|
return domain
|
||
|
|
||
|
|
||
|
class ProjectInfoFactory:
|
||
|
def __init__(self, app: Sphinx):
|
||
|
self.app = app
|
||
|
# note: don't access self.app.config now, as we are instantiated at setup-time.
|
||
|
|
||
|
# Assume general build directory is the doctree directory without the last component.
|
||
|
# We strip off any trailing slashes so that dirname correctly drops the last part.
|
||
|
# This can be overridden with the breathe_build_directory config variable
|
||
|
self._default_build_dir = os.path.dirname(app.doctreedir.rstrip(os.sep))
|
||
|
self.project_count = 0
|
||
|
self.project_info_store: Dict[str, ProjectInfo] = {}
|
||
|
self.project_info_for_auto_store: Dict[str, AutoProjectInfo] = {}
|
||
|
self.auto_project_info_store: Dict[str, AutoProjectInfo] = {}
|
||
|
|
||
|
@property
|
||
|
def build_dir(self) -> str:
|
||
|
config = self.app.config
|
||
|
if config.breathe_build_directory:
|
||
|
return config.breathe_build_directory
|
||
|
else:
|
||
|
return self._default_build_dir
|
||
|
|
||
|
def default_path(self) -> str:
|
||
|
config = self.app.config
|
||
|
if not config.breathe_default_project:
|
||
|
raise NoDefaultProjectError(
|
||
|
"No breathe_default_project config setting to fall back on "
|
||
|
"for directive with no 'project' or 'path' specified."
|
||
|
)
|
||
|
|
||
|
try:
|
||
|
return config.breathe_projects[config.breathe_default_project]
|
||
|
except KeyError:
|
||
|
raise ProjectError(
|
||
|
(
|
||
|
"breathe_default_project value '%s' does not seem to be a valid key for the "
|
||
|
"breathe_projects dictionary"
|
||
|
)
|
||
|
% config.breathe_default_project
|
||
|
)
|
||
|
|
||
|
def create_project_info(self, options) -> ProjectInfo:
|
||
|
config = self.app.config
|
||
|
name = config.breathe_default_project
|
||
|
|
||
|
if "project" in options:
|
||
|
try:
|
||
|
path = config.breathe_projects[options["project"]]
|
||
|
name = options["project"]
|
||
|
except KeyError:
|
||
|
raise ProjectError(
|
||
|
"Unable to find project '%s' in breathe_projects dictionary"
|
||
|
% options["project"]
|
||
|
)
|
||
|
elif "path" in options:
|
||
|
path = options["path"]
|
||
|
else:
|
||
|
path = self.default_path()
|
||
|
|
||
|
try:
|
||
|
return self.project_info_store[path]
|
||
|
except KeyError:
|
||
|
reference = name
|
||
|
if not name:
|
||
|
name = "project%s" % self.project_count
|
||
|
reference = path
|
||
|
self.project_count += 1
|
||
|
|
||
|
project_info = ProjectInfo(self.app, name, path, "NoSourcePath", reference)
|
||
|
self.project_info_store[path] = project_info
|
||
|
return project_info
|
||
|
|
||
|
def store_project_info_for_auto(self, name: str, project_info: AutoProjectInfo) -> None:
|
||
|
"""Stores the project info by name for later extraction by the auto directives.
|
||
|
|
||
|
Stored separately to the non-auto project info objects as they should never overlap.
|
||
|
"""
|
||
|
|
||
|
self.project_info_for_auto_store[name] = project_info
|
||
|
|
||
|
def retrieve_project_info_for_auto(self, options) -> AutoProjectInfo:
|
||
|
"""Retrieves the project info by name for later extraction by the auto directives.
|
||
|
|
||
|
Looks for the 'project' entry in the options dictionary. This is a less than ideal API but
|
||
|
it is designed to match the use of 'create_project_info' above for which it makes much more
|
||
|
sense.
|
||
|
"""
|
||
|
|
||
|
name = options.get("project", self.app.config.breathe_default_project)
|
||
|
if name is None:
|
||
|
raise NoDefaultProjectError(
|
||
|
"No breathe_default_project config setting to fall back on "
|
||
|
"for directive with no 'project' or 'path' specified."
|
||
|
)
|
||
|
return self.project_info_for_auto_store[name]
|
||
|
|
||
|
def create_auto_project_info(self, name: str, source_path) -> AutoProjectInfo:
|
||
|
key = source_path
|
||
|
try:
|
||
|
return self.auto_project_info_store[key]
|
||
|
except KeyError:
|
||
|
reference = name
|
||
|
if not name:
|
||
|
name = "project%s" % self.project_count
|
||
|
reference = source_path
|
||
|
self.project_count += 1
|
||
|
|
||
|
auto_project_info = AutoProjectInfo(
|
||
|
self.app, name, source_path, self.build_dir, reference
|
||
|
)
|
||
|
self.auto_project_info_store[key] = auto_project_info
|
||
|
return auto_project_info
|