URL: https://github.com/freeipa/freeipa/pull/4549
Author: tiran
 Title: #4549: Create ipasphinx package for Sphinx plugins
Action: opened

PR body:
"""
Sphinx is extensible with plugins that can add new syntax, roles,
directives, domains, and more.

Signed-off-by: Christian Heimes <chei...@redhat.com>
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/4549/head:pr4549
git checkout pr4549
From 2ff05dfb16a2c58ea8587f3bd11a656271afa59e Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
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 <chei...@redhat.com>
---
 Makefile.am           |   2 +-
 configure.ac          |   1 +
 doc/__init__.py       |   0
 doc/conf.py           |   9 ++-
 doc/requirements.txt  |  27 ++++++-
 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 +++++
 12 files changed, 250 insertions(+), 14 deletions(-)
 create mode 100644 doc/__init__.py
 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 f52c8842a5..f7d9c0f96a 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 ae31b91486..c3c01e41ce 100644
--- a/configure.ac
+++ b/configure.ac
@@ -592,6 +592,7 @@ AC_CONFIG_FILES([
     ipalib/Makefile
     ipaplatform/Makefile
     ipapython/Makefile
+    ipasphinx/Makefile
     ipaserver/Makefile
     ipatests/Makefile
     ipatests/man/Makefile
diff --git a/doc/__init__.py b/doc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/doc/conf.py b/doc/conf.py
index 8d80b7186c..fa8aa342c6 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 @@
 # 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 c1c3de4445..6cf46a6835 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,3 +1,28 @@
-# convert markdown to rest
+## m2r is not compatible with Sphinx 3.x yet
+sphinx < 3.0
+
+## 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 7beed1ebc6..a931b1b84c 100755
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -814,6 +814,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 3621f5ff67..d6d73a5e19 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -510,14 +510,17 @@ def bootstrap(self, parser=None, **overrides):
         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 39067aba71..74f2cca660 100644
--- a/ipalib/text.py
+++ b/ipalib/text.py
@@ -304,6 +304,10 @@ def __mod__(self, kw):
     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 @@ def __radd__(self, other):
         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 0000000000..8be72b25da
--- /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 0000000000..e69de29bb2
diff --git a/ipasphinx/ipabase.py b/ipasphinx/ipabase.py
new file mode 100644
index 0000000000..5a0824c96d
--- /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 0000000000..6fee5263b5
--- /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"],
+    )
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: 
https://docs.fedoraproject.org/en-US/project/code-of-conduct/
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org

Reply via email to