Change-Id: I0a1bfae9996c1d866a200507b59f5c76753a1aca
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-09-11 16:37:14 +01:00
parent ec56b04001
commit a3e4d6e9f2
12 changed files with 245 additions and 154 deletions

View File

@@ -12,6 +12,12 @@ repos:
- id: debug-statements
- id: check-yaml
files: .*\.(yaml|yml)$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.0
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']
- id: ruff-format
- repo: https://opendev.org/openstack/hacking
rev: 7.0.0
hooks:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -33,7 +32,7 @@
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'openstackdocstheme'
'openstackdocstheme',
]
# Add any paths that contain templates here, relative to this directory.
@@ -50,7 +49,7 @@ master_doc = 'index'
# General information about the project.
project = 'microversion-parse'
copyright = '2016, OpenStack'
author = u'OpenStack'
author = 'OpenStack'
# openstackdocstheme options
repository_name = 'openstack/microversion-parse'
@@ -92,7 +91,7 @@ latex_documents = [
'microversion-parse.tex',
'microversion-parse Documentation',
'OpenStack',
'manual'
'manual',
),
]
@@ -104,9 +103,9 @@ man_pages = [
(
master_doc,
'microversion-parse',
u'microversion-parse Documentation',
'microversion-parse Documentation',
[author],
1
1,
)
]
@@ -120,11 +119,11 @@ texinfo_documents = [
(
master_doc,
'microversion-parse',
u'microversion-parse Documentation',
'microversion-parse Documentation',
author,
'microversion-parse',
'One line description of project.',
'Miscellaneous'
'Miscellaneous',
),
]

View File

@@ -26,17 +26,16 @@ class Version(collections.namedtuple('Version', 'major minor')):
def __new__(cls, major, minor):
"""Add min and max version attributes to the tuple."""
self = super(Version, cls).__new__(cls, major, minor)
self = super().__new__(cls, major, minor)
self.max_version = (-1, 0)
self.min_version = (-1, 0)
return self
def __str__(self):
return '%s.%s' % (self.major, self.minor)
return f'{self.major}.{self.minor}'
def matches(self, min_version=None, max_version=None):
"""Is this version within min_version and max_version.
"""
"""Is this version within min_version and max_version."""
# NOTE(cdent): min_version and max_version are expected
# to be set by the code that is creating the Version, if
# they are known.
@@ -155,7 +154,8 @@ def _extract_header_value(headers, header_name):
value = headers[header_name]
except KeyError:
wsgi_header_name = ENVIRON_HTTP_HEADER_FMT.format(
header_name.replace('-', '_'))
header_name.replace('-', '_')
)
value = headers[wsgi_header_name]
return value
@@ -173,11 +173,9 @@ def parse_version_string(version_string):
# ValueError, TypeError or AttributeError when the incoming
# data is poorly formed but will, however, naturally adapt to
# extraneous whitespace.
return Version(*(int(value) for value
in version_string.split('.', 1)))
return Version(*(int(value) for value in version_string.split('.', 1)))
except (ValueError, TypeError, AttributeError) as exc:
raise TypeError('invalid version string: %s; %s' % (
version_string, exc))
raise TypeError(f'invalid version string: {version_string}; {exc}')
def extract_version(headers, service_type, versions_list):
@@ -213,4 +211,4 @@ def extract_version(headers, service_type, versions_list):
# to administratively disable a version if we really need to.
if str(request_version) in versions_list:
return request_version
raise ValueError('Unacceptable version header: %s' % version_string)
raise ValueError(f'Unacceptable version header: {version_string}')

View File

