Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sources/pyside6/PySide6/glue/qtcore.cpp4
-rw-r--r--sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp2
-rw-r--r--sources/shiboken6/ApiExtractor/clangparser/clangbuilder.h5
-rw-r--r--sources/shiboken6/ApiExtractor/parser/codemodel.cpp16
-rw-r--r--sources/shiboken6/libshiboken/pep384impl.cpp2
-rw-r--r--sources/shiboken6/libshiboken/pep384impl.h13
-rw-r--r--tools/example_gallery/main.py672
7 files changed, 373 insertions, 341 deletions
diff --git a/sources/pyside6/PySide6/glue/qtcore.cpp b/sources/pyside6/PySide6/glue/qtcore.cpp
index 2708bfeda..689946652 100644
--- a/sources/pyside6/PySide6/glue/qtcore.cpp
+++ b/sources/pyside6/PySide6/glue/qtcore.cpp
@@ -1536,9 +1536,7 @@ double in = %CONVERTTOCPP[double](%in);
// @snippet conversion-sbkobject
// a class supported by QVariant?
QMetaType metaType;
-if (Shiboken::Enum::check(%in))
-{
- const auto baseType = PyObject_Type(%in);
+if (Shiboken::Enum::check(%in)) {
const auto typeName = PySide::QEnum::getTypeName(Py_TYPE(%in));
if (!typeName.isEmpty())
metaType = QMetaType::fromName(typeName);
diff --git a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp
index 6a2ae9128..5188262de 100644
--- a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp
+++ b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.cpp
@@ -370,10 +370,12 @@ static inline CodeModel::FunctionType functionTypeFromCursor(const CXCursor &cur
result = CodeModel::Destructor;
break;
case CXCursor_CXXMethod:
+#ifdef CLANG_HAS_ASSIGNMENT_OPERATOR_CHECK
if (clang_CXXMethod_isCopyAssignmentOperator(cursor) != 0)
result = CodeModel::AssignmentOperator;
else if (clang_CXXMethod_isMoveAssignmentOperator(cursor) != 0)
result = CodeModel::MoveAssignmentOperator;
+#endif
break;
default:
break;
diff --git a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.h b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.h
index 218aa6163..f60bbe155 100644
--- a/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.h
+++ b/sources/shiboken6/ApiExtractor/clangparser/clangbuilder.h
@@ -8,6 +8,11 @@
#include <codemodel_fwd.h>
+
+#if CINDEX_VERSION_MAJOR > 0 || CINDEX_VERSION_MINOR >= 63 // Clang 16
+# define CLANG_HAS_ASSIGNMENT_OPERATOR_CHECK
+#endif
+
namespace clang {
class BuilderPrivate;
diff --git a/sources/shiboken6/ApiExtractor/parser/codemodel.cpp b/sources/shiboken6/ApiExtractor/parser/codemodel.cpp
index e3182db9d..a5994bd75 100644
--- a/sources/shiboken6/ApiExtractor/parser/codemodel.cpp
+++ b/sources/shiboken6/ApiExtractor/parser/codemodel.cpp
@@ -1117,8 +1117,22 @@ CodeModel::FunctionType _FunctionModelItem::_determineTypeHelper() const
auto newType = newTypeOpt.value();
// If clang did not pre-detect AssignmentOperator for some operator=(),
// it is an assignment from another type which we are not interested in.
- if (newType == CodeModel::AssignmentOperator)
+ if (newType == CodeModel::AssignmentOperator) {
+#ifndef CLANG_HAS_ASSIGNMENT_OPERATOR_CHECK
+ // For clang 14 (Yocto), add a manual check.
+ if (m_arguments.size() == 1 && !type().isVoid()
+ && type().qualifiedName() == m_arguments.constFirst()->type().qualifiedName()) {
+ switch (m_arguments.constFirst()->type().referenceType()) {
+ case NoReference:
+ case LValueReference:
+ return CodeModel::AssignmentOperator;
+ case RValueReference:
+ return CodeModel::MoveAssignmentOperator;
+ }
+ }
+#endif // !CLANG_HAS_ASSIGNMENT_OPERATOR_CHECK
return CodeModel::OtherAssignmentOperator;
+ }
// It's some sort of dereference operator?!
if (m_arguments.isEmpty()) {
switch (newType) {
diff --git a/sources/shiboken6/libshiboken/pep384impl.cpp b/sources/shiboken6/libshiboken/pep384impl.cpp
index bd7a4c51a..7136fc59d 100644
--- a/sources/shiboken6/libshiboken/pep384impl.cpp
+++ b/sources/shiboken6/libshiboken/pep384impl.cpp
@@ -464,7 +464,7 @@ Pep_GetVerboseFlag()
// Support for pyerrors.h
-#if defined(Py_LIMITED_API) || PY_VERSION_HEX < 0x030C0000
+#ifdef PEP_OLD_ERR_API
// Emulate PyErr_GetRaisedException() using the deprecated PyErr_Fetch()/PyErr_Store()
PyObject *PepErr_GetRaisedException()
{
diff --git a/sources/shiboken6/libshiboken/pep384impl.h b/sources/shiboken6/libshiboken/pep384impl.h
index 4c4e1b47e..0f0c30129 100644
--- a/sources/shiboken6/libshiboken/pep384impl.h
+++ b/sources/shiboken6/libshiboken/pep384impl.h
@@ -190,15 +190,20 @@ LIBSHIBOKEN_API int Pep_GetFlag(const char *name);
LIBSHIBOKEN_API int Pep_GetVerboseFlag(void);
#endif
+#if (defined(Py_LIMITED_API) && Py_LIMITED_API < 0x030C0000) || PY_VERSION_HEX < 0x030C0000
+# define PEP_OLD_ERR_API
+#endif
+
// pyerrors.h
-#if defined(Py_LIMITED_API) || PY_VERSION_HEX < 0x030C0000
+#ifdef PEP_OLD_ERR_API
LIBSHIBOKEN_API PyObject *PepErr_GetRaisedException();
LIBSHIBOKEN_API PyObject *PepException_GetArgs(PyObject *ex);
LIBSHIBOKEN_API void PepException_SetArgs(PyObject *ex, PyObject *args);
#else
-# define PepErr_GetRaisedException PyErr_GetRaisedException
-# define PepException_GetArgs PyException_GetArgs
-# define PepException_SetArgs PyException_SetArgs
+inline PyObject *PepErr_GetRaisedException() { return PyErr_GetRaisedException(); }
+inline PyObject *PepException_GetArgs(PyObject *ex) { return PyException_GetArgs(ex); }
+inline void PepException_SetArgs(PyObject *ex, PyObject *args)
+{ PyException_SetArgs(ex, args); }
#endif
/*****************************************************************************
diff --git a/tools/example_gallery/main.py b/tools/example_gallery/main.py
index 8dc0789fa..6469c0c35 100644
--- a/tools/example_gallery/main.py
+++ b/tools/example_gallery/main.py
@@ -2,19 +2,7 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
from __future__ import annotations
-
-"""
-This tool reads all the examples from the main repository that have a
-'.pyproject' file, and generates a special table/gallery in the documentation
-page.
-
-For the usage, simply run:
- python tools/example_gallery/main.py
-since there is no special requirements.
-"""
-
-import json
-import math
+import fnmatch
import os
import shutil
import zipfile
@@ -23,8 +11,12 @@ from argparse import ArgumentParser, RawTextHelpFormatter
from dataclasses import dataclass
from enum import IntEnum, Enum
from pathlib import Path
-from textwrap import dedent
from collections import defaultdict
+from typing import DefaultDict
+
+sys.path.append(os.fspath(Path(__file__).parent.parent.parent / "sources" / "pyside-tools"))
+from project_lib import parse_pyproject_json, parse_pyproject_toml, \
+ PYPROJECT_FILE_PATTERNS, PYPROJECT_TOML_PATTERN, PYPROJECT_JSON_PATTERN # noqa: E402
class Format(Enum):
@@ -32,41 +24,39 @@ class Format(Enum):
MD = 1
-class ModuleType(IntEnum):
- ESSENTIALS = 0
- ADDONS = 1
- M2M = 2
-
-
-SUFFIXES = {Format.RST: "rst", Format.MD: "md"}
-
-
-opt_quiet = False
-
+__doc__ = """\
+This tool scans the main repository for examples with project files and generates a documentation
+page formatted as a gallery, displaying the examples in a table
+For the usage, simply run:
+ python tools/example_gallery/main.py
+"""
+DIR = Path(__file__).parent
+EXAMPLES_DOC = Path(f"{DIR}/../../sources/pyside6/doc/examples").resolve()
+EXAMPLES_DIR = Path(f"{DIR}/../../examples/").resolve()
+TARGET_HELP = f"Directory into which to generate Doc files (default: {str(EXAMPLES_DOC)})"
+BASE_URL = "https://code.qt.io/cgit/pyside/pyside-setup.git/tree"
+DOC_SUFFIXES = {Format.RST: "rst", Format.MD: "md"}
LITERAL_INCLUDE = ".. literalinclude::"
-
-
IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".svgz", ".webp")
-
-
+# Suffixes to ignore when displaying source files that are referenced in the project file
IGNORED_SUFFIXES = IMAGE_SUFFIXES + (".pdf", ".pyc", ".obj", ".mesh")
-
-
-suffixes = {
- ".h": "cpp",
- ".cpp": "cpp",
- ".md": "markdown",
- ".py": "py",
- ".qml": "js",
- ".conf": "ini",
- ".qrc": "xml",
- ".ui": "xml",
- ".xbel": "xml",
- ".xml": "xml",
+LANGUAGE_PATTERNS = {
+ "*.h": "cpp",
+ "*.cpp": "cpp",
+ "*.md": "markdown",
+ "*.py": "py",
+ "*.qml": "js",
+ "*.qmlproject": "js",
+ "*.conf": "ini",
+ "*.qrc": "xml",
+ "*.ui": "xml",
+ "*.xbel": "xml",
+ "*.xml": "xml",
+ "*.html": "html",
+ "CMakeLists.txt": "cmake",
}
-
BASE_CONTENT = """\
.. _pyside6_examples:
@@ -82,42 +72,59 @@ Examples
directory.
"""
+# We generate a 'toctree' at the end of the file to include the new 'example' rst files, so we get
+# no warnings and also that users looking for them will be able to, since they are indexed
+# Notice that :hidden: will not add the list of files by the end of the main examples HTML page.
+FOOTER_INDEX = """\
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+"""
+TUTORIAL_HEADLINES = {
+ "tutorials/extending-qml/chapter": "Tutorial: Writing QML Extensions with Python",
+ "tutorials/extending-qml-advanced/advanced": "Tutorial: Writing advanced QML Extensions with"
+ "Python",
+ "tutorials/finance_manager": "Tutorial: Finance Manager - Integrating PySide6 with SQLAlchemy "
+ "and FastAPI",
+}
-def tutorial_headline(path: str):
- if "tutorials/extending-qml/chapter" in path:
- return "Tutorial: Writing QML Extensions with Python"
- if "tutorials/extending-qml-advanced/advanced" in path:
- return "Tutorial: Writing advanced QML Extensions with Python"
- if "tutorials/finance_manager" in path:
- return "Tutorial: Finance Manager - Integrating PySide6 with SQLAlchemy and FastAPI"
- return ""
+
+class ModuleType(IntEnum):
+ ESSENTIALS = 0
+ ADDONS = 1
+ M2M = 2
-def ind(x):
- return " " * 4 * x
+def get_lexer(path: Path) -> str:
+ """Given a file path, return the language lexer to use for syntax highlighting"""
+ for pattern, lexer in LANGUAGE_PATTERNS.items():
+ if fnmatch.fnmatch(path.name, pattern):
+ return lexer
+ # Default to text
+ return "text"
-def get_lexer(path):
- if path.name == "CMakeLists.txt":
- return "cmake"
- lexer = suffixes.get(path.suffix)
- return lexer if lexer else "text"
+def ind(level: int) -> str:
+ """Return a string of spaces for string indentation given certain level"""
+ return " " * 4 * level
-def add_indent(s, level):
+def add_indent(s: str, level: int) -> str:
+ """Add indentation to a string"""
new_s = ""
for line in s.splitlines():
if line.strip():
new_s += f"{ind(level)}{line}\n"
else:
+ # Empty line
new_s += "\n"
return new_s
-def check_img_ext(i):
- """Check whether path is an image."""
- return i.suffix in IMAGE_SUFFIXES
+def check_img_ext(image_path: Path) -> bool:
+ """Check whether a file path is an image depending on its suffix."""
+ return image_path.suffix in IMAGE_SUFFIXES
@dataclass
@@ -177,15 +184,15 @@ MODULE_DESCRIPTIONS = {
}
-def module_sort_key(name):
- """Return key for sorting modules."""
+def module_sort_key(name: str) -> str:
+ """Return a key for sorting the Qt modules."""
description = MODULE_DESCRIPTIONS.get(name)
module_type = int(description.module_type) if description else 5
sort_key = description.sort_key if description else 100
return f"{module_type}:{sort_key:04}:{name}"
-def module_title(name):
+def module_title(name: str) -> str:
"""Return title for a module."""
result = name.title()
description = MODULE_DESCRIPTIONS.get(name)
@@ -205,25 +212,22 @@ def module_title(name):
class ExampleData:
"""Example data for formatting the gallery."""
- def __init__(self):
- self.headline = ""
-
- example: str
- module: str
- extra: str
- doc_file: str
- file_format: Format
- abs_path: str
- has_doc: bool
- img_doc: Path
- headline: str
- tutorial: str
+ example_name: str = None
+ module: str = None
+ extra: str = None
+ doc_file: str = None
+ file_format: Format = None
+ abs_path: str = None
+ src_doc_file: Path = None
+ img_doc: Path = None
+ tutorial: str = None
+ headline: str = ""
-def get_module_gallery(examples):
+def get_module_gallery(examples: list[ExampleData]) -> str:
"""
- This function takes a list of dictionaries, that contain examples
- information, from one specific module.
+ This function takes a list of examples from one specific module and returns the resulting string
+ in RST format that can be used to generate the table for the examples
"""
gallery = (
@@ -231,25 +235,22 @@ def get_module_gallery(examples):
f"{ind(2)}:gutter: 3\n\n"
)
- # Iteration per rows
- for i in range(math.ceil(len(examples))):
- e = examples[i]
- suffix = SUFFIXES[e.file_format]
+ for i, example in enumerate(examples):
+ suffix = DOC_SUFFIXES[example.file_format]
# doc_file with suffix removed, to be used as a sphinx reference
- doc_file_name = e.doc_file.replace(f".{suffix}", "")
# lower case sphinx reference
# this seems to be a bug or a requirement from sphinx
- doc_file_name = doc_file_name.lower()
+ doc_file_name = example.doc_file.replace(f".{suffix}", "").lower()
- name = e.example
- underline = e.module
+ name = example.example_name
+ underline = example.module
- if e.extra:
- underline += f"/{e.extra}"
+ if example.extra:
+ underline += f"/{example.extra}"
if i > 0:
gallery += "\n"
- img_name = e.img_doc.name if e.img_doc else "../example_no_image.png"
+ img_name = example.img_doc.name if example.img_doc else "../example_no_image.png"
# Fix long names
if name.startswith("chapter"):
@@ -259,28 +260,17 @@ def get_module_gallery(examples):
# Handling description from original file
desc = ""
- original_dir = Path(e.abs_path) / "doc"
-
- if e.has_doc:
- # cannot use e.doc_file because that is the target file name
- # so finding the original file by the name
- original_file = (next(original_dir.glob("*.rst"), None)
- or next(original_dir.glob("*.md"), None))
- if not original_file:
- # ideally won't reach here because has_doc is True
- print(f"example_gallery: No .rst or .md file found in {original_dir}")
- continue
-
- with original_file.open("r", encoding="utf-8") as f:
+ if example.src_doc_file:
+ with example.src_doc_file.open("r", encoding="utf-8") as f:
# Read the first line
first_line = f.readline().strip()
# Check if the first line is a reference (starts with '(' and ends with ')=' for MD,
# or starts with '.. ' and ends with '::' for RST)
- if ((e.file_format == Format.MD and first_line.startswith('(')
+ if ((example.file_format == Format.MD and first_line.startswith('(')
and first_line.endswith(')='))
- or (e.file_format == Format.RST and first_line.startswith('.. ')
- and first_line.endswith('::'))):
+ or (example.file_format == Format.RST and first_line.startswith('.. ')
+ and first_line.endswith('::'))):
# The first line is a reference, so read the next lines until a non-empty line
# is found
while True:
@@ -294,21 +284,17 @@ def get_module_gallery(examples):
# The next line handling depends on the file format
line = f.readline().strip()
- if e.file_format == Format.MD:
+ if example.file_format == Format.MD:
# For markdown, the second line is the empty line
- if line != "":
- # If the line is not empty, raise a runtime error
- raise RuntimeError(f"Unexpected line: {line} in {original_file}. "
- "Needs handling.")
+ pass
else:
- # For RST and other formats
- # The second line is the underline under the title
- _ = line
+ # For RST and other formats, the second line is the underline under the title
# The next line should be empty
line = f.readline().strip()
- if line != "":
- raise RuntimeError(f"Unexpected line: {line} in {original_file}. "
- "Needs handling.")
+
+ if line != "":
+ raise RuntimeError(
+ f"{line} was expected to be empty. Doc file: {example.src_doc_file}")
# Now read until another empty line
lines = []
@@ -328,10 +314,14 @@ def get_module_gallery(examples):
if len(desc) > 120:
desc = desc[:120] + "..."
else:
- print(f"example_gallery: No .rst or .md file found in {original_dir}")
-
- title = e.headline
- if not title:
+ if not opt_quiet:
+ print(
+ f"example_gallery: No source doc file found in {example.abs_path}."
+ f"Skipping example",
+ file=sys.stderr,
+ )
+
+ if not (title := example.headline):
title = f"{name} from ``{underline}``."
# Clean refs from desc
@@ -344,28 +334,30 @@ def get_module_gallery(examples):
gallery += f"{ind(3)}:link: {doc_file_name}\n"
gallery += f"{ind(3)}:link-type: ref\n"
gallery += f"{ind(3)}:img-top: {img_name}\n\n"
- gallery += f"{ind(3)}+++\n{ind(3)}{desc}\n"
+ gallery += f"{ind(3)}+++\n"
+ gallery += f"{ind(3)}{desc}\n"
return f"{gallery}\n"
-def remove_licenses(s):
+def remove_licenses(file_content: str) -> str:
+ # Return the content of the file with the Qt license removed
new_s = []
- for line in s.splitlines():
+ for line in file_content.splitlines():
if line.strip().startswith(("/*", "**", "##")):
continue
new_s.append(line)
return "\n".join(new_s)
-def make_zip_archive(zip_file, src, skip_dirs=None):
- src_path = Path(src).expanduser().resolve(strict=True)
+def make_zip_archive(output_path: Path, src: Path, skip_dirs: list[str] = None):
+ # Create a .zip file from a source directory ignoring the specified directories
+ src_path = src.expanduser().resolve(strict=True)
if skip_dirs is None:
skip_dirs = []
if not isinstance(skip_dirs, list):
- print("Error: A list needs to be passed for 'skip_dirs'")
- return
- with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zf:
+ raise ValueError("Type error: 'skip_dirs' must be a list instance")
+ with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for file in src_path.rglob('*'):
skip = False
_parts = file.relative_to(src_path).parts
@@ -377,75 +369,76 @@ def make_zip_archive(zip_file, src, skip_dirs=None):
zf.write(file, file.relative_to(src_path.parent))
-def doc_file(project_dir, project_file_entry):
- """Return the (optional) .rstinc file describing a source file."""
+def get_rstinc_for_file(project_dir: Path, project_file: Path) -> Path | None:
+ """Return the .rstinc file in the doc folder describing a source file, if found"""
rst_file = project_dir
- if rst_file.name != "doc": # Special case: Dummy .pyproject file in doc dir
+ if project_dir.name != "doc": # Special case: Dummy .pyproject file in doc dir
rst_file /= "doc"
- rst_file /= Path(project_file_entry).name + ".rstinc"
+ rst_file /= project_file.name + ".rstinc"
return rst_file if rst_file.is_file() else None
-def get_code_tabs(files, project_dir, file_format):
+def get_code_tabs(files: list[Path], project_dir: Path, file_format: Format) -> str:
+ """
+ Return the string which contains the code tabs source for the example
+ Also creates a .zip file for downloading the source files
+ """
content = "\n"
# Prepare ZIP file, and copy to final destination
- # Handle examples which only have a dummy pyproject file in the "doc" dir
- zip_root = project_dir.parent if project_dir.name == "doc" else project_dir
- zip_name = f"{zip_root.name}.zip"
- make_zip_archive(EXAMPLES_DOC / zip_name, zip_root, skip_dirs=["doc"])
+ zip_name = f"{project_dir.name}.zip"
+ make_zip_archive(EXAMPLES_DOC / zip_name, project_dir, skip_dirs=["doc"])
if file_format == Format.RST:
content += f":download:`Download this example <{zip_name}>`\n\n"
- else:
+ elif file_format == Format.MD:
content += f"{{download}}`Download this example <{zip_name}>`\n\n"
+ # MD files wrap the content in a eval-rst block
content += "```{eval-rst}\n"
+ else:
+ raise ValueError(f"Unknown documentation file format {file_format}")
- for i, project_file in enumerate(files):
- if i == 0:
- content += ".. tab-set::\n\n"
+ if files:
+ content += ".. tab-set::\n\n"
- pfile = Path(project_file)
- if pfile.suffix in IGNORED_SUFFIXES:
+ for i, file in enumerate(files):
+ if file.suffix in IGNORED_SUFFIXES:
continue
- content += f"{ind(1)}.. tab-item:: {project_file}\n\n"
+ try:
+ tab_title = file.relative_to(project_dir).as_posix()
+ except ValueError:
+ # The file is outside project_dir, so the best we can do is to use the file name
+ tab_title = file.name
+
+ content += f"{ind(1)}.. tab-item:: {tab_title}\n\n"
- doc_rstinc_file = doc_file(project_dir, project_file)
- if doc_rstinc_file:
- indent = ind(2)
- for line in doc_rstinc_file.read_text("utf-8").split("\n"):
- content += indent + line + "\n"
+ if doc_rstinc_file := get_rstinc_for_file(project_dir, file):
+ content += add_indent(doc_rstinc_file.read_text("utf-8"), 2)
content += "\n"
- lexer = get_lexer(pfile)
- content += add_indent(f"{ind(1)}.. code-block:: {lexer}", 1)
+ content += add_indent(f"{ind(1)}.. code-block:: {get_lexer(file)}", 1)
content += "\n"
- _path = project_dir / project_file
- _file_content = ""
try:
- with open(_path, "r", encoding="utf-8") as _f:
- _file_content = remove_licenses(_f.read())
+ file_content = remove_licenses(file.read_text(encoding="utf-8"))
except UnicodeDecodeError as e:
- print(f"example_gallery: error decoding {project_dir}/{_path}:{e}",
- file=sys.stderr)
- raise
+ raise RuntimeError(f"example_gallery: error decoding {file}: {e}")
except FileNotFoundError as e:
- print(f"example_gallery: error opening {project_dir}/{_path}:{e}",
- file=sys.stderr)
- raise
+ raise RuntimeError(f"example_gallery: error opening {file}: {e}")
- content += add_indent(_file_content, 3)
+ content += add_indent(file_content, 3)
content += "\n\n"
if file_format == Format.MD:
+ # Close the eval-rst block
content += "```"
return content
-def get_header_title(example_dir):
+def get_default_header_title(example_dir: Path) -> str:
+ """Get a default header title for an example directory without a doc file"""
_index = example_dir.parts.index("examples")
rel_path = "/".join(example_dir.parts[_index:])
_title = rel_path
@@ -459,24 +452,29 @@ def get_header_title(example_dir):
)
-def rel_path(from_path, to_path):
- """Determine relative paths for paths that are not subpaths (where
- relative_to() fails) via a common root."""
- common = Path(*os.path.commonprefix([from_path.parts, to_path.parts]))
- up_dirs = len(from_path.parts) - len(common.parts)
+def rel_path(from_path: Path, to_path: Path) -> str:
+ """
+ Get a relative path for a given path that is not a subpath (where Path.relative_to() fails)
+ of from_path via a common ancestor path
+
+ For example: from_path = Path("/a/b/c/d"), to_path = Path("/a/b/e/f"). Returns: "../../e/f"
+ """
+ common_path = Path(*os.path.commonprefix([from_path.parts, to_path.parts]))
+ up_dirs = len(from_path.parts) - len(common_path.parts)
prefix = up_dirs * "../"
- rel_to_common = os.fspath(to_path.relative_to(common))
- return f"{prefix}{rel_to_common}"
+ relative_to_common = to_path.relative_to(common_path).as_posix()
+ return f"{prefix}{relative_to_common}"
-def read_rst_file(project_dir, project_files, doc_rst):
- """Read the example .rst file and expand literal includes to project files
- by relative paths to the example directory. Note: sphinx does not
- handle absolute paths as expected, they need to be relative."""
- content = ""
- with open(doc_rst, encoding="utf-8") as doc_f:
- content = doc_f.read()
+def read_rst_file(project_dir: Path, project_files: list[Path], doc_rst: Path) -> str:
+ """
+ Read the example .rst file and replace Sphinx literal includes of project files by paths
+ relative to the example directory
+ Note: Sphinx does not handle absolute paths as expected, they need to be relative
+ """
+ content = Path(doc_rst).read_text(encoding="utf-8")
if LITERAL_INCLUDE not in content:
+ # The file does not contain any literal includes, so we can return it as is
return content
result = []
@@ -484,14 +482,16 @@ def read_rst_file(project_dir, project_files, doc_rst):
for line in content.split("\n"):
if line.startswith(LITERAL_INCLUDE):
file = line[len(LITERAL_INCLUDE) + 1:].strip()
- if file in project_files:
- line = f"{LITERAL_INCLUDE} {path_to_example}/{file}"
+ file_path = project_dir / file
+ if file_path not in project_files:
+ raise RuntimeError(f"File {file} not found in project files: {project_files}")
+ line = f"{LITERAL_INCLUDE} {path_to_example}/{file}"
result.append(line)
return "\n".join(result)
-def get_headline(text, file_format):
- """Find the headline in the .rst file."""
+def get_headline(text: str, file_format: Format) -> str:
+ """Find the headline in the documentation file."""
if file_format == Format.RST:
underline = text.find("\n====")
if underline != -1:
@@ -503,23 +503,32 @@ def get_headline(text, file_format):
new_line = text.find("\n", headline + 1)
if new_line != -1:
return text[headline + 2:new_line].strip()
+ else:
+ raise ValueError(f"Unknown file format {file_format}")
return ""
-def get_doc_source_file(original_doc_dir, example_name):
- """Find the doc source file, return (Path, Format)."""
- if original_doc_dir.is_dir():
- for file_format in (Format.RST, Format.MD):
- suffix = SUFFIXES[file_format]
- result = original_doc_dir / f"{example_name}.{suffix}"
- if result.is_file():
- return result, file_format
- return None, Format.RST
+def get_doc_source_file(original_doc_dir: Path, example_name: str) -> tuple[Path, Format] | None:
+ """
+ Find the doc source file given a doc directory and an example name
+ Returns the doc file path and the file format, if found
+ """
+ if not original_doc_dir.is_dir():
+ return None
+
+ for file_format, suffix in DOC_SUFFIXES.items():
+ result = original_doc_dir / f"{example_name}.{suffix}"
+ if result.is_file():
+ return result, file_format
+ return None
-def get_screenshot(image_dir, example_name):
- """Find screen shot: We look for an image with the same
- example_name first, if not, we select the first."""
+def get_screenshot(image_dir: Path, example_name: str) -> Path | None:
+ """
+ Find an screenshot given an image directory and the example name
+ Returns the image with the same example_name, if found
+ If not found, the first image in the directory is returned
+ """
if not image_dir.is_dir():
return None
images = [i for i in image_dir.glob("*") if i.is_file() and check_img_ext(i)]
@@ -531,36 +540,30 @@ def get_screenshot(image_dir, example_name):
return None
-def write_resources(src_list, dst):
+def write_resources(src_list: list[Path], dst: Path):
"""Write a list of example resource paths to the dst path."""
for src in src_list:
resource_written = shutil.copy(src, dst / src.name)
if not opt_quiet:
- print("Written resource:", resource_written)
+ print(f"Written resource: {resource_written}")
@dataclass
class ExampleParameters:
"""Parameters obtained from scanning the examples directory."""
-
- def __init__(self):
- self.file_format = Format.RST
- self.src_doc_dir = self.src_doc_file_path = self.src_screenshot = None
- self.extra_names = ""
-
- example_dir: Path
- module_name: str
- example_name: str
- extra_names: str
- file_format: Format
- target_doc_file: str
- src_doc_dir: Path
- src_doc_file_path: Path
- src_screenshot: Path
-
-
-def detect_pyside_example(example_root, pyproject_file):
- """Detemine parameters of a PySide example."""
+ example_dir: Path = None
+ module_name: str = ""
+ example_name: str = ""
+ target_doc_file: str = None
+ extra_names: str = ""
+ src_doc_dir: Path = None
+ src_doc_file_path: Path = None
+ src_screenshot: Path = None
+ file_format: Format = Format.RST
+
+
+def get_pyside_example_parameters(example_root: Path, pyproject_file: Path) -> ExampleParameters:
+ """Analyze a PySide example folder to get the example parameters"""
p = ExampleParameters()
p.example_dir = pyproject_file.parent
@@ -568,7 +571,7 @@ def detect_pyside_example(example_root, pyproject_file):
# Design Studio project example
p.example_dir = pyproject_file.parent.parent
- if p.example_dir.name == "doc": # Dummy pyproject in doc dir (scriptableapplication)
+ if p.example_dir.name == "doc": # Dummy pyproject file in doc dir (e.g. scriptableapplication)
p.example_dir = p.example_dir.parent
parts = p.example_dir.parts[len(example_root.parts):]
@@ -581,21 +584,23 @@ def detect_pyside_example(example_root, pyproject_file):
src_doc_dir = p.example_dir / "doc"
if src_doc_dir.is_dir():
- src_doc_file_path, fmt = get_doc_source_file(src_doc_dir, p.example_name)
- if src_doc_file_path:
- p.src_doc_file_path = src_doc_file_path
- p.file_format = fmt
+ src_doc_file = get_doc_source_file(src_doc_dir, p.example_name)
+ if src_doc_file:
+ p.src_doc_file_path, p.file_format = src_doc_file
p.src_doc_dir = src_doc_dir
p.src_screenshot = get_screenshot(src_doc_dir, p.example_name)
- target_suffix = SUFFIXES[p.file_format]
+ target_suffix = DOC_SUFFIXES[p.file_format]
doc_file = f"example_{p.module_name}_{p.extra_names}_{p.example_name}.{target_suffix}"
p.target_doc_file = doc_file.replace("__", "_")
return p
-def detect_qt_example(example_root, pyproject_file):
- """Detemine parameters of an example from a Qt repository."""
+def get_qt_example_parameters(pyproject_file: Path) -> ExampleParameters:
+ """
+ Analyze a Qt repository example to get its parameters.
+ For instance, the qtdoc/examples/demos/mediaplayer example
+ """
p = ExampleParameters()
p.example_dir = pyproject_file.parent
@@ -604,142 +609,155 @@ def detect_qt_example(example_root, pyproject_file):
# Check for a 'doc' directory inside the example (qdoc)
doc_root = p.example_dir / "doc"
if doc_root.is_dir():
- src_doc_file_path, fmt = get_doc_source_file(doc_root / "src", p.example_name)
- if src_doc_file_path:
- p.src_doc_file_path = src_doc_file_path
- p.file_format = fmt
+ src_doc_file = get_doc_source_file(doc_root / "src", p.example_name)
+ if src_doc_file:
+ p.src_doc_file_path, p.file_format = src_doc_file
p.src_doc_dir = doc_root
p.src_screenshot = get_screenshot(doc_root / "images", p.example_name)
-
- target_suffix = SUFFIXES[p.file_format]
+ else:
+ raise ValueError(f"No source file found in {doc_root} / src given {p.example_name}")
+ else:
+ raise ValueError(f"No doc directory found in {p.example_dir}")
+ target_suffix = DOC_SUFFIXES[p.file_format]
p.target_doc_file = f"example_qtdemos_{p.example_name}.{target_suffix}"
return p
-def write_example(example_root, pyproject_file, pyside_example=True):
- """Read the project file and documentation, create the .rst file and
- copy the data. Return a tuple of module name and a dict of example data."""
- p = (detect_pyside_example(example_root, pyproject_file) if pyside_example
- else detect_qt_example(example_root, pyproject_file))
+def write_example(
+ example_root: Path, pyproject_file: Path, pyside_example: bool = True
+) -> tuple[str, ExampleData]:
+ """
+ Read the project file and documentation, create the .rst file and copy the example data
+ Return a tuple with the module name and an ExampleData instance
+ """
+ # Get the example parameters depending on whether it is a PySide example or a Qt one
+ p: ExampleParameters = (
+ get_pyside_example_parameters(example_root, pyproject_file)
+ if pyside_example else get_qt_example_parameters(pyproject_file))
result = ExampleData()
- result.example = p.example_name
+ result.example_name = p.example_name
result.module = p.module_name
result.extra = p.extra_names
result.doc_file = p.target_doc_file
result.file_format = p.file_format
result.abs_path = str(p.example_dir)
- result.has_doc = bool(p.src_doc_file_path)
+ result.src_doc_file = p.src_doc_file_path
result.img_doc = p.src_screenshot
- result.tutorial = tutorial_headline(result.abs_path)
-
- files = []
- try:
- with pyproject_file.open("r", encoding="utf-8") as pyf:
- pyproject = json.load(pyf)
- # iterate through the list of files in .pyproject and
- # check if they exist, before appending to the list.
- for f in pyproject["files"]:
- if not Path(f).exists:
- print(f"example_gallery: {f} listed in {pyproject_file} does not exist")
- raise FileNotFoundError
- else:
- files.append(f)
- except (json.JSONDecodeError, KeyError, FileNotFoundError) as e:
- print(f"example_gallery: error reading {pyproject_file}: {e}")
- raise
+ result.tutorial = TUTORIAL_HEADLINES.get(result.abs_path, "")
+ if pyproject_file.match(PYPROJECT_JSON_PATTERN):
+ pyproject_parse_result = parse_pyproject_json(pyproject_file)
+ elif pyproject_file.match(PYPROJECT_TOML_PATTERN):
+ pyproject_parse_result = parse_pyproject_toml(pyproject_file)
+ else:
+ raise RuntimeError(f"Invalid project file: {pyproject_file}")
+
+ if pyproject_parse_result.errors:
+ raise RuntimeError(f"Error reading {pyproject_file}: {pyproject_parse_result.errors}")
+
+ for file in pyproject_parse_result.files:
+ if not Path(file).exists:
+ raise FileNotFoundError(f"{file} listed in {pyproject_file} does not exist")
+
+ files = pyproject_parse_result.files
headline = ""
if files:
doc_file = EXAMPLES_DOC / p.target_doc_file
- sphnx_ref_example = p.target_doc_file.replace(f'.{SUFFIXES[p.file_format]}', '')
+ sphnx_ref_example = p.target_doc_file.replace(f'.{DOC_SUFFIXES[p.file_format]}', '')
# lower case sphinx reference
# this seems to be a bug or a requirement from sphinx
sphnx_ref_example = sphnx_ref_example.lower()
- content_f = ""
+
if p.file_format == Format.RST:
content_f = f".. _{sphnx_ref_example}:\n\n"
elif p.file_format == Format.MD:
content_f = f"({sphnx_ref_example})=\n\n"
else:
- print(f"example_gallery: Invalid file format {p.file_format}", file=sys.stderr)
- raise ValueError
+ raise ValueError(f"Invalid file format {p.file_format}")
with open(doc_file, "w", encoding="utf-8") as out_f:
if p.src_doc_file_path:
content_f += read_rst_file(p.example_dir, files, p.src_doc_file_path)
headline = get_headline(content_f, p.file_format)
- if not headline:
- print(f"example_gallery: No headline found in {doc_file}",
- file=sys.stderr)
+ if not headline and not opt_quiet:
+ print(f"example_gallery: No headline found in {doc_file}", file=sys.stderr)
- # Copy other files in the 'doc' directory, but
- # excluding the main '.rst' file and all the
- # directories.
+ # Copy other files in the 'doc' directory, but excluding the main '.rst' file and
+ # all the directories
resources = []
if pyside_example:
for _f in p.src_doc_dir.glob("*"):
if _f != p.src_doc_file_path and not _f.is_dir():
resources.append(_f)
- else: # Qt example: only use image.
- if p.src_screenshot:
- resources.append(p.src_screenshot)
+ elif p.src_screenshot:
+ # Qt example: only use image, if found
+ resources.append(p.src_screenshot)
write_resources(resources, EXAMPLES_DOC)
else:
- content_f += get_header_title(p.example_dir)
- content_f += get_code_tabs(files, pyproject_file.parent, p.file_format)
+ content_f += get_default_header_title(p.example_dir)
+ content_f += get_code_tabs(files, p.example_dir, p.file_format)
out_f.write(content_f)
if not opt_quiet:
print(f"Written: {doc_file}")
else:
if not opt_quiet:
- print("Empty '.pyproject' file, skipping")
+ print(f"{pyproject_file} does not contain any file, skipping")
result.headline = headline
- return (p.module_name, result)
+ return p.module_name, result
-def example_sort_key(example: ExampleData):
+def example_sort_key(example: ExampleData) -> str:
+ """
+ Return a key for sorting the examples. Tutorials are sorted first, then the examples which
+ contain "gallery" in their name, then alphabetically
+ """
result = ""
if example.tutorial:
result += "AA:" + example.tutorial + ":"
- elif "gallery" in example.example:
+ elif "gallery" in example.example_name:
result += "AB:"
- result += example.example
+ result += example.example_name
return result
-def sort_examples(example):
+def sort_examples(examples: dict[str, list[ExampleData]]) -> dict[str, list[ExampleData]]:
+ """Sort the examples using a custom function."""
result = {}
- for module in example.keys():
- result[module] = sorted(example.get(module), key=example_sort_key)
+ for module in examples.keys():
+ result[module] = sorted(examples.get(module), key=example_sort_key)
return result
-def scan_examples_dir(examples_dir, pyside_example=True):
- """Scan a directory of examples."""
- for pyproject_file in examples_dir.glob("**/*.pyproject"):
- if pyproject_file.name != "examples.pyproject":
- module_name, data = write_example(examples_dir, pyproject_file,
- pyside_example)
- if module_name not in examples:
- examples[module_name] = []
- examples[module_name].append(data)
+def scan_examples_dir(
+ examples_dir: Path, pyside_example: bool = True
+) -> dict[str, list[ExampleData]]:
+ """
+ Scan a directory of examples and return a dictionary with the found examples grouped by module
+ Also creates the .rst file for each example
+ """
+ examples: dict[str, list[ExampleData]] = defaultdict(list)
+ # Find all the project files contained in the examples directory
+ project_files: list[Path] = []
+ for project_file_pattern in PYPROJECT_FILE_PATTERNS:
+ project_files.extend(examples_dir.glob(f"**/{project_file_pattern}"))
+
+ for project_file in project_files:
+ if project_file.name == "examples.pyproject":
+ # Ignore this project file. It contains files from many examples
+ continue
+
+ module_name, data = write_example(examples_dir, project_file, pyside_example)
+ examples[module_name].append(data)
+ return dict(examples)
-if __name__ == "__main__":
- # Only examples with a '.pyproject' file will be listed.
- DIR = Path(__file__).parent
- EXAMPLES_DOC = Path(f"{DIR}/../../sources/pyside6/doc/examples").resolve()
- EXAMPLES_DIR = Path(f"{DIR}/../../examples/").resolve()
- BASE_URL = "https://code.qt.io/cgit/pyside/pyside-setup.git/tree"
- columns = 5
- gallery = ""
+if __name__ == "__main__":
parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
- TARGET_HELP = f"Directory into which to generate Doc files (default: {str(EXAMPLES_DOC)})"
parser.add_argument("--target", "-t", action="store", dest="target_dir", help=TARGET_HELP)
parser.add_argument("--qt-src-dir", "-s", action="store", help="Qt source directory")
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet")
@@ -748,55 +766,43 @@ if __name__ == "__main__":
if options.target_dir:
EXAMPLES_DOC = Path(options.target_dir).resolve()
- # This main loop will be in charge of:
- # * Getting all the .pyproject files,
- # * Gather the information of the examples and store them in 'examples'
- # * Read the .pyproject file to output the content of each file
- # on the final .rst file for that specific example.
- examples = {}
+ # This script will be in charge of:
+ # * Getting all the project files
+ # * Gather the information of the examples
+ # * Read the project file to output the content of each source file
+ # on the final .rst file for that specific example
# Create the 'examples' directory if it doesn't exist
- # If it does exist, remove it and create a new one to start fresh
+ # If it does exist, try to remove it and create a new one to start fresh
if EXAMPLES_DOC.is_dir():
shutil.rmtree(EXAMPLES_DOC, ignore_errors=True)
if not opt_quiet:
- print("WARNING: Deleted old html directory")
+ print("WARNING: Deleted existing examples HTML directory")
EXAMPLES_DOC.mkdir(exist_ok=True)
- scan_examples_dir(EXAMPLES_DIR)
+ examples = scan_examples_dir(EXAMPLES_DIR)
+
if options.qt_src_dir:
+ # Scan the Qt source directory for Qt examples and include them in the dictionary of
+ # discovered examples
qt_src = Path(options.qt_src_dir)
if not qt_src.is_dir():
- print("Invalid Qt source directory: {}", file=sys.stderr)
- sys.exit(-1)
- scan_examples_dir(qt_src.parent / "qtdoc", pyside_example=False)
+ raise RuntimeError(f"Invalid Qt source directory: {qt_src}")
+ examples.update(scan_examples_dir(qt_src.parent / "qtdoc", pyside_example=False))
examples = sort_examples(examples)
- # We generate a 'toctree' at the end of the file, to include the new
- # 'example' rst files, so we get no warnings, and also that users looking
- # for them will be able to, since they are indexed.
- # Notice that :hidden: will not add the list of files by the end of the
- # main examples HTML page.
- footer_index = dedent(
- """\
- .. toctree::
- :hidden:
- :maxdepth: 1
-
- """
- )
-
- # Writing the main example rst file.
- index_files = []
+ # List of all the example files found to be included in the index table of contents
+ index_files: list[str] = []
+ # Write the main example .rst file and the example files
with open(f"{EXAMPLES_DOC}/index.rst", "w") as f:
f.write(BASE_CONTENT)
for module_name in sorted(examples.keys(), key=module_sort_key):
- e = examples.get(module_name)
- tutorial_examples = defaultdict(list)
- non_tutorial_examples = []
+ module_examples = examples.get(module_name)
+ tutorial_examples: DefaultDict[str, list[ExampleData]] = defaultdict(list)
+ non_tutorial_examples: list[ExampleData] = []
- for example in e:
+ for example in module_examples:
index_files.append(example.doc_file)
if example.tutorial:
tutorial_examples[example.tutorial].append(example)
@@ -817,12 +823,14 @@ if __name__ == "__main__":
f.write(get_module_gallery(non_tutorial_examples))
# If no tutorials exist, list all examples
elif not tutorial_examples:
- f.write(get_module_gallery(e))
+ f.write(get_module_gallery(module_examples))
f.write("\n\n")
- f.write(footer_index)
- for i in index_files:
- f.write(f" {i}\n")
+
+ # Add the list of the example files found to the index table of contents
+ f.write(FOOTER_INDEX)
+ for index_file in index_files:
+ f.write(f"{ind(1)}{index_file}\n")
if not opt_quiet:
print(f"Written index: {EXAMPLES_DOC}/index.rst")