So far just aiming to produce valid code, skipping docstrings altogether.
Thus -- generating one file per module, and putting code that closely
resembles what's in the documentation, but in a way that is actual
Python, together with importing dependency modules.
To support incomplete or broken annotations, the tyoe_relative template
variable got specialized to type_quoted. In certain cases, such as base
classes for enums or (data / default) values, it's left as *_relative, as
in those cases no quoting is necessary. What isn't handled so far is
quoting forward references for types that were not yet listed in the
output.
.. |wink| replace:: 😉
A modern, mobile-friendly Sphinx-alike Python documentation generator with a
-first-class search functionality. Generated by inspecting Python modules and
-using either embedded docstrings or external :abbr:`reST <reStructuredText>`
+first-class search functionality and an ability to provide Python stubs for
+IDE autocompletion and type checking. Generated by inspecting Python modules
+and using either embedded docstrings or external :abbr:`reST <reStructuredText>`
files to populate the documentation.
One of the design goals is providing a similar user experience to the
- Can use both in-code docstrings and external :abbr:`reST <reStructuredText>`
files to describe the APIs, giving the user a control over the code size vs
documentation verbosity tradeoff
+- Opt-in specialized behavior for understanding native bindings written with
+ `pybind11`_ and Python code using `attrs`_
+- Ability to generate lightweight Python stubs for IDE autocompletion and
+ type checking that exactly matches the documentation, both for pure Python
+ and native bindings.
`Configuration`_
================
:py:`INPUT: str` Base input directory. If not set, config
file base dir is used. Relative paths are
relative to config file base dir.
-:py:`OUTPUT: str` Where to save the output. Relative paths
- are relative to :py:`INPUT`; if not set,
- ``output/`` is used.
+:py:`OUTPUT: Optional[str]` Where to save the HTML output. Relative
+ paths are relative to :py:`INPUT`. If not
+ set, ``output/`` is used, if set to
+ :py:`None`, no HTML output is generated.
+:py:`OUTPUT_STUBS: Optional[str]` Where to save generated Python stubs. See
+ `Stub generation`_ for more information.
+ Relative paths are relative to :py:`INPUT`.
+ If set to :py:`None`, no stubs are
+ generated, default is :py:`None`.
:py:`INPUT_MODULES: List[Any]` List of modules to generate the docs from.
Values can be either strings or module
objects. See `Module inspection`_ for more
module and class members. See
`Custom URL formatters`_ for more
information.
+:py:`STUB_EXTENSION: str` Extension to use for generated Python
+ stub files. If not set, ``.pyi`` is used.
+ See `Stub generation`_ for more
+ information.
+:py:`STUB_HEADER: str` Comment block or arbitrary other code to
+ put at the top generated Python stub files.
+ If not set, a default generic text is used.
+ If empty, no header is present in the files
+ at all. See `Stub generation`_ for more
+ information.
=================================== ===========================================
`Theme selection`_
option is enabled. In order to show them again, override the docstring to
something meaningful.
+`Stub generation`_
+==================
+
+By default, the tool produces just a HTML documentation. In many cases, and
+especially when native bindings are involved, it's useful to provide so-called
+*stubs* for the IDE in order for it to provide useful autocompletion and
+perform type checking. Another use case is when the amount of code in the
+actual implementation is too large for the IDE to handle.
+
+Stubs can be opted in by specifying a path where to generate them in
+:py:`OUTPUT_STUBS`. They can be generated either together with the HTML output,
+or alone, if :py:`OUTPUT` is set to :py:`None` instead. A common setup in that
+case is to create a second file, named ``conf-stubs.py``, that inherits
+everything from ``conf.py`` but enables only the stub output:
+
+.. code:: py
+
+ # Inherit everything from conf.py
+ import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
+ from conf import *
+
+ OUTPUT = None
+ OUTPUT_STUBS = 'stubs/'
+
+Now, when you run the script, the output directory will contain ``*.pyi`` files
+that you can supply to your IDE.
+
+.. code:: sh
+
+ ./python.py path/to/conf-stubs.py
+
`Command-line options`_
=======================
:py:`page.prefix_wbr` Fully-qualified symbol prefix for given
compound with trailing ``.`` with
:html:`<wbr/>` tag after every ``.``.
+:py:`page.dependencies` List of :py:`(prefix, module)`
+ tuples with module dependencies. To
+ match relative type names, it's either
+ :py:`import module` if ``prefix`` is
+ empty, :py:`from prefix import *` if
+ ``module`` is empty or
+ :py:`from prefix import module` if both
+ are non-empty.
:py:`page.modules` List of inner modules. See
`Module properties`_ for details.
:py:`page.classes` List of classes. See
`Property properties`_ for details.
:py:`page.has_property_details` If there is at least one property with
full description block [3]_
+:py:`page.has_members` If the class contains at least one
+ member or is completely empty
======================================= =======================================
Explicit documentation pages rendered with ``class.html`` have additional
:py:`function.content` Detailed documentation, if any
:py:`function.type` Fully qualified function return type
annotation [2]_
-:py:`function.type_relative` Like :py:`function.type`, but relative,
+:py:`function.type_quoted` Like :py:`function.type`, but relative,
+ i.e. with a common prefix of the type and
+ containing module / class omitted, and with
+ parts quoted if type inspection failed to
+ match the annotation to an actual name
+:py:`function.type_link` Like :py:`function.type`, but relative,
i.e. with a common prefix of the type and
- containing module / class omitted
-:py:`function.type_link` Like :py:`function.type_relative`, but with
+ containing module / class omitted, and with
cross-linked types
:py:`function.params` List of function parameters. See below for
details.
:py:`function.return_value` Return value documentation. Can be empty.
:py:`function.has_details` If there is enough content for the full
description block [3]_
+:py:`function.is_method` Set to :py:`True` if the function is a
+ class method, :py:`False` if it's
+ standalone.
:py:`function.is_classmethod` Set to :py:`True` if the function is
annotated with :py:`@classmethod`,
:py:`False` otherwise.
:py:`function.is_staticmethod` Set to :py:`True` if the function is
annotated with :py:`@staticmethod`,
:py:`False` otherwise.
+:py:`function.is_overloaded` Set to :py:`True` if the function has
+ multiple overloads with the same name,
+ :py:`False` otherwise.
=================================== ===========================================
The :py:`function.params` is a list of function parameters and their
=============================== ===============================================
:py:`param.name` Parameter name
:py:`param.type` Fully qualified parameter type annotation [2]_
-:py:`param.type_relative` Like :py:`param.type`, but relative, i.e. with
+:py:`param.type_quoted` Like :py:`param.type`, but relative, i.e. with
a common prefix of the type and containing
- module / class omitted
-:py:`param.type_link` Like :py:`param.type_relative`, but with
+ module / class omitted, and with parts quoted
+ if type inspection failed to match the
+ annotation to an actual name
+:py:`param.type_link` Like :py:`param.type_relative`, but relative,
+ i.e. with a common prefix of the type and
+ containing module / class omitted, and with
cross-linked types
:py:`param.default` Default parameter value, if any. If
:py:`param.type` is an enum, is a
:py:`property.id` Property ID [5]_
:py:`property.type` Fully qualified property getter return type
annotation [2]_
-:py:`property.type_relative` Like :py:`property.type`, but relative, i.e.
+:py:`property.type_quoted` Like :py:`property.type`, but relative, i.e.
with a common prefix of the type and containing
- module / class omitted
-:py:`property.type_link` Like :py:`property.type_relative`, but with
- cross-linked types
+ module / class omitted, and with parts quoted
+ if type inspection failed to match the
+ annotation to an actual name
+:py:`property.type_link` Like :py:`property.type`, but relative, i.e.
+ with a common prefix of the type and containing
+ module / class omitted, and with cross-linked
+ types
:py:`property.summary` Doc summary
:py:`property.content` Detailed documentation, if any
:py:`property.exceptions` List of exceptions raised when accessing this
:py:`data.name` Data name
:py:`data.id` Data ID [5]_
:py:`data.type` Fully qualified data type annotation [2]_
-:py:`data.type_relative` Like :py:`data.type`, but relative, i.e. with a
+:py:`data.type_quoted` Like :py:`data.type`, but relative, i.e. with a
common prefix of the type and containing module
- / class omitted
-:py:`data.type_link` Like :py:`data.type_relative`, but with
- cross-linked types
+ / class omitted, and with parts quoted
+ if type inspection failed to match the
+ annotation to an actual name
+:py:`data.type_link` Like :py:`data.type`, but relative, i.e.
+ with a common prefix of the type and containing
+ module / class omitted, and with cross-linked
+ types
:py:`data.summary` Doc summary
:py:`data.content` Detailed documentation, if any
:py:`data.value` Data value representation
from enum import Enum
from types import SimpleNamespace as Empty
from importlib.machinery import SourceFileLoader
-from typing import Tuple, Dict, Set, Any, List, Callable, Optional
+from typing import Tuple, Dict, Set, Any, List, Callable, Optional, Union
from urllib.parse import urljoin
from docutils.transforms import Transform
'INPUT': None,
'OUTPUT': 'output',
+ 'OUTPUT_STUBS': None,
'INPUT_MODULES': [],
'INPUT_PAGES': [],
'INPUT_DOCS': [],
'SEARCH_EXTERNAL_URL': None,
'URL_FORMATTER': default_url_formatter,
- 'ID_FORMATTER': default_id_formatter
+ 'ID_FORMATTER': default_id_formatter,
+
+ 'STUB_EXTENSION': '.pyi',
+ 'STUB_HEADER': "# This file is a stub generated by m.css out of actual Python code. Don't edit\n# directly, modify the original code and regenerate.",
}
class State:
self.crawled: Set[object] = set()
+ # For collecting module dependencies (i.e., what to import to have all
+ # used types known). The `current_module` gets set to the module name
+ # at start of render_module() and render_class(), is cleared again when
+ # the functions exit.
+ #
+ # Cannot be just `current_module_dependencies`, because
+ self.current_module: Optional[str] = None
+ # Should be filled only through add_module_dependency_for()
+ self.module_dependencies: Dict[str, Set[str]] = {}
+
+ # If we're genearating stubs, parsed classes have to be saved and then
+ # rendered together with the rest of the module
+ if self.config['OUTPUT_STUBS']:
+ # Key is path including the class name, value is a parsed class
+ self.parsed_classes: Dict[List[str], Empty] = {}
+ else:
+ self.parsed_classes = None
+
def map_name_prefix(state: State, type: str) -> str:
for prefix, replace in state.name_mapping.items():
if type == prefix or type.startswith(prefix + '.'):
entry = state.name_map[name]
return relative_name, '<a href="{}" class="{}">{}</a>'.format(entry.url, ' '.join(entry.css_classes), relative_name)
+def extract_type(type) -> str:
+ # For types we concatenate the type name with its module unless it's
+ # builtins (i.e., we want re.Match but not builtins.int). We need to use
+ # __qualname__ instead of __name__ because __name__ doesn't take nested
+ # classes into account.
+ return (type.__module__ + '.' if type.__module__ != 'builtins' else '') + type.__qualname__
+
+def enclosing_module_for(state: State, name: str) -> Optional[str]:
+ path = name.split('.')
+ for i in reversed(range(len(path))):
+ name = '.'.join(path[:i])
+ if name in state.name_map and state.name_map[name].type == EntryType.MODULE:
+ return name
+ return None
+
+def add_module_dependency_for(state: State, object: Union[Any, str]):
+ assert state.current_module
+
+ # If not a string and not a module, try looking if its name-mapped type is
+ # in our name map. If it is, and its enclosing module is as well, use that
+ # to have name mapping correctly applied for it.
+ name = None
+ if not isinstance(object, str) and not inspect.ismodule(object):
+ name_candidate = map_name_prefix(state, extract_type(object))
+ if name_candidate in state.name_map:
+ name = enclosing_module_for(state, name_candidate)
+
+ # If the above didn't succeed, try other options
+ if not name:
+ # If it's a string, assume it's a parsed name that's already in the
+ # name map, find the leaf module name and add it
+ if isinstance(object, str):
+ # We should get string names only for pybind11 types, nothing else
+ # TODO er wait, what about unknown annotations? those probably
+ # shouldn't get here at all?
+ assert state.config['PYBIND11_COMPATIBILITY']
+ name = enclosing_module_for(state, object)
+ # If there's no enclosing module, it's a builtin, for which we add
+ # no dependency. Given that str is passed only from pybind, all
+ # referenced names should be either builtin or known.
+ if not name:
+ assert '.' not in object
+ return
+
+ # If it's directly a module (such as `typing` or `enum` passed from
+ # certain parts of the codebase), apply name mapping to it
+ elif inspect.ismodule(object):
+ name = map_name_prefix(state, object.__name__)
+
+ # Otherwise it's a class/function/enum/..., extract module name from
+ # it, apply name mapping
+ else:
+ name = map_name_prefix(state, object.__module__)
+
+ # Add it only if it's not a module self-reference and if it's not builtins
+ if name != 'builtins' and name != state.current_module:
+ state.module_dependencies[state.current_module].add(name)
+
_pybind_name_rx = re.compile('[a-zA-Z0-9_]*')
_pybind_arg_name_rx = re.compile('[/*a-zA-Z0-9_]+')
_pybind_type_rx = re.compile('[a-zA-Z0-9_.]+')
if input_type_lowercase in ['dict', 'list', 'set', 'tuple']:
return input_type_lowercase
if input_type in ['Callable', 'Iterator', 'Iterable', 'Optional', 'Union']:
+ # current_module might be unset when calling this from unittests etc.
+ if state.current_module:
+ add_module_dependency_for(state, typing)
return 'typing.' + input_type
else:
- return map_name_prefix(state, input_type)
+ type = map_name_prefix(state, input_type)
+ # current_module might be unset when calling this from unittests etc.
+ if state.current_module:
+ add_module_dependency_for(state, type)
+ return type
def parse_pybind_type(state: State, referrer_path: List[str], signature: str) -> Tuple[str, str, str, str]:
match = _pybind_type_rx.match(signature)
# pybind enums don't inherit from enum.Enum but have the __members__
# attribute instead
if isinstance(value, enum.Enum) or (state.config['PYBIND11_COMPATIBILITY'] and hasattr(value.__class__, '__members__')):
+ name = '.'.join([value.__class__.__module__, value.__class__.__qualname__, value.name])
+ # Adding the `enum` module as a dependency isn't enough, here we need
+ # the actual enum value definitions
+ add_module_dependency_for(state, value.__class__)
# TODO Python 3.8+ supports `a, *b`, switch to that once 3.7 is dropped
- return (value.name, ) + make_name_relative_link(state, referrer_path, '.'.join([value.__class__.__module__, value.__class__.__qualname__, value.name]))
+ return (value.name, ) + make_name_relative_link(state, referrer_path, name)
# isbuiltin returns true if object is a builtin _function_ or _method_, not
# just any builtin such as the False literal
elif inspect.isfunction(value) or inspect.isbuiltin(value):
external_doc_entry['used'] = True
return summary, content
-def extract_type(type) -> str:
- # For types we concatenate the type name with its module unless it's
- # builtins (i.e., we want re.Match but not builtins.int). We need to use
- # __qualname__ instead of __name__ because __name__ doesn't take nested
- # classes into account.
- return (type.__module__ + '.' if type.__module__ != 'builtins' else '') + type.__qualname__
-
def get_type_hints_or_nothing(state: State, path: List[str], object) -> Dict:
# Calling get_type_hints on a pybind11 type (from extract_data_doc())
# results in KeyError because there's no sys.modules['pybind11_builtins'].
logging.warning("failed to dereference type hints for %s (%s), falling back to non-dereferenced", '.'.join(path), e.__class__.__name__)
return {}
+def quoted_annotation(annotation: str) -> str:
+ return '\'{}\''.format(annotation.replace('\'', '\\\''))
+
def extract_annotation(state: State, referrer_path: List[str], annotation) -> Tuple[str, str, str]:
# Empty annotation, as opposed to a None annotation, handled below
if annotation is inspect.Signature.empty:
# If dereferencing with typing.get_type_hints() failed, we might end up
# with forward-referenced types being plain strings. Keep them as is, since
- # those are most probably an error.
+ # those are most probably an error, but quote for stubs.
if type(annotation) == str:
- return (annotation, )*3
+ return annotation, quoted_annotation(annotation), annotation
# Or the plain strings might be inside (e.g. List['Foo']), which gets
# converted by Python to ForwardRef. Hammer out the actual string and again
- # leave it as-is, since it's most probably an error.
+ # leave it as-is, since it's most probably an error. Quote for stubs.
elif isinstance(annotation, typing.ForwardRef if sys.version_info >= (3, 7) else typing._ForwardRef):
- return (annotation.__forward_arg__, )*3
+ return annotation.__forward_arg__, quoted_annotation(annotation.__forward_arg__), annotation.__forward_arg__
# Generic type names -- use their name directly
+ # TODO define the TypeVar somewhere somehow instead of lazy quoting
elif isinstance(annotation, typing.TypeVar):
- return annotation.__name__, annotation.__name__, annotation.__name__
+ return annotation.__name__, quoted_annotation(annotation.__name__), annotation.__name__
# Ellipsis -- print a literal `...`
# TODO: any chance to link this to python official docs?
# could be a "bracketed" type, in which case we want to recurse to its
# types as well.
elif (hasattr(annotation, '__module__') and annotation.__module__ == 'typing'):
+ add_module_dependency_for(state, typing)
+
# Optional or Union, handle those first
if hasattr(annotation, '__origin__') and annotation.__origin__ is typing.Union:
# FOR SOME REASON `annotation.__args__[1] is None` is always False,
assert len(args) >= 1
nested_types = []
- nested_types_relative = []
+ nested_types_quoted = []
nested_type_links = []
for i in args[:-1]:
- nested_type, nested_type_relative, nested_type_link = extract_annotation(state, referrer_path, i)
+ nested_type, nested_type_quoted, nested_type_link = extract_annotation(state, referrer_path, i)
nested_types += [nested_type]
- nested_types_relative += [nested_type_relative]
+ nested_types_quoted += [nested_type_quoted]
nested_type_links += [nested_type_link]
- nested_return_type, nested_return_type_relative, nested_return_type_link = extract_annotation(state, referrer_path, args[-1])
+ nested_return_type, nested_return_type_quoted, nested_return_type_link = extract_annotation(state, referrer_path, args[-1])
# If nested parsing failed (the invalid annotation below),
# fail the whole thing
return (
'{}[[{}], {}]'.format(name, ', '.join(nested_types), nested_return_type),
- '{}[[{}], {}]'.format(name, ', '.join(nested_types_relative), nested_return_type_relative),
+ '{}[[{}], {}]'.format(name, ', '.join(nested_types_quoted), nested_return_type_quoted),
'{}[[{}], {}]'.format(name_link, ', '.join(nested_type_links), nested_return_type_link)
)
else:
nested_types = []
- nested_types_relative = []
+ nested_types_quoted = []
nested_type_links = []
for i in args:
- nested_type, nested_type_relative, nested_type_link = extract_annotation(state, referrer_path, i)
+ nested_type, nested_type_quoted, nested_type_link = extract_annotation(state, referrer_path, i)
nested_types += [nested_type]
- nested_types_relative += [nested_type_relative]
+ nested_types_quoted += [nested_type_quoted]
nested_type_links += [nested_type_link]
# If nested parsing failed (the invalid annotation below),
return (
'{}[{}]'.format(name, ', '.join(nested_types)),
- '{}[{}]'.format(name_relative, ', '.join(nested_types_relative)),
+ '{}[{}]'.format(name_relative, ', '.join(nested_types_quoted)),
'{}[{}]'.format(name_link, ', '.join(nested_type_links)),
)
return ('None', ) + make_name_relative_link(state, referrer_path, 'None')
# Otherwise it's a plain type. Turn it into a link.
+ add_module_dependency_for(state, annotation)
name = map_name_prefix(state, extract_type(annotation))
# TODO Python 3.8+ supports `a, *b`, switch to that once 3.7 is dropped
return (name, ) + make_name_relative_link(state, referrer_path, name)
return out
def extract_enum_doc(state: State, entry: Empty):
+ assert state.current_module
+
out = Empty()
out.name = entry.path[-1]
out.id = state.config['ID_FORMATTER'](EntryType.ENUM, entry.path[-1:])
else:
docstring = entry.object.__doc__
+ # TODO should probably do some name mapping here also?
out.base = extract_type(entry.object.__base__)
+ # Add the base as a dependency so the stubs can derive from it
+ add_module_dependency_for(state, entry.object.__base__)
out.base_relative, out.base_link = make_name_relative_link(state, entry.path, out.base)
for i in entry.object:
docstring = entry.object.__doc__.partition('\n\n')[0]
out.base = None
+ # Add the enum module as a dependency, the stub template will make
+ # enum.Enum a base to make it recognizable as an actual enum
+ add_module_dependency_for(state, enum)
for name, v in entry.object.__members__.items():
value = Empty()
return out
def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]:
+ assert state.current_module
assert inspect.isfunction(entry.object) or inspect.ismethod(entry.object) or inspect.isroutine(entry.object)
# Enclosing page URL for search
out.params = []
out.has_complex_params = False
out.has_details = False
- out.type, out.type_relative, out.type_link = type, type_relative, type_link
+ # The parsed pybind11 annotation either works as a whole, or not at
+ # all, so it's never quoted, only relative
+ out.type, out.type_quoted, out.type_link = type, type_relative, type_link
# There's no other way to check staticmethods than to check for
# self being the name of first parameter :( No support for
# classmethods, as C++11 doesn't have that
out.is_classmethod = False
if inspect.isclass(parent):
+ out.is_method = True
if args and args[0][0] == 'self':
out.is_staticmethod = False
else:
out.is_staticmethod = True
else:
+ out.is_method = False
out.is_staticmethod = False
# If the arguments contain a literal * or / (which is only if
# Don't include redundant type for the self argument
if i == 0 and arg_name == 'self':
- param.type, param.type_relative, param.type_link = None, None, None
+ param.type, param.type_quoted, param.type_link = None, None, None
param_types += [None]
signature += ['self']
else:
- param.type, param.type_relative, param.type_link = arg_type, arg_type_relative, arg_type_link
+ # The parsed pybind11 annotation either works as a whole,
+ # or not at all, so it's never quoted, only relative
+ param.type, param.type_quoted, param.type_link = arg_type, arg_type_relative, arg_type_link
param_types += [arg_type]
signature += ['{}: {}'.format(arg_name, arg_type)]
# Decide if classmethod or staticmethod in case this is a method
if inspect.isclass(parent):
+ out.is_method = True
out.is_classmethod = inspect.ismethod(entry.object)
out.is_staticmethod = out.name in parent.__dict__ and isinstance(parent.__dict__[out.name], staticmethod)
+ else:
+ out.is_method = False
# First try to get fully dereferenced type hints (with strings
# converted to actual annotations). If that fails (e.g. because a type
signature = inspect.signature(entry.object)
if 'return' in type_hints:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, type_hints['return'])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, type_hints['return'])
else:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, signature.return_annotation)
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, signature.return_annotation)
param_names = []
for i in signature.parameters.values():
param = Empty()
param.name = i.name
param_names += [i.name]
if i.name in type_hints:
- param.type, param.type_relative, param.type_link = extract_annotation(state, entry.path, type_hints[i.name])
+ param.type, param.type_quoted, param.type_link = extract_annotation(state, entry.path, type_hints[i.name])
else:
- param.type, param.type_relative, param.type_link = extract_annotation(state, entry.path, i.annotation)
+ param.type, param.type_quoted, param.type_link = extract_annotation(state, entry.path, i.annotation)
if param.type:
out.has_complex_params = True
if i.default is inspect.Signature.empty:
except ValueError:
param = Empty()
param.name = None
- param.type, param.type_relative, param.type_link = None, None, None
+ param.type, param.type_quoted, param.type_link = None, None, None
param.default, param.default_relative, param.default_link = None, None, None
out.params = [param]
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
param_names = []
# Call all scope enter hooks
overloads = [out]
+ # Mark the functions as overloaded if there's more than one overload
+ for out in overloads:
+ out.is_overloaded = len(overloads) != 1
+ # The stub template will decorate the function with @typing.overload
+ if len(overloads) != 1:
+ add_module_dependency_for(state, typing)
+
# Common path for parameter / exception / return value docs and search
path_str = '.'.join(entry.path)
for out in overloads:
result.prefix = entry.path[:-1]
result.name = entry.path[-1]
result.params = []
- # If the function has multiple overloads (e.g. pybind functions),
- # add arguments to each to distinguish between them
+ # If the function is overloaded, add arguments to each to
+ # distinguish between them
if len(overloads) != 1:
for i in range(len(out.params)):
param = out.params[i]
- result.params += ['{}: {}'.format(param.name, param.type_relative) if param.type_relative else param.name]
+ # TODO use param.type_relative if it ever exists again in
+ # addition to param.type_quoted
+ result.params += ['{}: {}'.format(param.name, make_relative_name(state, entry.path, param.type)) if param.type else param.name]
state.search += [result]
return overloads
def extract_property_doc(state: State, parent, entry: Empty):
+ assert state.current_module
+
out = Empty()
out.name = entry.path[-1]
out.id = state.config['ID_FORMATTER'](EntryType.PROPERTY, entry.path[-1:])
out.is_gettable = True
out.is_settable = True
out.is_deletable = True
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, entry.object.type)
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, entry.object.type)
# If this is a slot, there won't be any fget / fset / fdel. Assume they're
# gettable and settable (couldn't find any way to make them *inspectably*
type_hints = get_type_hints_or_nothing(state, entry.path, parent)
if out.name in type_hints:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, type_hints[out.name])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, type_hints[out.name])
elif hasattr(parent, '__annotations__') and out.name in parent.__annotations__:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, parent.__annotations__[out.name])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, parent.__annotations__[out.name])
else:
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
# The properties can be defined using the low-level descriptor protocol
# instead of the higher-level property() decorator. That means there's no
out.is_gettable = True
out.is_settable = False
out.is_deletable = False
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
# Otherwise it's a classic property
else:
assert inspect.isdatadescriptor(entry.object)
is_classic_property = True
+ # TODO figure out how to do pybind11 writeonly properties in the stub
+ # template
out.is_gettable = entry.object.fget is not None
out.is_settable = entry.object.fset is not None
out.is_deletable = entry.object.fdel is not None
type_hints = get_type_hints_or_nothing(state, entry.path, entry.object.fget)
if 'return' in type_hints:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, type_hints['return'])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, type_hints['return'])
else:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, signature.return_annotation)
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, signature.return_annotation)
else:
assert entry.object.fset
signature = inspect.signature(entry.object.fset)
# non-dereferenced version
value_parameter = list(signature.parameters.values())[1]
if value_parameter.name in type_hints:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, type_hints[value_parameter.name])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, type_hints[value_parameter.name])
else:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, value_parameter.annotation)
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, value_parameter.annotation)
except ValueError:
# pybind11 properties have the type in the docstring
if state.config['PYBIND11_COMPATIBILITY']:
if entry.object.fget:
- out.type, out.type_relative, out.type_link = parse_pybind_signature(state, entry.path, entry.object.fget.__doc__)[3:]
+ out.type, out.type_quoted, out.type_link = parse_pybind_signature(state, entry.path, entry.object.fget.__doc__)[3:]
else:
assert entry.object.fset
parsed_args = parse_pybind_signature(state, entry.path, entry.object.fset.__doc__)[2]
# If argument parsing failed, we're screwed
if len(parsed_args) == 1:
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
else:
- out.type, out.type_relative, out.type_link = parsed_args[1][1:4]
+ out.type, out.type_quoted, out.type_link = parsed_args[1][1:4]
else:
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
# Call all scope enter hooks before rendering the docs
for hook in state.hooks_pre_scope:
return out
def extract_data_doc(state: State, parent, entry: Empty):
+ assert state.current_module
assert not inspect.ismodule(entry.object) and not inspect.isclass(entry.object) and not inspect.isroutine(entry.object) and not inspect.isframe(entry.object) and not inspect.istraceback(entry.object) and not inspect.iscode(entry.object)
# Call all scope enter hooks before rendering the docs
type_hints = get_type_hints_or_nothing(state, entry.path, parent)
if out.name in type_hints:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, type_hints[out.name])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, type_hints[out.name])
elif hasattr(parent, '__annotations__') and out.name in parent.__annotations__:
- out.type, out.type_relative, out.type_link = extract_annotation(state, entry.path, parent.__annotations__[out.name])
+ out.type, out.type_quoted, out.type_link = extract_annotation(state, entry.path, parent.__annotations__[out.name])
else:
- out.type, out.type_relative, out.type_link = None, None, None
+ out.type, out.type_quoted, out.type_link = None, None, None
out.value, out.value_relative, out.value_link = format_value(state, entry.path, entry.object) or (None, None, None)
# patching test files to include a trailing newline to make Git
# happy. Can't use keep_trailing_newline because that'd add it
# also for nested templates :( The rendered file should never contain a
- # trailing newline on its own.
- assert not rendered.endswith('\n')
- f.write(b'\n')
+ # trailing newline on its own. Also add it only in case the file isn't
+ # empty, which can happen with generated stubs. If non-empty, it should
+ # never contain a trailing newline on its own.
+ if rendered:
+ assert not rendered.endswith('\n')
+ f.write(b'\n')
def render_module(state: State, path, module, env):
+ # Save name of current module for populating module dependencies and
+ # initialize the dependency set. It could already be present in
+ # module_dependencies if render_class() for an inner class was called
+ # before, don't overwrite in that case.
+ assert not state.current_module
+ path_str = '.'.join(path)
+ state.current_module = path_str
+ state.module_dependencies.setdefault(path_str, set())
+
# Call all scope enter hooks first
for hook in state.hooks_pre_scope:
hook(type=EntryType.MODULE, path=path)
page.has_data_details = False
# Find itself in the global map, save the summary back there for index
- entry = state.name_map['.'.join(path)]
+ entry = state.name_map[path_str]
entry.summary = page.summary
# Extract docs for all members
if member_entry.type == EntryType.MODULE:
page.modules += [extract_module_doc(state, member_entry)]
elif member_entry.type == EntryType.CLASS:
- page.classes += [extract_class_doc(state, member_entry)]
+ # If we're generating stubs, add the whole parsed class instead of
+ # just a name, summary and reference. All classes inside given
+ # module are parsed before the module itself because crawl_module()
+ # first puts all nested names into state.name_map and only then the
+ # module itself.
+ if state.config['OUTPUT_STUBS']:
+ page.classes += [state.parsed_classes[subpath_str]]
+ else:
+ page.classes += [extract_class_doc(state, member_entry)]
elif member_entry.type == EntryType.ENUM:
enum_ = extract_enum_doc(state, member_entry)
page.enums += [enum_]
else: # pragma: no cover
assert False
+ # At this point the module dependencies should be filled for everything in
+ # this module as well as all (recursive) classes. To verify that's indeed
+ # the case, remove current module name from the dict afterwards -- anything
+ # that'd attempt to insert afterwards would fail with a KeyError.
+ page.dependencies = []
+ for dependency in state.module_dependencies[path_str]:
+ dependency_path = dependency.split('.')
+ common_prefix_length = len(os.path.commonprefix([dependency_path, path]))
+ # If there's no common prefix, it's an unrelated module
+ if not common_prefix_length:
+ page.dependencies += [('', dependency)]
+ # If the common prefix is the whole module path, the dependency is from
+ # a submodule (so this file is a __init__.py)
+ elif len(path) == common_prefix_length:
+ page.dependencies += [('.', '.'.join(dependency_path[common_prefix_length:]))]
+ # Otherwise the dependency is a sibling module or siblings of parents,
+ # add one dot for each. Yes, this can also produce a single dot as
+ # above, but the difference is that above the file is a __init__.py so
+ # `from . import sub` refers to a submodule, while here
+ # `from . import sub` refers to the enclosing __init__.py so a sibling.
+ else:
+ page.dependencies += [('.'*(len(path) - common_prefix_length), '.'.join(dependency_path[common_prefix_length:]))]
+ # Make the list independent from the order in which the dependencies were
+ # discovered
+ page.dependencies.sort()
+ del state.module_dependencies[path_str]
+
if not state.config['SEARCH_DISABLED']:
result = Empty()
result.flags = ResultFlag.from_type(ResultFlag.NONE, EntryType.MODULE)
result.name = path[-1]
state.search += [result]
- # Perform HTML escaping for everything going into the template. Done here
- # instead of inside extract_*_doc() to avoid having to unescape in certain
- # cases.
- for enum in page.enums:
- for value in enum.values:
- value.value = html.escape(value.value)
- for function in page.functions:
- for param in function.params:
- if param.default:
- param.default = html.escape(param.default)
- param.default_relative = html.escape(param.default_relative)
- # param.default_link may contain HTML and thus had to be
- # escaped early
- for data in page.data:
- if data.value:
- data.value = html.escape(data.value)
- data.value_relative = html.escape(data.value_relative)
- # data.value_link may contain HTML and thus had to be escaped early
+ # Render also the python stub file if requested
+ if state.config['OUTPUT_STUBS'] is not None:
+ # If the module has submodules, put it into <module>/__init__.pyi so
+ # submodules can be next to it. It could be done always, but writing
+ # just <module>.pyi if there are no submodules makes the output
+ # cleaner.
+ if page.modules:
+ stub_filename = os.path.join(*path, '__init__' + state.config['STUB_EXTENSION'])
+ else:
+ stub_filename = os.path.join(*path[:-1], path[-1] + state.config['STUB_EXTENSION'])
- render(config=state.config,
- template='module.html',
- filename=os.path.join(state.config['OUTPUT'], filename),
- url=url,
- env=env,
- page=page)
+ render(config=state.config,
+ template='stub.pyi',
+ filename=os.path.join(state.config['OUTPUT_STUBS'], stub_filename),
+ url=url,
+ env=env,
+ page=page)
+
+ # Render the regular HTML output, unless disabled
+ if state.config['OUTPUT'] is not None:
+ # Perform HTML escaping for everything going into the template. Done
+ # here instead of inside extract_*_doc() to avoid having to unescape in
+ # certain cases.
+ for enum in page.enums:
+ for value in enum.values:
+ value.value = html.escape(value.value)
+ for function in page.functions:
+ for param in function.params:
+ if param.default:
+ param.default = html.escape(param.default)
+ param.default_relative = html.escape(param.default_relative)
+ # param.default_link may contain HTML and thus had to be
+ # escaped early
+ for data in page.data:
+ if data.value:
+ data.value = html.escape(data.value)
+ data.value_relative = html.escape(data.value_relative)
+ # data.value_link may contain HTML and thus had to be escaped
+ # early
+
+ render(config=state.config,
+ template='module.html',
+ filename=os.path.join(state.config['OUTPUT'], filename),
+ url=url,
+ env=env,
+ page=page)
# Call all scope exit hooks last
for hook in state.hooks_post_scope:
hook(type=EntryType.MODULE, path=path)
+ # Reset name of current module to ensure it isn't mistakenly filled with
+ # something unrelated
+ state.current_module = None
+
def render_class(state: State, path, class_, env):
+ # Save name of the enclosing module for populating module dependencies and
+ # initialize the dependency set. It could already be present in
+ # module_dependencies if render_class() for an inner class was called
+ # before, don't overwrite in that case.
+ #
+ # Can't use class_.__module__ as that may not respect name mapping either
+ # from config or from the __all__ members, have to iterate backwards until
+ # the path prefix is a module.
+ assert not state.current_module
+ path_str = '.'.join(path)
+ state.current_module = enclosing_module_for(state, path_str)
+ state.module_dependencies.setdefault(state.current_module, set())
+
# Call all scope enter hooks first
for hook in state.hooks_pre_scope:
hook(type=EntryType.CLASS, path=path)
page.methods = []
page.properties = []
page.data = []
+ page.has_members = False
page.has_enum_details = False
page.has_function_details = False
page.has_property_details = False
page.has_data_details = False
# Find itself in the global map, save the summary back there for index
- entry = state.name_map['.'.join(path)]
+ entry = state.name_map[path_str]
entry.summary = page.summary
# Extract docs for all members
logging.warning("%s is undocumented", subpath_str)
if member_entry.type == EntryType.CLASS:
- page.classes += [extract_class_doc(state, member_entry)]
+ # If we're generating stubs, add the whole parsed class instead of
+ # just a name, summary and reference. All inner classes are parsed
+ # before the class itself because crawl_class() first puts all
+ # nested names into state.name_map and only then the class itself.
+ if state.config['OUTPUT_STUBS']:
+ page.classes += [state.parsed_classes[subpath_str]]
+ else:
+ page.classes += [extract_class_doc(state, member_entry)]
+ page.has_members = True
elif member_entry.type == EntryType.ENUM:
enum_ = extract_enum_doc(state, member_entry)
page.enums += [enum_]
if enum_.has_details:
page.has_enum_details = True
+ page.has_members = True
elif member_entry.type in [EntryType.FUNCTION, EntryType.OVERLOADED_FUNCTION]:
for function in extract_function_doc(state, class_, member_entry):
if name.startswith('__'):
page.methods += [function]
if function.has_details:
page.has_function_details = True
+ page.has_members = True
elif member_entry.type == EntryType.PROPERTY:
property = extract_property_doc(state, class_, member_entry)
page.properties += [property]
if property.has_details:
page.has_property_details = True
+ page.has_members = True
elif member_entry.type == EntryType.DATA:
data = extract_data_doc(state, class_, member_entry)
page.data += [data]
if data.has_details:
page.has_data_details = True
+ page.has_members = True
else: # pragma: no cover
assert False
result.name = path[-1]
state.search += [result]
- # Perform HTML escaping for everything going into the template. Done here
- # instead of inside extract_*_doc() to avoid having to unescape in certain
- # cases.
- for enum in page.enums:
- for value in enum.values:
- value.value = html.escape(value.value)
- for function in page.classmethods + page.staticmethods + page.dunder_methods + page.methods:
- for param in function.params:
- if param.default:
- param.default = html.escape(param.default)
- param.default_relative = html.escape(param.default_relative)
- # param.default_link may contain HTML and thus had to be
- # escaped early
- for data in page.data:
- if data.value:
- data.value = html.escape(data.value)
- data.value_relative = html.escape(data.value_relative)
- # data.value_link may contain HTML and thus had to be escaped early
-
- render(config=state.config,
- template='class.html',
- filename=os.path.join(state.config['OUTPUT'], filename),
- url=url,
- env=env,
- page=page)
+ # If we're generating stubs, the parsed data gets used in render_module()
+ # instead of it using just the output of extract_class_doc(). It
+ # additionally needs a name member to be a superset of what
+ # extract_class_doc() produces so it works with regular HTML output as
+ # well.
+ if state.config['OUTPUT_STUBS']:
+ parsed_class = copy.deepcopy(page)
+ parsed_class.name = path[-1]
+ state.parsed_classes[path_str] = parsed_class
+
+ # Render the regular HTML output, unless disabled
+ if state.config['OUTPUT'] is not None:
+ # Perform HTML escaping for everything going into the template. Done
+ # here instead of inside extract_*_doc() to avoid having to unescape in
+ # certain cases.
+ for enum in page.enums:
+ for value in enum.values:
+ value.value = html.escape(value.value)
+ for function in page.classmethods + page.staticmethods + page.dunder_methods + page.methods:
+ for param in function.params:
+ if param.default:
+ param.default = html.escape(param.default)
+ param.default_relative = html.escape(param.default_relative)
+ # param.default_link may contain HTML and thus had to be
+ # escaped early
+ for data in page.data:
+ if data.value:
+ data.value = html.escape(data.value)
+ data.value_relative = html.escape(data.value_relative)
+ # data.value_link may contain HTML and thus had to be escaped
+ # early
+
+ render(config=state.config,
+ template='class.html',
+ filename=os.path.join(state.config['OUTPUT'], filename),
+ url=url,
+ env=env,
+ page=page)
# Call all scope exit hooks last
for hook in state.hooks_post_scope:
hook(type=EntryType.CLASS, path=path)
+ # Reset name of current module to ensure it isn't mistakenly filled with
+ # something unrelated
+ state.current_module = None
+
# Extracts image paths and transforms them to just the filenames
class ExtractImages(Transform):
# Max Docutils priority is 990, be sure that this is applied at the very
docutils.utils.assemble_option_dict = prev_assemble_option_dict
def render_page(state: State, path, input_filename, env):
+ # If not generating the regular HTML output, we shouldn't even be here
+ assert state.config['OUTPUT'] is not None
+
filename, url = state.config['URL_FORMATTER'](EntryType.PAGE, path)
logging.debug("generating %s", filename)
if config['INPUT'] is None: config['INPUT'] = basedir
else: config['INPUT'] = os.path.join(basedir, config['INPUT'])
- # Make the output dir absolute
- config['OUTPUT'] = os.path.join(config['INPUT'], config['OUTPUT'])
- if not os.path.exists(config['OUTPUT']):
- os.makedirs(config['OUTPUT'])
+ # Make the output dirs absolute
+ if config['OUTPUT'] is not None:
+ config['OUTPUT'] = os.path.join(config['INPUT'], config['OUTPUT'])
+ if not os.path.exists(config['OUTPUT']):
+ os.makedirs(config['OUTPUT'])
+ if config['OUTPUT_STUBS'] is not None:
+ config['OUTPUT_STUBS'] = os.path.join(config['INPUT'], config['OUTPUT_STUBS'])
+ if not os.path.exists(config['OUTPUT_STUBS']):
+ os.makedirs(config['OUTPUT_STUBS'])
# Guess MIME type of the favicon
if config['FAVICON']:
if unused_docs:
logging.warning("The following %s doc contents were unused: %s", docs, unused_docs)
- # Create module and class index from the toplevel name list. Recursively go
- # from the top-level index list and gather all class/module children.
- def fetch_class_index(entry):
- index_entry = Empty()
- index_entry.kind = 'module' if entry.type == EntryType.MODULE else 'class'
- index_entry.name = entry.path[-1]
- index_entry.url = state.config['URL_FORMATTER'](entry.type, entry.path)[1]
- index_entry.summary = entry.summary
- index_entry.has_nestable_children = False
- index_entry.children = []
-
- # Module children should go before class children, put them in a
- # separate list and then concatenate at the end
- class_children = []
- for member in entry.members:
- member_entry = state.name_map['.'.join(entry.path + [member])]
- if member_entry.type == EntryType.MODULE:
- index_entry.has_nestable_children = True
- index_entry.children += [fetch_class_index(state.name_map['.'.join(member_entry.path)])]
- elif member_entry.type == EntryType.CLASS:
- class_children += [fetch_class_index(state.name_map['.'.join(member_entry.path)])]
- index_entry.children += class_children
-
- return index_entry
-
- for i in range(len(class_index)):
- class_index[i] = fetch_class_index(state.name_map[class_index[i]])
-
- # Create page index from the toplevel name list
- # TODO: rework when we have nested page support
- for i in range(len(page_index)):
- entry = state.name_map[page_index[i]]
- assert entry.type == EntryType.PAGE, "page %s already used as %s (%s)" % (page_index[i], entry.type, entry.url)
-
- index_entry = Empty()
- index_entry.kind = 'page'
- index_entry.name = entry.name
- index_entry.url = entry.url
- index_entry.summary = entry.summary
- index_entry.has_nestable_children = False
- index_entry.children = []
-
- page_index[i] = index_entry
-
- index = Empty()
- index.classes = class_index
- index.pages = page_index
- for file in special_pages[1:]: # exclude index
- filename, url = config['URL_FORMATTER'](EntryType.SPECIAL, [file])
- render(config=config,
- template=file + '.html',
- filename=os.path.join(config['OUTPUT'], filename),
- url=url,
- env=env,
- index=index)
+ # All collected module dependencies should be consumed and removed by
+ # render_module() at this point. If not, it might be because the same class
+ # appears in two distinct modules, or a module somewhere in the path isn't
+ # crawled. Neither of those is an error.
+ if state.module_dependencies:
+ logging.debug("Some module dependencies were not consumed: {}".format(state.module_dependencies))
+
+ # The following is all relevant to the HTML output only, skip if disabled
+ if state.config['OUTPUT'] is not None:
+ # Create module and class index from the toplevel name list.
+ # Recursively go from the top-level index list and gather all
+ # class/module children.
+ def fetch_class_index(entry):
+ index_entry = Empty()
+ index_entry.kind = 'module' if entry.type == EntryType.MODULE else 'class'
+ index_entry.name = entry.path[-1]
+ index_entry.url = state.config['URL_FORMATTER'](entry.type, entry.path)[1]
+ index_entry.summary = entry.summary
+ index_entry.has_nestable_children = False
+ index_entry.children = []
+
+ # Module children should go before class children, put them in a
+ # separate list and then concatenate at the end
+ class_children = []
+ for member in entry.members:
+ member_entry = state.name_map['.'.join(entry.path + [member])]
+ if member_entry.type == EntryType.MODULE:
+ index_entry.has_nestable_children = True
+ index_entry.children += [fetch_class_index(state.name_map['.'.join(member_entry.path)])]
+ elif member_entry.type == EntryType.CLASS:
+ class_children += [fetch_class_index(state.name_map['.'.join(member_entry.path)])]
+ index_entry.children += class_children
+
+ return index_entry
+
+ for i in range(len(class_index)):
+ class_index[i] = fetch_class_index(state.name_map[class_index[i]])
+
+ # Create page index from the toplevel name list
+ # TODO: rework when we have nested page support
+ for i in range(len(page_index)):
+ entry = state.name_map[page_index[i]]
+ assert entry.type == EntryType.PAGE, "page %s already used as %s (%s)" % (page_index[i], entry.type, entry.url)
+
+ index_entry = Empty()
+ index_entry.kind = 'page'
+ index_entry.name = entry.name
+ index_entry.url = entry.url
+ index_entry.summary = entry.summary
+ index_entry.has_nestable_children = False
+ index_entry.children = []
+
+ page_index[i] = index_entry
+
+ index = Empty()
+ index.classes = class_index
+ index.pages = page_index
+ for file in special_pages[1:]: # exclude index
+ filename, url = config['URL_FORMATTER'](EntryType.SPECIAL, [file])
+ render(config=config,
+ template=file + '.html',
+ filename=os.path.join(config['OUTPUT'], filename),
+ url=url,
+ env=env,
+ index=index)
- # Create index.html if it was not provided by the user
- if 'index.rst' not in [os.path.basename(i) for i in config['INPUT_PAGES']]:
- logging.debug("writing index.html for an empty main page")
+ # Create index.html if it was not provided by the user
+ if 'index.rst' not in [os.path.basename(i) for i in config['INPUT_PAGES']]:
+ logging.debug("writing index.html for an empty main page")
- filename, url = config['URL_FORMATTER'](EntryType.SPECIAL, ['index'])
+ filename, url = config['URL_FORMATTER'](EntryType.SPECIAL, ['index'])
- page = Empty()
- page.filename = filename
- page.url = url
- page.breadcrumb = [(config['PROJECT_TITLE'], url)]
- render(config=config,
- template='page.html',
- filename=os.path.join(config['OUTPUT'], filename),
- url=url,
- env=env,
- page=page)
+ page = Empty()
+ page.filename = filename
+ page.url = url
+ page.breadcrumb = [(config['PROJECT_TITLE'], url)]
+ render(config=config,
+ template='page.html',
+ filename=os.path.join(config['OUTPUT'], filename),
+ url=url,
+ env=env,
+ page=page)
- if not state.config['SEARCH_DISABLED']:
- logging.debug("building search data for {} symbols".format(len(state.search)))
-
- data = build_search_data(state, add_lookahead_barriers=search_add_lookahead_barriers, merge_subtrees=search_merge_subtrees, merge_prefixes=search_merge_prefixes)
-
- # Joining twice, first before passing those to the URL formatter and
- # second after. If SEARCH_DOWNLOAD_BINARY is a string, use that as a
- # filename.
- # TODO: any chance we could write the file *before* it gets ever passed
- # to URL formatters so we can add cache buster hashes to its URL?
- if state.config['SEARCH_DOWNLOAD_BINARY']:
- with open(os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [os.path.join(config['OUTPUT'], state.config['SEARCH_DOWNLOAD_BINARY'] if isinstance(state.config['SEARCH_DOWNLOAD_BINARY'], str) else searchdata_filename.format(search_filename_prefix=state.config['SEARCH_FILENAME_PREFIX']))])[0]), 'wb') as f:
- f.write(data)
- else:
- with open(os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [os.path.join(config['OUTPUT'], searchdata_filename_b85.format(search_filename_prefix=state.config['SEARCH_FILENAME_PREFIX']))])[0]), 'wb') as f:
- f.write(base85encode_search_data(data))
-
- # OpenSearch metadata, in case we have the base URL
- if state.config['SEARCH_BASE_URL']:
- logging.debug("writing OpenSearch metadata file")
-
- template = env.get_template('opensearch.xml')
- rendered = template.render(**state.config)
- output = os.path.join(config['OUTPUT'], 'opensearch.xml')
- with open(output, 'wb') as f:
- f.write(rendered.encode('utf-8'))
- # Add back a trailing newline so we don't need to bother with
- # patching test files to include a trailing newline to make Git
- # happy. Can't use keep_trailing_newline because that'd add it
- # also for nested templates :( The rendered file should never
- # contain a trailing newline on its own.
- assert not rendered.endswith('\n')
- f.write(b'\n')
-
- # Copy referenced files
- for i in config['STYLESHEETS'] + config['EXTRA_FILES'] + ([config['PROJECT_LOGO']] if config['PROJECT_LOGO'] else []) + ([config['FAVICON'][0]] if config['FAVICON'] else []) + list(state.external_data) + ([] if config['SEARCH_DISABLED'] else ['search.js']):
- # Skip absolute URLs
- if urllib.parse.urlparse(i).netloc: continue
+ if not state.config['SEARCH_DISABLED']:
+ logging.debug("building search data for {} symbols".format(len(state.search)))
+
+ data = build_search_data(state, add_lookahead_barriers=search_add_lookahead_barriers, merge_subtrees=search_merge_subtrees, merge_prefixes=search_merge_prefixes)
+
+ # Joining twice, first before passing those to the URL formatter
+ # and second after. If SEARCH_DOWNLOAD_BINARY is a string, use that
+ # as a filename.
+ # TODO: any chance we could write the file *before* it gets ever
+ # passed to URL formatters so we can add cache buster hashes to its
+ # URL?
+ if state.config['SEARCH_DOWNLOAD_BINARY']:
+ with open(os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [os.path.join(config['OUTPUT'], state.config['SEARCH_DOWNLOAD_BINARY'] if isinstance(state.config['SEARCH_DOWNLOAD_BINARY'], str) else searchdata_filename.format(search_filename_prefix=state.config['SEARCH_FILENAME_PREFIX']))])[0]), 'wb') as f:
+ f.write(data)
+ else:
+ with open(os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [os.path.join(config['OUTPUT'], searchdata_filename_b85.format(search_filename_prefix=state.config['SEARCH_FILENAME_PREFIX']))])[0]), 'wb') as f:
+ f.write(base85encode_search_data(data))
+
+ # OpenSearch metadata, in case we have the base URL
+ if state.config['SEARCH_BASE_URL']:
+ logging.debug("writing OpenSearch metadata file")
+
+ template = env.get_template('opensearch.xml')
+ rendered = template.render(**state.config)
+ output = os.path.join(config['OUTPUT'], 'opensearch.xml')
+ with open(output, 'wb') as f:
+ f.write(rendered.encode('utf-8'))
+ # Add back a trailing newline so we don't need to bother
+ # with patching test files to include a trailing newline to
+ # make Git happy. Can't use keep_trailing_newline because
+ # that'd add it also for nested templates :( The rendered
+ # file should never contain a trailing newline on its own.
+ assert not rendered.endswith('\n')
+ f.write(b'\n')
+
+ # Copy referenced files
+ for i in config['STYLESHEETS'] + config['EXTRA_FILES'] + ([config['PROJECT_LOGO']] if config['PROJECT_LOGO'] else []) + ([config['FAVICON'][0]] if config['FAVICON'] else []) + list(state.external_data) + ([] if config['SEARCH_DISABLED'] else ['search.js']):
+ # Skip absolute URLs
+ if urllib.parse.urlparse(i).netloc: continue
- # If file is found relative to the conf file, use that
- if os.path.exists(os.path.join(config['INPUT'], i)):
- i = os.path.join(config['INPUT'], i)
+ # If file is found relative to the conf file, use that
+ if os.path.exists(os.path.join(config['INPUT'], i)):
+ i = os.path.join(config['INPUT'], i)
- # Otherwise use path relative to script directory
- else:
- i = os.path.join(os.path.dirname(os.path.realpath(__file__)), i)
+ # Otherwise use path relative to script directory
+ else:
+ i = os.path.join(os.path.dirname(os.path.realpath(__file__)), i)
- output = os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [i])[0])
- output_dir = os.path.dirname(output)
- if not os.path.exists(output_dir): os.makedirs(output_dir)
- logging.debug("copying %s to output", i)
- shutil.copy(i, output)
+ output = os.path.join(config['OUTPUT'], config['URL_FORMATTER'](EntryType.STATIC, [i])[0])
+ output_dir = os.path.dirname(output)
+ if not os.path.exists(output_dir): os.makedirs(output_dir)
+ logging.debug("copying %s to output", i)
+ shutil.copy(i, output)
# Call all registered finalization hooks
for hook in state.hooks_post_run: hook()
--- /dev/null
+{% set pj = joiner('\n\n') %}
+{% if STUB_HEADER %}{{ pj() }}{{ STUB_HEADER|trim }}{% endif %}
+{% if page.dependencies %}{{ pj() }}{% for prefix, module in page.dependencies %}
+{% if not loop.first %}
+
+{% endif %}
+{% if prefix %}from {{ prefix }} import {{ module or '*' }}{% else %}import {{ module }}{% endif %}
+{% endfor %}{% endif %}
+{% macro render_enums(enums) %}
+{% for enum in enums %}
+{% if not loop.first %}
+
+
+{% endif %}
+class {{ enum.name }}({% if enum.base_relative %}{{ enum.base_relative }}{% else %}enum.Enum{% endif %}):
+{% if not enum.values %}
+ ...
+{%- else %}
+ {% for value in enum.values %}
+ {% if not loop.first %}
+
+ {% endif %}
+ {{ value.name }} = {{ value.value }}{% endfor %}
+{% endif %}
+{% endfor %}{% endmacro %}
+{% macro render_functions(functions) %}
+{% for function in functions %}
+{% if not loop.first %}
+
+
+{% endif %}
+{% if function.is_classmethod %}
+@classmethod
+{% elif function.is_staticmethod %}
+@staticmethod
+{% endif %}
+{% if function.is_overloaded %}
+@typing.overload
+{% endif %}
+{% if function.params|length == 1 and not function.params[0].name %}
+def {{ function.name }}({% if function.is_classmethod %}cls, {% elif function.is_method and not function.is_staticmethod %}self, {% endif %}*args):
+{% else %}
+def {{ function.name }}({% for param in function.params %}{% if loop.index0 and function.params[loop.index0 - 1].kind == 'POSITIONAL_OR_KEYWORD' and param.kind == 'KEYWORD_ONLY' %}, *{% endif %}{% if not loop.first %}, {% endif %}{% if param.kind == 'VAR_POSITIONAL' %}*{% elif param.kind == 'VAR_KEYWORD' %}**{% endif %}{{ param.name }}{% if param.type_quoted %}: {{ param.type_quoted }}{% endif %}{% if param.default_relative %} = {{ param.default_relative }}{% endif %}{% if param.kind == 'POSITIONAL_ONLY' and (loop.last or function.params[loop.index0 + 1].kind != 'POSITIONAL_ONLY') %}, /{% endif %}{% endfor %}){% if function.type_quoted %} -> {{ function.type_quoted }}{% endif %}:
+{% endif %}
+ ...
+{%- endfor %}{% endmacro %}
+{% macro render_properties(properties) %}
+{% for property in properties %}
+{% if not loop.first %}
+
+
+{% endif %}
+@property
+def {{ property.name }}(self){% if property.type_quoted %} -> {{ property.type_quoted }}{% endif %}:
+ ...
+{%- if property.is_settable +%}
+@{{ property.name }}.setter
+def {{ property.name }}(self, value{% if property.type_quoted %}: {{ property.type_quoted }}{% endif %}):
+ ...
+{%- endif %}
+{%- if property.is_deletable +%}
+@{{ property.name }}.deleter
+def {{ property.name }}(self):
+ ...
+{%- endif %}
+{%- endfor %}{% endmacro %}
+{% macro render_data(data_) %}
+{% for data in data_ %}
+{% if not loop.first %}
+
+
+{% endif %}
+{{ data.name }}{% if data.type_quoted or data.value_relative %}{% if data.type_quoted %}: {{ data.type_quoted }}{% endif %}{% if data.value_relative %} = {{ data.value_relative }}{% endif %}{% else %}: ...{% endif %}
+{% endfor %}{% endmacro %}
+{% if page.enums %}{{ pj() }}{{ render_enums(page.enums) }}{% endif %}
+{% if page.classes %}{{ pj() -}}
+{% for class in page.classes recursive %}
+{% if not loop.first %}
+
+
+{% endif %}
+class {{ class.name }}:
+{% if not class.has_members %}
+ ...
+{%- endif %}
+{% set mj = joiner('\n\n') %}
+{% if class.enums %}{{ mj() }}{{ render_enums(class.enums)|indent(4, first=True) }}{% endif %}
+{% if class.classes %}{{ mj() }}{{ loop(class.classes)|indent(4, first=True) }}{% endif %}
+{% if class.data %}{{ mj() }}{{ render_data(class.data)|indent(4, first=True) }}{% endif %}
+{% if class.staticmethods %}{{ mj() }}{{ render_functions(class.staticmethods)|indent(4, first=True) -}}{% endif %}
+{% if class.classmethods %}{{ mj() }}{{ render_functions(class.classmethods)|indent(4, first=True) }}{% endif %}
+{% if class.methods %}{{ mj() }}{{ render_functions(class.methods)|indent(4, first=True) }}{% endif %}
+{% if class.dunder_methods %}{{ mj() }}{{ render_functions(class.dunder_methods)|indent(4, first=True) }}{% endif %}
+{% if class.properties %}{{ mj() }}{{ render_properties(class.properties)|indent(4, first=True) }}{% endif %}
+{%- endfor %}{% endif %}
+{% if page.data %}{{ pj() }}{{ render_data(page.data) }}{% endif %}
+{% if page.functions %}{{ pj() }}{{ render_functions(page.functions) }}{% endif %}
set_target_properties(pybind_submodules_package PROPERTIES
OUTPUT_NAME sub
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/pybind_submodules_package/pybind_submodules_package)
+
+# Need a special name for this one
+pybind11_add_module(pybind_stubs_module_dependencies stubs_module_dependencies/stubs_module_dependencies/pybind.cpp)
+set_target_properties(pybind_stubs_module_dependencies PROPERTIES
+ OUTPUT_NAME pybind
+ LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stubs_module_dependencies/stubs_module_dependencies)
def actual_expected_contents(self, actual, expected = None):
if not expected: expected = actual
- with open(os.path.join(self.path, expected)) as f:
+ with open(os.path.join(self.path, 'stubs', expected) if expected.endswith('.pyi') and not expected.startswith('../') else os.path.join(self.path, expected)) as f:
expected_contents = f.read()
with open(os.path.join(self.path, 'output', actual)) as f:
actual_contents = f.read()
config_overrides = config
BaseTestCase.run_python(self, config_overrides, templates)
+
+ def run_python_stubs(self, config_overrides={}, templates=default_templates):
+ # Defaults that make sense for stub-only tests
+ config = copy.deepcopy(default_config)
+ config.update({
+ 'FINE_PRINT': None,
+ 'THEME_COLOR': None,
+ 'FAVICON': None,
+ 'LINKS_NAVBAR1': [],
+ 'LINKS_NAVBAR2': [],
+ 'SEARCH_DISABLED': True,
+ 'OUTPUT': None,
+ 'OUTPUT_STUBS': os.path.join(self.path, 'output'),
+ 'STUB_HEADER': ''
+ })
+
+ if 'INPUT_MODULES' not in config_overrides:
+ sys.path.append(self.path)
+ config['INPUT_MODULES'] = [self.dirname]
+
+ # Update it with config overrides
+ config.update(config_overrides)
+
+ run(self.path, config, templates=templates)
--- /dev/null
+import enum
+
+class Enum(enum.Enum):
+ VALUE_THAT_SHOULD_BE_ESCAPED = '<&>'
+
+class Class:
+ class ClassEnum(enum.Enum):
+ VALUE_THAT_SHOULD_BE_ESCAPED = '<&>'
+
+ DATA_THAT_SHOULD_BE_ESCAPED = '<&>'
+
+ @staticmethod
+ def staticmethod(default_string_that_should_be_escaped = '<&>'):
+ ...
+
+ @classmethod
+ def classmethod(default_string_that_should_be_escaped = '<&>'):
+ ...
+
+ def method(self, default_string_that_should_be_escaped = '<&>'):
+ ...
+
+ def __dunder_method__(self, default_string_that_should_be_escaped = '<&>'):
+ ...
+
+DATA_THAT_SHOULD_BE_ESCAPED = '<&>'
+
+def function(default_string_that_should_be_escaped = '<&>'):
+ ...
--- /dev/null
+def default_value_should_be_escaped(string: str = '<&>') -> int:
+ ...
--- /dev/null
+import typing
+
+class AContainer:
+ def __new__(cls, *args, **kwds):
+ ...
+
+class AContainer2:
+ def __iter__(self):
+ ...
+
+ def __new__(cls, *args, **kwds):
+ ...
+
+ def __next__(self):
+ ...
+
+ def __subclasshook__(subclass):
+ ...
+
+class Foo:
+ @property
+ def a_property(self) -> typing.List[bool]:
+ ...
+
+class FooSlots:
+ @property
+ def annotated(self) -> typing.List[str]:
+ ...
+ @annotated.setter
+ def annotated(self, value: typing.List[str]):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def unannotated(self):
+ ...
+ @unannotated.setter
+ def unannotated(self, value):
+ ...
+ @unannotated.deleter
+ def unannotated(self):
+ ...
+
+ANNOTATED_VAR: typing.Tuple[bool, str] = (False, 'No.')
+
+UNANNOTATED_VAR = 3.45
+
+def annotated_positional_keyword(bar = False, *, foo: str, **kwargs):
+ ...
+
+def annotation(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_any(a: typing.Any):
+ ...
+
+def annotation_callable(a: typing.Callable[[float, int], str]):
+ ...
+
+def annotation_callable_no_args(a: typing.Callable[[], typing.Dict[int, float]]):
+ ...
+
+def annotation_ellipsis(a: typing.Callable[[...], int], b: typing.Tuple[str, ...]):
+ ...
+
+def annotation_func_instead_of_type(a):
+ ...
+
+def annotation_func_instead_of_type_nested(a, b, c):
+ ...
+
+def annotation_generic(a: typing.List['Tp']) -> 'Tp':
+ ...
+
+def annotation_invalid() -> 'Foo.Bar':
+ ...
+
+def annotation_list_noparam(a: typing.List):
+ ...
+
+def annotation_optional(a: typing.Optional[float]):
+ ...
+
+def annotation_strings(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_tuple_instead_of_tuple(a):
+ ...
+
+def annotation_union(a: typing.Union[float, int]):
+ ...
+
+def annotation_union_of_forward_reference(a: typing.Union[int, 'something.Undefined']):
+ ...
+
+def annotation_union_second_bracketed(a: typing.Union[float, typing.List[int]]):
+ ...
+
+def args_kwargs(a, b, *args, **kwargs):
+ ...
+
+def no_annotation(a, b, z):
+ ...
+
+def no_annotation_default_param(param, another, third = 'hello'):
+ ...
+
+def partial_annotation(foo, param: typing.Tuple[int, int], unannotated, cls: object):
+ ...
+
+def positional_keyword(positional_kw, *, kw_only):
+ ...
+
+def returns_none(a: typing.Callable[[], None]) -> None:
+ ...
+
+def returns_none_type(a: typing.Callable[[], None]) -> None:
+ ...
--- /dev/null
+import typing
+
+class AContainer:
+ def __new__(cls, *args, **kwds):
+ ...
+
+class AContainer2:
+ def __iter__(self):
+ ...
+
+ def __new__(cls, *args, **kwds):
+ ...
+
+ def __next__(self):
+ ...
+
+ @classmethod
+ def __subclasshook__(C):
+ ...
+
+class Foo:
+ @property
+ def a_property(self) -> typing.List[bool]:
+ ...
+
+class FooSlots:
+ @property
+ def annotated(self) -> typing.List[str]:
+ ...
+ @annotated.setter
+ def annotated(self, value: typing.List[str]):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def unannotated(self):
+ ...
+ @unannotated.setter
+ def unannotated(self, value):
+ ...
+ @unannotated.deleter
+ def unannotated(self):
+ ...
+
+ANNOTATED_VAR: typing.Tuple[bool, str] = (False, 'No.')
+
+UNANNOTATED_VAR = 3.45
+
+def annotated_positional_keyword(bar = False, *, foo: str, **kwargs):
+ ...
+
+def annotation(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_any(a: typing.Any):
+ ...
+
+def annotation_callable(a: typing.Callable[[float, int], str]):
+ ...
+
+def annotation_callable_no_args(a: typing.Callable[[], typing.Dict[int, float]]):
+ ...
+
+def annotation_ellipsis(a: typing.Callable[[...], int], b: typing.Tuple[str, ...]):
+ ...
+
+def annotation_func_instead_of_type(a):
+ ...
+
+def annotation_func_instead_of_type_nested(a, b, c):
+ ...
+
+def annotation_generic(a: typing.List['Tp']) -> 'Tp':
+ ...
+
+def annotation_invalid() -> 'Foo.Bar':
+ ...
+
+def annotation_list_noparam(a: typing.List['T']):
+ ...
+
+def annotation_optional(a: typing.Optional[float]):
+ ...
+
+def annotation_strings(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_tuple_instead_of_tuple(a):
+ ...
+
+def annotation_union(a: typing.Union[float, int]):
+ ...
+
+def annotation_union_of_forward_reference(a: typing.Union[int, 'something.Undefined']):
+ ...
+
+def annotation_union_second_bracketed(a: typing.Union[float, typing.List[int]]):
+ ...
+
+def args_kwargs(a, b, *args, **kwargs):
+ ...
+
+def no_annotation(a, b, z):
+ ...
+
+def no_annotation_default_param(param, another, third = 'hello'):
+ ...
+
+def partial_annotation(foo, param: typing.Tuple[int, int], unannotated, cls: object):
+ ...
+
+def positional_keyword(positional_kw, *, kw_only):
+ ...
+
+def returns_none(a: typing.Callable[[], None]) -> None:
+ ...
+
+def returns_none_type(a: typing.Callable[[], None]) -> None:
+ ...
--- /dev/null
+import typing
+
+class AContainer:
+ ...
+
+class AContainer2:
+ @classmethod
+ def __class_getitem__(cls, *args):
+ ...
+
+ def __iter__(self):
+ ...
+
+ def __next__(self):
+ ...
+
+ @classmethod
+ def __subclasshook__(C):
+ ...
+
+class Foo:
+ @property
+ def a_property(self) -> typing.List[bool]:
+ ...
+
+class FooSlots:
+ @property
+ def annotated(self) -> typing.List[str]:
+ ...
+ @annotated.setter
+ def annotated(self, value: typing.List[str]):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def unannotated(self):
+ ...
+ @unannotated.setter
+ def unannotated(self, value):
+ ...
+ @unannotated.deleter
+ def unannotated(self):
+ ...
+
+ANNOTATED_VAR: typing.Tuple[bool, str] = (False, 'No.')
+
+UNANNOTATED_VAR = 3.45
+
+def annotated_positional_keyword(bar = False, *, foo: str, **kwargs):
+ ...
+
+def annotation(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_any(a: typing.Any):
+ ...
+
+def annotation_callable(a: typing.Callable[[float, int], str]):
+ ...
+
+def annotation_callable_no_args(a: typing.Callable[[], typing.Dict[int, float]]):
+ ...
+
+def annotation_ellipsis(a: typing.Callable[[...], int], b: typing.Tuple[str, ...]):
+ ...
+
+def annotation_func_instead_of_type(a):
+ ...
+
+def annotation_func_instead_of_type_nested(a, b, c):
+ ...
+
+def annotation_generic(a: typing.List['Tp']) -> 'Tp':
+ ...
+
+def annotation_invalid() -> 'Foo.Bar':
+ ...
+
+def annotation_list_noparam(a: typing.List):
+ ...
+
+def annotation_optional(a: typing.Optional[float]):
+ ...
+
+def annotation_strings(param: typing.List[int], another: bool, third: str = 'hello') -> float:
+ ...
+
+def annotation_tuple_instead_of_tuple(a):
+ ...
+
+def annotation_union(a: typing.Union[float, int]):
+ ...
+
+def annotation_union_of_forward_reference(a: typing.Union[int, 'something.Undefined']):
+ ...
+
+def annotation_union_second_bracketed(a: typing.Union[float, typing.List[int]]):
+ ...
+
+def args_kwargs(a, b, *args, **kwargs):
+ ...
+
+def no_annotation(a, b, z):
+ ...
+
+def no_annotation_default_param(param, another, third = 'hello'):
+ ...
+
+def partial_annotation(foo, param: typing.Tuple[int, int], unannotated, cls: object):
+ ...
+
+def positional_keyword(positional_kw, *, kw_only):
+ ...
+
+def returns_none(a: typing.Callable[[], None]) -> None:
+ ...
+
+def returns_none_type(a: typing.Callable[[], None]) -> None:
+ ...
--- /dev/null
+import typing
+
+class MyClass:
+ plain_data: float = 35
+
+ def __init__(self, annotated: float, unannotated = 4, complex_annotation: typing.List[typing.Tuple[int, float]] = [], complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = [], hidden_property: float = 3) -> None:
+ ...
+
+ @property
+ def annotated(self) -> float:
+ ...
+ @annotated.setter
+ def annotated(self, value: float):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def complex_annotation(self) -> typing.List[typing.Tuple[int, float]]:
+ ...
+ @complex_annotation.setter
+ def complex_annotation(self, value: typing.List[typing.Tuple[int, float]]):
+ ...
+ @complex_annotation.deleter
+ def complex_annotation(self):
+ ...
+
+ @property
+ def unannotated(self):
+ ...
+ @unannotated.setter
+ def unannotated(self, value):
+ ...
+ @unannotated.deleter
+ def unannotated(self):
+ ...
+
+ @property
+ def complex_annotation_in_attr(self) -> typing.List[typing.Tuple[int, float]]:
+ ...
+ @complex_annotation_in_attr.setter
+ def complex_annotation_in_attr(self, value: typing.List[typing.Tuple[int, float]]):
+ ...
+ @complex_annotation_in_attr.deleter
+ def complex_annotation_in_attr(self):
+ ...
+
+class MyClassAutoAttribs:
+ unannotated = 4
+
+ def __init__(self, annotated: float, complex_annotation: typing.List[typing.Tuple[int, float]] = []) -> None:
+ ...
+
+ @property
+ def annotated(self) -> float:
+ ...
+ @annotated.setter
+ def annotated(self, value: float):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def complex_annotation(self) -> typing.List[typing.Tuple[int, float]]:
+ ...
+ @complex_annotation.setter
+ def complex_annotation(self, value: typing.List[typing.Tuple[int, float]]):
+ ...
+ @complex_annotation.deleter
+ def complex_annotation(self):
+ ...
+
+class MySlotClass:
+ def __init__(self, annotated: float, complex_annotation: typing.List[typing.Tuple[int, float]] = [], complex_annotation_in_attr: typing.List[typing.Tuple[int, float]] = []) -> None:
+ ...
+
+ @property
+ def annotated(self) -> float:
+ ...
+ @annotated.setter
+ def annotated(self, value: float):
+ ...
+ @annotated.deleter
+ def annotated(self):
+ ...
+
+ @property
+ def complex_annotation(self) -> typing.List[typing.Tuple[int, float]]:
+ ...
+ @complex_annotation.setter
+ def complex_annotation(self, value: typing.List[typing.Tuple[int, float]]):
+ ...
+ @complex_annotation.deleter
+ def complex_annotation(self):
+ ...
+
+ @property
+ def complex_annotation_in_attr(self) -> typing.List[typing.Tuple[int, float]]:
+ ...
+ @complex_annotation_in_attr.setter
+ def complex_annotation_in_attr(self, value: typing.List[typing.Tuple[int, float]]):
+ ...
+ @complex_annotation_in_attr.deleter
+ def complex_annotation_in_attr(self):
+ ...
--- /dev/null
+class BaseException:
+ def with_traceback(self, *args):
+ ...
+
+ def __reduce__(self, *args):
+ ...
+
+ def __setstate__(self, *args):
+ ...
+
+ @property
+ def __cause__(self):
+ ...
+
+ @property
+ def __context__(self):
+ ...
+
+ @property
+ def args(self):
+ ...
+
+def pow(x, y, /):
+ ...
+
+def log(*args):
+ ...
--- /dev/null
+class BaseException:
+ def with_traceback(self, *args):
+ ...
+
+ def __reduce__(self, *args):
+ ...
+
+ def __setstate__(self, *args):
+ ...
+
+ @property
+ def __cause__(self):
+ ...
+
+ @property
+ def __context__(self):
+ ...
+
+ @property
+ def args(self):
+ ...
+
+def log(*args):
+ ...
--- /dev/null
+class BaseException:
+ def add_note(self, *args):
+ ...
+
+ def with_traceback(self, *args):
+ ...
+
+ def __reduce__(self, *args):
+ ...
+
+ def __setstate__(self, *args):
+ ...
+
+ @property
+ def __cause__(self):
+ ...
+
+ @property
+ def __context__(self):
+ ...
+
+ @property
+ def args(self):
+ ...
+
+def pow(x, y, /):
+ ...
+
+def log(*args):
+ ...
--- /dev/null
+class Class:
+ def a_thing(self) -> Class:
+ ...
+
+def foo() -> Class:
+ ...
--- /dev/null
+from . import *
+
+def foo(a: Class, b: int) -> yay.ThisGotOverridenExternally:
+ ...
--- /dev/null
+import enum
+
+class MyEnum(enum.Enum):
+ VALUE = 0
+ ANOTHER = 1
+ YAY = 2
+
+class UndocumentedEnum(enum.IntFlag):
+ FLAG_ONE = 1
+ FLAG_SIXTEEN = 16
+
+class DerivedException:
+ def with_traceback(self, *args):
+ ...
+
+ def __reduce__(self, *args):
+ ...
+
+ def __setstate__(self, *args):
+ ...
+
+ @property
+ def __cause__(self):
+ ...
+
+ @property
+ def __context__(self):
+ ...
+
+ @property
+ def args(self):
+ ...
+
+class Foo:
+ class InnerEnum(enum.Enum):
+ VALUE = 0
+ ANOTHER = 1
+ YAY = 2
+
+ class UndocumentedInnerEnum(enum.IntFlag):
+ FLAG_ONE = 1
+ FLAG_SIXTEEN = 16
+
+ class Subclass:
+ ...
+
+ A_DATA = 'BOO'
+
+ DATA_DECLARATION: int = None
+
+ @staticmethod
+ def static_func(a):
+ ...
+
+ @classmethod
+ def func_on_class(a):
+ ...
+
+ def func(self, a, b):
+ ...
+
+ @property
+ def a_property(self):
+ ...
+
+ @property
+ def deletable_property(self):
+ ...
+ @deletable_property.deleter
+ def deletable_property(self):
+ ...
+
+ @property
+ def writable_property(self):
+ ...
+ @writable_property.setter
+ def writable_property(self, value):
+ ...
+
+ @property
+ def writeonly_property(self):
+ ...
+ @writeonly_property.setter
+ def writeonly_property(self, value):
+ ...
+
+class FooSlots:
+ @property
+ def first(self):
+ ...
+ @first.setter
+ def first(self, value):
+ ...
+ @first.deleter
+ def first(self):
+ ...
+
+ @property
+ def second(self):
+ ...
+ @second.setter
+ def second(self, value):
+ ...
+ @second.deleter
+ def second(self):
+ ...
+
+class Specials:
+ def __add__(self, other):
+ ...
+
+ def __and__(self, other):
+ ...
+
+ def __init__(self):
+ ...
+
+A_CONSTANT = 3.24
+
+foo: ...
+
+def function():
+ ...
--- /dev/null
+import enum
+
+class MyEnum(enum.Enum):
+ VALUE = 0
+ ANOTHER = 1
+ YAY = 2
+
+class UndocumentedEnum(enum.IntFlag):
+ FLAG_ONE = 1
+ FLAG_SIXTEEN = 16
+
+class DerivedException:
+ def add_note(self, *args):
+ ...
+
+ def with_traceback(self, *args):
+ ...
+
+ def __reduce__(self, *args):
+ ...
+
+ def __setstate__(self, *args):
+ ...
+
+ @property
+ def __cause__(self):
+ ...
+
+ @property
+ def __context__(self):
+ ...
+
+ @property
+ def args(self):
+ ...
+
+class Foo:
+ class InnerEnum(enum.Enum):
+ VALUE = 0
+ ANOTHER = 1
+ YAY = 2
+
+ class UndocumentedInnerEnum(enum.IntFlag):
+ FLAG_ONE = 1
+ FLAG_SIXTEEN = 16
+
+ class Subclass:
+ ...
+
+ A_DATA = 'BOO'
+
+ DATA_DECLARATION: int = None
+
+ @staticmethod
+ def static_func(a):
+ ...
+
+ @classmethod
+ def func_on_class(a):
+ ...
+
+ def func(self, a, b):
+ ...
+
+ @property
+ def a_property(self):
+ ...
+
+ @property
+ def deletable_property(self):
+ ...
+ @deletable_property.deleter
+ def deletable_property(self):
+ ...
+
+ @property
+ def writable_property(self):
+ ...
+ @writable_property.setter
+ def writable_property(self, value):
+ ...
+
+ @property
+ def writeonly_property(self):
+ ...
+ @writeonly_property.setter
+ def writeonly_property(self, value):
+ ...
+
+class FooSlots:
+ @property
+ def first(self):
+ ...
+ @first.setter
+ def first(self, value):
+ ...
+ @first.deleter
+ def first(self):
+ ...
+
+ @property
+ def second(self):
+ ...
+ @second.setter
+ def second(self, value):
+ ...
+ @second.deleter
+ def second(self):
+ ...
+
+class Specials:
+ def __add__(self, other):
+ ...
+
+ def __and__(self, other):
+ ...
+
+ def __init__(self):
+ ...
+
+A_CONSTANT = 3.24
+
+foo: ...
+
+def function():
+ ...
--- /dev/null
+import enum
+
+class MyEnum(enum.Enum):
+ YAY = 2
+
+class Foo:
+ ...
+
+AN_UNREPRESENTABLE_VALUE: ...
+
+A_FALSE_VALUE = False
+
+A_NONE_VALUE = None
+
+A_ZERO_VALUE = 0
+
+ENUM_THING = MyEnum.YAY
+
+LARGE_VALUE_WILL_BE_AN_ELLIPSIS = ...
+
+def basics(string_param = 'string', tuple_param = (3, 5), float_param = 1.2, unrepresentable_param = ...):
+ ...
+
+def setup_callback(unknown_function_is_an_ellipsis = ..., builtin_function_is_an_ellipsis = ..., lambda_is_an_ellipsis = ...):
+ ...
--- /dev/null
+import enum
+
+class MyEnum(enum.Enum):
+ First = 0
+ Second = 1
+ Third = 74
+ CONSISTANTE = -5
+
+class SixtyfourBitFlag(enum.Enum):
+ Yes = 1000000000000
+ No = 18446744073709551615
--- /dev/null
+class Class:
+ @staticmethod
+ def a_thing() -> Class:
+ ...
+
+def foo() -> Class:
+ ...
--- /dev/null
+from . import *
+
+def foo(arg0: Class, arg1: int, /) -> int:
+ ...
.def_property("foo", &MyClass::foo, &MyClass::setFoo, "A read/write property")
.def_property_readonly("bar", &MyClass::foo, "A read-only property");
+ m.def_submodule("just_overloads", "Stubs for this module should import typing as well")
+ .def("overloaded", static_cast<std::string(*)(int)>(&overloaded), "Overloaded for ints")
+ .def("overloaded", static_cast<bool(*)(float)>(&overloaded), "Overloaded for floats");
+
py::class_<MyClass23> pybind23{m, "MyClass23", "Testing pybind 2.3 features"};
/* Checker so the Python side can detect if testing pybind 2.3 features is
<li>
Reference
<ul>
+ <li><a href="#packages">Modules</a></li>
<li><a href="#classes">Classes</a></li>
<li><a href="#functions">Functions</a></li>
</ul>
</li>
</ul>
</nav>
+ <section id="namespaces">
+ <h2><a href="#namespaces">Modules</a></h2>
+ <dl class="m-doc">
+ <dt>module <a href="pybind_signatures.just_overloads.html" class="m-doc">just_overloads</a></dt>
+ <dd>Stubs for this module should import typing as well</dd>
+ </dl>
+ </section>
<section id="classes">
<h2><a href="#classes">Classes</a></h2>
<dl class="m-doc">
--- /dev/null
+import typing
+
+class MyClass:
+ @staticmethod
+ def static_function(arg0: int, arg1: float, /) -> MyClass:
+ ...
+
+ def another(self, /) -> int:
+ ...
+
+ def instance_function(self, arg0: int, arg1: str, /) -> tuple[float, int]:
+ ...
+
+ def instance_function_kwargs(self, hey: int, what: str = '<eh?>') -> tuple[float, int]:
+ ...
+
+ def __init__(self, /) -> None:
+ ...
+
+ @property
+ def bar(self) -> float:
+ ...
+
+ @property
+ def foo(self) -> float:
+ ...
+ @foo.setter
+ def foo(self, value: float):
+ ...
+
+class MyClass23:
+ is_pybind23 = False
+
+class MyClass26:
+ is_pybind26 = False
+
+def crazy_signature(*args):
+ ...
+
+def duck(*args, **kwargs) -> None:
+ ...
+
+def escape_docstring(arg0: int, /) -> None:
+ ...
+
+def failed_parse_docstring(*args):
+ ...
+
+def full_docstring(arg0: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def overloaded(arg0: int, /) -> str:
+ ...
+
+@typing.overload
+def overloaded(arg0: float, /) -> bool:
+ ...
+
+def scale(arg0: int, arg1: float, /) -> int:
+ ...
+
+def scale_kwargs(a: int, argument: float) -> int:
+ ...
+
+def takes_a_function(arg0: typing.Callable[[float, list[float]], int], /) -> None:
+ ...
+
+def takes_a_function_returning_none(arg0: typing.Callable[[], None], /) -> None:
+ ...
+
+def taking_a_list_returning_a_tuple(arg0: list[float], /) -> tuple[int, int, int]:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: str, arg1: str, /) -> None:
+ ...
+
+def void_function(arg0: int, /) -> None:
+ ...
--- /dev/null
+import typing
+
+class MyClass:
+ @staticmethod
+ def static_function(arg0: int, arg1: float, /) -> MyClass:
+ ...
+
+ def another(self, /) -> int:
+ ...
+
+ def instance_function(self, arg0: int, arg1: str, /) -> tuple[float, int]:
+ ...
+
+ def instance_function_kwargs(self, hey: int, what: str = '<eh?>') -> tuple[float, int]:
+ ...
+
+ def __init__(self, /) -> None:
+ ...
+
+ @property
+ def bar(self) -> float:
+ ...
+
+ @property
+ def foo(self) -> float:
+ ...
+ @foo.setter
+ def foo(self, value: float):
+ ...
+
+class MyClass23:
+ is_pybind23 = True
+
+ @property
+ def writeonly(self) -> float:
+ ...
+ @writeonly.setter
+ def writeonly(self, value: float):
+ ...
+
+ @property
+ def writeonly_crazy(self):
+ ...
+ @writeonly_crazy.setter
+ def writeonly_crazy(self, value):
+ ...
+
+class MyClass26:
+ is_pybind26 = False
+
+def crazy_signature(*args):
+ ...
+
+def duck(*args, **kwargs) -> None:
+ ...
+
+def escape_docstring(arg0: int, /) -> None:
+ ...
+
+def failed_parse_docstring(*args):
+ ...
+
+def full_docstring(arg0: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def overloaded(arg0: int, /) -> str:
+ ...
+
+@typing.overload
+def overloaded(arg0: float, /) -> bool:
+ ...
+
+def scale(arg0: int, arg1: float, /) -> int:
+ ...
+
+def scale_kwargs(a: int, argument: float) -> int:
+ ...
+
+def takes_a_function(arg0: typing.Callable[[float, list[float]], int], /) -> None:
+ ...
+
+def takes_a_function_returning_none(arg0: typing.Callable[[], None], /) -> None:
+ ...
+
+def taking_a_list_returning_a_tuple(arg0: list[float], /) -> tuple[int, int, int]:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: str, arg1: str, /) -> None:
+ ...
+
+def void_function(arg0: int, /) -> None:
+ ...
--- /dev/null
+import typing
+
+class MyClass:
+ @staticmethod
+ def static_function(arg0: int, arg1: float, /) -> MyClass:
+ ...
+
+ def another(self, /) -> int:
+ ...
+
+ def instance_function(self, arg0: int, arg1: str, /) -> tuple[float, int]:
+ ...
+
+ def instance_function_kwargs(self, hey: int, what: str = '<eh?>') -> tuple[float, int]:
+ ...
+
+ def __init__(self, /) -> None:
+ ...
+
+ @property
+ def bar(self) -> float:
+ ...
+
+ @property
+ def foo(self) -> float:
+ ...
+ @foo.setter
+ def foo(self, value: float):
+ ...
+
+class MyClass23:
+ is_pybind23 = True
+
+ @property
+ def writeonly(self) -> float:
+ ...
+ @writeonly.setter
+ def writeonly(self, value: float):
+ ...
+
+ @property
+ def writeonly_crazy(self):
+ ...
+ @writeonly_crazy.setter
+ def writeonly_crazy(self, value):
+ ...
+
+class MyClass26:
+ is_pybind26 = True
+
+ @staticmethod
+ def keyword_only(b: float, *, keyword: str = 'no') -> int:
+ ...
+
+ @staticmethod
+ def positional_keyword_only(a: int, /, b: float, *, keyword: str = 'no') -> int:
+ ...
+
+ @staticmethod
+ def positional_only(a: int, /, b: float) -> int:
+ ...
+
+def crazy_signature(*args):
+ ...
+
+def duck(*args, **kwargs) -> None:
+ ...
+
+def escape_docstring(arg0: int, /) -> None:
+ ...
+
+def failed_parse_docstring(*args):
+ ...
+
+def full_docstring(arg0: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def full_docstring_overloaded(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def overloaded(arg0: int, /) -> str:
+ ...
+
+@typing.overload
+def overloaded(arg0: float, /) -> bool:
+ ...
+
+def scale(arg0: int, arg1: float, /) -> int:
+ ...
+
+def scale_kwargs(a: int, argument: float) -> int:
+ ...
+
+def takes_a_function(arg0: typing.Callable[[float, list[float]], int], /) -> None:
+ ...
+
+def takes_a_function_returning_none(arg0: typing.Callable[[], None], /) -> None:
+ ...
+
+def taking_a_list_returning_a_tuple(arg0: list[float], /) -> tuple[int, int, int]:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: float, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: int, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: float, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: int, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: bool, arg1: bool, /) -> None:
+ ...
+
+@typing.overload
+def tenOverloads(arg0: str, arg1: str, /) -> None:
+ ...
+
+def void_function(arg0: int, /) -> None:
+ ...
--- /dev/null
+import typing
+
+@typing.overload
+def overloaded(arg0: int, /) -> str:
+ ...
+
+@typing.overload
+def overloaded(arg0: float, /) -> bool:
+ ...
--- /dev/null
+#!/usr/bin/env python3
+
+# This is a custom header containing no trailing newline on its own.
--- /dev/null
+#!/usr/bin/env python3
+
+# This is a custom header containing no trailing newline on its own.
--- /dev/null
+from . import sub
--- /dev/null
+class Another:
+ ...
--- /dev/null
+import abc
+import email.mime.audio
+import enum
+import json.decoder
+import logging
+import typing
+import unittest.loader
+import unparsed_enum_module
+from . import sub
+
+class RootEnum(enum.Enum):
+ A_VALUE = 3
+
+class Root:
+ CLASS_VALUE_REFERENCING_MODULE_ITSELF: Root = RootEnum.A_VALUE
+
+ def method(self, library_type_alias: abc.ABC, builtin: float, external_enum_value = unparsed_enum_module.UnparsedEnumSubclass.UNPARSED_VALUE) -> json.decoder.JSONDecodeError:
+ ...
+
+ @property
+ def prop(self) -> logging.Logger:
+ ...
+
+VALUE_TYPE: typing.Optional[unittest.loader.TestLoader] = None
+
+def function(should_only_bring_import_sub: sub.Type.InnerClass) -> email.mime.audio.MIMEAudio:
+ ...
--- /dev/null
+import typing
+from . import sub
+
+def function(arg0: sub.Foo, /) -> typing.Callable[[], int]:
+ ...
--- /dev/null
+import another_module
+import unparsed_enum_module
+import unparsed_module
+from . import *
+from .. import *
+
+class EnumSubclass(unparsed_enum_module.UnparsedEnumClass):
+ A_VALUE = 36
+
+def all_should_be_the_same_relative_type(a: Type, b: Type, c: Type):
+ ...
+
+def foo(type_three_levels_up: Root, unparsed_type: unparsed_module.UnparsedName, enum = RootEnum.A_VALUE) -> another_module.Another:
+ ...
--- /dev/null
+import unparsed_enum_module
+import enum
+from . import sub, pybind
+import json
+import logging
+import email.mime.audio
+import abc
+import typing
+import unittest
+
+# The enum dependency is handled explitcitly in the code
+class RootEnum(enum.Enum):
+ A_VALUE = 3
+
+class Root:
+ # is a string as it'd lead to a circular import otherwise
+ CLASS_VALUE_REFERENCING_MODULE_ITSELF: 'Root' = RootEnum.A_VALUE
+
+ def method(self, library_type_alias: abc.ABC, builtin: float, external_enum_value = unparsed_enum_module.UnparsedEnumSubclass.UNPARSED_VALUE) -> json.decoder.JSONDecodeError:
+ ...
+
+ @property
+ def prop(self) -> logging.Logger:
+ ...
+
+# The typing dependency is handled explitcitly in the code
+VALUE_TYPE: typing.Optional[unittest.TestLoader] = None
+
+def function(should_only_bring_import_sub: sub.Type.InnerClass) -> email.mime.audio.MIMEAudio:
+ ...
--- /dev/null
+#include <pybind11/pybind11.h>
+#include <pybind11/functional.h>
+
+namespace py = pybind11;
+
+namespace {
+
+struct Foo {};
+
+std::function<int()> function(const Foo&) { return []{ return 3; }; }
+
+}
+
+PYBIND11_MODULE(pybind, m) {
+ py::class_<Foo>{m.def_submodule("sub"), "Foo"};
+
+ m.def("function", &function);
+}
--- /dev/null
+class Type:
+ class InnerClass:
+ ...
--- /dev/null
+import stubs_module_dependencies
+from stubs_module_dependencies import Root, RootEnum, sub
+
+from . import Type
+
+import another_module
+import unparsed_enum_module
+import unparsed_module
+
+class EnumSubclass(unparsed_enum_module.UnparsedEnumClass):
+ A_VALUE = 36
+
+def all_should_be_the_same_relative_type(a: stubs_module_dependencies.sub.Type,
+ b: sub.Type,
+ c: Type):
+ ...
+
+def foo(type_three_levels_up: Root, unparsed_type: unparsed_module.UnparsedName, enum = RootEnum.A_VALUE) -> another_module.Another:
+ ...
--- /dev/null
+# This file isn't included in m.css parsing, so its types are unknown, but it's
+# still imported as a dependency and the type is correctly recognized as an
+# enum type
+
+import enum
+
+class UnparsedEnumClass(enum.Enum):
+ ...
+
+class UnparsedEnumSubclass(enum.Enum):
+ UNPARSED_VALUE = 1337
--- /dev/null
+# This file isn't included in m.css parsing, so its types are unknown, but it's
+# still imported as a dependency
+
+class UnparsedName:
+ ...
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>stubs_nested_classes.Class.Inner | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="stubs_nested_classes.html">stubs_nested_classes</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.html">Class</a>.<wbr/></span>Inner <span class="m-thin">class</span>
+ </h1>
+ <nav class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#staticmethods">Static methods</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="stubs_nested_classes.Class.Inner.OneMore.html" class="m-doc">OneMore</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="staticmethods">
+ <h2><a href="#staticmethods">Static methods</a></h2>
+ <dl class="m-doc">
+ <dt id="staticmethod">
+ <span class="m-doc-wrap-bumper">def <a href="#staticmethod" class="m-doc-self">staticmethod</a>(</span><span class="m-doc-wrap">)</span>
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>stubs_nested_classes.Class.InnerAnother.AndAnother.YetAnother | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="stubs_nested_classes.html">stubs_nested_classes</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.html">Class</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.InnerAnother.html">InnerAnother</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.InnerAnother.AndAnother.html">AndAnother</a>.<wbr/></span>YetAnother <span class="m-thin">class</span>
+ </h1>
+ <nav class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt id="DATA">
+ <a href="#DATA" class="m-doc-self">DATA</a>: float = 3
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>stubs_nested_classes.Class.InnerAnother.AndAnother | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="stubs_nested_classes.html">stubs_nested_classes</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.html">Class</a>.<wbr/></span><span class="m-breadcrumb"><a href="stubs_nested_classes.Class.InnerAnother.html">InnerAnother</a>.<wbr/></span>AndAnother <span class="m-thin">class</span>
+ </h1>
+ <nav class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#data">Data</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="stubs_nested_classes.Class.InnerAnother.AndAnother.YetAnother.html" class="m-doc">YetAnother</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="data">
+ <h2><a href="#data">Data</a></h2>
+ <dl class="m-doc">
+ <dt id="AND_DATA">
+ <a href="#AND_DATA" class="m-doc-self">AND_DATA</a>: str = 'b'
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>stubs_nested_classes.Class | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ <span class="m-breadcrumb"><a href="stubs_nested_classes.html">stubs_nested_classes</a>.<wbr/></span>Class <span class="m-thin">class</span>
+ </h1>
+ <nav class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ <li><a href="#properties">Properties</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="stubs_nested_classes.Class.Inner.html" class="m-doc">Inner</a></dt>
+ <dd></dd>
+ <dt>class <a href="stubs_nested_classes.Class.InnerAnother.html" class="m-doc">InnerAnother</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ <section id="properties">
+ <h2><a href="#properties">Properties</a></h2>
+ <dl class="m-doc">
+ <dt id="property">
+ <a href="#property" class="m-doc-self">property</a> <span class="m-label m-flat m-warning">get</span>
+ </dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>stubs_nested_classes | My Python Project</title>
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+ <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+ <div class="m-container">
+ <div class="m-row">
+ <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+ </div>
+ </div>
+</nav></header>
+<main><article>
+ <div class="m-container m-container-inflatable">
+ <div class="m-row">
+ <div class="m-col-l-10 m-push-l-1">
+ <h1>
+ stubs_nested_classes <span class="m-thin">module</span>
+ </h1>
+ <nav class="m-block m-default">
+ <h3>Contents</h3>
+ <ul>
+ <li>
+ Reference
+ <ul>
+ <li><a href="#classes">Classes</a></li>
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ <section id="classes">
+ <h2><a href="#classes">Classes</a></h2>
+ <dl class="m-doc">
+ <dt>class <a href="stubs_nested_classes.Class.html" class="m-doc">Class</a></dt>
+ <dd></dd>
+ <dt>class <a href="stubs_nested_classes.SomeOther.html" class="m-doc">SomeOther</a></dt>
+ <dd></dd>
+ </dl>
+ </section>
+ </div>
+ </div>
+ </div>
+</article></main>
+</body>
+</html>
--- /dev/null
+class Class:
+ class Inner:
+ class OneMore:
+ ...
+
+ @staticmethod
+ def staticmethod():
+ ...
+
+ class InnerAnother:
+ class AndAnother:
+ class YetAnother:
+ DATA: float = 3
+
+ AND_DATA: str = 'b'
+
+ @property
+ def property(self):
+ ...
+
+class SomeOther:
+ ...
--- /dev/null
+# This file is both an input and output, i.e. the generated stub should have
+# exactly the same spacing as the input.
+
+import enum
+
+class Enum1(enum.Enum):
+ VALUE1 = 0
+ VALUE2 = 1
+
+class Enum2(enum.Enum):
+ VALUE1 = 10
+ VALUE2 = 20
+ VALUE3 = 30
+
+class EnumEmpty(enum.Enum):
+ ...
+
+class Class1:
+ @staticmethod
+ def staticmethod1():
+ ...
+
+ @staticmethod
+ def staticmethod2():
+ ...
+
+ @staticmethod
+ def staticmethod3():
+ ...
+
+ @classmethod
+ def classmethod1(*args):
+ ...
+
+ @classmethod
+ def classmethod2(*args):
+ ...
+
+ @classmethod
+ def classmethod3(*args):
+ ...
+
+ def method1(self):
+ ...
+
+ def method2(self):
+ ...
+
+ def method3(self):
+ ...
+
+ def __dunder_method1__(self):
+ ...
+
+ def __dunder_method2__(self):
+ ...
+
+ def __dunder_method3__(self):
+ ...
+
+ @property
+ def property1(self):
+ ...
+
+ @property
+ def property2(self):
+ ...
+ @property2.setter
+ def property2(self, value):
+ ...
+
+ @property
+ def property3(self):
+ ...
+ @property3.deleter
+ def property3(self):
+ ...
+
+ @property
+ def property4(self):
+ ...
+ @property4.setter
+ def property4(self, value):
+ ...
+ @property4.deleter
+ def property4(self):
+ ...
+
+class Class2:
+ class InnerEnum1(enum.Enum):
+ VALUE1 = 0
+ VALUE2 = 1
+
+ class InnerEnum2(enum.Enum):
+ ...
+
+ class InnerClass1:
+ ...
+
+ class InnerClass2:
+ ...
+
+ INNER_DATA1: str = 'a'
+
+ INNER_DATA2: int = 3
+
+ INNER_DATA3: None = None
+
+class Class3:
+ class InnerClass:
+ class InnerInnerClass:
+ ...
+
+class ClassEmpty:
+ ...
+
+DATA1: str = 'b'
+
+DATA2: int = 7
+
+DATA3: float = 15.6
+
+def function1():
+ ...
+
+def function2():
+ ...
self.assertEqual(*self.actual_expected_contents('content_html_escape.Class.html'))
self.assertEqual(*self.actual_expected_contents('content_html_escape.pybind.html'))
+ def test_stubs(self):
+ self.run_python_stubs({
+ 'PYBIND11_COMPATIBILITY': True,
+ })
+
+ # Compared to the HTML output, *none* of these should have any HTML
+ # entities
+ self.assertEqual(*self.actual_expected_contents('content_html_escape/__init__.pyi'))
+ self.assertEqual(*self.actual_expected_contents('content_html_escape/pybind.pyi'))
+
@unittest.skip("Page names are currently not exposed to search and there's nothing else that would require escaping, nothing to test")
def test_search(self):
# Re-run everything with search enabled, the search data shouldn't be
self.assertEqual(*self.actual_expected_contents('classes.html'))
self.assertEqual(*self.actual_expected_contents('modules.html'))
+ def test_stubs(self):
+ sys.path.append(self.path)
+ self.run_python_stubs({
+ 'INPUT_MODULES': ['inspect_string', 'inspect_string.subpackage', 'inspect_string.subpackage.inner']
+ })
+
+ # Python 3.11 adds BaseException.add_note()
+ if sys.version_info >= (3, 11):
+ self.assertEqual(*self.actual_expected_contents('inspect_string/__init__.pyi'))
+ else:
+ self.assertEqual(*self.actual_expected_contents('inspect_string/__init__.pyi', 'inspect_string/__init__-310.pyi'))
+ self.assertEqual(*self.actual_expected_contents('inspect_string/subpackage/inner.pyi'))
+ self.assertEqual(*self.actual_expected_contents('inspect_string/another_module.pyi'))
+
class Object(BaseInspectTestCase):
def test(self):
# Reuse the stuff from inspect_string, but this time reference it via
self.assertEqual(*self.actual_expected_contents('classes.html', '../inspect_string/classes.html'))
self.assertEqual(*self.actual_expected_contents('modules.html', '../inspect_string/modules.html'))
+ def test_stubs(self):
+ # Reuse the stuff from inspect_string, but this time reference it via
+ # an object and not a string
+ sys.path.append(os.path.join(os.path.dirname(self.path), 'inspect_string'))
+ import inspect_string
+ import inspect_string.subpackage.inner
+ self.run_python_stubs({
+ 'INPUT_MODULES': [inspect_string, inspect_string.subpackage, inspect_string.subpackage.inner]
+ })
+
+ # Python 3.11 adds BaseException.add_note()
+ if sys.version_info >= (3, 11):
+ self.assertEqual(*self.actual_expected_contents('inspect_string/__init__.pyi', '../inspect_string/stubs/inspect_string/__init__.pyi'))
+ else:
+ self.assertEqual(*self.actual_expected_contents('inspect_string/__init__.pyi', '../inspect_string/stubs/inspect_string/__init__-310.pyi'))
+ self.assertEqual(*self.actual_expected_contents('inspect_string/subpackage/inner.pyi', '../inspect_string/stubs/inspect_string/subpackage/inner.pyi'))
+ self.assertEqual(*self.actual_expected_contents('inspect_string/another_module.pyi', '../inspect_string/stubs/inspect_string/another_module.pyi'))
+
class AllProperty(BaseInspectTestCase):
def test(self):
self.run_python()
else:
self.assertEqual(*self.actual_expected_contents('inspect_annotations.AContainer.html', 'inspect_annotations.AContainer-py36-38.html'))
+ def test_stubs(self):
+ self.run_python_stubs()
+ # TODO handle TypeVar correctly
+ if sys.version_info >= (3, 9):
+ self.assertEqual(*self.actual_expected_contents('inspect_annotations.pyi'))
+ elif sys.version_info >= (3, 7) and sys.version_info < (3, 9):
+ self.assertEqual(*self.actual_expected_contents('inspect_annotations.pyi', 'inspect_annotations-py37+38.pyi'))
+ else:
+ self.assertEqual(*self.actual_expected_contents('inspect_annotations.pyi', 'inspect_annotations-py36.pyi'))
+
class Builtin(BaseInspectTestCase):
def test(self):
self.run_python({
else:
self.assertEqual(*self.actual_expected_contents('inspect_builtin.BaseException.html', 'inspect_builtin.BaseException-310.html'))
+ def test_stubs(self):
+ self.run_python_stubs()
+
+ # Python 3.11 adds BaseException.add_note()
+ if sys.version_info >= (3, 11):
+ self.assertEqual(*self.actual_expected_contents('inspect_builtin.pyi'))
+ elif sys.version_info >= (3, 7):
+ self.assertEqual(*self.actual_expected_contents('inspect_builtin.pyi', 'inspect_builtin-310.pyi'))
+ else:
+ self.assertEqual(*self.actual_expected_contents('inspect_builtin.pyi', 'inspect_builtin-36.pyi'))
+
class NameMapping(BaseInspectTestCase):
def test(self):
self.run_python({
self.assertEqual(*self.actual_expected_contents('inspect_name_mapping.Class.html'))
self.assertEqual(*self.actual_expected_contents('inspect_name_mapping.submodule.html'))
+ def test_stubs(self):
+ self.run_python_stubs({
+ 'NAME_MAPPING': {
+ # There will not be any `import yay` or anything for this, the
+ # assumption is that the name mapping makes sense without
+ # having to do something extra
+ 'inspect_name_mapping._sub.bar._NameThatGetsOverridenExternally': 'yay.ThisGotOverridenExternally'
+ },
+ # So it looks like a regular Python file so I can verify the
+ # imports (KDevelop doesn't look for .pyi for imports)
+ 'STUB_EXTENSION': '.py'
+ })
+ # The stubs/ directory is implicitly prepended only for *.pyi files to
+ # make testing against the input itself possible, so here I have to do
+ # it manually.
+ self.assertEqual(*self.actual_expected_contents('inspect_name_mapping/__init__.py', 'stubs/inspect_name_mapping/__init__.py'))
+ self.assertEqual(*self.actual_expected_contents('inspect_name_mapping/submodule.py', 'stubs/inspect_name_mapping/submodule.py'))
+
class Recursive(BaseInspectTestCase):
def test(self):
self.run_python()
self.assertEqual(*self.actual_expected_contents('inspect_attrs.MyClassAutoAttribs.html', 'inspect_attrs.MyClassAutoAttribs-attrs193.html'))
self.assertEqual(*self.actual_expected_contents('inspect_attrs.MySlotClass.html', 'inspect_attrs.MySlotClass-attrs193.html'))
+ def test_stubs(self):
+ self.run_python_stubs({
+ 'PLUGINS': ['m.sphinx'],
+ 'INPUT_DOCS': ['docs.rst'],
+ 'ATTRS_COMPATIBILITY': True
+ })
+ self.assertEqual(*self.actual_expected_contents('inspect_attrs.pyi'))
+
class Underscored(BaseInspectTestCase):
def test(self):
self.run_python({
self.run_python({})
self.assertEqual(*self.actual_expected_contents('inspect_value_formatting.html'))
+ def test_stubs(self):
+ self.run_python_stubs()
+ self.assertEqual(*self.actual_expected_contents('inspect_value_formatting.pyi'))
+
class DuplicateClass(BaseInspectTestCase):
def test(self):
self.run_python({})
if pybind_signatures.MyClass26.is_pybind26:
self.assertEqual(*self.actual_expected_contents('pybind_signatures.MyClass26.html'))
+ def test_stubs(self):
+ sys.path.append(self.path)
+ import pybind_signatures
+ self.run_python_stubs({
+ # Nothing in false_positives that would affect stub generation and
+ # wouldn't be tested by the HTML output already
+ 'INPUT_MODULES': [pybind_signatures],
+ 'PYBIND11_COMPATIBILITY': True
+ })
+
+ # TODO handle writeonly properties correctly
+ if pybind_signatures.MyClass26.is_pybind26:
+ self.assertEqual(*self.actual_expected_contents('pybind_signatures/__init__.pyi'))
+ elif pybind_signatures.MyClass23.is_pybind23:
+ self.assertEqual(*self.actual_expected_contents('pybind_signatures/__init__.pyi', 'pybind_signatures/__init__-pybind25.pyi'))
+ else:
+ self.assertEqual(*self.actual_expected_contents('pybind_signatures/__init__.pyi', 'pybind_signatures/__init__-pybind22.pyi'))
+ self.assertEqual(*self.actual_expected_contents('pybind_signatures/just_overloads.pyi'))
+
class Enums(BaseInspectTestCase):
def test(self):
self.run_python({
})
self.assertEqual(*self.actual_expected_contents('pybind_enums.html'))
+ def test_stubs(self):
+ self.run_python_stubs({
+ 'PLUGINS': ['m.sphinx'],
+ 'INPUT_DOCS': ['docs.rst'],
+ 'PYBIND11_COMPATIBILITY': True,
+ })
+ self.assertEqual(*self.actual_expected_contents('pybind_enums.pyi'))
+
class Submodules(BaseInspectTestCase):
def test(self):
self.run_python({
})
self.assertEqual(*self.actual_expected_contents('pybind_submodules.html'))
+ # Nothing pybind-specific here that would affect stub generation and
+ # wouldn't be tested by the HTML output already
+
class SubmodulesPackage(BaseInspectTestCase):
def test(self):
self.run_python({
})
self.assertEqual(*self.actual_expected_contents('pybind_submodules_package.sub.html'))
+ # Nothing pybind-specific here that would affect stub generation and
+ # wouldn't be tested by the HTML output already
+
class NameMapping(BaseInspectTestCase):
def test(self):
self.run_python({
self.assertEqual(*self.actual_expected_contents('pybind_name_mapping.Class.html'))
self.assertEqual(*self.actual_expected_contents('pybind_name_mapping.submodule.html'))
+ def test_stubs(self):
+ self.run_python_stubs({
+ 'PYBIND11_COMPATIBILITY': True,
+ # So it looks like a regular Python file so I can verify the
+ # imports (KDevelop doesn't look for .pyi for imports)
+ 'STUB_EXTENSION': '.py'
+ })
+ # The stubs/ directory is implicitly prepended only for *.pyi files to
+ # make testing against the input itself possible, so here I have to do
+ # it manually.
+ self.assertEqual(*self.actual_expected_contents('pybind_name_mapping/__init__.py', 'stubs/pybind_name_mapping/__init__.py'))
+ self.assertEqual(*self.actual_expected_contents('pybind_name_mapping/submodule.py', 'stubs/pybind_name_mapping/submodule.py'))
+
class TypeLinks(BaseInspectTestCase):
def test(self):
sys.path.append(self.path)
--- /dev/null
+#
+# This file is part of m.css.
+#
+# Copyright © 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024
+# Vladimír Vondruš <mosra@centrum.cz>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+
+import os
+import sys
+import unittest
+
+from . import BaseInspectTestCase
+
+from _search import pretty_print, searchdata_filename
+from python import EntryType
+
+class CustomHeaderExtension(BaseInspectTestCase):
+ def test(self):
+ self.run_python_stubs({
+ 'STUB_HEADER': '#!/usr/bin/env python3\n\n# This is a custom header containing no trailing newline on its own.',
+ 'STUB_EXTENSION': '.custom.py'
+ })
+
+ # The stubs/ directory is implicitly prepended only for *.pyi files to
+ # make testing against the input itself possible, so here I have to do
+ # it manually.
+ self.assertEqual(*self.actual_expected_contents('stubs_custom_header_extension/__init__.custom.py', 'stubs/stubs_custom_header_extension/__init__.custom.py'))
+ self.assertEqual(*self.actual_expected_contents('stubs_custom_header_extension/sub.custom.py', 'stubs/stubs_custom_header_extension/__init__.custom.py'))
+
+class ModuleDependencies(BaseInspectTestCase):
+ def test(self):
+ sys.path.append(self.path)
+ self.run_python_stubs({
+ # unparsed_module explicitly not included
+ 'INPUT_MODULES': ['stubs_module_dependencies', 'stubs_module_dependencies.sub', 'stubs_module_dependencies.sub.inner', 'another_module'],
+ 'PYBIND11_COMPATIBILITY': True,
+ # So it looks like a regular Python file so I can verify the
+ # imports (KDevelop doesn't look for .pyi for imports)
+ 'STUB_EXTENSION': '.py'
+ })
+
+ # The stubs/ directory is implicitly prepended only for *.pyi files to
+ # make testing against the input itself possible, so here I have to do
+ # it manually.
+ self.assertEqual(*self.actual_expected_contents('stubs_module_dependencies/__init__.py', 'stubs/stubs_module_dependencies/__init__.py'))
+ self.assertEqual(*self.actual_expected_contents('stubs_module_dependencies/sub/inner.py', 'stubs/stubs_module_dependencies/sub/inner.py'))
+
+class NestedClasses(BaseInspectTestCase):
+ def test(self):
+ self.run_python_stubs({
+ 'STUB_EXTENSION': '.py'
+ })
+
+ # The output should be the same as the input, yes
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.py'))
+
+ def test_html(self):
+ self.run_python()
+
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.Inner.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.InnerAnother.AndAnother.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.InnerAnother.AndAnother.YetAnother.html'))
+
+ def test_both(self):
+ self.run_python({
+ 'OUTPUT_STUBS': os.path.join(self.path, 'output'),
+ 'STUB_HEADER': '',
+ 'STUB_EXTENSION': '.py'
+ })
+
+ # There shouldn't be any difference compared to running these separately
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.py'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.Inner.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.InnerAnother.AndAnother.html'))
+ self.assertEqual(*self.actual_expected_contents('stubs_nested_classes.Class.InnerAnother.AndAnother.YetAnother.html'))
+
+class Spacing(BaseInspectTestCase):
+ def test(self):
+ self.run_python_stubs({
+ 'STUB_HEADER': '# This file is both an input and output, i.e. the generated stub should have\n# exactly the same spacing as the input.',
+ 'STUB_EXTENSION': '.py'
+ })
+
+ # The output should be the same as the input, yes
+ # TODO make classmethod accept cls instead of *args once it's fixed
+ self.assertEqual(*self.actual_expected_contents('stubs_spacing.py'))
+
+ def test_empty_module(self):
+ sys.path.append(self.path)
+ self.run_python_stubs({
+ 'INPUT_MODULES': ['empty_module'],
+ 'STUB_EXTENSION': '.py'
+ })
+
+ # The output should be the same as the input, yes
+ self.assertEqual(*self.actual_expected_contents('empty_module.py'))