@@ -18,7 +18,7 @@ import webob.dec
import microversion_parse
class MicroversionMiddleware(object):
class MicroversionMiddleware:
"""WSGI middleware for getting microversion info.
The application will get a WSGI environ with a
@@ -37,8 +37,9 @@ class MicroversionMiddleware(object):
Otherwise the application is called.
"""
def __init__(self, application, service_type, versions,
json_error_formatter=None):
def __init__(
self, application, service_type, versions, json_error_formatter=None
):
"""Create the WSGI middleware.
:param application: The application hosting the service.
@@ -51,7 +52,7 @@ class MicroversionMiddleware(object):
"""
self.application = application
self.service_type = service_type
self.microversion_environ = '%s.microversion' % service_type
self.microversion_environ = f'{service_type}.microversion'
self.versions = versions
self.json_error_formatter = json_error_formatter
@@ -59,21 +60,24 @@ class MicroversionMiddleware(object):
def __call__(self, req):
try:
microversion = microversion_parse.extract_version(
req.headers, self.service_type, self.versions)
req.headers, self.service_type, self.versions
)
# TODO(cdent): These error response are not formatted according to
# api-sig guidelines, unless a json_error_formatter is provided
# that can do it. For an example, see the placement service.
except ValueError as exc:
raise webob.exc.HTTPNotAcceptable(
('Invalid microversion: %(error)s') % {'error': exc},
json_formatter=self.json_error_formatter)
(f'Invalid microversion: {exc}'),
json_formatter=self.json_error_formatter,
)
except TypeError as exc:
raise webob.exc.HTTPBadRequest(
('Invalid microversion: %(error)s') % {'error': exc},
json_formatter=self.json_error_formatter)
(f'Invalid microversion: {exc}'),
json_formatter=self.json_error_formatter,
)
req.environ[self.microversion_environ] = microversion
microversion_header = '%s %s' % (self.service_type, microversion)
microversion_header = f'{self.service_type} {microversion}'
standard_header = microversion_parse.STANDARD_HEADER
try:

View File

@@ -18,9 +18,8 @@ import microversion_parse
class TestVersion(testtools.TestCase):
def setUp(self):
super(TestVersion, self).setUp()
super().setUp()
self.version = microversion_parse.Version(1, 5)
def test_version_is_tuple(self):
@@ -69,7 +68,6 @@ class TestVersion(testtools.TestCase):
class TestParseVersionString(testtools.TestCase):
def test_good_version(self):
version = microversion_parse.parse_version_string('1.1')
self.assertEqual((1, 1), version)
@@ -81,57 +79,68 @@ class TestParseVersionString(testtools.TestCase):
self.assertEqual(microversion_parse.Version(1, 1), version)
def test_non_numeric(self):
self.assertRaises(TypeError,
microversion_parse.parse_version_string,
'hello')
self.assertRaises(
TypeError, microversion_parse.parse_version_string, 'hello'
)
def test_mixed_alphanumeric(self):
self.assertRaises(TypeError,
microversion_parse.parse_version_string,
'1.a')
self.assertRaises(
TypeError, microversion_parse.parse_version_string, '1.a'
)
def test_too_many_numeric(self):
self.assertRaises(TypeError,
microversion_parse.parse_version_string,
'1.1.1')
self.assertRaises(
TypeError, microversion_parse.parse_version_string, '1.1.1'
)
def test_not_string(self):
self.assertRaises(TypeError,
microversion_parse.parse_version_string,
1.1)
self.assertRaises(
TypeError, microversion_parse.parse_version_string, 1.1
)
class TestExtractVersion(testtools.TestCase):
def setUp(self):
super(TestExtractVersion, self).setUp()
super().setUp()
self.headers = [
('OpenStack-API-Version', 'service1 1.2'),
('OpenStack-API-Version', 'service2 1.5'),
('OpenStack-API-Version', 'service3 latest'),
('OpenStack-API-Version', 'service4 2.5'),
]
self.version_list = ['1.1', '1.2', '1.3', '1.4',
'2.1', '2.2', '2.3', '2.4']
self.version_list = [
'1.1',
'1.2',
'1.3',
'1.4',
'2.1',
'2.2',
'2.3',
'2.4',
]
def test_simple_extract(self):
version = microversion_parse.extract_version(
self.headers, 'service1', self.version_list)
self.headers, 'service1', self.version_list
)
self.assertEqual((1, 2), version)
def test_default_min(self):
version = microversion_parse.extract_version(
self.headers, 'notlisted', self.version_list)
self.headers, 'notlisted', self.version_list
)
self.assertEqual((1, 1), version)
def test_latest(self):
version = microversion_parse.extract_version(
self.headers, 'service3', self.version_list)
self.headers, 'service3', self.version_list
)
self.assertEqual((2, 4), version)
def test_min_max_extract(self):
version = microversion_parse.extract_version(
self.headers, 'service1', self.version_list)
self.headers, 'service1', self.version_list
)
# below min
self.assertFalse(version.matches((1, 3)))
@@ -144,13 +153,24 @@ class TestExtractVersion(testtools.TestCase):
# explicit min
self.assertFalse(version.matches(min_version=(2, 3)))
# explicit both
self.assertTrue(version.matches(min_version=(0, 3),
max_version=(1, 5)))
self.assertTrue(
version.matches(min_version=(0, 3), max_version=(1, 5))
)
def test_version_disabled(self):
self.assertRaises(ValueError, microversion_parse.extract_version,
self.headers, 'service2', self.version_list)
self.assertRaises(
ValueError,
microversion_parse.extract_version,
self.headers,
'service2',
self.version_list,
)
def test_version_out_of_range(self):
self.assertRaises(ValueError, microversion_parse.extract_version,
self.headers, 'service4', self.version_list)
self.assertRaises(
ValueError,
microversion_parse.extract_version,
self.headers,
'service4',
self.version_list,
)

