创建一个自动为
root_dir/docs/source/conf.py
(和 .rst
)目录(及其子目录)中的每个 .py
文件生成 root_dir/src
文件后,我在链接到 root_dir/test/
时遇到了一些困难和 src/projectname/__main__.py
文件中的 root_dir/test/<test files>.py
。存储库结构如下:
.rst
(其中
src/projectname/__main__.py
src/projectname/helper.py
test/test_adder.py
docs/source/conf.py
是:
projectname
。)错误信息
pythontemplate
构建Sphinx文档时,我收到以下“警告”:
cd docs && make html
设计选择
WARNING: Failed to import pythontemplate.test.test_adder.
Possible hints:
* AttributeError: module 'pythontemplate' has no attribute 'test'
* ModuleNotFoundError: No module named 'pythontemplate.test'
...
WARNING: autodoc: failed to import module 'test.test_adder' from module 'pythontemplate'; the following exception was raised:
No module named 'pythontemplate.test'
中包含
test/
文件,有些项目将测试文件放入根目录中,本项目中遵循后者。通过将测试目录命名为 src/test
而不是 test
,它们将自动包含在使用 tests
创建的 dist
中。这是通过打开:pip install -e .
文件并验证 dist/pythontemplate-1.0.tar.gz
目录包含 pythontemplate-1.0
目录(以及 test
目录)来验证的。但是 src
目录不包含在 test
文件中。 (这是所希望的,因为用户不必运行测试,但如果他们想要使用 whl
则应该能够这样做)。生成的.rst文档文件
tar.gz
文件
test/test_adder.py
内容为:root_dir/docs/source/autogen/test/test_adder.rst
无法导入
.. _test_adder-module:
test_adder Module
=================
.. automodule:: test.test_adder
:members:
:undoc-members:
:show-inheritance:
文件的地方。 (我也尝试过
test.test_adder.py
虽然也没有导入它)。问题
.. automodule:: pythontemplate.test.test_adder
文件中(自动生成的)
test_<something>.py
文档引用 root_dir/test
文件夹中的 .rst
文件,以便 Sphinx 能够导入它?Conf.py
docs/source/autogen/test/test_<something>.rst
文件:
conf.py
注意
"""Configuration file for the Sphinx documentation builder.
For the full list of built-in configuration values, see the documentation:
https://www.sphinx-doc.org/en/master/usage/configuration.html
-- Project information -----------------------------------------------------
https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
""" #
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# 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.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
import os
import shutil
import sys
# This makes the Sphinx documentation tool look at the root of the repository
# for .py files.
from datetime import datetime
from pathlib import Path
from typing import List, Tuple
sys.path.insert(0, os.path.abspath(".."))
def split_filepath_into_three(*, filepath: str) -> Tuple[str, str, str]:
"""Split a file path into directory path, filename, and extension.
Args:
filepath (str): The input file path.
Returns:
Tuple[str, str, str]: A tuple containing directory path, filename, and
extension.
"""
path_obj: Path = Path(filepath)
directory_path: str = str(path_obj.parent)
filename = os.path.splitext(path_obj.name)[0]
extension = path_obj.suffix
return directory_path, filename, extension
def get_abs_root_path() -> str:
"""Returns the absolute path of the root dir of this repository.
Throws an error if the current path does not end in /docs/source.
"""
current_abs_path: str = os.getcwd()
assert_abs_path_ends_in_docs_source(current_abs_path=current_abs_path)
abs_root_path: str = current_abs_path[:-11]
return abs_root_path
def assert_abs_path_ends_in_docs_source(*, current_abs_path: str) -> None:
"""Asserts the current absolute path ends in /docs/source."""
if current_abs_path[-12:] != "/docs/source":
print(f"current_abs_path={current_abs_path}")
raise ValueError(
"Error, current_abs_path is expected to end in: /docs/source"
)
def loop_over_files(*, abs_search_path: str, extension: str) -> List[str]:
"""Loop over all files in the specified root directory and its child
directories.
Args:
root_directory (str): The root directory to start the traversal from.
"""
filepaths: List[str] = []
for root, _, files in os.walk(abs_search_path):
for filename in files:
extension_len: int = -len(extension)
if filename[extension_len:] == extension:
filepath = os.path.join(root, filename)
filepaths.append(filepath)
return filepaths
def is_unwanted(*, filepath: str) -> bool:
"""Hardcoded filter of unwanted datatypes."""
base_name = os.path.basename(filepath)
if base_name == "__init__.py":
return True
if base_name.endswith("pyc"):
return True
if "something/another" in filepath:
return True
return False
def filter_unwanted_files(*, filepaths: List[str]) -> List[str]:
"""Filters out unwanted files from a list of file paths.
Unwanted files include:
- Files named "init__.py"
- Files ending with "swag.py"
- Files in the subdirectory "something/another"
Args:
filepaths (List[str]): List of file paths.
Returns:
List[str]: List of filtered file paths.
"""
return [
filepath
for filepath in filepaths
if not is_unwanted(filepath=filepath)
]
def get_abs_python_filepaths(
*, abs_root_path: str, extension: str, root_folder_name: str
) -> List[str]:
"""Returns all the Python files in this repo."""
# Get the file lists.
py_files: List[str] = loop_over_files(
abs_search_path=f"{abs_root_path}docs/source/../../{root_folder_name}",
extension=extension,
)
# Merge and filter to preserve the relevant files.
filtered_filepaths: List[str] = filter_unwanted_files(filepaths=py_files)
return filtered_filepaths
def abs_to_relative_python_paths_from_root(
*, abs_py_paths: List[str], abs_root_path: str
) -> List[str]:
"""Converts the absolute Python paths to relative Python filepaths as seen
from the root dir."""
rel_py_filepaths: List[str] = []
for abs_py_path in abs_py_paths:
flattened_filepath = os.path.normpath(abs_py_path)
print(f"flattened_filepath={flattened_filepath}")
print(f"abs_root_path={abs_root_path}")
if abs_root_path not in flattened_filepath:
print(f"abs_root_path={abs_root_path}")
print(f"flattened_filepath={flattened_filepath}")
raise ValueError(
"Error, root dir should be in flattened_filepath."
)
rel_py_filepaths.append(
os.path.relpath(flattened_filepath, abs_root_path)
)
return rel_py_filepaths
def delete_directory(*, directory_path: str) -> None:
"""Deletes a directory and its contents.
Args:
directory_path (Union[str, bytes]): Path to the directory to be
deleted.
Raises:
FileNotFoundError: If the specified directory does not exist.
PermissionError: If the function lacks the necessary permissions to
delete the directory.
OSError: If an error occurs while deleting the directory.
Returns:
None
"""
if os.path.exists(directory_path) and os.path.isdir(directory_path):
shutil.rmtree(directory_path)
def create_relative_path(*, relative_path: str) -> None:
"""Creates a relative path if it does not yet exist.
Args:
relative_path (str): Relative path to create.
Returns:
None
"""
if not os.path.exists(relative_path):
os.makedirs(relative_path)
if not os.path.exists(relative_path):
raise NotADirectoryError(f"Error, did not find:{relative_path}")
def create_rst(
*,
autogen_dir: str,
rel_filedir: str,
filename: str,
pyproject_name: str,
py_type: str,
) -> None:
"""Creates a reStructuredText (.rst) file with automodule directives.
Args:
rel_filedir (str): Path to the directory where the .rst file will be
created.
filename (str): Name of the .rst file (without the .rst extension).
Returns:
None
"""
if py_type == "src":
prelude: str = pyproject_name
elif py_type == "test":
prelude = f"{pyproject_name}.test"
else:
raise ValueError(f"Error, py_type={py_type} is not supported.")
# if filename != "__main__":
title_underline = "=" * len(f"{filename}-module")
rst_content = f"""
.. _{filename}-module:
{filename} Module
{title_underline}
.. automodule:: {prelude}.{filename}
:members:
:undoc-members:
:show-inheritance:
"""
# .. automodule:: {rel_filedir.replace("/", ".")}.{filename}
rst_filepath: str = os.path.join(
f"{autogen_dir}{rel_filedir}", f"{filename}.rst"
)
with open(rst_filepath, "w", encoding="utf-8") as rst_file:
rst_file.write(rst_content)
def generate_rst_per_code_file(
*, extension: str, pyproject_name: str
) -> List[str]:
"""Generates a parameterised .rst file for each .py file of the project, to
automatically include its documentation in Sphinx.
Returns rst filepaths.
"""
abs_root_path: str = get_abs_root_path()
abs_src_py_paths: List[str] = get_abs_python_filepaths(
abs_root_path=abs_root_path,
extension=extension,
root_folder_name="src",
)
abs_test_py_paths: List[str] = get_abs_python_filepaths(
abs_root_path=abs_root_path,
extension=extension,
root_folder_name="test",
)
current_abs_path: str = os.getcwd()
autogen_dir: str = f"{current_abs_path}/autogen/"
prepare_rst_directories(autogen_dir=autogen_dir)
rst_paths: List[str] = []
rst_paths.extend(
create_rst_files(
pyproject_name=pyproject_name,
abs_root_path=abs_root_path,
autogen_dir=autogen_dir,
abs_py_paths=abs_src_py_paths,
py_type="src",
)
)
rst_paths.extend(
create_rst_files(
pyproject_name=pyproject_name,
abs_root_path=abs_root_path,
autogen_dir=autogen_dir,
abs_py_paths=abs_test_py_paths,
py_type="test",
)
)
return rst_paths
def prepare_rst_directories(*, autogen_dir: str) -> None:
"""Creates the output directory for the auto-generated .rst documentation
files."""
delete_directory(directory_path=autogen_dir)
create_relative_path(relative_path=autogen_dir)
def create_rst_files(
*,
pyproject_name: str,
abs_root_path: str,
autogen_dir: str,
abs_py_paths: List[str],
py_type: str,
) -> List[str]:
"""Loops over the python files of py_type src or test, and creates the .rst
files that point to the actual .py file such that Sphinx can generate its
documentation on the fly."""
rel_root_py_paths: List[str] = abs_to_relative_python_paths_from_root(
abs_py_paths=abs_py_paths, abs_root_path=abs_root_path
)
rst_paths: List[str] = []
# Create file for each py file.
for rel_root_py_path in rel_root_py_paths:
rel_filedir: str
filename: str
rel_filedir, filename, _ = split_filepath_into_three(
filepath=rel_root_py_path
)
create_relative_path(relative_path=f"{autogen_dir}{rel_filedir}")
create_rst(
autogen_dir=autogen_dir,
rel_filedir=rel_filedir,
filename=filename,
pyproject_name=pyproject_name,
py_type=py_type,
)
rst_path: str = os.path.join(f"autogen/{rel_filedir}", f"{filename}")
rst_paths.append(rst_path)
return rst_paths
def generate_index_rst(*, filepaths: List[str]) -> str:
"""Generates the list of all the auto-generated rst files."""
now = datetime.now().strftime("%a %b %d %H:%M:%S %Y")
content = f"""\
.. jsonmodipy documentation main file, created by
sphinx-quickstart on {now}.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. include:: manual.rst
Auto-generated documentation from Python code
=============================================
.. toctree::
:maxdepth: 2
"""
for filepath in filepaths:
content += f"\n {filepath}"
content += """
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
"""
return content
def write_index_rst(*, filepaths: List[str], output_file: str) -> None:
"""Creates an index.rst file that is used to generate the Sphinx
documentation."""
index_rst_content = generate_index_rst(filepaths=filepaths)
with open(output_file, "w", encoding="utf-8") as index_file:
index_file.write(index_rst_content)
# Call functions to generate rst Sphinx documentation structure.
# Readthedocs sets it to contents.rst, but it is index.rst in the used example.
# -- General configuration ---------------------------------------------------
project: str = "Decentralised-SAAS-Investment-Structure"
main_doc: str = "index"
PYPROJECT_NAME: str = "pythontemplate"
# pylint:disable=W0622
copyright: str = "2024, a-t-0"
author: str = "a-t-0"
the_rst_paths: List[str] = generate_rst_per_code_file(
extension=".py", pyproject_name=PYPROJECT_NAME
)
if len(the_rst_paths) == 0:
raise ValueError(
"Error, did not find any Python files for which documentation needs"
+ " to be generated."
)
write_index_rst(filepaths=the_rst_paths, output_file="index.rst")
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions: List[str] = [
"sphinx.ext.duration",
"sphinx.ext.doctest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
# Include markdown files in Sphinx documentation
"myst_parser",
]
# Add any paths that contain templates here, relative to this directory.
templates_path: List[str] = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns: List[str] = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme: str = "alabaster"
# 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,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path: List[str] = ["_static"]
pip 包中。如上所述,这是一种设计选择。问题是如何从
pythontemplate
文件导入这些测试文件,而不将 .rst
添加到 pip 包中。我可以将 test
文件的内容导入到应该执行自动文档的
test_adder.py
文件中,使用:.rst
但是,自动模块无法识别该路径,
.. _test_adder-module:
test_adder Module
=================
Hello
=====
.. include:: ../../../../test/test_adder.py
.. automodule:: ../../../../test/test_adder.py
:members:
:undoc-members:
:show-inheritance:
也无法识别。
automodule ........test/test_adder
使测试文件可以在 Sphinx 文档中找到:
conf.py