diff options
Diffstat (limited to 'doc')
42 files changed, 1541 insertions, 815 deletions
diff --git a/doc/blender_file_format/BlendFileDnaExporter_25.py b/doc/blender_file_format/BlendFileDnaExporter_25.py index f85d496b9b5..f84f7482baf 100755 --- a/doc/blender_file_format/BlendFileDnaExporter_25.py +++ b/doc/blender_file_format/BlendFileDnaExporter_25.py @@ -378,7 +378,8 @@ def usage(): def main(): - import os, os.path + import os + import os.path try: bpy = __import__('bpy') @@ -410,7 +411,7 @@ def main(): # read blend header from blend file log.info("2: read file:") - if not dir in sys.path: + if dir not in sys.path: sys.path.append(dir) import BlendFileReader diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile index 2f004491c89..18c16a60993 100644 --- a/doc/doxygen/Doxyfile +++ b/doc/doxygen/Doxyfile @@ -1,4 +1,4 @@ -# Doxyfile 1.9.1 +# Doxyfile 1.9.3 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -38,7 +38,7 @@ PROJECT_NAME = Blender # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = V3.2 +PROJECT_NUMBER = V3.4 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a @@ -93,14 +93,6 @@ ALLOW_UNICODE_NAMES = NO OUTPUT_LANGUAGE = English -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. @@ -258,16 +250,16 @@ TAB_SIZE = 4 # the documentation. An alias has the form: # name=value # For example adding -# "sideeffect=@par Side Effects:\n" +# "sideeffect=@par Side Effects:^^" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) ALIASES = @@ -312,8 +304,8 @@ OPTIMIZE_OUTPUT_SLICE = NO # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and # language is one of the parsers supported by doxygen: IDL, Java, JavaScript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser # tries to guess whether the code is fixed or free formatted code, this is the # default for Fortran type files). For instance to make doxygen treat .inc files @@ -466,7 +458,7 @@ LOOKUP_CACHE_SIZE = 3 # than 0 to get more control over the balance between CPU load and processing # speed. At this moment only the input processing can be done using multiple # threads. Since this is still an experimental feature the default is set to 1, -# which efficively disables parallel processing. Please report any issues you +# which effectively disables parallel processing. Please report any issues you # encounter. Generating dot graphs in parallel is controlled by the # DOT_NUM_THREADS setting. # Minimum value: 0, maximum value: 32, default value: 1. @@ -610,6 +602,12 @@ HIDE_SCOPE_NAMES = NO HIDE_COMPOUND_REFERENCE= NO +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -767,7 +765,8 @@ FILE_VERSION_FILTER = # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. # # Note that if you run doxygen from a directory containing a file called # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE @@ -813,18 +812,26 @@ WARNINGS = YES WARN_IF_UNDOCUMENTED = NO # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = YES +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC # The default value is: NO. WARN_NO_PARAMDOC = NO @@ -850,7 +857,10 @@ WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard -# error (stderr). +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). WARN_LOGFILE = @@ -894,10 +904,10 @@ INPUT_ENCODING = UTF-8 # # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, # *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), -# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl, -# *.ucf, *.qsf and *.ice. +# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, +# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C +# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = @@ -939,7 +949,7 @@ EXCLUDE_PATTERNS = .svn \ # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test +# ANamespace::AClass, ANamespace::*Test # # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* @@ -1224,7 +1234,7 @@ HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see +# this color. Hue is specified as an angle on a color-wheel, see # https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. @@ -1234,7 +1244,7 @@ HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A +# in the HTML output. For a value of 0 the output will use gray-scales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1316,6 +1326,13 @@ GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doxygen generated docs" +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. @@ -1341,8 +1358,12 @@ DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: -# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows. +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML @@ -1501,16 +1522,28 @@ DISABLE_INDEX = NO # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = NO +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. # @@ -1535,6 +1568,13 @@ TREEVIEW_WIDTH = 246 EXT_LINKS_IN_WINDOW = NO +# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + # If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg # tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see # https://inkscape.org) to generate formulas as SVG images instead of PNGs for @@ -1583,11 +1623,29 @@ FORMULA_MACROFILE = USE_MATHJAX = NO +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + # When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). # Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1600,15 +1658,21 @@ MATHJAX_FORMAT = HTML-CSS # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://www.mathjax.org/mathjax # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = @@ -1788,29 +1852,31 @@ PAPER_TYPE = a4 EXTRA_PACKAGES = -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -# generated LaTeX document. The header should contain everything until the first -# chapter. If it is left blank doxygen will generate a standard header. See -# section "Doxygen usage" for information on how to let doxygen write the -# default header to a separate file. +# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for +# the generated LaTeX document. The header should contain everything until the +# first chapter. If it is left blank doxygen will generate a standard header. It +# is highly recommended to start with a default header using +# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty +# and then modify the file new_header.tex. See also section "Doxygen usage" for +# information on how to generate the default header that doxygen normally uses. # -# Note: Only use a user-defined header if you know what you are doing! The -# following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -# string, for the replacement values of the other commands the user is referred -# to HTML_HEADER. +# Note: Only use a user-defined header if you know what you are doing! +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. The following +# commands have a special meaning inside the header (and footer): For a +# description of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HEADER = -# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -# generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. See +# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for +# the generated LaTeX document. The footer should contain everything after the +# last chapter. If it is left blank doxygen will generate a standard footer. See # LATEX_HEADER for more information on how to generate a default footer and what -# special commands can be used inside the footer. -# -# Note: Only use a user-defined footer if you know what you are doing! +# special commands can be used inside the footer. See also section "Doxygen +# usage" for information on how to generate the default footer that doxygen +# normally uses. Note: Only use a user-defined footer if you know what you are +# doing! # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_FOOTER = @@ -1855,8 +1921,7 @@ USE_PDFLATEX = NO # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode # command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. This option is also used -# when generating formulas in HTML. +# if errors occur, instead of asking the user for help. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -1869,16 +1934,6 @@ LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO -# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -# code with syntax highlighting in the LaTeX output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_SOURCE_CODE = NO - # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See # https://en.wikipedia.org/wiki/BibTeX and \cite for more info. @@ -1959,16 +2014,6 @@ RTF_STYLESHEET_FILE = RTF_EXTENSIONS_FILE = -# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -# with syntax highlighting in the RTF output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_SOURCE_CODE = NO - #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- @@ -2065,15 +2110,6 @@ GENERATE_DOCBOOK = NO DOCBOOK_OUTPUT = docbook -# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -# program listings (including syntax highlighting and cross-referencing -# information) to the DOCBOOK output. Note that enabling this will significantly -# increase the size of the DOCBOOK output. -# The default value is: NO. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. - -DOCBOOK_PROGRAMLISTING = NO - #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- @@ -2254,15 +2290,6 @@ EXTERNAL_PAGES = YES # Configuration options related to the dot tool #--------------------------------------------------------------------------- -# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -# NO turns the diagrams off. Note that this option also works with HAVE_DOT -# disabled, but it is recommended to install and use dot, since it yields more -# powerful graphs. -# The default value is: YES. - -CLASS_DIAGRAMS = NO - # You can include diagrams made with dia in doxygen documentation. Doxygen will # then run dia to produce the diagram and insert it in the documentation. The # DIA_PATH tag allows you to specify the directory where the dia binary resides. @@ -2319,13 +2346,16 @@ DOT_FONTSIZE = 10 DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -# each documented class showing the direct and indirect inheritance relations. -# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a +# graph for each documented class showing the direct and indirect inheritance +# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, +# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set +# to TEXT the direct and indirect inheritance relations will be shown as texts / +# links. +# Possible values are: NO, YES, TEXT and GRAPH. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. -CLASS_GRAPH = YES +CLASS_GRAPH = TEXT # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation @@ -2452,6 +2482,13 @@ GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES +# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels +# of child directories generated in directory dependency graphs by dot. +# Minimum value: 1, maximum value: 25, default value: 1. +# This tag requires that the tag DIRECTORY_GRAPH is set to YES. + +DIR_GRAPH_MAX_DEPTH = 1 + # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: @@ -2505,10 +2542,10 @@ MSCFILE_DIRS = DIAFILE_DIRS = # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -# path where java can find the plantuml.jar file. If left blank, it is assumed -# PlantUML is not used or called during a preprocessing step. Doxygen will -# generate a warning when it encounters a \startuml command in this case and -# will not generate output for the diagram. +# path where java can find the plantuml.jar file or to the filename of jar file +# to be used. If left blank, it is assumed PlantUML is not used or called during +# a preprocessing step. Doxygen will generate a warning when it encounters a +# \startuml command in this case and will not generate output for the diagram. PLANTUML_JAR_PATH = @@ -2570,6 +2607,8 @@ DOT_MULTI_TARGETS = YES # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. +# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal +# graphical representation for inheritance and collaboration diagrams is used. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2578,8 +2617,8 @@ GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate # files that are used to generate the various graphs. # -# Note: This setting is not only used for dot files but also for msc and -# plantuml temporary files. +# Note: This setting is not only used for dot files but also for msc temporary +# files. # The default value is: YES. DOT_CLEANUP = YES diff --git a/doc/manpage/blender.1.py b/doc/manpage/blender.1.py index 640b00b0c00..49dae35e0c5 100755 --- a/doc/manpage/blender.1.py +++ b/doc/manpage/blender.1.py @@ -11,8 +11,6 @@ where <path-to-blender> is the path to the Blender executable, and <output-filename> is where to write the generated man page. ''' -# <pep8 compliant> - import argparse import os import subprocess @@ -103,10 +101,10 @@ blender \- a full-featured 3D application''') .PP .B blender is a full-featured 3D application. It supports the entirety of the 3D pipeline - ''' -'''modeling, rigging, animation, simulation, rendering, compositing, motion tracking, and video editing. + '''modeling, rigging, animation, simulation, rendering, compositing, motion tracking, and video editing. Use Blender to create 3D images and animations, films and commercials, content for games, ''' -r'''architectural and industrial visualizations, and scientific visualizations. + r'''architectural and industrial visualizations, and scientific visualizations. https://www.blender.org''') @@ -141,7 +139,7 @@ https://www.blender.org''') l = lines.pop(0) if l: - assert(l.startswith('\t')) + assert l.startswith('\t') l = l[1:] # Remove first white-space (tab). fh.write('%s\n' % man_format(l)) diff --git a/doc/python_api/examples/bpy.app.timers.5.py b/doc/python_api/examples/bpy.app.timers.5.py index ddda0576f05..9770c84d4be 100644 --- a/doc/python_api/examples/bpy.app.timers.5.py +++ b/doc/python_api/examples/bpy.app.timers.5.py @@ -11,6 +11,7 @@ import queue execution_queue = queue.Queue() + # This function can safely be called in another thread. # The function will be executed when the timer runs the next time. def run_in_main_thread(function): diff --git a/doc/python_api/examples/bpy.ops.1.py b/doc/python_api/examples/bpy.ops.1.py index efc9f00b0ae..9b690823a18 100644 --- a/doc/python_api/examples/bpy.ops.1.py +++ b/doc/python_api/examples/bpy.ops.1.py @@ -9,7 +9,7 @@ operator in the different part of the user interface. The context overrides are passed as a dictionary, with keys matching the context member names in bpy.context. For example to override ``bpy.context.active_object``, -you would pass ``{'active_object': object}``. +you would pass ``{'active_object': object}`` to :class:`bpy.types.Context.temp_override`. .. note:: @@ -17,8 +17,10 @@ you would pass ``{'active_object': object}``. (otherwise, you'll have to find and gather all needed data yourself). """ -# remove all objects in scene rather than the selected ones +# Remove all objects in scene rather than the selected ones. import bpy -override = bpy.context.copy() -override['selected_objects'] = list(bpy.context.scene.objects) -bpy.ops.object.delete(override) +from bpy import context +override = context.copy() +override["selected_objects"] = list(context.scene.objects) +with context.temp_override(**override): + bpy.ops.object.delete() diff --git a/doc/python_api/examples/bpy.ops.3.py b/doc/python_api/examples/bpy.ops.3.py index 7dec69cf566..e068ab0c0cf 100644 --- a/doc/python_api/examples/bpy.ops.3.py +++ b/doc/python_api/examples/bpy.ops.3.py @@ -1,17 +1,16 @@ """ It is also possible to run an operator in a particular part of the user -interface. For this we need to pass the window, screen, area and sometimes -a region. +interface. For this we need to pass the window, area and sometimes a region. """ -# maximize 3d view in all windows +# Maximize 3d view in all windows. import bpy +from bpy import context -for window in bpy.context.window_manager.windows: +for window in context.window_manager.windows: screen = window.screen - for area in screen.areas: if area.type == 'VIEW_3D': - override = {'window': window, 'screen': screen, 'area': area} - bpy.ops.screen.screen_full_area(override) + with context.temp_override(window=window, area=area): + bpy.ops.screen.screen_full_area() break diff --git a/doc/python_api/examples/bpy.ops.py b/doc/python_api/examples/bpy.ops.py index 76c494ad4f5..e8d545fc855 100644 --- a/doc/python_api/examples/bpy.ops.py +++ b/doc/python_api/examples/bpy.ops.py @@ -33,6 +33,11 @@ There are 3 optional positional arguments (documented in detail below). bpy.ops.test.operator(override_context, execution_context, undo) - override_context - ``dict`` type. + + .. deprecated:: 3.2 + + :class:`bpy.types.Context.temp_override` should be used instead of this argument. + - execution_context - ``str`` (enum). - undo - ``bool`` type. diff --git a/doc/python_api/examples/bpy.props.4.py b/doc/python_api/examples/bpy.props.4.py index daab18c8986..93e4a9fda62 100644 --- a/doc/python_api/examples/bpy.props.4.py +++ b/doc/python_api/examples/bpy.props.4.py @@ -6,6 +6,10 @@ It can be useful to perform an action when a property is changed and can be used to update other properties or synchronize with external data. All properties define update functions except for CollectionProperty. + +.. warning:: + Remember that these callbacks may be executed in threaded context. + """ import bpy diff --git a/doc/python_api/examples/bpy.props.5.py b/doc/python_api/examples/bpy.props.5.py index c428b4743f8..49853168661 100644 --- a/doc/python_api/examples/bpy.props.5.py +++ b/doc/python_api/examples/bpy.props.5.py @@ -6,6 +6,10 @@ Getter/setter functions can be used for boolean, int, float, string and enum pro If these callbacks are defined the property will not be stored in the ID properties automatically. Instead, the `get` and `set` functions will be called when the property is respectively read or written from the API. + +.. warning:: + Remember that these callbacks may be executed in threaded context. + """ import bpy diff --git a/doc/python_api/examples/bpy.props.py b/doc/python_api/examples/bpy.props.py index d19b57b13a5..a2a51addc65 100644 --- a/doc/python_api/examples/bpy.props.py +++ b/doc/python_api/examples/bpy.props.py @@ -7,6 +7,15 @@ Custom properties can be added to any subclass of an :class:`ID`, These properties can be animated, accessed by the user interface and python like Blender's existing properties. + +.. warning:: + + Access to these properties might happen in threaded context, on a per-data-block level. + This has to be carefully considered when using accessors or update callbacks. + + Typically, these callbacks should not affect any other data that the one owned by their data-block. + When accessing external non-Blender data, thread safety mechanisms should be considered. + """ import bpy diff --git a/doc/python_api/examples/bpy.types.Bone.convert_local_to_pose.py b/doc/python_api/examples/bpy.types.Bone.convert_local_to_pose.py index 4a88096cf6f..36b161640c2 100644 --- a/doc/python_api/examples/bpy.types.Bone.convert_local_to_pose.py +++ b/doc/python_api/examples/bpy.types.Bone.convert_local_to_pose.py @@ -4,6 +4,7 @@ the middle of updating the armature without having to update dependencies after each change, by manually carrying updated matrices in a recursive walk. """ + def set_pose_matrices(obj, matrix_map): "Assign pose space matrices of all bones at once, ignoring constraints." @@ -11,7 +12,7 @@ def set_pose_matrices(obj, matrix_map): if pbone.name in matrix_map: matrix = matrix_map[pbone.name] - ## Instead of: + # # Instead of: # pbone.matrix = matrix # bpy.context.view_layer.update() diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.1.py b/doc/python_api/examples/bpy.types.Context.temp_override.1.py new file mode 100644 index 00000000000..6213db30e00 --- /dev/null +++ b/doc/python_api/examples/bpy.types.Context.temp_override.1.py @@ -0,0 +1,19 @@ +""" +Overriding the context can be used to temporarily activate another ``window`` / ``area`` & ``region``, +as well as other members such as the ``active_object`` or ``bone``. + +Notes: + +- When overriding window, area and regions: the arguments must be consistent, + so any region argument that's passed in must be contained by the current area or the area passed in. + The same goes for the area needing to be contained in the current window. + +- Temporary context overrides may be nested, when this is done, members will be added to the existing overrides. + +- Context members are restored outside the scope of the context-manager. + The only exception to this is when the data is no longer available. + + In the event windowing data was removed (for example), the state of the context is left as-is. + While this isn't likely to happen, explicit window operation such as closing windows or loading a new file + remove the windowing data that was set before the temporary context was created. +""" diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.2.py b/doc/python_api/examples/bpy.types.Context.temp_override.2.py new file mode 100644 index 00000000000..ce3e1594baa --- /dev/null +++ b/doc/python_api/examples/bpy.types.Context.temp_override.2.py @@ -0,0 +1,15 @@ +""" +Overriding the context can be useful to set the context after loading files +(which would otherwise by None). For example: +""" + +import bpy +from bpy import context + +# Reload the current file and select all. +bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath) +window = context.window_manager.windows[0] +with context.temp_override(window=window): + bpy.ops.mesh.primitive_uv_sphere_add() + # The context override is needed so it's possible to set edit-mode. + bpy.ops.object.mode_set(mode='EDIT') diff --git a/doc/python_api/examples/bpy.types.Context.temp_override.3.py b/doc/python_api/examples/bpy.types.Context.temp_override.3.py new file mode 100644 index 00000000000..e670bb7bafa --- /dev/null +++ b/doc/python_api/examples/bpy.types.Context.temp_override.3.py @@ -0,0 +1,16 @@ +""" +This example shows how it's possible to add an object to the scene in another window. +""" +import bpy +from bpy import context + +win_active = context.window +win_other = None +for win_iter in context.window_manager.windows: + if win_iter != win_active: + win_other = win_iter + break + +# Add cube in the other window. +with context.temp_override(window=win_other): + bpy.ops.mesh.primitive_cube_add() diff --git a/doc/python_api/examples/bpy.types.Image.py b/doc/python_api/examples/bpy.types.Image.py new file mode 100644 index 00000000000..f0c1a780e24 --- /dev/null +++ b/doc/python_api/examples/bpy.types.Image.py @@ -0,0 +1,46 @@ +""" +Image Data +++++++++++ + +The Image data-block is a shallow wrapper around image or video file(s) +(on disk, as packed data, or generated). + +All actual data like the pixel buffer, size, resolution etc. is +cached in an :class:`imbuf.types.ImBuf` image buffer (or several buffers +in some cases, like UDIM textures, multi-views, animations...). + +Several properties and functions of the Image data-block are then actually +using/modifying its image buffer, and not the Image data-block itself. + +.. warning:: + + One key limitation is that image buffers are not shared between different + Image data-blocks, and they are not duplicated when copying an image. + + So until a modified image buffer is saved on disk, duplicating its Image + data-block will not propagate the underlying buffer changes to the new Image. + + +This example script generates an Image data-block with a given size, +change its first pixel, rescale it, and duplicates the image. + +The duplicated image still has the same size and colors as the original image +at its creation, all editing in the original image's buffer is 'lost' in its copy. +""" + +import bpy + +image_src = bpy.data.images.new('src', 1024, 102) +print(image_src.size) +print(image_src.pixels[0:4]) + +image_src.scale(1024, 720) +image_src.pixels[0:4] = (0.5, 0.5, 0.5, 0.5) +image_src.update() +print(image_src.size) +print(image_src.pixels[0:4]) + +image_dest = image_src.copy() +image_dest.update() +print(image_dest.size) +print(image_dest.pixels[0:4]) diff --git a/doc/python_api/examples/bpy.types.Menu.4.py b/doc/python_api/examples/bpy.types.Menu.4.py index 869def8bfe0..4d1ae2d4a19 100644 --- a/doc/python_api/examples/bpy.types.Menu.4.py +++ b/doc/python_api/examples/bpy.types.Menu.4.py @@ -3,8 +3,8 @@ Extending the Button Context Menu +++++++++++++++++++++++++++++++++ This example enables you to insert your own menu entry into the common -right click menu that you get while hovering over a value field, -color, string, etc. +right click menu that you get while hovering over a UI button (e.g. operator, +value field, color, string, etc.) To make the example work, you have to first select an object then right click on an user interface element (maybe a color in the @@ -14,7 +14,6 @@ Executing the operator will then print all values. """ import bpy -from bpy.types import Menu def dump(obj, text): @@ -47,36 +46,20 @@ class WM_OT_button_context_test(bpy.types.Operator): return {'FINISHED'} -# This class has to be exactly named like that to insert an entry in the right click menu -class WM_MT_button_context(Menu): - bl_label = "Unused" - - def draw(self, context): - pass - - -def menu_func(self, context): +def draw_menu(self, context): layout = self.layout layout.separator() layout.operator(WM_OT_button_context_test.bl_idname) -classes = ( - WM_OT_button_context_test, - WM_MT_button_context, -) - - def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.types.WM_MT_button_context.append(menu_func) + bpy.utils.register_class(WM_OT_button_context_test) + bpy.types.UI_MT_button_context_menu.append(draw_menu) def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - bpy.types.WM_MT_button_context.remove(menu_func) + bpy.types.UI_MT_button_context_menu.remove(draw_menu) + bpy.utils.unregister_class(WM_OT_button_context_test) if __name__ == "__main__": diff --git a/doc/python_api/examples/bpy.types.Operator.1.py b/doc/python_api/examples/bpy.types.Operator.1.py index 84397574856..a28db7e84bb 100644 --- a/doc/python_api/examples/bpy.types.Operator.1.py +++ b/doc/python_api/examples/bpy.types.Operator.1.py @@ -42,10 +42,12 @@ class SimpleMouseOperator(bpy.types.Operator): self.y = event.mouse_y return self.execute(context) -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(SimpleMouseOperator.bl_idname, text="Simple Mouse Operator") + # Register and add to the view menu (required to also use F3 search "Simple Mouse Operator" for quick access) bpy.utils.register_class(SimpleMouseOperator) bpy.types.VIEW3D_MT_view.append(menu_func) diff --git a/doc/python_api/examples/bpy.types.Operator.2.py b/doc/python_api/examples/bpy.types.Operator.2.py index 7ab0143b925..2a14d6b00f5 100644 --- a/doc/python_api/examples/bpy.types.Operator.2.py +++ b/doc/python_api/examples/bpy.types.Operator.2.py @@ -37,7 +37,7 @@ class ExportSomeData(bpy.types.Operator): return {'RUNNING_MODAL'} -# Only needed if you want to add into a dynamic menu +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator_context = 'INVOKE_DEFAULT' self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator") diff --git a/doc/python_api/examples/bpy.types.Operator.3.py b/doc/python_api/examples/bpy.types.Operator.3.py index 9a12a7e3615..fb40616d15d 100644 --- a/doc/python_api/examples/bpy.types.Operator.3.py +++ b/doc/python_api/examples/bpy.types.Operator.3.py @@ -27,7 +27,8 @@ class DialogOperator(bpy.types.Operator): wm = context.window_manager return wm.invoke_props_dialog(self) -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(DialogOperator.bl_idname, text="Dialog Operator") diff --git a/doc/python_api/examples/bpy.types.Operator.4.py b/doc/python_api/examples/bpy.types.Operator.4.py index 00c9cd250b1..d33f6a6113d 100644 --- a/doc/python_api/examples/bpy.types.Operator.4.py +++ b/doc/python_api/examples/bpy.types.Operator.4.py @@ -41,11 +41,13 @@ class CustomDrawOperator(bpy.types.Operator): col.prop(self, "my_string") -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(CustomDrawOperator.bl_idname, text="Custom Draw Operator") -# Register and add to the object menu (required to also use F3 search "Custom Draw Operator" for quick access) + +# Register and add to the object menu (required to also use F3 search "Custom Draw Operator" for quick access). bpy.utils.register_class(CustomDrawOperator) bpy.types.VIEW3D_MT_object.append(menu_func) diff --git a/doc/python_api/examples/bpy.types.Operator.5.py b/doc/python_api/examples/bpy.types.Operator.5.py index a0b4a6d6841..0b6035fc3da 100644 --- a/doc/python_api/examples/bpy.types.Operator.5.py +++ b/doc/python_api/examples/bpy.types.Operator.5.py @@ -1,4 +1,6 @@ """ +.. _modal_operator: + Modal Execution +++++++++++++++ @@ -55,11 +57,13 @@ class ModalOperator(bpy.types.Operator): context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(ModalOperator.bl_idname, text="Modal Operator") -# Register and add to the object menu (required to also use F3 search "Modal Operator" for quick access) + +# Register and add to the object menu (required to also use F3 search "Modal Operator" for quick access). bpy.utils.register_class(ModalOperator) bpy.types.VIEW3D_MT_object.append(menu_func) diff --git a/doc/python_api/examples/bpy.types.Operator.6.py b/doc/python_api/examples/bpy.types.Operator.6.py index 20ee4c21446..cf556c63ab8 100644 --- a/doc/python_api/examples/bpy.types.Operator.6.py +++ b/doc/python_api/examples/bpy.types.Operator.6.py @@ -31,10 +31,12 @@ class SearchEnumOperator(bpy.types.Operator): context.window_manager.invoke_search_popup(self) return {'RUNNING_MODAL'} -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(SearchEnumOperator.bl_idname, text="Search Enum Operator") + # Register and add to the object menu (required to also use F3 search "Search Enum Operator" for quick access) bpy.utils.register_class(SearchEnumOperator) bpy.types.VIEW3D_MT_object.append(menu_func) diff --git a/doc/python_api/examples/bpy.types.Operator.py b/doc/python_api/examples/bpy.types.Operator.py index 5299b198774..41b96ac402f 100644 --- a/doc/python_api/examples/bpy.types.Operator.py +++ b/doc/python_api/examples/bpy.types.Operator.py @@ -22,13 +22,15 @@ class HelloWorldOperator(bpy.types.Operator): print("Hello World") return {'FINISHED'} -# Only needed if you want to add into a dynamic menu + +# Only needed if you want to add into a dynamic menu. def menu_func(self, context): self.layout.operator(HelloWorldOperator.bl_idname, text="Hello World Operator") -# Register and add to the view menu (required to also use F3 search "Hello World Operator" for quick access) + +# Register and add to the view menu (required to also use F3 search "Hello World Operator" for quick access). bpy.utils.register_class(HelloWorldOperator) bpy.types.VIEW3D_MT_view.append(menu_func) -# test call to the newly defined operator +# Test call to the newly defined operator. bpy.ops.wm.hello_world() diff --git a/doc/python_api/examples/gpu.10.py b/doc/python_api/examples/gpu.10.py index 8e354dd09a8..b47ff732e2b 100644 --- a/doc/python_api/examples/gpu.10.py +++ b/doc/python_api/examples/gpu.10.py @@ -14,33 +14,36 @@ from random import random from mathutils import Vector from gpu_extras.batch import batch_for_shader -vertex_shader = ''' - uniform mat4 u_ViewProjectionMatrix; +vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") +vert_out.smooth('FLOAT', "v_ArcLength") - in vec3 position; - in float arcLength; +shader_info = gpu.types.GPUShaderCreateInfo() +shader_info.push_constant('MAT4', "u_ViewProjectionMatrix") +shader_info.push_constant('FLOAT', "u_Scale") +shader_info.vertex_in(0, 'VEC3', "position") +shader_info.vertex_in(1, 'FLOAT', "arcLength") +shader_info.vertex_out(vert_out) +shader_info.fragment_out(0, 'VEC4', "FragColor") - out float v_ArcLength; - - void main() - { - v_ArcLength = arcLength; - gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f); - } -''' - -fragment_shader = ''' - uniform float u_Scale; +shader_info.vertex_source( + "void main()" + "{" + " v_ArcLength = arcLength;" + " gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);" + "}" +) - in float v_ArcLength; - out vec4 FragColor; +shader_info.fragment_source( + "void main()" + "{" + " if (step(sin(v_ArcLength * u_Scale), 0.5) == 1) discard;" + " FragColor = vec4(1.0);" + "}" +) - void main() - { - if (step(sin(v_ArcLength * u_Scale), 0.5) == 1) discard; - FragColor = vec4(1.0); - } -''' +shader = gpu.shader.create_from_info(shader_info) +del vert_out +del shader_info coords = [Vector((random(), random(), random())) * 5 for _ in range(5)] @@ -48,7 +51,6 @@ arc_lengths = [0] for a, b in zip(coords[:-1], coords[1:]): arc_lengths.append(arc_lengths[-1] + (a - b).length) -shader = gpu.types.GPUShader(vertex_shader, fragment_shader) batch = batch_for_shader( shader, 'LINE_STRIP', {"position": coords, "arcLength": arc_lengths}, diff --git a/doc/python_api/examples/gpu.2.py b/doc/python_api/examples/gpu.2.py index 565ec5d5d0c..2a46e833752 100644 --- a/doc/python_api/examples/gpu.2.py +++ b/doc/python_api/examples/gpu.2.py @@ -6,33 +6,37 @@ import bpy import gpu from gpu_extras.batch import batch_for_shader -vertex_shader = ''' - uniform mat4 viewProjectionMatrix; - in vec3 position; - out vec3 pos; - - void main() - { - pos = position; - gl_Position = viewProjectionMatrix * vec4(position, 1.0f); - } -''' - -fragment_shader = ''' - uniform float brightness; - - in vec3 pos; - out vec4 FragColor; - - void main() - { - FragColor = vec4(pos * brightness, 1.0); - } -''' +vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") +vert_out.smooth('VEC3', "pos") + +shader_info = gpu.types.GPUShaderCreateInfo() +shader_info.push_constant('MAT4', "viewProjectionMatrix") +shader_info.push_constant('FLOAT', "brightness") +shader_info.vertex_in(0, 'VEC3', "position") +shader_info.vertex_out(vert_out) +shader_info.fragment_out(0, 'VEC4', "FragColor") + +shader_info.vertex_source( + "void main()" + "{" + " pos = position;" + " gl_Position = viewProjectionMatrix * vec4(position, 1.0f);" + "}" +) + +shader_info.fragment_source( + "void main()" + "{" + " FragColor = vec4(pos * brightness, 1.0);" + "}" +) + +shader = gpu.shader.create_from_info(shader_info) +del vert_out +del shader_info coords = [(1, 1, 1), (2, 0, 0), (-2, -1, 3)] -shader = gpu.types.GPUShader(vertex_shader, fragment_shader) batch = batch_for_shader(shader, 'TRIS', {"position": coords}) diff --git a/doc/python_api/examples/gpu.6.py b/doc/python_api/examples/gpu.6.py index be164a03028..5576b2d0bfe 100644 --- a/doc/python_api/examples/gpu.6.py +++ b/doc/python_api/examples/gpu.6.py @@ -29,3 +29,36 @@ def draw(): bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL') + +""" +3D Image +-------- + +Similar to the 2D Image shader, but works with 3D positions for the image vertices. +To use this example you have to provide an image that should be displayed. +""" +import bpy +import gpu +from gpu_extras.batch import batch_for_shader + +IMAGE_NAME = "Untitled" +image = bpy.data.images[IMAGE_NAME] +texture = gpu.texture.from_image(image) + +shader = gpu.shader.from_builtin('3D_IMAGE') +batch = batch_for_shader( + shader, 'TRIS', + { + "pos": ((0, 0, 0), (0, 1, 1), (1, 1, 1), (1, 1, 1), (1, 0, 0), (0, 0, 0)), + "texCoord": ((0, 0), (0, 1), (1, 1), (1, 1), (1, 0), (0, 0)), + }, +) + + +def draw(): + shader.bind() + shader.uniform_sampler("image", texture) + batch.draw(shader) + + +bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW') diff --git a/doc/python_api/examples/gpu.7.py b/doc/python_api/examples/gpu.7.py index 9d881831c21..e3bfbd14e34 100644 --- a/doc/python_api/examples/gpu.7.py +++ b/doc/python_api/examples/gpu.7.py @@ -35,35 +35,37 @@ with offscreen.bind(): # Drawing the generated texture in 3D space ############################################# -vertex_shader = ''' - uniform mat4 modelMatrix; - uniform mat4 viewProjectionMatrix; - - in vec2 position; - in vec2 uv; - - out vec2 uvInterp; - - void main() - { - uvInterp = uv; - gl_Position = viewProjectionMatrix * modelMatrix * vec4(position, 0.0, 1.0); - } -''' - -fragment_shader = ''' - uniform sampler2D image; +vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") +vert_out.smooth('VEC2', "uvInterp") + +shader_info = gpu.types.GPUShaderCreateInfo() +shader_info.push_constant('MAT4', "viewProjectionMatrix") +shader_info.push_constant('MAT4', "modelMatrix") +shader_info.sampler(0, 'FLOAT_2D', "image") +shader_info.vertex_in(0, 'VEC2', "position") +shader_info.vertex_in(1, 'VEC2', "uv") +shader_info.vertex_out(vert_out) +shader_info.fragment_out(0, 'VEC4', "FragColor") + +shader_info.vertex_source( + "void main()" + "{" + " uvInterp = uv;" + " gl_Position = viewProjectionMatrix * modelMatrix * vec4(position, 0.0, 1.0);" + "}" +) - in vec2 uvInterp; - out vec4 FragColor; +shader_info.fragment_source( + "void main()" + "{" + " FragColor = texture(image, uvInterp);" + "}" +) - void main() - { - FragColor = texture(image, uvInterp); - } -''' +shader = gpu.shader.create_from_info(shader_info) +del vert_out +del shader_info -shader = gpu.types.GPUShader(vertex_shader, fragment_shader) batch = batch_for_shader( shader, 'TRI_FAN', { diff --git a/doc/python_api/examples/gpu.8.py b/doc/python_api/examples/gpu.8.py index 86a242b0258..385b71d0c1a 100644 --- a/doc/python_api/examples/gpu.8.py +++ b/doc/python_api/examples/gpu.8.py @@ -43,7 +43,7 @@ with offscreen.bind(): offscreen.free() -if not IMAGE_NAME in bpy.data.images: +if IMAGE_NAME not in bpy.data.images: bpy.data.images.new(IMAGE_NAME, WIDTH, HEIGHT) image = bpy.data.images[IMAGE_NAME] image.scale(WIDTH, HEIGHT) diff --git a/doc/python_api/requirements.txt b/doc/python_api/requirements.txt index 51440046430..7d9bb497329 100644 --- a/doc/python_api/requirements.txt +++ b/doc/python_api/requirements.txt @@ -1,12 +1,12 @@ -sphinx==4.1.1 +sphinx==5.1.1 # Sphinx dependencies that are important -Jinja2==3.0.1 -Pygments==2.10.0 +Jinja2==3.1.2 +Pygments==2.13.0 docutils==0.17.1 -snowballstemmer==2.1.0 -babel==2.9.1 -requests==2.26.0 +snowballstemmer==2.2.0 +babel==2.10.3 +requests==2.27.1 # Only needed to match the theme used for the official documentation. # Without this theme, the default theme will be used. diff --git a/doc/python_api/rst/bgl.rst b/doc/python_api/rst/bgl.rst index 2369a422396..48d7d31489c 100644 --- a/doc/python_api/rst/bgl.rst +++ b/doc/python_api/rst/bgl.rst @@ -4,6 +4,12 @@ OpenGL Wrapper (bgl) .. module:: bgl +.. warning:: + + This module is deprecated and will be removed in a future release, + when OpenGL is replaced by Metal and Vulkan. + Use the graphics API independent :mod:`gpu` module instead. + This module wraps OpenGL constants and functions, making them available from within Blender Python. diff --git a/doc/python_api/rst/change_log.rst b/doc/python_api/rst/change_log.rst index 957bf8605e3..ef289cd0762 100644 --- a/doc/python_api/rst/change_log.rst +++ b/doc/python_api/rst/change_log.rst @@ -1,7 +1,9 @@ :tocdepth: 2 -Blender API Change Log -********************** +Change Log +********** + +Changes in Blender's Python API between releases. .. note, this document is auto generated by sphinx_changelog_gen.py diff --git a/doc/python_api/rst/info_advanced.rst b/doc/python_api/rst/info_advanced.rst new file mode 100644 index 00000000000..cae1e711722 --- /dev/null +++ b/doc/python_api/rst/info_advanced.rst @@ -0,0 +1,15 @@ +.. _info_advanced-index: + +******** +Advanced +******** + +This chapter covers advanced use (topics which may not be required for typical usage). + +.. NOTE(@campbellbarton): Blender-as-a-Python-module is too obscure a topic to list directly on the main-page, + so opt for an "Advanced" page which can be expanded on as needed. + +.. toctree:: + :maxdepth: 1 + + info_advanced_blender_as_bpy.rst diff --git a/doc/python_api/rst/info_advanced_blender_as_bpy.rst b/doc/python_api/rst/info_advanced_blender_as_bpy.rst new file mode 100644 index 00000000000..b3fbfd2fe6d --- /dev/null +++ b/doc/python_api/rst/info_advanced_blender_as_bpy.rst @@ -0,0 +1,124 @@ + +************************** +Blender as a Python Module +************************** + +Blender supports being built as a Python module, +allowing ``import bpy`` to be added to any Python script, providing access to Blender's features. + +.. note:: + + At time of writing official builds are not available, + using this requires compiling Blender yourself see + `build instructions <https://wiki.blender.org/w/index.php?title=Building_Blender/Other/BlenderAsPyModule>`__. + + +Use Cases +========= + +Python developers may wish to integrate Blender scripts which don't center around Blender. + +Possible uses include: + +- Visualizing data by rendering images and animations. +- Image processing using Blender's compositor. +- Video editing (using Blender's sequencer). +- 3D file conversion. +- Development, accessing ``bpy`` from Python IDE's and debugging tools for example. +- Automation. + + +Usage +===== + +For the most part using Blender as a Python module is equivalent to running a script in background-mode +(passing the command-line arguments ``--background`` or ``-b``), +however there are some differences to be aware of. + +.. Sorted alphabetically as there isn't an especially a logical order to show them. + +Blender's Executable Access + The attribute :class:`bpy.app.binary_path` defaults to an empty string. + + If you wish to point this to the location of a known executable you may set the value. + + This example searches for the binary, setting it when found: + + .. code-block:: python + + import bpy + import shutil + + blender_bin = shutil.which("blender") + if blender_bin: + print("Found:", blender_bin) + bpy.app.binary_path = blender_bin + else: + print("Unable to find blender!") + +Blender's Internal Modules + There are many modules included with Blender such as :mod:`gpu` and :mod:`mathuils`. + It's important that these are imported after ``bpy`` or they will not be found. + +Command Line Arguments Unsupported + Functionality controlled by command line arguments (shown by calling ``blender --help`` aren't accessible). + + Typically this isn't such a limitation although there are some command line arguments that don't have + equivalents in Blender's Python API (``--threads`` and ``--log`` for example). + + .. note:: + + Access to these settings may be added in the future as needed. + +Resource Sharing (GPU) + It's possible other Python modules make use of the GPU in a way that prevents Blender/Cycles from accessing the GPU. + +Signal Handlers + Blender's typical signal handlers are not initialized, so there is no special handling for ``Control-C`` + to cancel a render and a crash log is not written in the event of a crash. + +Startup and Preferences + When the ``bpy`` module loads, the file is not empty as you might expect, + there is a default cube, camera and light. If you wish to start from a blank file use: + ``bpy.ops.wm.read_factory_settings(use_empty=True)``. + + The users startup and preferences are ignored to prevent your local configuration from impacting scripts behavior. + The Python module behaves as if ``--factory-startup`` was passed as a command line argument. + + The users preferences and startup can be loaded using operators: + + .. code-block:: python + + import bpy + + bpy.ops.wm.read_userpref() + bpy.ops.wm.read_homefile() + + +Limitations +=========== + +Most constraints of Blender as an application still apply: + +Reloading Unsupported + Reloading via ``importlib.reload`` will raise an exception instead of reloading and resetting the module. + + The operator ``bpy.ops.wm.read_factory_settings()`` can be used to reset the internal state. + +Single Blend File Restriction + Only a single ``.blend`` file can be edited at a time. + + .. hint:: + + As with the application it's possible to start multiple instances, + each with their own ``bpy`` and therefor Blender state. + Python provides the ``multiprocessing`` module to make communicating with sub-processes more convenient. + + In some cases the library API may be an alternative to starting separate processes, + although this API operates on reading and writing ID data-blocks and isn't + a complete substitute for loading ``.blend`` files, see: + + - :meth:`bpy.types.BlendDataLibraries.load` + - :meth:`bpy.types.BlendDataLibraries.write` + - :meth:`bpy.types.BlendData.temp_data` + supports a temporary data-context to avoid manipulating the current ``.blend`` file. diff --git a/doc/python_api/rst/info_api_reference.rst b/doc/python_api/rst/info_api_reference.rst index 70d201016f0..3deb96dbb52 100644 --- a/doc/python_api/rst/info_api_reference.rst +++ b/doc/python_api/rst/info_api_reference.rst @@ -1,6 +1,6 @@ ******************* -Reference API Usage +API Reference Usage ******************* Blender has many interlinking data types which have an auto-generated reference API which often has the information diff --git a/doc/python_api/rst/info_best_practice.rst b/doc/python_api/rst/info_best_practice.rst index e88adcc0d70..6342680e149 100644 --- a/doc/python_api/rst/info_best_practice.rst +++ b/doc/python_api/rst/info_best_practice.rst @@ -40,15 +40,6 @@ As well as pep8 we have additional conventions used for Blender Python scripts: - pep8 also defines that lines should not exceed 79 characters, we have decided that this is too restrictive so it is optional per script. -Periodically we run checks for pep8 compliance on Blender scripts, -for scripts to be included in this check add this line as a comment at the top of the script: - -``# <pep8 compliant>`` - -To enable line length checks use this instead: - -``# <pep8-80 compliant>`` - User Interface Layout ===================== diff --git a/doc/python_api/rst/info_gotcha.rst b/doc/python_api/rst/info_gotcha.rst index bef76a5e479..2dd6c3c92b1 100644 --- a/doc/python_api/rst/info_gotcha.rst +++ b/doc/python_api/rst/info_gotcha.rst @@ -86,14 +86,15 @@ No updates after setting values Sometimes you want to modify values from Python and immediately access the updated values, e.g: Once changing the objects :class:`bpy.types.Object.location` you may want to access its transformation right after from :class:`bpy.types.Object.matrix_world`, -but this doesn't work as you might expect. +but this doesn't work as you might expect. There are similar issues with changes to the UI, that +are covered in the next section. Consider the calculations that might contribute to the object's final transformation, this includes: - Animation function curves. - Drivers and their Python expressions. - Constraints -- Parent objects and all of their F-curves, constraints, etc. +- Parent objects and all of their F-Curves, constraints, etc. To avoid expensive recalculations every time a property is modified, Blender defers the evaluation until the results are needed. @@ -110,6 +111,35 @@ Now all dependent data (child objects, modifiers, drivers, etc.) have been recalculated and are available to the script within the active view layer. +No updates after changing UI context +------------------------------------ + +Similar to the previous issue, some changes to the UI may also not have an immediate effect. For example, setting +:class:`bpy.types.Window.workspace` doesn't seem to cause an observable effect in the immediately following code +(:class:`bpy.types.Window.workspace` is still the same), but the UI will in fact reflect the change. Some of the +properties that behave that way are: + +- :class:`bpy.types.Window.workspace` +- :class:`bpy.types.Window.screen` +- :class:`bpy.types.Window.scene` +- :class:`bpy.types.Area.type` +- :class:`bpy.types.Area.uitype` + +Such changes impact the UI, and with that the context (:class:`bpy.context`) quite drastically. This can break +Blender's context management. So Blender delays this change until after operators have run and just before the UI is +redrawn, making sure that context can be changed safely. + +If you rely on executing code with an updated context this can be worked around by executing the code in a delayed +fashion as well. Possible options include: + + - :ref:`Modal Operator <modal_operator>`. + - :class:`bpy.app.handlers`. + - :class:`bpy.app.timer`. + +It's also possible to depend on drawing callbacks although these should generally be avoided as failure to draw a +hidden panel, region, cursor, etc. could cause your script to be unreliable + + Can I redraw during script execution? ------------------------------------- @@ -802,7 +832,7 @@ Removing Data ------------- **Any** data that you remove shouldn't be modified or accessed afterwards, -this includes: F-curves, drivers, render layers, timeline markers, modifiers, constraints +this includes: F-Curves, drivers, render layers, timeline markers, modifiers, constraints along with objects, scenes, collections, bones, etc. The ``remove()`` API calls will invalidate the data they free to prevent common mistakes. diff --git a/doc/python_api/rst/info_overview.rst b/doc/python_api/rst/info_overview.rst index 50928963f60..638420a53ae 100644 --- a/doc/python_api/rst/info_overview.rst +++ b/doc/python_api/rst/info_overview.rst @@ -1,8 +1,8 @@ .. _info_overview: -******************* -Python API Overview -******************* +************ +API Overview +************ The purpose of this document is to explain how Python and Blender fit together, covering some of the functionality that may not be obvious from reading the API references diff --git a/doc/python_api/rst_from_bmesh_opdefines.py b/doc/python_api/rst_from_bmesh_opdefines.py index c97b05b96b3..0614538964d 100644 --- a/doc/python_api/rst_from_bmesh_opdefines.py +++ b/doc/python_api/rst_from_bmesh_opdefines.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: GPL-2.0-or-later -# <pep8 compliant> - # This is a quite stupid script which extracts bmesh api docs from # 'bmesh_opdefines.c' in order to avoid having to add a lot of introspection # data access into the api. @@ -243,9 +241,9 @@ def main(): comment_washed = [] comment = [] if comment is None else comment for i, l in enumerate(comment): - assert((l.strip() == "") or - (l in {"/*", " *"}) or - (l.startswith(("/* ", " * ")))) + assert ((l.strip() == "") or + (l in {"/*", " *"}) or + (l.startswith(("/* ", " * ")))) l = l[3:] if i == 0 and not l.strip(): @@ -272,7 +270,7 @@ def main(): tp_sub = None else: print(arg) - assert(0) + assert 0 tp_str = "" @@ -317,7 +315,7 @@ def main(): tp_str += " or any sequence of 3 floats" elif tp == BMO_OP_SLOT_PTR: tp_str = "dict" - assert(tp_sub is not None) + assert tp_sub is not None if tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_BMESH: tp_str = ":class:`bmesh.types.BMesh`" elif tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_SCENE: @@ -332,10 +330,10 @@ def main(): tp_str = ":class:`bpy.types.bpy_struct`" else: print("Can't find", vars_dict_reverse[tp_sub]) - assert(0) + assert 0 elif tp == BMO_OP_SLOT_ELEMENT_BUF: - assert(tp_sub is not None) + assert tp_sub is not None ls = [] if tp_sub & BM_VERT: @@ -344,7 +342,7 @@ def main(): ls.append(":class:`bmesh.types.BMEdge`") if tp_sub & BM_FACE: ls.append(":class:`bmesh.types.BMFace`") - assert(ls) # must be at least one + assert ls # Must be at least one. if tp_sub & BMO_OP_SLOT_SUBTYPE_ELEM_IS_SINGLE: tp_str = "/".join(ls) @@ -369,10 +367,10 @@ def main(): tp_str += "unknown internal data, not compatible with python" else: print("Can't find", vars_dict_reverse[tp_sub]) - assert(0) + assert 0 else: print("Can't find", vars_dict_reverse[tp]) - assert(0) + assert 0 args_wash.append((name, tp_str, comment)) return args_wash @@ -396,7 +394,7 @@ def main(): fw(" :return:\n\n") for (name, tp, comment) in args_out_wash: - assert(name.endswith(".out")) + assert name.endswith(".out") name = name[:-4] fw(" - ``%s``: %s\n\n" % (name, comment)) fw(" **type** %s\n" % tp) diff --git a/doc/python_api/sphinx_changelog_gen.py b/doc/python_api/sphinx_changelog_gen.py index 6c06178d603..fb93c301280 100644 --- a/doc/python_api/sphinx_changelog_gen.py +++ b/doc/python_api/sphinx_changelog_gen.py @@ -1,61 +1,111 @@ # SPDX-License-Identifier: GPL-2.0-or-later -# <pep8 compliant> - """ -Dump the python API into a text file so we can generate changelogs. +--------------- -output from this tool should be added into "doc/python_api/rst/change_log.rst" +Dump the python API into a JSON file, or generate changelogs from those JSON API dumps. -# dump api blender_version.py in CWD -blender --background --python doc/python_api/sphinx_changelog_gen.py -- --dump +Typically, changelog output from this tool should be added into "doc/python_api/rst/change_log.rst" -# create changelog -blender --background --factory-startup --python doc/python_api/sphinx_changelog_gen.py -- \ - --api_from blender_2_63_0.py \ - --api_to blender_2_64_0.py \ - --api_out changes.rst +API dump files are saved together with the generated API doc on the server, with a general index file. +This way the changelog generation simply needs to re-download the previous version's dump for the diffing process. +--------------- -# Api comparison can also run without blender +# Dump api blender_version.json in CWD: +blender --background --factory-startup --python doc/python_api/sphinx_changelog_gen.py -- \ + --indexpath="path/to/api/docs/api_dump_index.json" \ + dump --filepath-out="path/to/api/docs/<version>/api_dump.json" + +# Create changelog: +blender --background --factory-startup --python doc/python_api/sphinx_changelog_gen.py -- \ + --indexpath="path/to/api/docs/api_dump_index.json" \ + changelog --filepath-out doc/python_api/rst/change_log.rst + +# Api comparison can also run without blender, +# will by default generate changeloig between the last two available versions listed in the index, +# unless input files are provided explicitely: python doc/python_api/sphinx_changelog_gen.py -- \ - --api_from blender_api_2_63_0.py \ - --api_to blender_api_2_64_0.py \ - --api_out changes.rst + --indexpath="path/to/api/docs/api_dump_index.json" \ + changelog --filepath-in-from blender_api_2_63_0.json \ + --filepath-in-to blender_api_2_64_0.json \ + --filepath-out changes.rst -# Save the latest API dump in this folder, renaming it with its revision. -# This way the next person updating it doesn't need to build an old Blender only for that +-------------- -""" +API dump index format: -# format -''' -{"module.name": - {"parent.class": - {"basic_type", "member_name": - ("Name", type, range, length, default, descr, f_args, f_arg_types, f_ret_types)}, ... - }, ... +{[version_main, version_sub]: "<version>/api_dump.json", ... } -''' -api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types" +API dump format: +[ + [version_main, vserion_sub, version_path], + {"module.name": + {"parent.class": + {"basic_type", "member_name": + ["Name", type, range, length, default, descr, f_args, f_arg_types, f_ret_types]}, ... + }, ... + } +] + +""" + +import json +import os + + +api_names = "basic_type" "name", "type", "range", "length", "default", "descr", "f_args", "f_arg_types", "f_ret_types" API_BASIC_TYPE = 0 API_F_ARGS = 7 -def api_dunp_fname(): - import bpy - return "blender_api_%s.py" % "_".join([str(i) for i in bpy.app.version]) +def api_version(): + try: + import bpy + except: + return None, None + version = tuple(bpy.app.version[:2]) + version_key = "%d.%d" % (version[0], version[1]) + return version, version_key + + +def api_version_previous_in_index(index, version): + print("Searching for previous version to %s in %r" % (version, index)) + version_prev = (version[0], version[1]) + while True: + version_prev = (version_prev[0], version_prev[1] - 1) + if version_prev[1] < 0: + version_prev = (version_prev[0] - 1, 99) + if version_prev[0] < 0: + return None, None + version_prev_key = "%d.%d" % (version_prev[0], version_prev[1]) + if version_prev_key in index: + print("Found previous version %s: %r" % (version_prev, index[version_prev_key])) + return version_prev, version_prev_key + + +class JSONEncoderAPIDump(json.JSONEncoder): + def default(self, o): + if o is ...: + return "..." + if isinstance(o, set): + return tuple(o) + return json.JSONEncoder.default(self, o) + + +def api_dump(args): + import rna_info + import inspect + version, version_key = api_version() + if version is None: + raise ValueError("API dumps can only be generated from within Blender.") -def api_dump(): dump = {} dump_module = dump["bpy.types"] = {} - import rna_info - import inspect - struct = rna_info.BuildRNAInfo()[0] for struct_id, struct_info in sorted(struct.items()): @@ -157,17 +207,25 @@ def api_dump(): ) del funcs - import pprint + filepath_out = args.filepath_out + with open(filepath_out, 'w', encoding='utf-8') as file_handle: + json.dump((version, dump), file_handle, cls=JSONEncoderAPIDump) - filename = api_dunp_fname() - filehandle = open(filename, 'w', encoding='utf-8') - tot = filehandle.write(pprint.pformat(dump, width=1)) - filehandle.close() - print("%s, %d bytes written" % (filename, tot)) + indexpath = args.indexpath + rootpath = os.path.dirname(indexpath) + if os.path.exists(indexpath): + with open(indexpath, 'r', encoding='utf-8') as file_handle: + index = json.load(file_handle) + else: + index = {} + index[version_key] = os.path.relpath(filepath_out, rootpath) + with open(indexpath, 'w', encoding='utf-8') as file_handle: + json.dump(index, file_handle) + print("API version %s dumped into %r, and index %r has been updated" % (version_key, filepath_out, indexpath)) -def compare_props(a, b, fuzz=0.75): +def compare_props(a, b, fuzz=0.75): # must be same basic_type, function != property if a[0] != b[0]: return False @@ -182,15 +240,48 @@ def compare_props(a, b, fuzz=0.75): return ((tot / totlen) >= fuzz) -def api_changelog(api_from, api_to, api_out): +def api_changelog(args): + indexpath = args.indexpath + filepath_in_from = args.filepath_in_from + filepath_in_to = args.filepath_in_to + filepath_out = args.filepath_out + + rootpath = os.path.dirname(indexpath) + + version, version_key = api_version() + if version is None and (filepath_in_from is None or filepath_in_to is None): + raise ValueError("API dumps files must be given when ran outside of Blender.") + + with open(indexpath, 'r', encoding='utf-8') as file_handle: + index = json.load(file_handle) + + if filepath_in_to is None: + filepath_in_to = index.get(version_key, None) + if filepath_in_to is None: + raise ValueError("Cannot find API dump file for Blender version " + str(version) + " in index file.") - file_handle = open(api_from, 'r', encoding='utf-8') - dict_from = eval(file_handle.read()) - file_handle.close() + print("Found to file: %r" % filepath_in_to) - file_handle = open(api_to, 'r', encoding='utf-8') - dict_to = eval(file_handle.read()) - file_handle.close() + if filepath_in_from is None: + version_from, version_from_key = api_version_previous_in_index(index, version) + if version_from is None: + raise ValueError("No previous version of Blender could be found in the index.") + filepath_in_from = index.get(version_from_key, None) + if filepath_in_from is None: + raise ValueError( + "Cannot find API dump file for previous Blender version " + + str(version_from) + + " in index file." + ) + + print("Found from file: %r" % filepath_in_from) + + with open(os.path.join(rootpath, filepath_in_from), 'r', encoding='utf-8') as file_handle: + _, dict_from = json.load(file_handle) + + with open(os.path.join(rootpath, filepath_in_to), 'r', encoding='utf-8') as file_handle: + dump_version, dict_to = json.load(file_handle) + assert tuple(dump_version) == version api_changes = [] @@ -251,63 +342,68 @@ def api_changelog(api_from, api_to, api_out): # also document function argument changes - fout = open(api_out, 'w', encoding='utf-8') - fw = fout.write - # print(api_changes) - - # :class:`bpy_struct.id_data` - - def write_title(title, title_char): - fw("%s\n%s\n\n" % (title, title_char * len(title))) - - for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes: - class_name = class_id.split(".")[-1] - title = mod_id + "." + class_name - write_title(title, "-") - - if props_new: - write_title("Added", "^") - for prop_id in props_new: - fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id)) - fw("\n") - - if props_old: - write_title("Removed", "^") - for prop_id in props_old: - fw("* **%s**\n" % prop_id) # can't link to removed docs - fw("\n") - - if props_moved: - write_title("Renamed", "^") - for prop_id_old, prop_id in props_moved: - fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id)) - fw("\n") - - if func_args: - write_title("Function Arguments", "^") - for func_id, args_old, args_new in func_args: - args_new = ", ".join(args_new) - args_old = ", ".join(args_old) - fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, func_id, args_new, args_old)) - fw("\n") - - fout.close() - - print("Written: %r" % api_out) - - -def main(): + with open(filepath_out, 'w', encoding='utf-8') as fout: + fw = fout.write + + # Write header. + fw("" + ":tocdepth: 2\n" + "\n" + "Change Log\n" + "**********\n" + "\n" + "Changes in Blender's Python API between releases.\n" + "\n" + ".. note, this document is auto generated by sphinx_changelog_gen.py\n" + "\n" + "\n" + "%s to %s\n" + "============\n" + "\n" % (version_from_key, version_key)) + + def write_title(title, title_char): + fw("%s\n%s\n\n" % (title, title_char * len(title))) + + for mod_id, class_id, props_moved, props_new, props_old, func_args in api_changes: + class_name = class_id.split(".")[-1] + title = mod_id + "." + class_name + write_title(title, "-") + + if props_new: + write_title("Added", "^") + for prop_id in props_new: + fw("* :class:`%s.%s.%s`\n" % (mod_id, class_name, prop_id)) + fw("\n") + + if props_old: + write_title("Removed", "^") + for prop_id in props_old: + fw("* **%s**\n" % prop_id) # can't link to removed docs + fw("\n") + + if props_moved: + write_title("Renamed", "^") + for prop_id_old, prop_id in props_moved: + fw("* **%s** -> :class:`%s.%s.%s`\n" % (prop_id_old, mod_id, class_name, prop_id)) + fw("\n") + + if func_args: + write_title("Function Arguments", "^") + for func_id, args_old, args_new in func_args: + args_new = ", ".join(args_new) + args_old = ", ".join(args_old) + fw("* :class:`%s.%s.%s` (%s), *was (%s)*\n" % (mod_id, class_name, func_id, args_new, args_old)) + fw("\n") + + print("Written: %r" % filepath_out) + + +def main(argv=None): import sys - import os - - try: - import argparse - except ImportError: - print("Old Blender, just dumping") - api_dump() - return + import argparse - argv = sys.argv + if argv is None: + argv = sys.argv if "--" not in argv: argv = [] # as if no args are passed @@ -318,42 +414,42 @@ def main(): usage_text = "Run blender in background mode with this script: " "blender --background --factory-startup --python %s -- [options]" % os.path.basename(__file__) - epilog = "Run this before releases" - - parser = argparse.ArgumentParser(description=usage_text, epilog=epilog) - - parser.add_argument( - "--dump", dest="dump", action='store_true', - help="When set the api will be dumped into blender_version.py") - + parser = argparse.ArgumentParser(description=usage_text, + epilog=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( - "--api_from", dest="api_from", metavar='FILE', - help="File to compare from (previous version)") - parser.add_argument( - "--api_to", dest="api_to", metavar='FILE', - help="File to compare from (current)") - parser.add_argument( - "--api_out", dest="api_out", metavar='FILE', - help="Output sphinx changelog") - - args = parser.parse_args(argv) # In this example we won't use the args - - if not argv: - print("No args given!") - parser.print_help() - return - - if args.dump: - api_dump() - else: - if args.api_from and args.api_to and args.api_out: - api_changelog(args.api_from, args.api_to, args.api_out) - else: - print("Error: --api_from/api_to/api_out args needed") - parser.print_help() - return - - print("batch job finished, exiting") + "--indexpath", dest="indexpath", metavar='FILE', required=True, + help="Path of the JSON file containing the index of all available API dumps.") + + parser_commands = parser.add_subparsers(required=True) + + parser_dump = parser_commands.add_parser('dump', help="Dump the current Blender Python API into a JSON file.") + parser_dump.add_argument( + "--filepath-out", dest="filepath_out", metavar='FILE', required=True, + help="Path of the JSON file containing the dump of the API.") + parser_dump.set_defaults(func=api_dump) + + parser_changelog = parser_commands.add_parser( + 'changelog', + help="Generate the RST changelog page based on two Blender Python API JSON dumps.", + ) + + parser_changelog.add_argument( + "--filepath-in-from", dest="filepath_in_from", metavar='FILE', default=None, + help="JSON dump file to compare from (typically, previous version). " + "If not given, will be automatically determined from current Blender version and index file.") + parser_changelog.add_argument( + "--filepath-in-to", dest="filepath_in_to", metavar='FILE', default=None, + help="JSON dump file to compare to (typically, current version). " + "If not given, will be automatically determined from current Blender version and index file.") + parser_changelog.add_argument( + "--filepath-out", dest="filepath_out", metavar='FILE', required=True, + help="Output sphinx changelog RST file.") + parser_changelog.set_defaults(func=api_changelog) + + args = parser.parse_args(argv) + + args.func(args) if __name__ == "__main__": diff --git a/doc/python_api/sphinx_doc_gen.py b/doc/python_api/sphinx_doc_gen.py index b427fedebc1..7b5f13177ef 100644 --- a/doc/python_api/sphinx_doc_gen.py +++ b/doc/python_api/sphinx_doc_gen.py @@ -1,9 +1,6 @@ # SPDX-License-Identifier: GPL-2.0-or-later -# <pep8 compliant> - -SCRIPT_HELP_MSG = """ - +""" API dump in RST files --------------------- Run this script from Blender's root path once you have compiled Blender @@ -14,10 +11,10 @@ API dump in RST files providing ./blender is or links to the blender executable To choose sphinx-in directory: - blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --output ../python_api + blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --output=../python_api For quick builds: - blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --partial bmesh.* + blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --partial=bmesh.* Sphinx: HTML generation @@ -36,18 +33,17 @@ Sphinx: PDF generation sphinx-build -b latex doc/python_api/sphinx-in doc/python_api/sphinx-out cd doc/python_api/sphinx-out make - """ try: - import bpy # Blender module + import bpy # Blender module. except ImportError: print("\nERROR: this script must run from inside Blender") - print(SCRIPT_HELP_MSG) + print(__doc__) import sys sys.exit() -import rna_info # Blender module +import rna_info # Blender module. def rna_info_BuildRNAInfo_cache(): @@ -63,113 +59,170 @@ import os import sys import inspect import shutil +import time import logging import warnings from textwrap import indent -from platform import platform -PLATFORM = platform().split('-')[0].lower() # 'linux', 'darwin', 'windows' - SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) -# For now, ignore add-ons and internal subclasses of 'bpy.types.PropertyGroup'. +# For now, ignore add-ons and internal sub-classes of `bpy.types.PropertyGroup`. # # Besides disabling this line, the main change will be to add a -# 'toctree' to 'write_rst_index' which contains the generated rst files. +# 'toctree' to 'write_rst_index' which contains the generated RST files. # This 'toctree' can be generated automatically. # # See: D6261 for reference. USE_ONLY_BUILTIN_RNA_TYPES = True +# Write a page for each static enum defined in: +# `source/blender/makesrna/RNA_enum_items.h` so the enums can be linked to instead of being expanded everywhere. +USE_SHARED_RNA_ENUM_ITEMS_STATIC = True + +if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + from _bpy import rna_enum_items_static + rna_enum_dict = rna_enum_items_static() + for key in ("DummyRNA_DEFAULT_items", "DummyRNA_NULL_items"): + del rna_enum_dict[key] + del key, rna_enum_items_static + + # Build enum `{pointer: identifier}` map, so any enum property pointer can + # lookup an identifier using `InfoPropertyRNA.enum_pointer` as the key. + rna_enum_pointer_to_id_map = { + enum_prop.as_pointer(): key + for key, enum_items in rna_enum_dict.items() + # It's possible the first item is a heading (which has no identifier). + # skip these as the `EnumProperty.enum_items` does not expose them. + if (enum_prop := next(iter(enum_prop for enum_prop in enum_items if enum_prop.identifier), None)) + } + def handle_args(): - ''' + """ Parse the args passed to Blender after "--", ignored by Blender - ''' + """ import argparse # When --help is given, print the usage text parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, - usage=SCRIPT_HELP_MSG + usage=__doc__ ) # optional arguments - parser.add_argument("-p", "--partial", - dest="partial", - type=str, - default="", - help="Use a wildcard to only build specific module(s)\n" - "Example: --partial bmesh*\n", - required=False) - - parser.add_argument("-f", "--fullrebuild", - dest="full_rebuild", - default=False, - action='store_true', - help="Rewrite all rst files in sphinx-in/ " - "(default=False)", - required=False) - - parser.add_argument("-b", "--bpy", - dest="bpy", - default=False, - action='store_true', - help="Write the rst file of the bpy module " - "(default=False)", - required=False) - - parser.add_argument("-o", "--output", - dest="output_dir", - type=str, - default=SCRIPT_DIR, - help="Path of the API docs (default=<script dir>)", - required=False) - - parser.add_argument("-B", "--sphinx-build", - dest="sphinx_build", - default=False, - action='store_true', - help="Build the html docs by running:\n" - "sphinx-build SPHINX_IN SPHINX_OUT\n" - "(default=False; does not depend on -P)", - required=False) - - parser.add_argument("-P", "--sphinx-build-pdf", - dest="sphinx_build_pdf", - default=False, - action='store_true', - help="Build the pdf by running:\n" - "sphinx-build -b latex SPHINX_IN SPHINX_OUT_PDF\n" - "(default=False; does not depend on -B)", - required=False) - - parser.add_argument("-R", "--pack-reference", - dest="pack_reference", - default=False, - action='store_true', - help="Pack all necessary files in the deployed dir.\n" - "(default=False; use with -B and -P)", - required=False) - - parser.add_argument("-l", "--log", - dest="log", - default=False, - action='store_true', - help="Log the output of the API dump and sphinx|latex " - "warnings and errors (default=False).\n" - "If given, save logs in:\n" - "* OUTPUT_DIR/.bpy.log\n" - "* OUTPUT_DIR/.sphinx-build.log\n" - "* OUTPUT_DIR/.sphinx-build_pdf.log\n" - "* OUTPUT_DIR/.latex_make.log", - required=False) - - # parse only the args passed after '--' + parser.add_argument( + "-p", "--partial", + dest="partial", + type=str, + default="", + help="Use a wildcard to only build specific module(s)\n" + "Example: --partial\"=bmesh*\"\n", + required=False, + ) + + parser.add_argument( + "-f", "--fullrebuild", + dest="full_rebuild", + default=False, + action='store_true', + help="Rewrite all RST files in sphinx-in/ " + "(default=False)", + required=False, + ) + + parser.add_argument( + "-b", "--bpy", + dest="bpy", + default=False, + action='store_true', + help="Write the RST file of the bpy module " + "(default=False)", + required=False, + ) + + parser.add_argument( + "--api-changelog-generate", + dest="changelog", + default=False, + action='store_true', + help="Generate the API changelog RST file " + "(default=False, requires `--api-dump-index-path` parameter)", + required=False, + ) + + parser.add_argument( + "--api-dump-index-path", + dest="api_dump_index_path", + metavar='FILE', + default=None, + help="Path to the API dump index JSON file " + "(required when `--api-changelog-generate` is True)", + required=False, + ) + + parser.add_argument( + "-o", "--output", + dest="output_dir", + type=str, + default=SCRIPT_DIR, + help="Path of the API docs (default=<script dir>)", + required=False, + ) + + parser.add_argument( + "-B", "--sphinx-build", + dest="sphinx_build", + default=False, + action='store_true', + help="Build the html docs by running:\n" + "sphinx-build SPHINX_IN SPHINX_OUT\n" + "(default=False; does not depend on -P)", + required=False, + ) + + parser.add_argument( + "-P", "--sphinx-build-pdf", + dest="sphinx_build_pdf", + default=False, + action='store_true', + help="Build the pdf by running:\n" + "sphinx-build -b latex SPHINX_IN SPHINX_OUT_PDF\n" + "(default=False; does not depend on -B)", + required=False, + ) + + parser.add_argument( + "-R", "--pack-reference", + dest="pack_reference", + default=False, + action='store_true', + help="Pack all necessary files in the deployed dir.\n" + "(default=False; use with -B and -P)", + required=False, + ) + + parser.add_argument( + "-l", "--log", + dest="log", + default=False, + action='store_true', + help=( + "Log the output of the API dump and sphinx|latex " + "warnings and errors (default=False).\n" + "If given, save logs in:\n" + "* OUTPUT_DIR/.bpy.log\n" + "* OUTPUT_DIR/.sphinx-build.log\n" + "* OUTPUT_DIR/.sphinx-build_pdf.log\n" + "* OUTPUT_DIR/.latex_make.log" + ), + required=False, + ) + + # Parse only the arguments passed after "--". argv = [] if "--" in sys.argv: - argv = sys.argv[sys.argv.index("--") + 1:] # get all args after "--" + argv = sys.argv[sys.argv.index("--") + 1:] # Get all arguments after "--". return parser.parse_args(argv) @@ -178,7 +231,7 @@ ARGS = handle_args() # ----------------------------------BPY----------------------------------------- -BPY_LOGGER = logging.getLogger('bpy') +BPY_LOGGER = logging.getLogger("bpy") BPY_LOGGER.setLevel(logging.DEBUG) """ @@ -192,7 +245,7 @@ or ./blender -b -noaudio --factory-startup -P doc/python_api/sphinx_doc_gen.py -- -f -B """ -# Switch for quick testing so doc-builds don't take so long +# Switch for quick testing so doc-builds don't take so long. if not ARGS.partial: # full build FILTER_BPY_OPS = None @@ -260,7 +313,7 @@ else: # ------ # Filter # - # TODO, support bpy.ops and bpy.types filtering + # TODO: support `bpy.ops` and `bpy.types` filtering. import fnmatch m = None EXCLUDE_MODULES = [m for m in EXCLUDE_MODULES if not fnmatch.fnmatchcase(m, ARGS.partial)] @@ -274,7 +327,7 @@ else: if FILTER_BPY_TYPES: EXCLUDE_MODULES.remove("bpy.types") - print(FILTER_BPY_TYPES) + # print(FILTER_BPY_TYPES) EXCLUDE_INFO_DOCS = (not fnmatch.fnmatchcase("info", ARGS.partial)) @@ -282,7 +335,7 @@ else: del fnmatch BPY_LOGGER.debug( - "Partial Doc Build, Skipping: %s\n" % + "Partial Doc Build, Skipping: %s\n", "\n ".join(sorted(EXCLUDE_MODULES))) # @@ -292,13 +345,13 @@ else: try: __import__("aud") except ImportError: - BPY_LOGGER.debug("Warning: Built without 'aud' module, docs incomplete...") + BPY_LOGGER.debug("Warning: Built without \"aud\" module, docs incomplete...") EXCLUDE_MODULES.append("aud") try: __import__("freestyle") except ImportError: - BPY_LOGGER.debug("Warning: Built without 'freestyle' module, docs incomplete...") + BPY_LOGGER.debug("Warning: Built without \"freestyle\" module, docs incomplete...") EXCLUDE_MODULES.extend([ "freestyle", "freestyle.chainingiterators", @@ -328,68 +381,94 @@ EXTRA_SOURCE_FILES = ( # examples EXAMPLES_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "examples")) -EXAMPLE_SET = set() -for f in os.listdir(EXAMPLES_DIR): - if f.endswith(".py"): - EXAMPLE_SET.add(os.path.splitext(f)[0]) +EXAMPLE_SET = set(os.path.splitext(f)[0] for f in os.listdir(EXAMPLES_DIR) if f.endswith(".py")) EXAMPLE_SET_USED = set() -# rst files dir +# RST files directory. RST_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "rst")) -# extra info, not api reference docs -# stored in ./rst/info_* +# Extra info, not api reference docs stored in `./rst/info_*`. +# Pairs of (file, description), the title makes from the RST files are displayed before the description. INFO_DOCS = ( ("info_quickstart.rst", - "Quickstart: New to Blender or scripting and want to get your feet wet?"), + "New to Blender or scripting and want to get your feet wet?"), ("info_overview.rst", - "API Overview: A more complete explanation of Python integration"), + "A more complete explanation of Python integration."), ("info_api_reference.rst", - "API Reference Usage: examples of how to use the API reference docs"), + "Examples of how to use the API reference docs."), ("info_best_practice.rst", - "Best Practice: Conventions to follow for writing good scripts"), + "Conventions to follow for writing good scripts."), ("info_tips_and_tricks.rst", - "Tips and Tricks: Hints to help you while writing scripts for Blender"), + "Hints to help you while writing scripts for Blender."), ("info_gotcha.rst", - "Gotcha's: Some of the problems you may encounter when writing scripts"), - ("change_log.rst", "Change Log: List of changes since last Blender release"), + "Some of the problems you may encounter when writing scripts."), + ("info_advanced.rst", + "Topics which may not be required for typical usage."), + ("change_log.rst", + "List of changes since last Blender release"), +) +# Referenced indirectly. +INFO_DOCS_OTHER = ( + # Included by: `info_advanced.rst`. + "info_advanced_blender_as_bpy.rst", ) +# Hide the actual TOC, use a separate list that links to the items. +# This is done so a short description can be included with each link. +USE_INFO_DOCS_FANCY_INDEX = True + # only support for properties atm. RNA_BLACKLIST = { # XXX messes up PDF!, really a bug but for now just workaround. "PreferencesSystem": {"language", } } +# Support suppressing errors when attributes collide with methods, +# use `noindex` on the attributes / data declarations. +# +# NOTE: in general this should be avoided but changing it would break the API, +# so explicitly suppress warnings instead. +# +# NOTE: Currently some API generation doesn't support this is it is not used yet, +# see references to `RST_NOINDEX_ATTR` in code comments. +# +# A set of tuple identifiers: `(module, type, attr)`. +RST_NOINDEX_ATTR = { + # Render is both a method and an attribute, from looking into this + # having both doesn't cause problems in practice since the `render` method + # is registered and called from C code where the attribute is accessed from the instance. + ("bpy.types", "RenderEngine", "render"), +} + MODULE_GROUPING = { "bmesh.types": ( - ("Base Mesh Type", '-'), + ("Base Mesh Type", "-"), "BMesh", - ("Mesh Elements", '-'), + ("Mesh Elements", "-"), "BMVert", "BMEdge", "BMFace", "BMLoop", - ("Sequence Accessors", '-'), + ("Sequence Accessors", "-"), "BMElemSeq", "BMVertSeq", "BMEdgeSeq", "BMFaceSeq", "BMLoopSeq", "BMIter", - ("Selection History", '-'), + ("Selection History", "-"), "BMEditSelSeq", "BMEditSelIter", - ("Custom-Data Layer Access", '-'), + ("Custom-Data Layer Access", "-"), "BMLayerAccessVert", "BMLayerAccessEdge", "BMLayerAccessFace", "BMLayerAccessLoop", "BMLayerCollection", "BMLayerItem", - ("Custom-Data Layer Types", '-'), + ("Custom-Data Layer Types", "-"), "BMLoopUV", - "BMDeformVert" + "BMDeformVert", ) } @@ -399,6 +478,7 @@ MODULE_GROUPING = { # converting bytes to strings, due to T30154 BLENDER_REVISION = str(bpy.app.build_hash, 'utf_8') +BLENDER_REVISION_TIMESTAMP = bpy.app.build_commit_timestamp # '2.83.0 Beta' or '2.83.0' or '2.83.1' BLENDER_VERSION_STRING = bpy.app.version_string @@ -407,11 +487,17 @@ BLENDER_VERSION_DOTS = "%d.%d" % (bpy.app.version[0], bpy.app.version[1]) if BLENDER_REVISION != "Unknown": # SHA1 Git hash BLENDER_VERSION_HASH = BLENDER_REVISION + BLENDER_VERSION_HASH_HTML_LINK = "<a href=https://developer.blender.org/rB%s>%s</a>" % ( + BLENDER_VERSION_HASH, BLENDER_VERSION_HASH, + ) + BLENDER_VERSION_DATE = time.strftime("%d/%m/%Y", time.localtime(BLENDER_REVISION_TIMESTAMP)) else: # Fallback: Should not be used BLENDER_VERSION_HASH = "Hash Unknown" + BLENDER_VERSION_HASH_HTML_LINK = BLENDER_VERSION_HASH + BLENDER_VERSION_DATE = time.strftime("%Y-%m-%d") -# '2_83' +# Example: `2_83`. BLENDER_VERSION_PATH = "%d_%d" % (bpy.app.version[0], bpy.app.version[1]) # --------------------------DOWNLOADABLE FILES---------------------------------- @@ -460,10 +546,46 @@ if ARGS.sphinx_build_pdf: sphinx_make_pdf_log = os.path.join(ARGS.output_dir, ".latex_make.log") SPHINX_MAKE_PDF_STDOUT = open(sphinx_make_pdf_log, "w", encoding="utf-8") + +# --------------------------------CHANGELOG GENERATION-------------------------------------- + +def generate_changelog(): + import importlib.util + spec = importlib.util.spec_from_file_location( + "sphinx_changelog_gen", + os.path.abspath(os.path.join(SCRIPT_DIR, "sphinx_changelog_gen.py")), + ) + sphinx_changelog_gen = importlib.util.module_from_spec(spec) + spec.loader.exec_module(sphinx_changelog_gen) + + API_DUMP_INDEX_FILEPATH = ARGS.api_dump_index_path + API_DUMP_ROOT = os.path.dirname(API_DUMP_INDEX_FILEPATH) + API_DUMP_FILEPATH = os.path.abspath(os.path.join(API_DUMP_ROOT, BLENDER_VERSION_DOTS, "api_dump.json")) + API_CHANGELOG_FILEPATH = os.path.abspath(os.path.join(SPHINX_IN_TMP, "change_log.rst")) + + sphinx_changelog_gen.main(( + "--", + "--indexpath", + API_DUMP_INDEX_FILEPATH, + "dump", + "--filepath-out", + API_DUMP_FILEPATH, + )) + + sphinx_changelog_gen.main(( + "--", + "--indexpath", + API_DUMP_INDEX_FILEPATH, + "changelog", + "--filepath-out", + API_CHANGELOG_FILEPATH, + )) + + # --------------------------------API DUMP-------------------------------------- -# lame, python won't give some access -ClassMethodDescriptorType = type(dict.__dict__['fromkeys']) +# Lame, python won't give some access. +ClassMethodDescriptorType = type(dict.__dict__["fromkeys"]) MethodDescriptorType = type(dict.get) GetSetDescriptorType = type(int.real) StaticMethodType = type(staticmethod(lambda: None)) @@ -494,8 +616,15 @@ def import_value_from_module(module_name, import_name): return ns["value"] +def execfile(filepath): + global_namespace = {"__file__": filepath, "__name__": "__main__"} + with open(filepath, encoding="utf-8") as file_handle: + exec(compile(file_handle.read(), filepath, 'exec'), global_namespace) + + def escape_rst(text): - """ Escape plain text which may contain characters used by RST. + """ + Escape plain text which may contain characters used by RST. """ return text.translate(escape_rst.trans) @@ -509,36 +638,41 @@ escape_rst.trans = str.maketrans({ def is_struct_seq(value): - return isinstance(value, tuple) and type(tuple) != tuple and hasattr(value, "n_fields") + return isinstance(value, tuple) and type(value) != tuple and hasattr(value, "n_fields") def undocumented_message(module_name, type_name, identifier): + BPY_LOGGER.debug( + "Undocumented: module %s, type: %s, id: %s is not documented", + module_name, type_name, identifier, + ) + return "Undocumented, consider `contributing <https://developer.blender.org/T51061>`__." def range_str(val): - ''' + """ Converts values to strings for the range directive. (unused function it seems) - ''' + """ if val < -10000000: - return '-inf' + return "-inf" elif val > 10000000: - return 'inf' + return "inf" elif type(val) == float: - return '%g' % val + return "%g" % val else: return str(val) def example_extract_docstring(filepath): - ''' + """ Return (text, line_no, line_no_has_content) where: - ``text`` is the doc-string text. - ``line_no`` is the line the doc-string text ends. - ``line_no_has_content`` when False, this file only contains a doc-string. There is no need to include the remainder. - ''' + """ file = open(filepath, "r", encoding="utf-8") line = file.readline() line_no = 0 @@ -553,8 +687,7 @@ def example_extract_docstring(filepath): line_no += 1 if line.startswith('"""'): break - else: - text.append(line.rstrip()) + text.append(line.rstrip()) line_no += 1 line_no_has_content = False @@ -567,7 +700,7 @@ def example_extract_docstring(filepath): line_no += 1 file.close() - return "\n".join(text), line_no, line_no_has_content + return "\n".join(text).rstrip("\n"), line_no, line_no_has_content def title_string(text, heading_char, double=False): @@ -582,14 +715,18 @@ def title_string(text, heading_char, double=False): def write_example_ref(ident, fw, example_id, ext="py"): if example_id in EXAMPLE_SET: - # extract the comment + # Extract the comment. filepath = os.path.join("..", "examples", "%s.%s" % (example_id, ext)) filepath_full = os.path.join(os.path.dirname(fw.__self__.name), filepath) text, line_no, line_no_has_content = example_extract_docstring(filepath_full) + if text: + # Ensure a blank line, needed since in some cases the indentation doesn't match the previous line. + # which causes Sphinx not to warn about bad indentation. + fw("\n") + for line in text.split("\n"): + fw("%s\n" % (ident + line).rstrip()) - for line in text.split("\n"): - fw("%s\n" % (ident + line).rstrip()) fw("\n") # Some files only contain a doc-string. @@ -601,9 +738,9 @@ def write_example_ref(ident, fw, example_id, ext="py"): EXAMPLE_SET_USED.add(example_id) else: if bpy.app.debug: - BPY_LOGGER.debug("\tskipping example: " + example_id) + BPY_LOGGER.debug("\tskipping example: %s", example_id) - # Support for numbered files bpy.types.Operator -> bpy.types.Operator.1.py + # Support for numbered files `bpy.types.Operator` -> `bpy.types.Operator.1.py`. i = 1 while True: example_id_num = "%s.%d" % (example_id, i) @@ -615,22 +752,22 @@ def write_example_ref(ident, fw, example_id, ext="py"): def write_indented_lines(ident, fn, text, strip=True): - ''' - Apply same indentation to all lines in a multilines text. - ''' + """ + Apply same indentation to all lines in a multi-lines text. + """ if text is None: return lines = text.split("\n") - # strip empty lines from the start/end + # Strip empty lines from the start/end. while lines and not lines[0].strip(): del lines[0] while lines and not lines[-1].strip(): del lines[-1] if strip: - # set indentation to <indent> + # Set indentation to `<indent>`. ident_strip = 1000 for l in lines: if l.strip(): @@ -638,35 +775,15 @@ def write_indented_lines(ident, fn, text, strip=True): for l in lines: fn(ident + l[ident_strip:] + "\n") else: - # add <indent> number of blanks to the current indentation + # Add <indent> number of blanks to the current indentation. for l in lines: fn(ident + l + "\n") -def pymethod2sphinx(ident, fw, identifier, py_func): - ''' - class method to sphinx - ''' - arg_str = inspect.formatargspec(*inspect.getargspec(py_func)) - if arg_str.startswith("(self, "): - arg_str = "(" + arg_str[7:] - func_type = "method" - elif arg_str.startswith("(cls, "): - arg_str = "(" + arg_str[6:] - func_type = "classmethod" - else: - func_type = "staticmethod" - - fw(ident + ".. %s:: %s%s\n\n" % (func_type, identifier, arg_str)) - if py_func.__doc__: - write_indented_lines(ident + " ", fw, py_func.__doc__) - fw("\n") - - def pyfunc2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_class=True): - ''' + """ function or class method to sphinx - ''' + """ if type(py_func) == MethodType: return @@ -676,7 +793,7 @@ def pyfunc2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_cla if not is_class: func_type = "function" - # the rest are class methods + # The rest are class methods. elif arg_str.startswith("(self, ") or arg_str == "(self)": arg_str = "()" if (arg_str == "(self)") else ("(" + arg_str[7:]) func_type = "method" @@ -714,10 +831,12 @@ def py_descr2sphinx(ident, fw, descr, module_name, type_name, identifier): if type(descr) == GetSetDescriptorType: fw(ident + ".. attribute:: %s\n\n" % identifier) + # NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used). write_indented_lines(ident + " ", fw, doc, False) fw("\n") - elif type(descr) == MemberDescriptorType: # same as above but use 'data' + elif type(descr) == MemberDescriptorType: # same as above but use "data" fw(ident + ".. data:: %s\n\n" % identifier) + # NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used). write_indented_lines(ident + " ", fw, doc, False) fw("\n") elif type(descr) in {MethodDescriptorType, ClassMethodDescriptorType}: @@ -731,11 +850,11 @@ def py_descr2sphinx(ident, fw, descr, module_name, type_name, identifier): def py_c_func2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_class=True): - ''' - c defined function to sphinx. - ''' + """ + C defined function to sphinx. + """ - # dump the docstring, assume its formatted correctly + # Dump the doc-string, assume its formatted correctly. if py_func.__doc__: write_indented_lines(ident, fw, py_func.__doc__, False) fw("\n") @@ -752,14 +871,16 @@ def py_c_func2sphinx(ident, fw, module_name, type_name, identifier, py_func, is_ def pyprop2sphinx(ident, fw, identifier, py_prop): - ''' + """ Python property to sphinx - ''' - # readonly properties use "data" directive, variables use "attribute" directive + """ + # Read-only properties use "data" directive, variables use "attribute" directive. if py_prop.fset is None: fw(ident + ".. data:: %s\n\n" % identifier) else: fw(ident + ".. attribute:: %s\n\n" % identifier) + + # NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used). write_indented_lines(ident + " ", fw, py_prop.__doc__) fw("\n") if py_prop.fset is None: @@ -777,8 +898,8 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): if module_all: module_dir = module_all - # TODO - currently only used for classes - # grouping support + # TODO: currently only used for classes. + # Grouping support. module_grouping = MODULE_GROUPING.get(module_name) def module_grouping_index(name): @@ -798,7 +919,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): def module_grouping_sort_key(name): return module_grouping_index(name) - # done grouping support + # Done grouping support. file = open(filepath, "w", encoding="utf-8") @@ -813,8 +934,8 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): fw(module.__doc__.strip()) fw("\n\n") - # write submodules - # we could also scan files but this ensures __all__ is used correctly + # Write sub-modules. + # We could also scan files but this ensures `__all__` is used correctly. if module_all or module_all_extra: submod_name = None submod = None @@ -847,7 +968,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): pymodule2sphinx(basepath, submod_name_full, submod, "%s submodule" % module_name, ()) fw("\n") del submod_ls - # done writing submodules! + # Done writing sub-modules! write_example_ref("", fw, module_name) @@ -858,10 +979,10 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): continue if key in module_all_extra: continue - # naughty, we also add getset's into PyStructs, this is not typical py but also not incorrect. + # Naughty! We also add `getset` to `PyStruct`, this is not typical Python but also not incorrect. - # type_name is only used for examples and messages - # "<class 'bpy.app.handlers'>" --> bpy.app.handlers + # `type_name` is only used for examples and messages: + # `<class 'bpy.app.handlers'>` -> `bpy.app.handlers`. type_name = str(type(module)).strip("<>").split(" ", 1)[-1][1:-1] if type(descr) == types.GetSetDescriptorType: py_descr2sphinx("", fw, descr, module_name, type_name, key) @@ -877,13 +998,13 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): value_type = type(value) descr_sorted.append((key, descr, value, type(value))) - # sort by the valye type + # Sort by the value type. descr_sorted.sort(key=lambda descr_data: str(descr_data[3])) for key, descr, value, value_type in descr_sorted: if key in module_all_extra: continue - # must be documented as a submodule + # Must be documented as a sub-module. if is_struct_seq(value): continue @@ -897,7 +1018,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): classes = [] submodules = [] - # use this list so we can sort by type + # Use this list so we can sort by type. module_dir_value_type = [] for attribute in module_dir: @@ -907,7 +1028,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): if attribute in attribute_set: continue - if attribute.startswith("n_"): # annoying exception, needed for bpy.app + if attribute.startswith("n_"): # Annoying exception, needed for `bpy.app`. continue # workaround for bpy.app documenting .index() and .count() @@ -928,31 +1049,31 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): if value_type == FunctionType: pyfunc2sphinx("", fw, module_name, None, attribute, value, is_class=False) - # both the same at the moment but to be future proof + # Both the same at the moment but to be future proof. elif value_type in {types.BuiltinMethodType, types.BuiltinFunctionType}: - # note: can't get args from these, so dump the string as is - # this means any module used like this must have fully formatted docstrings. + # NOTE: can't get args from these, so dump the string as is + # this means any module used like this must have fully formatted doc-strings. py_c_func2sphinx("", fw, module_name, None, attribute, value, is_class=False) elif value_type == type: classes.append((attribute, value)) elif issubclass(value_type, types.ModuleType): submodules.append((attribute, value)) elif issubclass(value_type, (bool, int, float, str, tuple)): - # constant, not much fun we can do here except to list it. - # TODO, figure out some way to document these! + # Constant, not much fun we can do here except to list it. + # TODO: figure out some way to document these! fw(".. data:: %s\n\n" % attribute) write_indented_lines(" ", fw, "Constant value %s" % repr(value), False) fw("\n") else: - BPY_LOGGER.debug("\tnot documenting %s.%s of %r type" % (module_name, attribute, value_type.__name__)) + BPY_LOGGER.debug("\tnot documenting %s.%s of %r type", module_name, attribute, value_type.__name__) continue attribute_set.add(attribute) - # TODO, more types... + # TODO: more types. del module_dir_value_type - # TODO, bpy_extras does this already, mathutils not. - ''' + # TODO: `bpy_extras` does this already, `mathutils` not. + """ if submodules: fw("\n" "**********\n" @@ -963,12 +1084,12 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): for attribute, submod in submodules: fw("* :mod:`%s.%s`\n" % (module_name, attribute)) fw("\n") - ''' + """ if module_grouping is not None: classes.sort(key=lambda pair: module_grouping_sort_key(pair[0])) - # write collected classes now + # Write collected classes now. for (type_name, value) in classes: if module_grouping is not None: @@ -976,7 +1097,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): if heading: fw(title_string(heading, heading_char)) - # May need to be its own function + # May need to be its own function. if value.__doc__: if value.__doc__.startswith(".. class::"): fw(value.__doc__) @@ -995,7 +1116,7 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): if type(descr) == ClassMethodDescriptorType: py_descr2sphinx(" ", fw, descr, module_name, type_name, key) - # needed for pure Python classes + # Needed for pure Python classes. for key, descr in descr_items: if type(descr) == FunctionType: pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, is_class=True) @@ -1019,9 +1140,10 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra): file.close() -# Changes in Blender will force errors here +# Changes In Blender will force errors here. context_type_map = { # context_member: (RNA type, is_collection) + "active_action": ("Action", False), "active_annotation_layer": ("GPencilLayer", False), "active_bone": ("EditBone", False), "active_file": ("FileSelectEntry", False), @@ -1094,7 +1216,6 @@ context_type_map = { "selected_editable_keyframes": ("Keyframe", True), "selected_editable_objects": ("Object", True), "selected_editable_sequences": ("Sequence", True), - "selected_ids": ("ID", True), "selected_files": ("FileSelectEntry", True), "selected_ids": ("ID", True), "selected_nla_strips": ("NlaStrip", True), @@ -1128,7 +1249,7 @@ context_type_map = { def pycontext2sphinx(basepath): - # Only use once. very irregular + # Only use once. very irregular. filepath = os.path.join(basepath, "bpy.context.rst") file = open(filepath, "w", encoding="utf-8") @@ -1141,13 +1262,16 @@ def pycontext2sphinx(basepath): fw("Note that all context values are readonly,\n") fw("but may be modified through the data API or by running operators\n\n") + # Track all unique properties to properly use `noindex`. + unique = set() + def write_contex_cls(): fw(title_string("Global Context", "-")) fw("These properties are available in any contexts.\n\n") - # very silly. could make these global and only access once. - # structs, funcs, ops, props = rna_info.BuildRNAInfo() + # Very silly. could make these global and only access once: + # `structs, funcs, ops, props = rna_info.BuildRNAInfo()`. structs, funcs, ops, props = rna_info_BuildRNAInfo_cache() struct = structs[("", "Context")] struct_blacklist = RNA_BLACKLIST.get(struct.identifier, ()) @@ -1156,26 +1280,36 @@ def pycontext2sphinx(basepath): sorted_struct_properties = struct.properties[:] sorted_struct_properties.sort(key=lambda prop: prop.identifier) - # First write RNA + # First write RNA. for prop in sorted_struct_properties: - # support blacklisting props + # Support blacklisting props. if prop.identifier in struct_blacklist: continue + # No need to check if there are duplicates yet as it's known there wont be. + unique.add(prop.identifier) + + enum_descr_override = None + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + enum_descr_override = pyrna_enum2sphinx_shared_link(prop) type_descr = prop.get_type_description( - class_fmt=":class:`bpy.types.%s`", collection_id=_BPY_PROP_COLLECTION_ID) + class_fmt=":class:`bpy.types.%s`", + collection_id=_BPY_PROP_COLLECTION_ID, + enum_descr_override=enum_descr_override, + ) fw(".. data:: %s\n\n" % prop.identifier) if prop.description: fw(" %s\n\n" % prop.description) - # special exception, can't use generic code here for enums + # Special exception, can't use generic code here for enums. if prop.type == "enum": - enum_text = pyrna_enum2sphinx(prop) + # If the link has been written, no need to inline the enum items. + enum_text = "" if enum_descr_override else pyrna_enum2sphinx(prop) if enum_text: write_indented_lines(" ", fw, enum_text) fw("\n") del enum_text - # end enum exception + # End enum exception. fw(" :type: %s\n\n" % type_descr) @@ -1183,58 +1317,59 @@ def pycontext2sphinx(basepath): del write_contex_cls # end - # nasty, get strings directly from Blender because there is no other way to get it - import ctypes - - context_strings = ( - "screen_context_dir", - "view3d_context_dir", - "buttons_context_dir", - "image_context_dir", - "node_context_dir", - "text_context_dir", - "clip_context_dir", - "sequencer_context_dir", - "file_context_dir", - ) + # Internal API call only intended to be used to extract context members. + from _bpy import context_members + context_member_map = context_members() + del context_members - unique = set() - blend_cdll = ctypes.CDLL("") - for ctx_str in context_strings: + # Track unique for `context_strings` to validate `context_type_map`. + unique_context_strings = set() + for ctx_str, ctx_members in sorted(context_member_map.items()): subsection = "%s Context" % ctx_str.split("_")[0].title() - fw("\n%s\n%s\n\n" % (subsection, (len(subsection) * '-'))) - - attr = ctypes.addressof(getattr(blend_cdll, ctx_str)) - c_char_p_p = ctypes.POINTER(ctypes.c_char_p) - char_array = c_char_p_p.from_address(attr) - i = 0 - while char_array[i] is not None: - member = ctypes.string_at(char_array[i]).decode(encoding="ascii") - fw(".. data:: %s\n\n" % member) + fw("\n%s\n%s\n\n" % (subsection, (len(subsection) * "-"))) + for member in ctx_members: + unique_all_len = len(unique) + unique.add(member) + member_visited = unique_all_len == len(unique) + + unique_context_strings.add(member) + + fw(".. data:: %s\n" % member) + # Avoid warnings about the member being included multiple times. + if member_visited: + fw(" :noindex:\n") + fw("\n") + try: member_type, is_seq = context_type_map[member] except KeyError: - raise SystemExit("Error: context key %r not found in context_type_map; update %s" % (member, __file__)) from None + raise SystemExit( + "Error: context key %r not found in context_type_map; update %s" % + (member, __file__)) from None fw(" :type: %s :class:`bpy.types.%s`\n\n" % ("sequence of " if is_seq else "", member_type)) - unique.add(member) - i += 1 - # generate typemap... - # for member in sorted(unique): + # Generate type-map: + # for member in sorted(unique_context_strings): # print(' "%s": ("", False),' % member) - if len(context_type_map) > len(unique): + if len(context_type_map) > len(unique_context_strings): warnings.warn( "Some types are not used: %s" % - str([member for member in context_type_map if member not in unique])) + str([member for member in context_type_map if member not in unique_context_strings])) else: - pass # will have raised an error above + pass # Will have raised an error above. file.close() def pyrna_enum2sphinx(prop, use_empty_descriptions=False): - """ write a bullet point list of enum + descriptions """ + Write a bullet point list of enum + descriptions. + """ + + # Write a link to the enum if this is part of `rna_enum_pointer_map`. + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + if (result := pyrna_enum2sphinx_shared_link(prop)) is not None: + return result if use_empty_descriptions: ok = True @@ -1260,13 +1395,14 @@ def pyrna_enum2sphinx(prop, use_empty_descriptions=False): def pyrna2sphinx(basepath): - """ bpy.types and bpy.ops """ - # structs, funcs, ops, props = rna_info.BuildRNAInfo() - structs, funcs, ops, props = rna_info_BuildRNAInfo_cache() + ``bpy.types`` and ``bpy.ops``. + """ + # `structs, funcs, ops, props = rna_info.BuildRNAInfo()` + structs, _funcs, ops, _props = rna_info_BuildRNAInfo_cache() if USE_ONLY_BUILTIN_RNA_TYPES: - # Ignore properties that use non 'bpy.types' properties. + # Ignore properties that use non `bpy.types` properties. structs_blacklist = { v.identifier for v in structs.values() if v.module_name != "bpy.types" @@ -1313,17 +1449,22 @@ def pyrna2sphinx(basepath): kwargs["collection_id"] = _BPY_PROP_COLLECTION_ID - type_descr = prop.get_type_description(**kwargs) + enum_descr_override = None + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + enum_descr_override = pyrna_enum2sphinx_shared_link(prop) + kwargs["enum_descr_override"] = enum_descr_override - enum_text = pyrna_enum2sphinx(prop) + type_descr = prop.get_type_description(**kwargs) + # If the link has been written, no need to inline the enum items. + enum_text = "" if enum_descr_override else pyrna_enum2sphinx(prop) if prop.name or prop.description or enum_text: fw(ident + ":%s%s:\n\n" % (id_name, identifier)) if prop.name or prop.description: fw(indent(", ".join(val for val in (prop.name, prop.description) if val), ident + " ") + "\n\n") - # special exception, can't use generic code here for enums + # Special exception, can't use generic code here for enums. if enum_text: write_indented_lines(ident + " ", fw, enum_text) fw("\n") @@ -1341,7 +1482,7 @@ def pyrna2sphinx(basepath): struct_module_name = struct.module_name if USE_ONLY_BUILTIN_RNA_TYPES: - assert(struct_module_name == "bpy.types") + assert struct_module_name == "bpy.types" filepath = os.path.join(basepath, "%s.%s.rst" % (struct_module_name, struct.identifier)) file = open(filepath, "w", encoding="utf-8") fw = file.write @@ -1401,49 +1542,71 @@ def pyrna2sphinx(basepath): else: fw(".. class:: %s\n\n" % struct_id) - fw(" %s\n\n" % struct.description) + write_indented_lines(" ", fw, struct.description, False) + fw("\n") - # properties sorted in alphabetical order + # Properties sorted in alphabetical order. sorted_struct_properties = struct.properties[:] sorted_struct_properties.sort(key=lambda prop: prop.identifier) - # support blacklisting props + # Support blacklisting props. struct_blacklist = RNA_BLACKLIST.get(struct_id, ()) for prop in sorted_struct_properties: + identifier = prop.identifier - # support blacklisting props - if prop.identifier in struct_blacklist: + # Support blacklisting props. + if identifier in struct_blacklist: continue - type_descr = prop.get_type_description(class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID) - # readonly properties use "data" directive, variables properties use "attribute" directive - if 'readonly' in type_descr: - fw(" .. data:: %s\n\n" % prop.identifier) + enum_descr_override = None + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + enum_descr_override = pyrna_enum2sphinx_shared_link(prop) + + type_descr = prop.get_type_description( + class_fmt=":class:`%s`", + collection_id=_BPY_PROP_COLLECTION_ID, + enum_descr_override=enum_descr_override, + ) + # Read-only properties use "data" directive, variables properties use "attribute" directive. + if "readonly" in type_descr: + fw(" .. data:: %s\n" % identifier) else: - fw(" .. attribute:: %s\n\n" % prop.identifier) + fw(" .. attribute:: %s\n" % identifier) + # Also write `noindex` on requerst. + if ("bpy.types", struct_id, identifier) in RST_NOINDEX_ATTR: + fw(" :noindex:\n") + fw("\n") + if prop.description: write_indented_lines(" ", fw, prop.description, False) fw("\n") - # special exception, can't use generic code here for enums + # Special exception, can't use generic code here for enums. if prop.type == "enum": - enum_text = pyrna_enum2sphinx(prop) + # If the link has been written, no need to inline the enum items. + enum_text = "" if enum_descr_override else pyrna_enum2sphinx(prop) if enum_text: write_indented_lines(" ", fw, enum_text) fw("\n") del enum_text - # end enum exception + # End enum exception. fw(" :type: %s\n\n" % type_descr) - # Python attributes + # Python attributes. py_properties = struct.get_py_properties() py_prop = None for identifier, py_prop in py_properties: pyprop2sphinx(" ", fw, identifier, py_prop) del py_properties, py_prop + # C/Python attributes: `GetSetDescriptorType`. + key = descr = None + for key, descr in sorted(struct.get_py_c_properties_getset()): + py_descr2sphinx(" ", fw, descr, "bpy.types", struct_id, key) + del key, descr + for func in struct.functions: args_str = ", ".join(prop.get_arg_default(force=False) for prop in func.args) @@ -1456,17 +1619,25 @@ def pyrna2sphinx(basepath): if len(func.return_values) == 1: write_param(" ", fw, func.return_values[0], is_return=True) - elif func.return_values: # multiple return values + elif func.return_values: # Multiple return values. fw(" :return (%s):\n" % ", ".join(prop.identifier for prop in func.return_values)) for prop in func.return_values: - # TODO, pyrna_enum2sphinx for multiple return values... actually don't - # think we even use this but still!!! + # TODO: pyrna_enum2sphinx for multiple return values... actually don't + # think we even use this but still! + + enum_descr_override = None + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + enum_descr_override = pyrna_enum2sphinx_shared_link(prop) + type_descr = prop.get_type_description( - as_ret=True, class_fmt=":class:`%s`", collection_id=_BPY_PROP_COLLECTION_ID) + as_ret=True, class_fmt=":class:`%s`", + collection_id=_BPY_PROP_COLLECTION_ID, + enum_descr_override=enum_descr_override, + ) descr = prop.description if not descr: descr = prop.name - # In rare cases descr may be empty + # In rare cases `descr` may be empty. fw(" `%s`, %s\n\n" % (prop.identifier, ", ".join((val for val in (descr, type_descr) if val)))) @@ -1475,7 +1646,7 @@ def pyrna2sphinx(basepath): fw("\n") - # Python methods + # Python methods. py_funcs = struct.get_py_functions() py_func = None @@ -1494,7 +1665,7 @@ def pyrna2sphinx(basepath): if struct.base or _BPY_STRUCT_FAKE: bases = list(reversed(struct.get_bases())) - # props + # Properties. del lines[:] if _BPY_STRUCT_FAKE: @@ -1525,7 +1696,7 @@ def pyrna2sphinx(basepath): fw(line) fw("\n") - # funcs + # Functions. del lines[:] if _BPY_STRUCT_FAKE: @@ -1580,7 +1751,7 @@ def pyrna2sphinx(basepath): if "bpy.types" not in EXCLUDE_MODULES: for struct in structs.values(): - # TODO, rna_info should filter these out! + # TODO: rna_info should filter these out! if "_OT_" in struct.identifier: continue write_struct(struct) @@ -1615,7 +1786,7 @@ def pyrna2sphinx(basepath): ] for key, descr in descr_items: - # GetSetDescriptorType, GetSetDescriptorType's are not documented yet + # `GetSetDescriptorType`, `GetSetDescriptorType` types are not documented yet. if type(descr) == MethodDescriptorType: py_descr2sphinx(" ", fw, descr, "bpy.types", class_name, key) @@ -1646,6 +1817,7 @@ def pyrna2sphinx(basepath): API_BASEURL_ADDON_CONTRIB = "https://developer.blender.org/diffusion/BAC" op_modules = {} + op = None for op in ops.values(): op_modules.setdefault(op.module_name, []).append(op) del op @@ -1670,7 +1842,7 @@ def pyrna2sphinx(basepath): # if the description isn't valid, we output the standard warning # with a link to the wiki so that people can help if not op.description or op.description == "(undocumented operator)": - operator_description = undocumented_message('bpy.ops', op.module_name, op.func_name) + operator_description = undocumented_message("bpy.ops", op.module_name, op.func_name) else: operator_description = op.description @@ -1699,9 +1871,9 @@ def pyrna2sphinx(basepath): def write_sphinx_conf_py(basepath): - ''' - Write sphinx's conf.py - ''' + """ + Write sphinx's ``conf.py``. + """ filepath = os.path.join(basepath, "conf.py") file = open(filepath, "w", encoding="utf-8") fw = file.write @@ -1710,11 +1882,16 @@ def write_sphinx_conf_py(basepath): fw("extensions = ['sphinx.ext.intersphinx']\n\n") fw("intersphinx_mapping = {'blender_manual': ('https://docs.blender.org/manual/en/dev/', None)}\n\n") fw("project = 'Blender %s Python API'\n" % BLENDER_VERSION_STRING) - fw("master_doc = 'index'\n") - fw("copyright = u'Blender Foundation'\n") + fw("root_doc = 'index'\n") + fw("copyright = 'Blender Foundation'\n") fw("version = '%s'\n" % BLENDER_VERSION_DOTS) fw("release = '%s'\n" % BLENDER_VERSION_DOTS) + # Set this as the default is a super-set of Python3. + fw("highlight_language = 'python3'\n") + # No need to detect encoding. + fw("highlight_options = {'default': {'encoding': 'utf-8'}}\n\n") + # Quiet file not in table-of-contents warnings. fw("exclude_patterns = [\n") fw(" 'include__bmesh.rst',\n") @@ -1739,7 +1916,7 @@ except ModuleNotFoundError: # fw(" 'collapse_navigation': True,\n") fw(" 'sticky_navigation': False,\n") fw(" 'navigation_depth': 1,\n") - # fw(" 'includehidden': True,\n") + fw(" 'includehidden': False,\n") # fw(" 'titles_only': False\n") fw(" }\n\n") @@ -1752,11 +1929,12 @@ except ModuleNotFoundError: fw("html_split_index = True\n") fw("html_static_path = ['static']\n") fw("templates_path = ['templates']\n") - fw("html_context = {'commit': '%s'}\n" % BLENDER_VERSION_HASH) + fw("html_context = {'commit': '%s - %s'}\n" % (BLENDER_VERSION_HASH_HTML_LINK, BLENDER_VERSION_DATE)) fw("html_extra_path = ['static/favicon.ico', 'static/blender_logo.svg']\n") fw("html_favicon = 'static/favicon.ico'\n") fw("html_logo = 'static/blender_logo.svg'\n") - fw("html_last_updated_fmt = '%m/%d/%Y'\n\n") + # Disable default `last_updated` value, since this is the date of doc generation, not the one of the source commit. + fw("html_last_updated_fmt = None\n\n") fw("if html_theme == 'sphinx_rtd_theme':\n") fw(" html_css_files = ['css/version_switch.css']\n") fw(" html_js_files = ['js/version_switch.js']\n") @@ -1789,17 +1967,10 @@ class PatchedPythonDomain(PythonDomain): file.close() -def execfile(filepath): - global_namespace = {"__file__": filepath, "__name__": "__main__"} - file_handle = open(filepath) - exec(compile(file_handle.read(), filepath, 'exec'), global_namespace) - file_handle.close() - - def write_rst_index(basepath): - ''' - Write the rst file of the main page, needed for sphinx (index.html) - ''' + """ + Write the RST file of the main page, needed for sphinx: ``index.html``. + """ filepath = os.path.join(basepath, "index.rst") file = open(filepath, "w", encoding="utf-8") fw = file.write @@ -1817,12 +1988,21 @@ def write_rst_index(basepath): if not EXCLUDE_INFO_DOCS: fw(".. toctree::\n") + if USE_INFO_DOCS_FANCY_INDEX: + fw(" :hidden:\n") fw(" :maxdepth: 1\n") fw(" :caption: Documentation\n\n") for info, info_desc in INFO_DOCS: - fw(" %s <%s>\n" % (info_desc, info)) + fw(" %s\n" % info) fw("\n") + if USE_INFO_DOCS_FANCY_INDEX: + # Show a fake TOC, allowing for an extra description to be shown as well as the title. + fw(title_string("Documentation", "=")) + for info, info_desc in INFO_DOCS: + fw("- :doc:`%s`: %s\n" % (info.removesuffix(".rst"), info_desc)) + fw("\n") + fw(".. toctree::\n") fw(" :maxdepth: 1\n") fw(" :caption: Application Modules\n\n") @@ -1877,7 +2057,7 @@ def write_rst_index(basepath): fw("* :ref:`genindex`\n") fw("* :ref:`modindex`\n\n") - # special case, this 'bmesh.ops.rst' is extracted from C source + # Special case, this `bmesh.ops.rst` is extracted from C source. if "bmesh.ops" not in EXCLUDE_MODULES: execfile(os.path.join(SCRIPT_DIR, "rst_from_bmesh_opdefines.py")) @@ -1885,9 +2065,9 @@ def write_rst_index(basepath): def write_rst_bpy(basepath): - ''' - Write rst file of bpy module (disabled by default) - ''' + """ + Write RST file of ``bpy`` module (disabled by default) + """ if ARGS.bpy: filepath = os.path.join(basepath, "bpy.rst") file = open(filepath, "w", encoding="utf-8") @@ -1904,9 +2084,9 @@ def write_rst_bpy(basepath): def write_rst_types_index(basepath): - ''' - Write the rst file of bpy.types module (index) - ''' + """ + Write the RST file of ``bpy.types`` module (index) + """ if "bpy.types" not in EXCLUDE_MODULES: filepath = os.path.join(basepath, "bpy.types.rst") file = open(filepath, "w", encoding="utf-8") @@ -1916,13 +2096,21 @@ def write_rst_types_index(basepath): fw(".. toctree::\n") fw(" :glob:\n\n") fw(" bpy.types.*\n\n") + + # This needs to be included somewhere, while it's hidden, list to avoid warnings. + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + fw(".. toctree::\n") + fw(" :hidden:\n") + fw(" :maxdepth: 1\n\n") + fw(" Shared Enum Types <bpy_types_enum_items/index>\n\n") + file.close() def write_rst_ops_index(basepath): - ''' - Write the rst file of bpy.ops module (index) - ''' + """ + Write the RST file of bpy.ops module (index) + """ if "bpy.ops" not in EXCLUDE_MODULES: filepath = os.path.join(basepath, "bpy.ops.rst") file = open(filepath, "w", encoding="utf-8") @@ -1939,7 +2127,7 @@ def write_rst_ops_index(basepath): def write_rst_msgbus(basepath): """ - Write the rst files of bpy.msgbus module + Write the RST files of ``bpy.msgbus`` module """ if 'bpy.msgbus' in EXCLUDE_MODULES: return @@ -1961,12 +2149,11 @@ def write_rst_msgbus(basepath): def write_rst_data(basepath): - ''' - Write the rst file of bpy.data module - ''' + """ + Write the RST file of ``bpy.data`` module. + """ if "bpy.data" not in EXCLUDE_MODULES: - # not actually a module, only write this file so we - # can reference in the TOC + # Not actually a module, only write this file so we can reference in the TOC. filepath = os.path.join(basepath, "bpy.data.rst") file = open(filepath, "w", encoding="utf-8") fw = file.write @@ -1987,10 +2174,85 @@ def write_rst_data(basepath): EXAMPLE_SET_USED.add("bpy.data") +def pyrna_enum2sphinx_shared_link(prop): + """ + Return a reference to the enum used by ``prop`` or None when not found. + """ + if ( + (prop.type == "enum") and + (pointer := prop.enum_pointer) and + (identifier := rna_enum_pointer_to_id_map.get(pointer)) + ): + return ":ref:`%s`" % identifier + return None + + +def write_rst_enum_items(basepath, key, key_no_prefix, enum_items): + """ + Write a single page for a static enum in RST. + + This helps avoiding very large lists being in-lined in many places which is an issue + especially with icons in ``bpy.types.UILayout``. See T87008. + """ + filepath = os.path.join(basepath, "%s.rst" % key_no_prefix) + with open(filepath, "w", encoding="utf-8") as fh: + fw = fh.write + # fw(".. noindex::\n\n") + fw(".. _%s:\n\n" % key) + + fw(title_string(key_no_prefix.replace("_", " ").title(), "#")) + # fw(".. rubric:: %s\n\n" % key_no_prefix.replace("_", " ").title()) + + for item in enum_items: + identifier = item.identifier + name = item.name + description = item.description + if identifier: + fw(":%s: %s\n" % (item.identifier, (escape_rst(name) + ".") if name else "")) + if description: + fw("\n") + write_indented_lines(" ", fw, escape_rst(description) + ".") + else: + fw("\n") + else: + if name: + fw("\n\n**%s**\n\n" % name) + else: + fw("\n\n----\n\n") + + if description: + fw(escape_rst(description) + ".") + fw("\n\n") + + +def write_rst_enum_items_and_index(basepath): + """ + Write shared enum items. + """ + subdir = "bpy_types_enum_items" + basepath_bpy_types_rna_enum = os.path.join(basepath, subdir) + os.makedirs(basepath_bpy_types_rna_enum, exist_ok=True) + with open(os.path.join(basepath_bpy_types_rna_enum, "index.rst"), "w", encoding="utf-8") as fh: + fw = fh.write + fw(title_string("Shared Enum Items", "#")) + fw(".. toctree::\n") + fw("\n") + for key, enum_items in rna_enum_dict.items(): + if not key.startswith("rna_enum_"): + raise Exception("Found RNA enum identifier that doesn't use the 'rna_enum_' prefix, found %r!" % key) + key_no_prefix = key.removeprefix("rna_enum_") + fw(" %s\n" % key_no_prefix) + + for key, enum_items in rna_enum_dict.items(): + key_no_prefix = key.removeprefix("rna_enum_") + write_rst_enum_items(basepath_bpy_types_rna_enum, key, key_no_prefix, enum_items) + fw("\n") + + def write_rst_importable_modules(basepath): - ''' - Write the rst files of importable modules - ''' + """ + Write the RST files of importable modules. + """ importable_modules = { # Python_modules "bpy.path": "Path Utilities", @@ -2052,7 +2314,7 @@ def write_rst_importable_modules(basepath): # access such as `bpy.app.sdl` which doesn't seem useful since it hides more useful # module-like objects among library data access. importable_modules_parent_map = {} - for mod_name in importable_modules.keys(): + for mod_name in importable_modules: # Iterate over keys. if mod_name in EXCLUDE_MODULES: continue if "." in mod_name: @@ -2069,12 +2331,14 @@ def write_rst_importable_modules(basepath): def copy_handwritten_rsts(basepath): - # info docs + # Info docs. if not EXCLUDE_INFO_DOCS: - for info, info_desc in INFO_DOCS: + for info, _info_desc in INFO_DOCS: + shutil.copy2(os.path.join(RST_DIR, info), basepath) + for info in INFO_DOCS_OTHER: shutil.copy2(os.path.join(RST_DIR, info), basepath) - # TODO put this docs in Blender's code and use import as per modules above + # TODO: put this docs in Blender's code and use import as per modules above. handwritten_modules = [ "bgl", # "Blender OpenGl wrapper" "bmesh.ops", # generated by rst_from_bmesh_opdefines.py @@ -2085,13 +2349,13 @@ def copy_handwritten_rsts(basepath): for mod_name in handwritten_modules: if mod_name not in EXCLUDE_MODULES: - # copy2 keeps time/date stamps + # Copy2 keeps time/date stamps. shutil.copy2(os.path.join(RST_DIR, "%s.rst" % mod_name), basepath) - # changelog + # Change-log. shutil.copy2(os.path.join(RST_DIR, "change_log.rst"), basepath) - # copy images, could be smarter but just glob for now. + # Copy images, could be smarter but just glob for now. for f in os.listdir(RST_DIR): if f.endswith(".png"): shutil.copy2(os.path.join(RST_DIR, f), basepath) @@ -2113,12 +2377,16 @@ def copy_handwritten_extra(basepath): def copy_theme_assets(basepath): - shutil.copytree(os.path.join(SCRIPT_DIR, "static"), - os.path.join(basepath, "static"), - copy_function=shutil.copy) - shutil.copytree(os.path.join(SCRIPT_DIR, "templates"), - os.path.join(basepath, "templates"), - copy_function=shutil.copy) + shutil.copytree( + os.path.join(SCRIPT_DIR, "static"), + os.path.join(basepath, "static"), + copy_function=shutil.copy, + ) + shutil.copytree( + os.path.join(SCRIPT_DIR, "templates"), + os.path.join(basepath, "templates"), + copy_function=shutil.copy, + ) def rna2sphinx(basepath): @@ -2136,10 +2404,7 @@ def rna2sphinx(basepath): # context if "bpy.context" not in EXCLUDE_MODULES: - # one of a kind, context doc (uses ctypes to extract info!) - # doesn't work on mac and windows - if PLATFORM not in {"darwin", "windows"}: - pycontext2sphinx(basepath) + pycontext2sphinx(basepath) # internal modules write_rst_bpy(basepath) # bpy, disabled by default @@ -2150,6 +2415,10 @@ def rna2sphinx(basepath): write_rst_data(basepath) # bpy.data write_rst_importable_modules(basepath) + # `bpy_types_enum_items/*` (referenced from `bpy.types`). + if USE_SHARED_RNA_ENUM_ITEMS_STATIC: + write_rst_enum_items_and_index(basepath) + # copy the other rsts copy_handwritten_rsts(basepath) @@ -2161,21 +2430,21 @@ def rna2sphinx(basepath): def align_sphinx_in_to_sphinx_in_tmp(dir_src, dir_dst): - ''' + """ Move changed files from SPHINX_IN_TMP to SPHINX_IN - ''' + """ import filecmp - # possible the dir doesn't exist when running recursively + # Possible the dir doesn't exist when running recursively. os.makedirs(dir_dst, exist_ok=True) sphinx_dst_files = set(os.listdir(dir_dst)) sphinx_src_files = set(os.listdir(dir_src)) - # remove deprecated files that have been removed + # Remove deprecated files that have been removed. for f in sorted(sphinx_dst_files): if f not in sphinx_src_files: - BPY_LOGGER.debug("\tdeprecated: %s" % f) + BPY_LOGGER.debug("\tdeprecated: %s", f) f_dst = os.path.join(dir_dst, f) if os.path.isdir(f_dst): shutil.rmtree(f_dst, True) @@ -2196,7 +2465,7 @@ def align_sphinx_in_to_sphinx_in_tmp(dir_src, dir_dst): do_copy = False if do_copy: - BPY_LOGGER.debug("\tupdating: %s" % f) + BPY_LOGGER.debug("\tupdating: %s", f) shutil.copy(f_src, f_dst) @@ -2218,16 +2487,11 @@ def refactor_sphinx_log(sphinx_logfile): def setup_monkey_patch(): filepath = os.path.join(SCRIPT_DIR, "sphinx_doc_gen_monkeypatch.py") - global_namespace = {"__file__": filepath, "__name__": "__main__"} - file = open(filepath, 'rb') - exec(compile(file.read(), filepath, 'exec'), global_namespace) - file.close() + execfile(filepath) # Avoid adding too many changes here. def setup_blender(): - import bpy - # Remove handlers since the functions get included # in the doc-string and don't have meaningful names. lists_to_restore = [] @@ -2254,109 +2518,122 @@ def main(): # Perform changes to Blender itself. setup_data = setup_blender() - # eventually, create the dirs + # Eventually, create the directories. for dir_path in [ARGS.output_dir, SPHINX_IN]: if not os.path.exists(dir_path): os.mkdir(dir_path) - # eventually, log in files + # Eventually, log in files. if ARGS.log: bpy_logfile = os.path.join(ARGS.output_dir, ".bpy.log") bpy_logfilehandler = logging.FileHandler(bpy_logfile, mode="w") bpy_logfilehandler.setLevel(logging.DEBUG) BPY_LOGGER.addHandler(bpy_logfilehandler) - # using a FileHandler seems to disable the stdout, so we add a StreamHandler + # using a `FileHandler` seems to disable the `stdout`, so we add a `StreamHandler`. bpy_log_stdout_handler = logging.StreamHandler(stream=sys.stdout) bpy_log_stdout_handler.setLevel(logging.DEBUG) BPY_LOGGER.addHandler(bpy_log_stdout_handler) - # in case of out-of-source build, copy the needed dirs + # In case of out-of-source build, copy the needed directories. if ARGS.output_dir != SCRIPT_DIR: - # examples dir + # Examples directory. examples_dir_copy = os.path.join(ARGS.output_dir, "examples") if os.path.exists(examples_dir_copy): shutil.rmtree(examples_dir_copy, True) - shutil.copytree(EXAMPLES_DIR, - examples_dir_copy, - ignore=shutil.ignore_patterns(*(".svn",)), - copy_function=shutil.copy) - - # dump the api in rst files + shutil.copytree( + EXAMPLES_DIR, + examples_dir_copy, + ignore=shutil.ignore_patterns(*(".svn",)), + copy_function=shutil.copy, + ) + + # Dump the API in RST files. if os.path.exists(SPHINX_IN_TMP): shutil.rmtree(SPHINX_IN_TMP, True) rna2sphinx(SPHINX_IN_TMP) + if ARGS.changelog: + generate_changelog() + if ARGS.full_rebuild: - # only for full updates + # Only for full updates. shutil.rmtree(SPHINX_IN, True) - shutil.copytree(SPHINX_IN_TMP, - SPHINX_IN, - copy_function=shutil.copy) + shutil.copytree( + SPHINX_IN_TMP, + SPHINX_IN, + copy_function=shutil.copy, + ) if ARGS.sphinx_build and os.path.exists(SPHINX_OUT): shutil.rmtree(SPHINX_OUT, True) if ARGS.sphinx_build_pdf and os.path.exists(SPHINX_OUT_PDF): shutil.rmtree(SPHINX_OUT_PDF, True) else: - # move changed files in SPHINX_IN + # Move changed files in `SPHINX_IN`. align_sphinx_in_to_sphinx_in_tmp(SPHINX_IN_TMP, SPHINX_IN) - # report which example files weren't used + # Report which example files weren't used. EXAMPLE_SET_UNUSED = EXAMPLE_SET - EXAMPLE_SET_USED if EXAMPLE_SET_UNUSED: - BPY_LOGGER.debug("\nUnused examples found in '%s'..." % EXAMPLES_DIR) + BPY_LOGGER.debug("\nUnused examples found in '%s'...", EXAMPLES_DIR) for f in sorted(EXAMPLE_SET_UNUSED): - BPY_LOGGER.debug(" %s.py" % f) - BPY_LOGGER.debug(" %d total\n" % len(EXAMPLE_SET_UNUSED)) + BPY_LOGGER.debug(" %s.py", f) + BPY_LOGGER.debug(" %d total\n", len(EXAMPLE_SET_UNUSED)) - # eventually, build the html docs + # Eventually, build the html docs. if ARGS.sphinx_build: import subprocess subprocess.call(SPHINX_BUILD) - # sphinx-build log cleanup+sort + # Sphinx-build log cleanup+sort. if ARGS.log: if os.stat(SPHINX_BUILD_LOG).st_size: refactor_sphinx_log(SPHINX_BUILD_LOG) - # eventually, build the pdf docs + # Eventually, build the PDF docs. if ARGS.sphinx_build_pdf: import subprocess subprocess.call(SPHINX_BUILD_PDF) subprocess.call(SPHINX_MAKE_PDF, stdout=SPHINX_MAKE_PDF_STDOUT) - # sphinx-build log cleanup+sort + # Sphinx-build log cleanup+sort. if ARGS.log: if os.stat(SPHINX_BUILD_PDF_LOG).st_size: refactor_sphinx_log(SPHINX_BUILD_PDF_LOG) - # eventually, prepare the dir to be deployed online (REFERENCE_PATH) + # Eventually, prepare the dir to be deployed online (REFERENCE_PATH). if ARGS.pack_reference: if ARGS.sphinx_build: - # delete REFERENCE_PATH + # Delete REFERENCE_PATH. if os.path.exists(REFERENCE_PATH): shutil.rmtree(REFERENCE_PATH, True) - # copy SPHINX_OUT to the REFERENCE_PATH - ignores = ('.doctrees', '.buildinfo') - shutil.copytree(SPHINX_OUT, - REFERENCE_PATH, - ignore=shutil.ignore_patterns(*ignores)) + # Copy SPHINX_OUT to the REFERENCE_PATH. + ignores = (".doctrees", ".buildinfo") + shutil.copytree( + SPHINX_OUT, + REFERENCE_PATH, + ignore=shutil.ignore_patterns(*ignores), + ) - # zip REFERENCE_PATH + # Zip REFERENCE_PATH. basename = os.path.join(ARGS.output_dir, REFERENCE_NAME) - tmp_path = shutil.make_archive(basename, 'zip', - root_dir=ARGS.output_dir, - base_dir=REFERENCE_NAME) + tmp_path = shutil.make_archive( + basename, "zip", + root_dir=ARGS.output_dir, + base_dir=REFERENCE_NAME, + ) final_path = os.path.join(REFERENCE_PATH, BLENDER_ZIP_FILENAME) os.rename(tmp_path, final_path) if ARGS.sphinx_build_pdf: - # copy the pdf to REFERENCE_PATH - shutil.copy(os.path.join(SPHINX_OUT_PDF, "contents.pdf"), - os.path.join(REFERENCE_PATH, BLENDER_PDF_FILENAME)) + # Copy the pdf to REFERENCE_PATH. + shutil.copy( + os.path.join(SPHINX_OUT_PDF, "contents.pdf"), + os.path.join(REFERENCE_PATH, BLENDER_PDF_FILENAME), + ) teardown_blender(setup_data) diff --git a/doc/python_api/sphinx_doc_gen_monkeypatch.py b/doc/python_api/sphinx_doc_gen_monkeypatch.py index 09911e9b8a1..e3fba024446 100644 --- a/doc/python_api/sphinx_doc_gen_monkeypatch.py +++ b/doc/python_api/sphinx_doc_gen_monkeypatch.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: GPL-2.0-or-later -# <pep8 compliant> - bpy_types_Operator_bl_property__doc__ = ( """ The name of a property to use as this operators primary property. diff --git a/doc/python_api/static/css/theme_overrides.css b/doc/python_api/static/css/theme_overrides.css index 0fea27a8ebd..5ab449044db 100644 --- a/doc/python_api/static/css/theme_overrides.css +++ b/doc/python_api/static/css/theme_overrides.css @@ -1,10 +1,3 @@ -/* T76453: Prevent Long enum lists */ -.field-list > dd p { - max-height: 245px; - overflow-y: auto !important; - word-break: break-word; -} - /* Hide home icon in search area */ .wy-side-nav-search > a:hover {background: none; opacity: 0.9} .wy-side-nav-search > a.icon::before {content: none} |