View File

@@ -17,7 +17,6 @@ import microversion_parse
class TestFoldHeaders(testtools.TestCase):
def test_dict_headers(self):
headers = {
'header-one': 'alpha',
@@ -39,20 +38,19 @@ class TestFoldHeaders(testtools.TestCase):
folded_headers = microversion_parse.fold_headers(headers)
self.assertEqual(2, len(folded_headers))
self.assertEqual(set(['header-one', 'header-two']),
set(folded_headers.keys()))
self.assertEqual(
set(['header-one', 'header-two']), set(folded_headers.keys())
)
self.assertEqual('alpha,gamma', folded_headers['header-one'])
def test_bad_headers(self):
headers = 'wow this is not a headers'
self.assertRaises(ValueError, microversion_parse.fold_headers,
headers)
self.assertRaises(ValueError, microversion_parse.fold_headers, headers)
# TODO(cdent): Test with request objects from frameworks.
class TestStandardHeader(testtools.TestCase):
def test_simple_match(self):
headers = {
'header-one': 'alpha',
@@ -88,8 +86,7 @@ class TestStandardHeader(testtools.TestCase):
'openstack-api-version': 'network 5.9 ',
'header-two': 'beta',
}
version = microversion_parse.check_standard_header(
headers, 'compute')
version = microversion_parse.check_standard_header(headers, 'compute')
self.assertEqual(None, version)
def test_match_multiple_services(self):
@@ -98,11 +95,11 @@ class TestStandardHeader(testtools.TestCase):
'openstack-api-version': 'network 5.9 ,compute 2.1,telemetry 7.8',
'header-two': 'beta',
}
version = microversion_parse.check_standard_header(
headers, 'compute')
version = microversion_parse.check_standard_header(headers, 'compute')
self.assertEqual('2.1', version)
version = microversion_parse.check_standard_header(
headers, 'telemetry')
headers, 'telemetry'
)
self.assertEqual('7.8', version)
def test_match_multiple_same_service(self):
@@ -111,13 +108,11 @@ class TestStandardHeader(testtools.TestCase):
'openstack-api-version': 'compute 5.9 ,compute 2.1,compute 7.8',
'header-two': 'beta',
}
version = microversion_parse.check_standard_header(
headers, 'compute')
version = microversion_parse.check_standard_header(headers, 'compute')
self.assertEqual('7.8', version)
class TestLegacyHeaders(testtools.TestCase):
def test_legacy_headers_straight(self):
headers = {
'header-one': 'alpha',
@@ -125,8 +120,10 @@ class TestLegacyHeaders(testtools.TestCase):
'header-two': 'beta',
}
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['openstack-CoMpUte-api-version'])
headers,
service_type='compute',
legacy_headers=['openstack-CoMpUte-api-version'],
)
self.assertEqual('2.1', version)
def test_legacy_headers_folded(self):
@@ -136,8 +133,10 @@ class TestLegacyHeaders(testtools.TestCase):
'header-two': 'beta',
}
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['openstack-compute-api-version'])
headers,
service_type='compute',
legacy_headers=['openstack-compute-api-version'],
)
self.assertEqual('9.2', version)
def test_older_legacy_headers(self):
@@ -147,9 +146,13 @@ class TestLegacyHeaders(testtools.TestCase):
'header-two': 'beta',
}
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['openstack-nova-api-version',
'x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=[
'openstack-nova-api-version',
'x-openstack-nova-api-version',
],
)
# We don't do x- for service types.
self.assertEqual('9.2', version)
@@ -161,19 +164,26 @@ class TestLegacyHeaders(testtools.TestCase):
'header-two': 'beta',
}
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['openstack-compute-api-version',
'x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=[
'openstack-compute-api-version',
'x-openstack-nova-api-version',
],
)
self.assertEqual('3.7', version)
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['x-openstack-nova-api-version',
'openstack-compute-api-version'])
headers,
service_type='compute',
legacy_headers=[
'x-openstack-nova-api-version',
'openstack-compute-api-version',
],
)
self.assertEqual('9.2', version)
class TestGetHeaders(testtools.TestCase):
def test_preference(self):
headers = {
'header-one': 'alpha',
@@ -183,15 +193,20 @@ class TestGetHeaders(testtools.TestCase):
'header-two': 'beta',
}
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['openstack-compute-api-version',
'x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=[
'openstack-compute-api-version',
'x-openstack-nova-api-version',
],
)
self.assertEqual('11.12', version)
def test_no_headers(self):
headers = {}
version = microversion_parse.get_version(
headers, service_type='compute')
headers, service_type='compute'
)
self.assertEqual(None, version)
def test_unfolded_service(self):
@@ -202,7 +217,8 @@ class TestGetHeaders(testtools.TestCase):
('openstack-api-version', '3.0'),
]
version = microversion_parse.get_version(
headers, service_type='compute')
headers, service_type='compute'
)
self.assertEqual('2.0', version)
def test_unfolded_in_name(self):
@@ -213,15 +229,17 @@ class TestGetHeaders(testtools.TestCase):
('openstack-telemetry-api-version', '3.0'),
]
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=['x-openstack-nova-api-version'],
)
self.assertEqual('2.0', version)
def test_capitalized_headers(self):
headers = {
'X-Openstack-Ironic-Api-Version': '123.456'
}
headers = {'X-Openstack-Ironic-Api-Version': '123.456'}
version = microversion_parse.get_version(
headers, service_type='ironic',
legacy_headers=['X-Openstack-Ironic-Api-Version'])
headers,
service_type='ironic',
legacy_headers=['X-Openstack-Ironic-Api-Version'],
)
self.assertEqual('123.456', version)

