Skip to content

Commit

Permalink
Create ipasphinx package for Sphinx plugins
Browse files Browse the repository at this point in the history
Sphinx is extensible with plugins that can add new syntax, roles,
directives, domains, and more.

Signed-off-by: Christian Heimes <[email protected]>
Reviewed-By: Alexander Bokovoy <[email protected]>
  • Loading branch information
tiran committed Apr 28, 2020
1 parent 87408ee commit e00dc40
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ AC_CONFIG_FILES([
ipalib/Makefile
ipaplatform/Makefile
ipapython/Makefile
ipasphinx/Makefile
ipaserver/Makefile
ipatests/Makefile
ipatests/man/Makefile
Expand Down
9 changes: 5 additions & 4 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----------------------------------------------------
Expand All @@ -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',
]

Expand Down
26 changes: 24 additions & 2 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions freeipa.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
19 changes: 11 additions & 8 deletions ipalib/plugable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
8 changes: 8 additions & 0 deletions ipalib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down
1 change: 1 addition & 0 deletions ipasphinx/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include $(top_srcdir)/Makefile.python.am
Empty file added ipasphinx/__init__.py
Empty file.
173 changes: 173 additions & 0 deletions ipasphinx/ipabase.py
Original file line number Diff line number Diff line change
@@ -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,
}
21 changes: 21 additions & 0 deletions ipasphinx/setup.py
Original file line number Diff line number Diff line change
@@ -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"],
)

0 comments on commit e00dc40

Please sign in to comment.