From e00dc40feadf7be9d5b1aec995d59cadb83b5b92 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 8 Apr 2020 11:54:28 +0200 Subject: [PATCH] Create ipasphinx package for Sphinx plugins Sphinx is extensible with plugins that can add new syntax, roles, directives, domains, and more. Signed-off-by: Christian Heimes Reviewed-By: Alexander Bokovoy --- Makefile.am | 2 +- configure.ac | 1 + doc/conf.py | 9 ++- doc/requirements.txt | 26 ++++++- freeipa.spec.in | 3 + ipalib/plugable.py | 19 +++-- ipalib/text.py | 8 ++ ipasphinx/Makefile.am | 1 + ipasphinx/__init__.py | 0 ipasphinx/ipabase.py | 173 ++++++++++++++++++++++++++++++++++++++++++ ipasphinx/setup.py | 21 +++++ 11 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 ipasphinx/Makefile.am create mode 100644 ipasphinx/__init__.py create mode 100644 ipasphinx/ipabase.py create mode 100644 ipasphinx/setup.py diff --git a/Makefile.am b/Makefile.am index 21101f30c..e2516351e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3,7 +3,7 @@ NULL = ACLOCAL_AMFLAGS = -I m4 if ENABLE_SERVER - IPASERVER_SUBDIRS = ipaserver + IPASERVER_SUBDIRS = ipaserver ipasphinx SERVER_SUBDIRS = daemons init install endif diff --git a/configure.ac b/configure.ac index 0f9f22ceb..49c6abb7a 100644 --- a/configure.ac +++ b/configure.ac @@ -598,6 +598,7 @@ AC_CONFIG_FILES([ ipalib/Makefile ipaplatform/Makefile ipapython/Makefile + ipasphinx/Makefile ipaserver/Makefile ipatests/Makefile ipatests/man/Makefile diff --git a/doc/conf.py b/doc/conf.py index cce236e62..3caaf69f2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -10,9 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +# insert parent directory with ipalib and ipasphinx +sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- @@ -35,10 +36,10 @@ master_doc = 'index' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'ipasphinx.ipabase', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', 'm2r', ] diff --git a/doc/requirements.txt b/doc/requirements.txt index 7c08f9987..6cf46a683 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,28 @@ -# m2r is not compatible with Sphinx 3.x +## m2r is not compatible with Sphinx 3.x yet sphinx < 3.0 -# convert markdown to rest +## convert markdown to rest m2r +## ipa dependencies +dnspython +jwcrypto +netaddr +qrcode +six +pyasn1 +pyasn1-modules +requests + +## C libraries with binary wheels +cffi +cryptography +lxml + +## C libraries without binaries wheels +# gssapi +# dbus-python +# python-ldap + +## No sufficient PyPI packages +# dogtag-pki diff --git a/freeipa.spec.in b/freeipa.spec.in index b2afc20a6..33d7a28d5 100755 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -819,6 +819,9 @@ make %{?_smp_mflags} check VERBOSE=yes LIBDIR=%{_libdir} %make_install +# don't package ipasphinx for now +rm -rf %{buildroot}%{python3_sitelib}/ipasphinx* + %if 0%{?with_ipatests} mv %{buildroot}%{_bindir}/ipa-run-tests %{buildroot}%{_bindir}/ipa-run-tests-%{python3_version} mv %{buildroot}%{_bindir}/ipa-test-config %{buildroot}%{_bindir}/ipa-test-config-%{python3_version} diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 3621f5ff6..d6d73a5e1 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -510,14 +510,17 @@ class API(ReadOnly): level = logging.INFO if self.env.debug: # pylint: disable=using-constant-test level = logging.DEBUG - try: - handler = logging.FileHandler(self.env.log) - except IOError as e: - logger.error('Cannot open log file %r: %s', self.env.log, e) - return - handler.setLevel(level) - handler.setFormatter(ipa_log_manager.Formatter(LOGGING_FORMAT_FILE)) - root_logger.addHandler(handler) + if self.env.log is not None: + try: + handler = logging.FileHandler(self.env.log) + except IOError as e: + logger.error('Cannot open log file %r: %s', self.env.log, e) + else: + handler.setLevel(level) + handler.setFormatter( + ipa_log_manager.Formatter(LOGGING_FORMAT_FILE) + ) + root_logger.addHandler(handler) def build_global_parser(self, parser=None, context=None): """ diff --git a/ipalib/text.py b/ipalib/text.py index 39067aba7..74f2cca66 100644 --- a/ipalib/text.py +++ b/ipalib/text.py @@ -304,6 +304,10 @@ class Gettext(LazyText): def format(self, *args, **kwargs): return unicode(self).format(*args, **kwargs) + def expandtabs(self, tabsize=8): + """Compatibility for sphinx prepare_docstring()""" + return str(self).expandtabs(tabsize) + @six.python_2_unicode_compatible class FixMe(Gettext): @@ -524,6 +528,10 @@ class ConcatenatedLazyText: else: return ConcatenatedLazyText(*[other] + self.components) + def expandtabs(self, tabsize=8): + """Compatibility for sphinx prepare_docstring()""" + return str(self).expandtabs(tabsize) + class GettextFactory: """ diff --git a/ipasphinx/Makefile.am b/ipasphinx/Makefile.am new file mode 100644 index 000000000..8be72b25d --- /dev/null +++ b/ipasphinx/Makefile.am @@ -0,0 +1 @@ +include $(top_srcdir)/Makefile.python.am diff --git a/ipasphinx/__init__.py b/ipasphinx/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ipasphinx/ipabase.py b/ipasphinx/ipabase.py new file mode 100644 index 000000000..5a0824c96 --- /dev/null +++ b/ipasphinx/ipabase.py @@ -0,0 +1,173 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# +"""IPA API initialization for Sphinx +""" +import os +import re +import sys + +from sphinx.util import progress_message +from sphinx.ext.autodoc import mock as autodoc_mock + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, os.pardir)) +VERSION_M4 = os.path.abspath(os.path.join(ROOT, "VERSION.m4")) + +if ROOT not in sys.path: + sys.path.insert(0, ROOT) + + +ipa_mock_imports = [ + # no binary wheels available + "dbus", + "gssapi", + "ldap", + "ldif", # python-ldap + "ldapurl", # python-ldap + # dogtag-pki is client-only + "pki", + # PyPI packages not available + "pyhbac", + "pysss", + "pysss_murmur", + "pysss_nss_idmap", + "samba", + "SSSDConfig", +] + + +def parse_version_m4(filename=VERSION_M4): + """Poor man's macro parser for VERSION.m4 + """ + def_re = re.compile(r"^define\(([\w]+)+,\s*(.*)\)\s*$") + defs = {} + + with open(filename) as f: + for line in f: + mo = def_re.match(line) + if mo is not None: + k, v = mo.groups() + try: + v = int(v) + except ValueError: + pass + defs[k] = v + + defs["IPA_NUM_VERSION"] = ( + "{IPA_VERSION_MAJOR:d}" + "{IPA_VERSION_MINOR:02d}" + "{IPA_VERSION_RELEASE:02d}" + ).format(**defs) + + defs["IPA_API_VERSION"] = ( + "{IPA_API_VERSION_MAJOR}.{IPA_API_VERSION_MINOR}" + ).format(**defs) + + if defs["IPA_VERSION_IS_GIT_SNAPSHOT"] == "yes": + defs["IPA_GIT_VERSION"] = ".dev" + else: + defs["IPA_GIT_VERSION"] = "" + + defs["IPA_VERSION"] = ( + "{IPA_VERSION_MAJOR}." + "{IPA_VERSION_MINOR}." + "{IPA_VERSION_RELEASE}" + "{IPA_VERSION_PRE_RELEASE}" + "{IPA_GIT_VERSION}" + ).format(**defs) + return defs + + +def fake_ipaython_version(defs): + """Fake ipapython.version module + + We don't want and cannot run autoconf on read the docs. Fake the auto- + generated ipapython.version module. + """ + + class FakeIpapythonVersion: + __name__ = "ipapython.version" + + VERSION = defs["IPA_VERSION"] + VENDOR_VERSION = defs["IPA_VERSION"] + NUM_VERSION = defs["IPA_NUM_VERSION"] + API_VERSION = defs["IPA_API_VERSION"] + DEFAULT_PLUGINS = frozenset() + + fake = FakeIpapythonVersion() + sys.modules[fake.__name__] = fake + + +def init_api( + context="doc", + domain="ipa.example", + server="server.ipa.example", + in_server=True, +): + import ipalib + + ipalib.api.bootstrap( + context=context, + in_server=in_server, + logdir=None, + log=None, + domain=domain, + realm=domain.upper(), + server=server, + ) + ipalib.api.finalize() + return ipalib.api + + +def inject_mock_imports(app, config): + """Add additional module mocks for ipaserver + """ + mock_imports = set(getattr(config, "autodoc_mock_imports", [])) + mock_imports.update(ipa_mock_imports) + config.autodoc_mock_imports = list(mock_imports) + + # ldap is a mocked package + # ensure that ipapython.dn still use ctypes wrappers for str2dn/dn2str + # otherwise api won't be able to initialize properly + import ipapython.dn + + assert ipapython.dn.str2dn("cn=ipa") == [[("cn", "ipa", 1)]] + + +def init_ipalib_api(app, config): + """Initialize ipalib.api + + 1. Parse VERSION.m4 + 2. Create fake ipapython.version module + 3. Initialize the API with mocked imports + """ + defs = parse_version_m4() + fake_ipaython_version(defs) + + with progress_message("initializing ipalib.api"): + with autodoc_mock(config.autodoc_mock_imports): + init_api( + context=config.ipa_context, + domain=config.ipa_domain, + server=config.ipa_server_fqdn, + in_server=config.ipa_in_server, + ) + + +def setup(app): + app.setup_extension("sphinx.ext.autodoc") + + app.add_config_value("ipa_context", "doc", "env") + app.add_config_value("ipa_domain", "ipa.example", "env") + app.add_config_value("ipa_server_fqdn", "server.ipa.example", "env") + app.add_config_value("ipa_in_server", True, "env") + + app.connect("config-inited", inject_mock_imports) + app.connect("config-inited", init_ipalib_api) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/ipasphinx/setup.py b/ipasphinx/setup.py new file mode 100644 index 000000000..6fee5263b --- /dev/null +++ b/ipasphinx/setup.py @@ -0,0 +1,21 @@ +# +# Copyright (C) 2020 FreeIPA Contributors see COPYING for license +# +"""Sphinx documentation plugins for IPA +""" +from os.path import abspath, dirname +import sys + +if __name__ == "__main__": + # include ../ for ipasetup.py + sys.path.append(dirname(dirname(abspath(__file__)))) + from ipasetup import ipasetup # noqa: E402 + + ipasetup( + name="ipasphinx", + doc=__doc__, + package_dir={"ipasphinx": ""}, + packages=["ipasphinx"], + # m2r is not compatible with Sphinx 3.x yet + install_requires=["ipaserver", "ipalib", "sphinx < 3.0", "m2r"], + )