View File

@@ -17,13 +17,12 @@ import microversion_parse
class TestHeadersFromWSGIEnviron(testtools.TestCase):
def test_empty_environ(self):
environ = {}
expected = {}
self.assertEqual(
expected,
microversion_parse.headers_from_wsgi_environ(environ))
expected, microversion_parse.headers_from_wsgi_environ(environ)
)
def test_non_empty_no_headers(self):
environ = {'PATH_INFO': '/foo/bar'}
@@ -32,30 +31,40 @@ class TestHeadersFromWSGIEnviron(testtools.TestCase):
self.assertEqual(expected, found_headers)
def test_headers(self):
environ = {'PATH_INFO': '/foo/bar',
'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json'}
expected = {'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json'}
environ = {
'PATH_INFO': '/foo/bar',
'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json',
}
expected = {
'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json',
}
found_headers = microversion_parse.headers_from_wsgi_environ(environ)
self.assertEqual(expected, found_headers)
def test_get_version_from_environ(self):
environ = {'PATH_INFO': '/foo/bar',
'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json'}
environ = {
'PATH_INFO': '/foo/bar',
'HTTP_OPENSTACK_API_VERSION': 'placement 2.1',
'HTTP_CONTENT_TYPE': 'application/json',
}
expected_version = '2.1'
headers = microversion_parse.headers_from_wsgi_environ(environ)
version = microversion_parse.get_version(headers, 'placement')
self.assertEqual(expected_version, version)
def test_get_version_from_environ_legacy(self):
environ = {'PATH_INFO': '/foo/bar',
'HTTP_X_OPENSTACK_PLACEMENT_API_VERSION': '2.1',
'HTTP_CONTENT_TYPE': 'application/json'}
environ = {
'PATH_INFO': '/foo/bar',
'HTTP_X_OPENSTACK_PLACEMENT_API_VERSION': '2.1',
'HTTP_CONTENT_TYPE': 'application/json',
}
expected_version = '2.1'
headers = microversion_parse.headers_from_wsgi_environ(environ)
version = microversion_parse.get_version(
headers, 'placement',
legacy_headers=['x-openstack-placement-api-version'])
headers,
'placement',
legacy_headers=['x-openstack-placement-api-version'],
)
self.assertEqual(expected_version, version)

