Skip to content
This repository has been archived by the owner on Jan 13, 2024. It is now read-only.

Commit

Permalink
Fix #169, implements option debug for autosignature
Browse files Browse the repository at this point in the history
  • Loading branch information
sdpython committed Aug 5, 2018
1 parent fd5ba5a commit 5cae53e
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 30 deletions.
5 changes: 5 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ History
current - 2018-08-05 - 0.00Mb
=============================

* `169`: add option debug to autosignature (2018-08-05)

1.8.2702 - 2018-08-05 - 1.99Mb
==============================

* `168`: documentation does not produce a page for a compiled module in pure C++ (not with pybind11) (2018-08-05)
* `166`: fix github link when link points to a compile module (2018-08-05)
* `167`: autosignature fails for function implemented in pure C++ (not with pybind11) (2018-08-04)
Expand Down
16 changes: 16 additions & 0 deletions _unittests/ut_sphinxext/test_autosignature.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ def test_extract_signature(self):
self.assertEqual(len(res), 1)
self.assertEqual(res[0], exp)

def test_autosignature_class_onemethod2_debug(self):
this = os.path.abspath(os.path.dirname(__file__))
data = os.path.join(this, "datadoc")
sys.path.append(data)

newstring = ["AAAAAAAAAAAAAAAA",
"",
".. autosignature:: exdocassert2.onefunction",
" :debug:",
"",
"CCCCCCCCCCCCCCCC"]
newstring = "\n".join(newstring)
htmls = rst2html(newstring, writer="rst")
self.assertIn('[debug]', htmls)
self.assertIn('[import_any_object]', htmls)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion src/pyquickhelper/helpgen/sphinx_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ def lay_build_override_newconf(t3):
fLOG(" -pattern '{0}'".format(nbneg_pattern))
notebooks = explore_folder(notebook_dir, pattern=".*[.]ipynb", neg_pattern=nbneg_pattern,
fullname=True, fLOG=fLOG)[1]
notebooks = [_ for _ in notebooks if "checkpoint" not in _ and \
notebooks = [_ for _ in notebooks if "checkpoint" not in _ and
"/build/" not in _.replace("\\", "/")]
fLOG(" found {0} notebooks".format(len(notebooks)))
if len(notebooks) > 0:
Expand Down
3 changes: 2 additions & 1 deletion src/pyquickhelper/pycode/setup_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def process_standard_options_for_setup(argv, file_or_folder, project_var_name, m
if nbconvert_main is None:
raise AttributeError("nbconvert_main is None")
except AttributeError as e:
raise ImportError("Unable to import nbconvert, cannot generate the documentation") from e
raise ImportError(
"Unable to import nbconvert, cannot generate the documentation") from e

if "--help" in argv or "--help-commands" in argv:
process_standard_options_for_setup_help(argv)
Expand Down
48 changes: 36 additions & 12 deletions src/pyquickhelper/sphinxext/import_object_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,37 @@ def stat(self):
pass


def import_object(docname, kind, use_init=True) -> Tuple[object, str]:
def import_object(docname, kind, use_init=True, fLOG=None) -> Tuple[object, str]:
"""
Extracts an object defined by its name including the module name.
@param docname full name of the object
(example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``)
@param kind ``'function'`` or ``'class'`` or ``'kind'``
@param use_init return the constructor instead of the class
@return tuple(object, name)
@raises :epkg:`*py:RuntimeError` if cannot be imported,
:epkg:`*py:TypeError` if it is a method or a property,
:epkg:`*py:ValueError` if *kind* is unknown.
@param docname full name of the object
(example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``)
@param kind ``'function'`` or ``'class'`` or ``'kind'``
@param use_init return the constructor instead of the class
@param fLOG logging function
@return tuple(object, name)
@raises :epkg:`*py:RuntimeError` if cannot be imported,
:epkg:`*py:TypeError` if it is a method or a property,
:epkg:`*py:ValueError` if *kind* is unknown.
"""
spl = docname.split(".")
name = spl[-1]
if kind not in ("method", "property", "staticmethod"):
modname = ".".join(spl[:-1])
code = 'from {0} import {1}\nmyfunc = {1}'.format(modname, name)
codeobj = compile(code, 'conf{0}.py'.format(kind), 'exec')
if fLOG:
fLOG("[import_object] modname='{0}' code='{1}'".format(
modname, code))
else:
modname = ".".join(spl[:-2])
classname = spl[-2]
code = 'from {0} import {1}\nmyfunc = {1}'.format(modname, classname)
codeobj = compile(code, 'conf{0}2.py'.format(kind), 'exec')
if fLOG:
fLOG("[import_object] modname='{0}' code='{1}' classname='{2}'".format(
modname, code, classname))

context = {}
with warnings.catch_warnings():
Expand Down Expand Up @@ -115,13 +122,14 @@ def import_object(docname, kind, use_init=True) -> Tuple[object, str]:
return myfunc, name


def import_any_object(docname, use_init=True) -> Tuple[object, str, str]:
def import_any_object(docname, use_init=True, fLOG=None) -> Tuple[object, str, str]:
"""
Extracts an object defined by its name including the module name.
:param docname: full name of the object
(example: ``pyquickhelper.sphinxext.sphinx_docassert_extension.import_object``)
:param use_init: return the constructor instead of the class
:param fLOG: logging function
:returns: tuple(object, name, kind)
:raises: :epkg:`*py:ImportError` if unable to import
Expand All @@ -132,19 +140,30 @@ def import_any_object(docname, use_init=True) -> Tuple[object, str, str]:
excs = []
for kind in ("function", "method", "staticmethod", "property", "class"):
try:
myfunc, name = import_object(docname, kind, use_init=use_init)
myfunc, name = import_object(
docname, kind, use_init=use_init, fLOG=fLOG)
if fLOG:
fLOG(
"[import_any_object] ok '{0}' for '{1}' - use_unit={2}".format(kind, docname, use_init))
fLOG("[import_any_object] __doc__={0} __name__={1} __module__={2}".format(hasattr(myfunc, '__doc__'),
hasattr(myfunc, '__name__'), hasattr(myfunc, '__module__')))
fLOG("[import_any_object] name='{0}' - module='{1}'".format(
name, getattr(myfunc, '__module__', None)))
return myfunc, name, kind
except Exception as e:
# not this kind
excs.append((kind, e))
if fLOG:
fLOG(
"[import_any_object] not '{0}' for '{1}' - use_unit={2}".format(kind, docname, use_init))

sec = " ### ".join("{0}-{1}-{2}".format(k, type(e), e).replace("\n", " ")
for k, e in excs)
raise ImportError(
"Unable to import '{0}'. Exceptions met: {1}".format(docname, sec))


def import_path(obj, class_name=None):
def import_path(obj, class_name=None, fLOG=None):
"""
Determines the import path which is
the shortest way to import the function. In case the
Expand All @@ -156,6 +175,7 @@ def import_path(obj, class_name=None):
static method and functions. If not None, this parameter
should contain the name of the class which holds the static
method given in *obj*
:param fLOG: logging function
:returns: import path
:raises: :epkg:`*py:TypeError` if object is a property,
:epkg:`*py:RuntimeError` if cannot be imported
Expand Down Expand Up @@ -185,8 +205,12 @@ def import_path(obj, class_name=None):
try:
exec(codeobj, context, context)
found = path
if fLOG:
fLOG("[import_path] succeeds: '{0}'".format(code))
break
except Exception:
if fLOG:
fLOG("[import_path] fails: '{0}'".format(code))
continue

if found is None:
Expand Down
67 changes: 51 additions & 16 deletions src/pyquickhelper/sphinxext/sphinx_autosignature.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class AutoSignatureDirective(Directive):
submodules, *name* displays the last name,
*import* displays the shortest syntax to import it
(default).
* *debug*: diplays debug information
The signature is not always available for builtin functions
or :epkg:`C++` functions depending on the way to bind them to :epkg:`Python`.
Expand All @@ -112,6 +113,7 @@ class AutoSignatureDirective(Directive):
'nolink': directives.unchanged,
'members': directives.unchanged,
'path': directives.unchanged,
'debug': directives.unchanged,
}

has_content = True
Expand All @@ -128,10 +130,20 @@ def run(self):
opt_annotation = 'annotation' in self.options
opt_link = 'nolink' not in self.options
opt_members = self.options.get('members', None)
opt_debug = 'debug' in self.options
if opt_members in (None, '') and 'members' in self.options:
opt_members = "all"
opt_path = self.options.get('path', 'import')

if opt_debug:
keep_logged = []

def keep_logging(*els):
keep_logged.append(" ".join(str(_) for _ in els))
logging_function = keep_logging
else:
logging_function = None

try:
source, lineno = self.reporter.get_source_and_line(self.lineno)
except AttributeError:
Expand All @@ -140,11 +152,15 @@ def run(self):
# object name
object_name = " ".join(_.strip("\n\r\t ") for _ in self.content)
try:
obj, _, kind = import_any_object(object_name, use_init=False)
obj, _, kind = import_any_object(
object_name, use_init=False, fLOG=logging_function)
except ImportError as e:
mes = "[AutoSignature] unable to import '{0}' due to '{1}'".format(
object_name, e)
logger = logging.getLogger("AutoSignature")
logger.warning(
"[AutoSignature] unable to import '{0}' due to '{1}'".format(object_name, e))
logger.warning(mes)
if logging_function:
logging_function(mes)
if lineno is not None:
logger.warning(
' File "{0}", line {1}'.format(source, lineno))
Expand All @@ -170,7 +186,7 @@ def run(self):
anchor = object_name
elif kind == "staticmethod":
cl, fu = object_name.split(".")[-2:]
pimp = import_path(obj, class_name=cl)
pimp = import_path(obj, class_name=cl, fLOG=logging_function)
anchor = '{0}.{1}.{2}'.format(pimp, cl, fu)
else:
pimp = import_path(obj)
Expand All @@ -196,9 +212,12 @@ def run(self):
signature = inspect.signature(obj_sig)
parameters = signature.parameters
except TypeError as e:
mes = "[autosignature](1) unable to get signature of '{0}' - {1}.".format(
object_name, str(e).replace("\n", "\\n"))
logger = logging.getLogger("autosignature")
logger.warning(
"[autosignature](1) unable to get signature of '{0}' - {1}.".format(object_name, str(e).replace("\n", "\\n")))
logger.warning(mes)
if logging_function:
logging_function(mes)
signature = None
parameters = None
except ValueError as e:
Expand All @@ -207,16 +226,21 @@ def run(self):
doc = obj_sig.__doc__
sigs = set(enumerate_cleaned_signature(doc))
if len(sigs) == 0:
mes = "[autosignature](2) unable to get signature of '{0}' - {1}.".format(
object_name, str(e).replace("\n", "\\n"))
logger = logging.getLogger("autosignature")
logger.warning(
"[autosignature](2) unable to get signature of '{0}' - {1}.".format(object_name, str(e).replace("\n", "\\n")))
logger.warning(mes)
if logging_function:
logging_function(mes)
signature = None
parameters = None
elif len(sigs) > 1:
mes = "[autosignature](2) too many signatures for '{0}' - {1} - {2}.".format(
object_name, str(e).replace("\n", "\\n"), " *** ".join(sigs))
logger = logging.getLogger("autosignature")
logger.warning(
"[autosignature](2) too many signatures for '{0}' - {1} - {2}.".format(
object_name, str(e).replace("\n", "\\n"), " *** ".join(sigs)))
logger.warning(mes)
if logging_function:
logging_function(mes)
signature = None
parameters = None
else:
Expand All @@ -225,9 +249,12 @@ def run(self):
inspect.Signature, obj_sig, list(sigs)[0])
parameters = signature.parameters
except TypeError as e:
mes = "[autosignature](3) unable to get signature of '{0}' - {1}.".format(
object_name, str(e).replace("\n", "\\n"))
logger = logging.getLogger("autosignature")
logger.warning(
"[autosignature](3) unable to get signature of '{0}' - {1}.".format(object_name, str(e).replace("\n", "\\n")))
logger.warning(mes)
if logging_function:
logging_function(mes)
signature = None
parameters = None

Expand All @@ -250,9 +277,12 @@ def run(self):
# Documentation.
doc = obj.__doc__ # if kind != "class" else obj.__class__.__doc__
if doc is None:
mes = "[autosignature] docstring empty for '{0}'.".format(
object_name)
logger = logging.getLogger("autosignature")
logger.warning(
"[autosignature] docstring empty for '{0}'.".format(object_name))
logger.warning(mes)
if logging_function:
logging_function(mes)
else:
if "type(object_or_name, bases, dict)" in doc:
raise Exception("issue with {0}\n{1}".format(obj, doc))
Expand All @@ -266,7 +296,12 @@ def run(self):
map(lambda s: " " + s, docstring.split("\n")))
text += docstring + "\n\n"

st = StringList(text.split("\n"))
text_lines = text.split("\n")
if logging_function:
text_lines.extend([' ::', '', ' [debug]', ''])
text_lines.extend(' ' + li for li in keep_logged)
text_lines.append('')
st = StringList(text_lines)
nested_parse_with_titles(self.state, st, node)
return [node]

Expand Down

0 comments on commit 5cae53e

Please sign in to comment.