mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
[format] Add ruff formating for apidoc/autosummary (#12447)
This commit is contained in:
parent
13b16c9b04
commit
ee92847a0a
27
.ruff.toml
27
.ruff.toml
@ -448,7 +448,32 @@ exclude = [
|
||||
"sphinx/directives/*",
|
||||
"sphinx/domains/*",
|
||||
"sphinx/environment/*",
|
||||
"sphinx/ext/*",
|
||||
"sphinx/ext/autodoc/__init__.py",
|
||||
"sphinx/ext/autodoc/directive.py",
|
||||
"sphinx/ext/autodoc/importer.py",
|
||||
"sphinx/ext/autodoc/mock.py",
|
||||
"sphinx/ext/autodoc/preserve_defaults.py",
|
||||
"sphinx/ext/autodoc/type_comment.py",
|
||||
"sphinx/ext/autodoc/typehints.py",
|
||||
"sphinx/ext/autosectionlabel.py",
|
||||
"sphinx/ext/autosummary/__init__.py",
|
||||
"sphinx/ext/coverage.py",
|
||||
"sphinx/ext/doctest.py",
|
||||
"sphinx/ext/duration.py",
|
||||
"sphinx/ext/extlinks.py",
|
||||
"sphinx/ext/githubpages.py",
|
||||
"sphinx/ext/graphviz.py",
|
||||
"sphinx/ext/ifconfig.py",
|
||||
"sphinx/ext/imgconverter.py",
|
||||
"sphinx/ext/imgmath.py",
|
||||
"sphinx/ext/inheritance_diagram.py",
|
||||
"sphinx/ext/intersphinx/*",
|
||||
"sphinx/ext/linkcode.py",
|
||||
"sphinx/ext/mathjax.py",
|
||||
"sphinx/ext/napoleon/__init__.py",
|
||||
"sphinx/ext/napoleon/docstring.py",
|
||||
"sphinx/ext/todo.py",
|
||||
"sphinx/ext/viewcode.py",
|
||||
"sphinx/pycode/*",
|
||||
"sphinx/pygments_styles.py",
|
||||
"sphinx/registry.py",
|
||||
|
@ -95,8 +95,9 @@ def write_file(name: str, text: str, opts: Any) -> None:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def create_module_file(package: str | None, basename: str, opts: Any,
|
||||
user_template_dir: str | None = None) -> None:
|
||||
def create_module_file(
|
||||
package: str | None, basename: str, opts: Any, user_template_dir: str | None = None
|
||||
) -> None:
|
||||
"""Build the text of the file and write the file."""
|
||||
options = copy(OPTIONS)
|
||||
if opts.includeprivate and 'private-members' not in options:
|
||||
@ -117,24 +118,32 @@ def create_module_file(package: str | None, basename: str, opts: Any,
|
||||
write_file(qualname, text, opts)
|
||||
|
||||
|
||||
def create_package_file(root: str, master_package: str | None, subroot: str,
|
||||
def create_package_file(
|
||||
root: str,
|
||||
master_package: str | None,
|
||||
subroot: str,
|
||||
py_files: list[str],
|
||||
opts: Any, subs: list[str], is_namespace: bool,
|
||||
opts: Any,
|
||||
subs: list[str],
|
||||
is_namespace: bool,
|
||||
excludes: Sequence[re.Pattern[str]] = (),
|
||||
user_template_dir: str | None = None,
|
||||
) -> None:
|
||||
) -> None:
|
||||
"""Build the text of the file and write the file."""
|
||||
# build a list of sub packages (directories containing an __init__ file)
|
||||
subpackages = [module_join(master_package, subroot, pkgname)
|
||||
subpackages = [
|
||||
module_join(master_package, subroot, pkgname)
|
||||
for pkgname in subs
|
||||
if not is_skipped_package(path.join(root, pkgname), opts, excludes)]
|
||||
if not is_skipped_package(path.join(root, pkgname), opts, excludes)
|
||||
]
|
||||
# build a list of sub modules
|
||||
submodules = [sub.split('.')[0] for sub in py_files
|
||||
if not is_skipped_module(path.join(root, sub), opts, excludes) and
|
||||
not is_initpy(sub)]
|
||||
submodules = [
|
||||
sub.split('.')[0]
|
||||
for sub in py_files
|
||||
if not is_skipped_module(path.join(root, sub), opts, excludes) and not is_initpy(sub)
|
||||
]
|
||||
submodules = sorted(set(submodules))
|
||||
submodules = [module_join(master_package, subroot, modname)
|
||||
for modname in submodules]
|
||||
submodules = [module_join(master_package, subroot, modname) for modname in submodules]
|
||||
options = copy(OPTIONS)
|
||||
if opts.includeprivate and 'private-members' not in options:
|
||||
options.append('private-members')
|
||||
@ -163,8 +172,9 @@ def create_package_file(root: str, master_package: str | None, subroot: str,
|
||||
create_module_file(None, submodule, opts, user_template_dir)
|
||||
|
||||
|
||||
def create_modules_toc_file(modules: list[str], opts: Any, name: str = 'modules',
|
||||
user_template_dir: str | None = None) -> None:
|
||||
def create_modules_toc_file(
|
||||
modules: list[str], opts: Any, name: str = 'modules', user_template_dir: str | None = None
|
||||
) -> None:
|
||||
"""Create the module's index."""
|
||||
modules.sort()
|
||||
prev_module = ''
|
||||
@ -188,8 +198,9 @@ def create_modules_toc_file(modules: list[str], opts: Any, name: str = 'modules'
|
||||
write_file(name, text, opts)
|
||||
|
||||
|
||||
def is_skipped_package(dirname: str, opts: Any,
|
||||
excludes: Sequence[re.Pattern[str]] = ()) -> bool:
|
||||
def is_skipped_package(
|
||||
dirname: str, opts: Any, excludes: Sequence[re.Pattern[str]] = ()
|
||||
) -> bool:
|
||||
"""Check if we want to skip this module."""
|
||||
if not path.isdir(dirname):
|
||||
return False
|
||||
@ -213,17 +224,22 @@ def is_skipped_module(filename: str, opts: Any, _excludes: Sequence[re.Pattern[s
|
||||
return path.basename(filename).startswith('_') and not opts.includeprivate
|
||||
|
||||
|
||||
def walk(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
|
||||
) -> Iterator[tuple[str, list[str], list[str]]]:
|
||||
def walk(
|
||||
rootpath: str,
|
||||
excludes: Sequence[re.Pattern[str]],
|
||||
opts: Any,
|
||||
) -> Iterator[tuple[str, list[str], list[str]]]:
|
||||
"""Walk through the directory and list files and subdirectories up."""
|
||||
followlinks = getattr(opts, 'followlinks', False)
|
||||
includeprivate = getattr(opts, 'includeprivate', False)
|
||||
|
||||
for root, subs, files in os.walk(rootpath, followlinks=followlinks):
|
||||
# document only Python module files (that aren't excluded)
|
||||
files = sorted(f for f in files
|
||||
if f.endswith(PY_SUFFIXES) and
|
||||
not is_excluded(path.join(root, f), excludes))
|
||||
files = sorted(
|
||||
f
|
||||
for f in files
|
||||
if f.endswith(PY_SUFFIXES) and not is_excluded(path.join(root, f), excludes)
|
||||
)
|
||||
|
||||
# remove hidden ('.') and private ('_') directories, as well as
|
||||
# excluded dirs
|
||||
@ -232,22 +248,27 @@ def walk(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
|
||||
else:
|
||||
exclude_prefixes = ('.', '_')
|
||||
|
||||
subs[:] = sorted(sub for sub in subs if not sub.startswith(exclude_prefixes) and
|
||||
not is_excluded(path.join(root, sub), excludes))
|
||||
subs[:] = sorted(
|
||||
sub
|
||||
for sub in subs
|
||||
if not sub.startswith(exclude_prefixes)
|
||||
and not is_excluded(path.join(root, sub), excludes)
|
||||
)
|
||||
|
||||
yield root, subs, files
|
||||
|
||||
|
||||
def has_child_module(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any) -> bool:
|
||||
"""Check the given directory contains child module/s (at least one)."""
|
||||
return any(
|
||||
files
|
||||
for _root, _subs, files in walk(rootpath, excludes, opts)
|
||||
)
|
||||
return any(files for _root, _subs, files in walk(rootpath, excludes, opts))
|
||||
|
||||
|
||||
def recurse_tree(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
|
||||
user_template_dir: str | None = None) -> list[str]:
|
||||
def recurse_tree(
|
||||
rootpath: str,
|
||||
excludes: Sequence[re.Pattern[str]],
|
||||
opts: Any,
|
||||
user_template_dir: str | None = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Look for every file in the directory tree and create the corresponding
|
||||
ReST files.
|
||||
@ -279,14 +300,21 @@ def recurse_tree(rootpath: str, excludes: Sequence[re.Pattern[str]], opts: Any,
|
||||
if is_pkg or is_namespace:
|
||||
# we are in a package with something to document
|
||||
if subs or len(files) > 1 or not is_skipped_package(root, opts):
|
||||
subpackage = root[len(rootpath):].lstrip(path.sep).\
|
||||
replace(path.sep, '.')
|
||||
subpackage = root[len(rootpath) :].lstrip(path.sep).replace(path.sep, '.')
|
||||
# if this is not a namespace or
|
||||
# a namespace and there is something there to document
|
||||
if not is_namespace or has_child_module(root, excludes, opts):
|
||||
create_package_file(root, root_package, subpackage,
|
||||
files, opts, subs, is_namespace, excludes,
|
||||
user_template_dir)
|
||||
create_package_file(
|
||||
root,
|
||||
root_package,
|
||||
subpackage,
|
||||
files,
|
||||
opts,
|
||||
subs,
|
||||
is_namespace,
|
||||
excludes,
|
||||
user_template_dir,
|
||||
)
|
||||
toplevels.append(module_join(root_package, subpackage))
|
||||
else:
|
||||
# if we are at the root level, we don't require it to be a package
|
||||
@ -312,8 +340,7 @@ def is_excluded(root: str, excludes: Sequence[re.Pattern[str]]) -> bool:
|
||||
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> '
|
||||
'[EXCLUDE_PATTERN, ...]',
|
||||
usage='%(prog)s [OPTIONS] -o <OUTPUT_PATH> <MODULE_PATH> ' '[EXCLUDE_PATTERN, ...]',
|
||||
epilog=__('For more information, visit <https://www.sphinx-doc.org/>.'),
|
||||
description=__("""
|
||||
Look recursively in <MODULE_PATH> for Python modules and packages and create
|
||||
@ -322,87 +349,196 @@ one reST file with automodule directives per package in the <OUTPUT_PATH>.
|
||||
The <EXCLUDE_PATTERN>s can be file and/or directory patterns that will be
|
||||
excluded from generation.
|
||||
|
||||
Note: By default this script will not overwrite already created files."""))
|
||||
Note: By default this script will not overwrite already created files."""),
|
||||
)
|
||||
|
||||
parser.add_argument('--version', action='version', dest='show_version',
|
||||
version='%%(prog)s %s' % __display_version__)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
dest='show_version',
|
||||
version='%%(prog)s %s' % __display_version__,
|
||||
)
|
||||
|
||||
parser.add_argument('module_path',
|
||||
help=__('path to module to document'))
|
||||
parser.add_argument('exclude_pattern', nargs='*',
|
||||
help=__('fnmatch-style file and/or directory patterns '
|
||||
'to exclude from generation'))
|
||||
parser.add_argument('module_path', help=__('path to module to document'))
|
||||
parser.add_argument(
|
||||
'exclude_pattern',
|
||||
nargs='*',
|
||||
help=__('fnmatch-style file and/or directory patterns ' 'to exclude from generation'),
|
||||
)
|
||||
|
||||
parser.add_argument('-o', '--output-dir', action='store', dest='destdir',
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--output-dir',
|
||||
action='store',
|
||||
dest='destdir',
|
||||
required=True,
|
||||
help=__('directory to place all output'))
|
||||
parser.add_argument('-q', action='store_true', dest='quiet',
|
||||
help=__('no output on stdout, just warnings on stderr'))
|
||||
parser.add_argument('-d', '--maxdepth', action='store', dest='maxdepth',
|
||||
type=int, default=4,
|
||||
help=__('maximum depth of submodules to show in the TOC '
|
||||
'(default: 4)'))
|
||||
parser.add_argument('-f', '--force', action='store_true', dest='force',
|
||||
help=__('overwrite existing files'))
|
||||
parser.add_argument('-l', '--follow-links', action='store_true',
|
||||
dest='followlinks', default=False,
|
||||
help=__('follow symbolic links. Powerful when combined '
|
||||
'with collective.recipe.omelette.'))
|
||||
parser.add_argument('-n', '--dry-run', action='store_true', dest='dryrun',
|
||||
help=__('run the script without creating files'))
|
||||
parser.add_argument('-e', '--separate', action='store_true',
|
||||
help=__('directory to place all output'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
action='store_true',
|
||||
dest='quiet',
|
||||
help=__('no output on stdout, just warnings on stderr'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d',
|
||||
'--maxdepth',
|
||||
action='store',
|
||||
dest='maxdepth',
|
||||
type=int,
|
||||
default=4,
|
||||
help=__('maximum depth of submodules to show in the TOC ' '(default: 4)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--force', action='store_true', dest='force', help=__('overwrite existing files')
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--follow-links',
|
||||
action='store_true',
|
||||
dest='followlinks',
|
||||
default=False,
|
||||
help=__(
|
||||
'follow symbolic links. Powerful when combined ' 'with collective.recipe.omelette.'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
dest='dryrun',
|
||||
help=__('run the script without creating files'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--separate',
|
||||
action='store_true',
|
||||
dest='separatemodules',
|
||||
help=__('put documentation for each module on its own page'))
|
||||
parser.add_argument('-P', '--private', action='store_true',
|
||||
help=__('put documentation for each module on its own page'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-P',
|
||||
'--private',
|
||||
action='store_true',
|
||||
dest='includeprivate',
|
||||
help=__('include "_private" modules'))
|
||||
parser.add_argument('--tocfile', action='store', dest='tocfile', default='modules',
|
||||
help=__("filename of table of contents (default: modules)"))
|
||||
parser.add_argument('-T', '--no-toc', action='store_false', dest='tocfile',
|
||||
help=__("don't create a table of contents file"))
|
||||
parser.add_argument('-E', '--no-headings', action='store_true',
|
||||
help=__('include "_private" modules'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--tocfile',
|
||||
action='store',
|
||||
dest='tocfile',
|
||||
default='modules',
|
||||
help=__('filename of table of contents (default: modules)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-T',
|
||||
'--no-toc',
|
||||
action='store_false',
|
||||
dest='tocfile',
|
||||
help=__("don't create a table of contents file"),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-E',
|
||||
'--no-headings',
|
||||
action='store_true',
|
||||
dest='noheadings',
|
||||
help=__("don't create headings for the module/package "
|
||||
"packages (e.g. when the docstrings already "
|
||||
"contain them)"))
|
||||
parser.add_argument('-M', '--module-first', action='store_true',
|
||||
help=__(
|
||||
"don't create headings for the module/package "
|
||||
'packages (e.g. when the docstrings already '
|
||||
'contain them)'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-M',
|
||||
'--module-first',
|
||||
action='store_true',
|
||||
dest='modulefirst',
|
||||
help=__('put module documentation before submodule '
|
||||
'documentation'))
|
||||
parser.add_argument('--implicit-namespaces', action='store_true',
|
||||
help=__('put module documentation before submodule ' 'documentation'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--implicit-namespaces',
|
||||
action='store_true',
|
||||
dest='implicit_namespaces',
|
||||
help=__('interpret module paths according to PEP-0420 '
|
||||
'implicit namespaces specification'))
|
||||
parser.add_argument('-s', '--suffix', action='store', dest='suffix',
|
||||
help=__(
|
||||
'interpret module paths according to PEP-0420 ' 'implicit namespaces specification'
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s',
|
||||
'--suffix',
|
||||
action='store',
|
||||
dest='suffix',
|
||||
default='rst',
|
||||
help=__('file suffix (default: rst)'))
|
||||
parser.add_argument('-F', '--full', action='store_true', dest='full',
|
||||
help=__('generate a full project with sphinx-quickstart'))
|
||||
parser.add_argument('-a', '--append-syspath', action='store_true',
|
||||
help=__('file suffix (default: rst)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-F',
|
||||
'--full',
|
||||
action='store_true',
|
||||
dest='full',
|
||||
help=__('generate a full project with sphinx-quickstart'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a',
|
||||
'--append-syspath',
|
||||
action='store_true',
|
||||
dest='append_syspath',
|
||||
help=__('append module_path to sys.path, used when --full is given'))
|
||||
parser.add_argument('-H', '--doc-project', action='store', dest='header',
|
||||
help=__('project name (default: root module name)'))
|
||||
parser.add_argument('-A', '--doc-author', action='store', dest='author',
|
||||
help=__('project author(s), used when --full is given'))
|
||||
parser.add_argument('-V', '--doc-version', action='store', dest='version',
|
||||
help=__('project version, used when --full is given'))
|
||||
parser.add_argument('-R', '--doc-release', action='store', dest='release',
|
||||
help=__('project release, used when --full is given, '
|
||||
'defaults to --doc-version'))
|
||||
help=__('append module_path to sys.path, used when --full is given'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-H',
|
||||
'--doc-project',
|
||||
action='store',
|
||||
dest='header',
|
||||
help=__('project name (default: root module name)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-A',
|
||||
'--doc-author',
|
||||
action='store',
|
||||
dest='author',
|
||||
help=__('project author(s), used when --full is given'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-V',
|
||||
'--doc-version',
|
||||
action='store',
|
||||
dest='version',
|
||||
help=__('project version, used when --full is given'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-R',
|
||||
'--doc-release',
|
||||
action='store',
|
||||
dest='release',
|
||||
help=__('project release, used when --full is given, ' 'defaults to --doc-version'),
|
||||
)
|
||||
|
||||
group = parser.add_argument_group(__('extension options'))
|
||||
group.add_argument('--extensions', metavar='EXTENSIONS', dest='extensions',
|
||||
action='append', help=__('enable arbitrary extensions'))
|
||||
group.add_argument(
|
||||
'--extensions',
|
||||
metavar='EXTENSIONS',
|
||||
dest='extensions',
|
||||
action='append',
|
||||
help=__('enable arbitrary extensions'),
|
||||
)
|
||||
for ext in EXTENSIONS:
|
||||
group.add_argument('--ext-%s' % ext, action='append_const',
|
||||
const='sphinx.ext.%s' % ext, dest='extensions',
|
||||
help=__('enable %s extension') % ext)
|
||||
group.add_argument(
|
||||
'--ext-%s' % ext,
|
||||
action='append_const',
|
||||
const='sphinx.ext.%s' % ext,
|
||||
dest='extensions',
|
||||
help=__('enable %s extension') % ext,
|
||||
)
|
||||
|
||||
group = parser.add_argument_group(__('Project templating'))
|
||||
group.add_argument('-t', '--templatedir', metavar='TEMPLATEDIR',
|
||||
group.add_argument(
|
||||
'-t',
|
||||
'--templatedir',
|
||||
metavar='TEMPLATEDIR',
|
||||
dest='templatedir',
|
||||
help=__('template directory for template files'))
|
||||
help=__('template directory for template files'),
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@ -436,6 +572,7 @@ def main(argv: Sequence[str] = (), /) -> int:
|
||||
|
||||
if args.full:
|
||||
from sphinx.cmd import quickstart as qs
|
||||
|
||||
modules.sort()
|
||||
prev_module = ''
|
||||
text = ''
|
||||
@ -455,8 +592,7 @@ def main(argv: Sequence[str] = (), /) -> int:
|
||||
'suffix': '.' + args.suffix,
|
||||
'master': 'index',
|
||||
'epub': True,
|
||||
'extensions': ['sphinx.ext.autodoc', 'sphinx.ext.viewcode',
|
||||
'sphinx.ext.todo'],
|
||||
'extensions': ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.todo'],
|
||||
'makefile': True,
|
||||
'batchfile': True,
|
||||
'make_mode': True,
|
||||
@ -477,8 +613,7 @@ def main(argv: Sequence[str] = (), /) -> int:
|
||||
d['extensions'].extend(ext.split(','))
|
||||
|
||||
if not args.dryrun:
|
||||
qs.generate(d, silent=True, overwrite=args.force,
|
||||
templatedir=args.templatedir)
|
||||
qs.generate(d, silent=True, overwrite=args.force, templatedir=args.templatedir)
|
||||
elif args.tocfile:
|
||||
create_modules_toc_file(modules, args, args.tocfile, args.templatedir)
|
||||
|
||||
@ -486,5 +621,5 @@ def main(argv: Sequence[str] = (), /) -> int:
|
||||
|
||||
|
||||
# So program can be started with "python -m sphinx.apidoc ..."
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
@ -65,7 +65,7 @@ class DummyApplication:
|
||||
self.config = Config()
|
||||
self.registry = SphinxComponentRegistry()
|
||||
self.messagelog: list[str] = []
|
||||
self.srcdir = "/"
|
||||
self.srcdir = '/'
|
||||
self.translator = translator
|
||||
self.verbosity = 0
|
||||
self._warncount = 0
|
||||
@ -98,10 +98,17 @@ def setup_documenters(app: Any) -> None:
|
||||
ModuleDocumenter,
|
||||
PropertyDocumenter,
|
||||
)
|
||||
|
||||
documenters: list[type[Documenter]] = [
|
||||
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
|
||||
FunctionDocumenter, MethodDocumenter,
|
||||
AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
|
||||
ModuleDocumenter,
|
||||
ClassDocumenter,
|
||||
ExceptionDocumenter,
|
||||
DataDocumenter,
|
||||
FunctionDocumenter,
|
||||
MethodDocumenter,
|
||||
AttributeDocumenter,
|
||||
DecoratorDocumenter,
|
||||
PropertyDocumenter,
|
||||
]
|
||||
for documenter in documenters:
|
||||
app.registry.add_documenter(documenter.objtype, documenter)
|
||||
@ -123,8 +130,9 @@ class AutosummaryRenderer:
|
||||
raise ValueError(msg)
|
||||
|
||||
system_templates_path = [os.path.join(package_dir, 'ext', 'autosummary', 'templates')]
|
||||
loader = SphinxTemplateLoader(app.srcdir, app.config.templates_path,
|
||||
system_templates_path)
|
||||
loader = SphinxTemplateLoader(
|
||||
app.srcdir, app.config.templates_path, system_templates_path
|
||||
)
|
||||
|
||||
self.env = SandboxedEnvironment(loader=loader)
|
||||
self.env.filters['escape'] = rst.escape
|
||||
@ -132,7 +140,7 @@ class AutosummaryRenderer:
|
||||
self.env.filters['underline'] = _underline
|
||||
|
||||
if app.translator:
|
||||
self.env.add_extension("jinja2.ext.i18n")
|
||||
self.env.add_extension('jinja2.ext.i18n')
|
||||
# ``install_gettext_translations`` is injected by the ``jinja2.ext.i18n`` extension
|
||||
self.env.install_gettext_translations(app.translator) # type: ignore[attr-defined]
|
||||
|
||||
@ -168,17 +176,17 @@ def _split_full_qualified_name(name: str) -> tuple[str | None, str]:
|
||||
parts = name.split('.')
|
||||
for i, _part in enumerate(parts, 1):
|
||||
try:
|
||||
modname = ".".join(parts[:i])
|
||||
modname = '.'.join(parts[:i])
|
||||
importlib.import_module(modname)
|
||||
except ImportError:
|
||||
if parts[:i - 1]:
|
||||
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
|
||||
if parts[: i - 1]:
|
||||
return '.'.join(parts[: i - 1]), '.'.join(parts[i - 1 :])
|
||||
else:
|
||||
return None, ".".join(parts)
|
||||
return None, '.'.join(parts)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return name, ""
|
||||
return name, ''
|
||||
|
||||
|
||||
# -- Generating output ---------------------------------------------------------
|
||||
@ -194,12 +202,19 @@ class ModuleScanner:
|
||||
|
||||
def is_skipped(self, name: str, value: Any, objtype: str) -> bool:
|
||||
try:
|
||||
return self.app.emit_firstresult('autodoc-skip-member', objtype,
|
||||
name, value, False, {})
|
||||
return self.app.emit_firstresult(
|
||||
'autodoc-skip-member', objtype, name, value, False, {}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(__('autosummary: failed to determine %r to be documented, '
|
||||
'the following exception was raised:\n%s'),
|
||||
name, exc, type='autosummary')
|
||||
logger.warning(
|
||||
__(
|
||||
'autosummary: failed to determine %r to be documented, '
|
||||
'the following exception was raised:\n%s'
|
||||
),
|
||||
name,
|
||||
exc,
|
||||
type='autosummary',
|
||||
)
|
||||
return False
|
||||
|
||||
def scan(self, imported_members: bool) -> list[str]:
|
||||
@ -257,12 +272,19 @@ def members_of(obj: Any, conf: Config) -> Sequence[str]:
|
||||
return getall(obj) or dir(obj)
|
||||
|
||||
|
||||
def generate_autosummary_content(name: str, obj: Any, parent: Any,
|
||||
template: AutosummaryRenderer, template_name: str,
|
||||
imported_members: bool, app: Any,
|
||||
recursive: bool, context: dict,
|
||||
def generate_autosummary_content(
|
||||
name: str,
|
||||
obj: Any,
|
||||
parent: Any,
|
||||
template: AutosummaryRenderer,
|
||||
template_name: str,
|
||||
imported_members: bool,
|
||||
app: Any,
|
||||
recursive: bool,
|
||||
context: dict,
|
||||
modname: str | None = None,
|
||||
qualname: str | None = None) -> str:
|
||||
qualname: str | None = None,
|
||||
) -> str:
|
||||
doc = get_documenter(app, obj, parent)
|
||||
|
||||
ns: dict[str, Any] = {}
|
||||
@ -275,23 +297,25 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
|
||||
respect_module_all = not app.config.autosummary_ignore_module_all
|
||||
imported_members = imported_members or ('__all__' in dir(obj) and respect_module_all)
|
||||
|
||||
ns['functions'], ns['all_functions'] = \
|
||||
_get_members(doc, app, obj, {'function'}, imported=imported_members)
|
||||
ns['classes'], ns['all_classes'] = \
|
||||
_get_members(doc, app, obj, {'class'}, imported=imported_members)
|
||||
ns['exceptions'], ns['all_exceptions'] = \
|
||||
_get_members(doc, app, obj, {'exception'}, imported=imported_members)
|
||||
ns['attributes'], ns['all_attributes'] = \
|
||||
_get_module_attrs(name, ns['members'])
|
||||
ns['functions'], ns['all_functions'] = _get_members(
|
||||
doc, app, obj, {'function'}, imported=imported_members
|
||||
)
|
||||
ns['classes'], ns['all_classes'] = _get_members(
|
||||
doc, app, obj, {'class'}, imported=imported_members
|
||||
)
|
||||
ns['exceptions'], ns['all_exceptions'] = _get_members(
|
||||
doc, app, obj, {'exception'}, imported=imported_members
|
||||
)
|
||||
ns['attributes'], ns['all_attributes'] = _get_module_attrs(name, ns['members'])
|
||||
ispackage = hasattr(obj, '__path__')
|
||||
if ispackage and recursive:
|
||||
# Use members that are not modules as skip list, because it would then mean
|
||||
# that module was overwritten in the package namespace
|
||||
skip = (
|
||||
ns["all_functions"]
|
||||
+ ns["all_classes"]
|
||||
+ ns["all_exceptions"]
|
||||
+ ns["all_attributes"]
|
||||
ns['all_functions']
|
||||
+ ns['all_classes']
|
||||
+ ns['all_exceptions']
|
||||
+ ns['all_attributes']
|
||||
)
|
||||
|
||||
# If respect_module_all and module has a __all__ attribute, first get
|
||||
@ -301,40 +325,44 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
|
||||
#
|
||||
# Otherwise, use get_modules method normally
|
||||
if respect_module_all and '__all__' in dir(obj):
|
||||
imported_modules, all_imported_modules = \
|
||||
_get_members(doc, app, obj, {'module'}, imported=True)
|
||||
imported_modules, all_imported_modules = _get_members(
|
||||
doc, app, obj, {'module'}, imported=True
|
||||
)
|
||||
skip += all_imported_modules
|
||||
imported_modules = [name + '.' + modname for modname in imported_modules]
|
||||
all_imported_modules = \
|
||||
[name + '.' + modname for modname in all_imported_modules]
|
||||
all_imported_modules = [
|
||||
name + '.' + modname for modname in all_imported_modules
|
||||
]
|
||||
public_members = getall(obj)
|
||||
else:
|
||||
imported_modules, all_imported_modules = [], []
|
||||
public_members = None
|
||||
|
||||
modules, all_modules = _get_modules(obj, skip=skip, name=name,
|
||||
public_members=public_members)
|
||||
modules, all_modules = _get_modules(
|
||||
obj, skip=skip, name=name, public_members=public_members
|
||||
)
|
||||
ns['modules'] = imported_modules + modules
|
||||
ns["all_modules"] = all_imported_modules + all_modules
|
||||
ns['all_modules'] = all_imported_modules + all_modules
|
||||
elif doc.objtype == 'class':
|
||||
ns['members'] = dir(obj)
|
||||
ns['inherited_members'] = \
|
||||
set(dir(obj)) - set(obj.__dict__.keys())
|
||||
ns['methods'], ns['all_methods'] = \
|
||||
_get_members(doc, app, obj, {'method'}, include_public={'__init__'})
|
||||
ns['attributes'], ns['all_attributes'] = \
|
||||
_get_members(doc, app, obj, {'attribute', 'property'})
|
||||
ns['inherited_members'] = set(dir(obj)) - set(obj.__dict__.keys())
|
||||
ns['methods'], ns['all_methods'] = _get_members(
|
||||
doc, app, obj, {'method'}, include_public={'__init__'}
|
||||
)
|
||||
ns['attributes'], ns['all_attributes'] = _get_members(
|
||||
doc, app, obj, {'attribute', 'property'}
|
||||
)
|
||||
|
||||
if modname is None or qualname is None:
|
||||
modname, qualname = _split_full_qualified_name(name)
|
||||
|
||||
if doc.objtype in ('method', 'attribute', 'property'):
|
||||
ns['class'] = qualname.rsplit(".", 1)[0]
|
||||
ns['class'] = qualname.rsplit('.', 1)[0]
|
||||
|
||||
if doc.objtype == 'class':
|
||||
shortname = qualname
|
||||
else:
|
||||
shortname = qualname.rsplit(".", 1)[-1]
|
||||
shortname = qualname.rsplit('.', 1)[-1]
|
||||
|
||||
ns['fullname'] = name
|
||||
ns['module'] = modname
|
||||
@ -352,12 +380,17 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
|
||||
|
||||
def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool:
|
||||
try:
|
||||
return app.emit_firstresult('autodoc-skip-member', objtype, name,
|
||||
obj, False, {})
|
||||
return app.emit_firstresult('autodoc-skip-member', objtype, name, obj, False, {})
|
||||
except Exception as exc:
|
||||
logger.warning(__('autosummary: failed to determine %r to be documented, '
|
||||
'the following exception was raised:\n%s'),
|
||||
name, exc, type='autosummary')
|
||||
logger.warning(
|
||||
__(
|
||||
'autosummary: failed to determine %r to be documented, '
|
||||
'the following exception was raised:\n%s'
|
||||
),
|
||||
name,
|
||||
exc,
|
||||
type='autosummary',
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@ -384,9 +417,15 @@ def _get_all_members(doc: type[Documenter], app: Sphinx, obj: Any) -> dict[str,
|
||||
return {}
|
||||
|
||||
|
||||
def _get_members(doc: type[Documenter], app: Sphinx, obj: Any, types: set[str], *,
|
||||
def _get_members(
|
||||
doc: type[Documenter],
|
||||
app: Sphinx,
|
||||
obj: Any,
|
||||
types: set[str],
|
||||
*,
|
||||
include_public: Set[str] = frozenset(),
|
||||
imported: bool = True) -> tuple[list[str], list[str]]:
|
||||
imported: bool = True,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
items: list[str] = []
|
||||
public: list[str] = []
|
||||
|
||||
@ -428,15 +467,11 @@ def _get_module_attrs(name: str, members: Any) -> tuple[list[str], list[str]]:
|
||||
|
||||
|
||||
def _get_modules(
|
||||
obj: Any,
|
||||
*,
|
||||
skip: Sequence[str],
|
||||
name: str,
|
||||
public_members: Sequence[str] | None = None) -> tuple[list[str], list[str]]:
|
||||
obj: Any, *, skip: Sequence[str], name: str, public_members: Sequence[str] | None = None
|
||||
) -> tuple[list[str], list[str]]:
|
||||
items: list[str] = []
|
||||
public: list[str] = []
|
||||
for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__):
|
||||
|
||||
if modname in skip:
|
||||
# module was overwritten in __init__.py, so not accessible
|
||||
continue
|
||||
@ -458,17 +493,20 @@ def _get_modules(
|
||||
return public, items
|
||||
|
||||
|
||||
def generate_autosummary_docs(sources: list[str],
|
||||
def generate_autosummary_docs(
|
||||
sources: list[str],
|
||||
output_dir: str | os.PathLike[str] | None = None,
|
||||
suffix: str = '.rst',
|
||||
base_path: str | os.PathLike[str] | None = None,
|
||||
imported_members: bool = False, app: Any = None,
|
||||
overwrite: bool = True, encoding: str = 'utf-8') -> None:
|
||||
imported_members: bool = False,
|
||||
app: Any = None,
|
||||
overwrite: bool = True,
|
||||
encoding: str = 'utf-8',
|
||||
) -> None:
|
||||
showed_sources = sorted(sources)
|
||||
if len(showed_sources) > 20:
|
||||
showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
|
||||
logger.info(__('[autosummary] generating autosummary for: %s'),
|
||||
', '.join(showed_sources))
|
||||
logger.info(__('[autosummary] generating autosummary for: %s'), ', '.join(showed_sources))
|
||||
|
||||
if output_dir:
|
||||
logger.info(__('[autosummary] writing to %s'), output_dir)
|
||||
@ -501,30 +539,43 @@ def generate_autosummary_docs(sources: list[str],
|
||||
|
||||
try:
|
||||
name, obj, parent, modname = import_by_name(entry.name)
|
||||
qualname = name.replace(modname + ".", "")
|
||||
qualname = name.replace(modname + '.', '')
|
||||
except ImportExceptionGroup as exc:
|
||||
try:
|
||||
# try to import as an instance attribute
|
||||
name, obj, parent, modname = import_ivar_by_name(entry.name)
|
||||
qualname = name.replace(modname + ".", "")
|
||||
qualname = name.replace(modname + '.', '')
|
||||
except ImportError as exc2:
|
||||
if exc2.__cause__:
|
||||
exceptions: list[BaseException] = [*exc.exceptions, exc2.__cause__]
|
||||
else:
|
||||
exceptions = [*exc.exceptions, exc2]
|
||||
|
||||
errors = list({f"* {type(e).__name__}: {e}" for e in exceptions})
|
||||
logger.warning(__('[autosummary] failed to import %s.\nPossible hints:\n%s'),
|
||||
entry.name, '\n'.join(errors))
|
||||
errors = list({f'* {type(e).__name__}: {e}' for e in exceptions})
|
||||
logger.warning(
|
||||
__('[autosummary] failed to import %s.\nPossible hints:\n%s'),
|
||||
entry.name,
|
||||
'\n'.join(errors),
|
||||
)
|
||||
continue
|
||||
|
||||
context: dict[str, Any] = {}
|
||||
if app:
|
||||
context.update(app.config.autosummary_context)
|
||||
|
||||
content = generate_autosummary_content(name, obj, parent, template, entry.template,
|
||||
imported_members, app, entry.recursive, context,
|
||||
modname, qualname)
|
||||
content = generate_autosummary_content(
|
||||
name,
|
||||
obj,
|
||||
parent,
|
||||
template,
|
||||
entry.template,
|
||||
imported_members,
|
||||
app,
|
||||
entry.recursive,
|
||||
context,
|
||||
modname,
|
||||
qualname,
|
||||
)
|
||||
|
||||
filename = os.path.join(path, filename_map.get(name, name) + suffix)
|
||||
if os.path.isfile(filename):
|
||||
@ -544,14 +595,20 @@ def generate_autosummary_docs(sources: list[str],
|
||||
|
||||
# descend recursively to new files
|
||||
if new_files:
|
||||
generate_autosummary_docs(new_files, output_dir=output_dir,
|
||||
suffix=suffix, base_path=base_path,
|
||||
imported_members=imported_members, app=app,
|
||||
overwrite=overwrite)
|
||||
generate_autosummary_docs(
|
||||
new_files,
|
||||
output_dir=output_dir,
|
||||
suffix=suffix,
|
||||
base_path=base_path,
|
||||
imported_members=imported_members,
|
||||
app=app,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
|
||||
|
||||
# -- Finding documented entries in files ---------------------------------------
|
||||
|
||||
|
||||
def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]:
|
||||
"""Find out what items are documented in source/*.rst.
|
||||
|
||||
@ -566,7 +623,8 @@ def find_autosummary_in_files(filenames: list[str]) -> list[AutosummaryEntry]:
|
||||
|
||||
|
||||
def find_autosummary_in_docstring(
|
||||
name: str, filename: str | None = None,
|
||||
name: str,
|
||||
filename: str | None = None,
|
||||
) -> list[AutosummaryEntry]:
|
||||
"""Find out what items are documented in the given object's docstring.
|
||||
|
||||
@ -579,16 +637,21 @@ def find_autosummary_in_docstring(
|
||||
except AttributeError:
|
||||
pass
|
||||
except ImportExceptionGroup as exc:
|
||||
errors = '\n'.join({f"* {type(e).__name__}: {e}" for e in exc.exceptions})
|
||||
errors = '\n'.join({f'* {type(e).__name__}: {e}' for e in exc.exceptions})
|
||||
logger.warning(f'Failed to import {name}.\nPossible hints:\n{errors}') # NoQA: G004
|
||||
except SystemExit:
|
||||
logger.warning("Failed to import '%s'; the module executes module level "
|
||||
'statement and it might call sys.exit().', name)
|
||||
logger.warning(
|
||||
"Failed to import '%s'; the module executes module level "
|
||||
'statement and it might call sys.exit().',
|
||||
name,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
def find_autosummary_in_lines(
|
||||
lines: list[str], module: str | None = None, filename: str | None = None,
|
||||
lines: list[str],
|
||||
module: str | None = None,
|
||||
filename: str | None = None,
|
||||
) -> list[AutosummaryEntry]:
|
||||
"""Find out what items appear in autosummary:: directives in the
|
||||
given lines.
|
||||
@ -601,10 +664,8 @@ def find_autosummary_in_lines(
|
||||
corresponding options set.
|
||||
"""
|
||||
autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*')
|
||||
automodule_re = re.compile(
|
||||
r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$')
|
||||
module_re = re.compile(
|
||||
r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$')
|
||||
automodule_re = re.compile(r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$')
|
||||
module_re = re.compile(r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$')
|
||||
autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?')
|
||||
recursive_arg_re = re.compile(r'^\s+:recursive:\s*$')
|
||||
toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$')
|
||||
@ -617,7 +678,7 @@ def find_autosummary_in_lines(
|
||||
template = ''
|
||||
current_module = module
|
||||
in_autosummary = False
|
||||
base_indent = ""
|
||||
base_indent = ''
|
||||
|
||||
for line in lines:
|
||||
if in_autosummary:
|
||||
@ -630,8 +691,7 @@ def find_autosummary_in_lines(
|
||||
if m:
|
||||
toctree = m.group(1)
|
||||
if filename:
|
||||
toctree = os.path.join(os.path.dirname(filename),
|
||||
toctree)
|
||||
toctree = os.path.join(os.path.dirname(filename), toctree)
|
||||
continue
|
||||
|
||||
m = template_arg_re.match(line)
|
||||
@ -647,13 +707,12 @@ def find_autosummary_in_lines(
|
||||
name = m.group(1).strip()
|
||||
if name.startswith('~'):
|
||||
name = name[1:]
|
||||
if current_module and \
|
||||
not name.startswith(current_module + '.'):
|
||||
name = f"{current_module}.{name}"
|
||||
if current_module and not name.startswith(current_module + '.'):
|
||||
name = f'{current_module}.{name}'
|
||||
documented.append(AutosummaryEntry(name, toctree, template, recursive))
|
||||
continue
|
||||
|
||||
if not line.strip() or line.startswith(base_indent + " "):
|
||||
if not line.strip() or line.startswith(base_indent + ' '):
|
||||
continue
|
||||
|
||||
in_autosummary = False
|
||||
@ -671,8 +730,7 @@ def find_autosummary_in_lines(
|
||||
if m:
|
||||
current_module = m.group(1).strip()
|
||||
# recurse into the automodule docstring
|
||||
documented.extend(find_autosummary_in_docstring(
|
||||
current_module, filename=filename))
|
||||
documented.extend(find_autosummary_in_docstring(current_module, filename=filename))
|
||||
continue
|
||||
|
||||
m = module_re.match(line)
|
||||
@ -698,33 +756,62 @@ The format of the autosummary directive is documented in the
|
||||
``sphinx.ext.autosummary`` Python module and can be read using::
|
||||
|
||||
pydoc sphinx.ext.autosummary
|
||||
"""))
|
||||
"""),
|
||||
)
|
||||
|
||||
parser.add_argument('--version', action='version', dest='show_version',
|
||||
version='%%(prog)s %s' % __display_version__)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
dest='show_version',
|
||||
version='%%(prog)s %s' % __display_version__,
|
||||
)
|
||||
|
||||
parser.add_argument('source_file', nargs='+',
|
||||
help=__('source files to generate rST files for'))
|
||||
parser.add_argument(
|
||||
'source_file', nargs='+', help=__('source files to generate rST files for')
|
||||
)
|
||||
|
||||
parser.add_argument('-o', '--output-dir', action='store',
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--output-dir',
|
||||
action='store',
|
||||
dest='output_dir',
|
||||
help=__('directory to place all output in'))
|
||||
parser.add_argument('-s', '--suffix', action='store', dest='suffix',
|
||||
help=__('directory to place all output in'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s',
|
||||
'--suffix',
|
||||
action='store',
|
||||
dest='suffix',
|
||||
default='rst',
|
||||
help=__('default suffix for files (default: '
|
||||
'%(default)s)'))
|
||||
parser.add_argument('-t', '--templates', action='store', dest='templates',
|
||||
help=__('default suffix for files (default: ' '%(default)s)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t',
|
||||
'--templates',
|
||||
action='store',
|
||||
dest='templates',
|
||||
default=None,
|
||||
help=__('custom template directory (default: '
|
||||
'%(default)s)'))
|
||||
parser.add_argument('-i', '--imported-members', action='store_true',
|
||||
dest='imported_members', default=False,
|
||||
help=__('document imported members (default: '
|
||||
'%(default)s)'))
|
||||
parser.add_argument('-a', '--respect-module-all', action='store_true',
|
||||
dest='respect_module_all', default=False,
|
||||
help=__('document exactly the members in module __all__ attribute. '
|
||||
'(default: %(default)s)'))
|
||||
help=__('custom template directory (default: ' '%(default)s)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--imported-members',
|
||||
action='store_true',
|
||||
dest='imported_members',
|
||||
default=False,
|
||||
help=__('document imported members (default: ' '%(default)s)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a',
|
||||
'--respect-module-all',
|
||||
action='store_true',
|
||||
dest='respect_module_all',
|
||||
default=False,
|
||||
help=__(
|
||||
'document exactly the members in module __all__ attribute. '
|
||||
'(default: %(default)s)'
|
||||
),
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@ -740,12 +827,15 @@ def main(argv: Sequence[str] = (), /) -> None:
|
||||
|
||||
if args.templates:
|
||||
app.config.templates_path.append(path.abspath(args.templates))
|
||||
app.config.autosummary_ignore_module_all = (not args.respect_module_all)
|
||||
app.config.autosummary_ignore_module_all = not args.respect_module_all
|
||||
|
||||
generate_autosummary_docs(args.source_file, args.output_dir,
|
||||
generate_autosummary_docs(
|
||||
args.source_file,
|
||||
args.output_dir,
|
||||
'.' + args.suffix,
|
||||
imported_members=args.imported_members,
|
||||
app=app)
|
||||
app=app,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
Loading…
Reference in New Issue
Block a user