View File

@@ -32,7 +32,7 @@ VERSIONS = [
]
class SimpleWSGI(object):
class SimpleWSGI:
"""A WSGI application that can be contiained within a middlware."""
def __call__(self, environ, start_response):
@@ -41,12 +41,13 @@ class SimpleWSGI(object):
start_response('200 OK', [('content-type', 'text/plain')])
return [b'good']
raise webob.exc.HTTPNotFound('%s not found' % path_info)
raise webob.exc.HTTPNotFound(f'{path_info} not found')
def app():
app = middleware.MicroversionMiddleware(
SimpleWSGI(), SERVICE_TYPE, VERSIONS)
SimpleWSGI(), SERVICE_TYPE, VERSIONS
)
return app
@@ -54,4 +55,5 @@ def load_tests(loader, tests, pattern):
"""Provide a TestSuite to the discovery process."""
test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR)
return driver.build_tests(
test_dir, loader, test_loader_name=__name__, intercept=app)
test_dir, loader, test_loader_name=__name__, intercept=app
)

View File

@@ -22,59 +22,74 @@ class TestWebobHeaders(testtools.TestCase):
"""Webob uses a dict-like header which is not actually a dict."""
def test_simple_headers(self):
headers = wb_headers.EnvironHeaders({
'HTTP_HEADER_ONE': 'alpha',
'HTTP_HEADER_TWO': 'beta',
'HTTP_HEADER_THREE': 'gamma',
})
headers = wb_headers.EnvironHeaders(
{
'HTTP_HEADER_ONE': 'alpha',
'HTTP_HEADER_TWO': 'beta',
'HTTP_HEADER_THREE': 'gamma',
}
)
folded_headers = microversion_parse.fold_headers(headers)
self.assertEqual(3, len(folded_headers))
self.assertEqual(set(['header-one', 'header-three', 'header-two']),
set(folded_headers.keys()))
self.assertEqual(
set(['header-one', 'header-three', 'header-two']),
set(folded_headers.keys()),
)
self.assertEqual('gamma', folded_headers['header-three'])
def test_simple_match(self):
headers = wb_headers.EnvironHeaders({
'HTTP_HEADER_ONE': 'alpha',
'HTTP_OPENSTACK_API_VERSION': 'compute 2.1',
'HTTP_HEADER_TWO': 'beta',
})
headers = wb_headers.EnvironHeaders(
{
'HTTP_HEADER_ONE': 'alpha',
'HTTP_OPENSTACK_API_VERSION': 'compute 2.1',
'HTTP_HEADER_TWO': 'beta',
}
)
version = microversion_parse.check_standard_header(headers, 'compute')
self.assertEqual('2.1', version)
def test_match_multiple_services(self):
headers = wb_headers.EnvironHeaders({
'HTTP_HEADER_ONE': 'alpha',
'HTTP_OPENSTACK_API_VERSION':
'network 5.9 ,compute 2.1,telemetry 7.8',
'HTTP_HEADER_TWO': 'beta',
})
version = microversion_parse.check_standard_header(
headers, 'compute')
headers = wb_headers.EnvironHeaders(
{
'HTTP_HEADER_ONE': 'alpha',
'HTTP_OPENSTACK_API_VERSION': 'network 5.9 ,compute 2.1,telemetry 7.8',
'HTTP_HEADER_TWO': 'beta',
}
)
version = microversion_parse.check_standard_header(headers, 'compute')
self.assertEqual('2.1', version)
version = microversion_parse.check_standard_header(
headers, 'telemetry')
headers, 'telemetry'
)
self.assertEqual('7.8', version)
def test_legacy_headers_straight(self):
headers = wb_headers.EnvironHeaders({
'HTTP_HEADER_ONE': 'alpha',
'HTTP_X_OPENSTACK_NOVA_API_VERSION': ' 2.1 ',
'HTTP_HEADER_TWO': 'beta',
})
headers = wb_headers.EnvironHeaders(
{
'HTTP_HEADER_ONE': 'alpha',
'HTTP_X_OPENSTACK_NOVA_API_VERSION': ' 2.1 ',
'HTTP_HEADER_TWO': 'beta',
}
)
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=['x-openstack-nova-api-version'],
)
self.assertEqual('2.1', version)
def test_legacy_headers_folded(self):
headers = wb_headers.EnvironHeaders({
'HTTP_HEADER_ONE': 'alpha',
'HTTP_X_OPENSTACK_NOVA_API_VERSION': ' 2.1, 9.2 ',
'HTTP_HEADER_TWO': 'beta',
})
headers = wb_headers.EnvironHeaders(
{
'HTTP_HEADER_ONE': 'alpha',
'HTTP_X_OPENSTACK_NOVA_API_VERSION': ' 2.1, 9.2 ',
'HTTP_HEADER_TWO': 'beta',
}
)
version = microversion_parse.get_version(
headers, service_type='compute',
legacy_headers=['x-openstack-nova-api-version'])
headers,
service_type='compute',
legacy_headers=['x-openstack-nova-api-version'],
)
self.assertEqual('9.2', version)

View File

@@ -33,3 +33,20 @@ Repository = "https://opendev.org/openstack/microversion-parse"
packages = [
"microversion_parse"
]
[tool.ruff]
line-length = 79
[tool.ruff.format]
quote-style = "single"
docstring-code-format = true
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "S", "U"]
ignore = [
# we only use asserts for type narrowing
"S101",
]
[tool.ruff.lint.per-file-ignores]
"microversion_parse/tests/*" = ["S"]

View File

@@ -15,4 +15,5 @@ import setuptools
setuptools.setup(
setup_requires=['pbr'],
pbr=True)
pbr=True,
)

View File

@@ -48,6 +48,8 @@ whitelist_externals =
rm
[flake8]
ignore = H405,E126
# We only enable the hacking (H) checks
select = H
ignore = H405
exclude = .venv,.git,.tox,dist,*egg,*.egg-info,build,examples,doc
show-source = true