added podman, json and yaml

This commit is contained in:
2022-11-27 19:11:46 +01:00
parent 01135dea09
commit 5226e858bb
790 changed files with 114578 additions and 16 deletions

View File

@ -0,0 +1,118 @@
from __future__ import annotations
import sys
import warnings
from distutils.errors import DistutilsOptionError
from os import path
from babel.messages import frontend as babel
from pkg_resources import EntryPoint
warnings.warn(
"mkdocs.commands.babel is never used in MkDocs and will be removed soon.", DeprecationWarning
)
DEFAULT_MAPPING_FILE = path.normpath(
path.join(path.abspath(path.dirname(__file__)), '../themes/babel.cfg')
)
class ThemeMixin:
def get_theme_dir(self):
"""Validate theme option and return path to theme's root obtained from entry point."""
entry_points = EntryPoint.parse_map(self.distribution.entry_points, self.distribution)
if 'mkdocs.themes' not in entry_points:
raise DistutilsOptionError("no mkdocs.themes are defined in entry_points")
if self.theme is None and len(entry_points['mkdocs.themes']) == 1:
# Default to the only theme defined in entry_points as none specified.
self.theme = tuple(entry_points['mkdocs.themes'].keys())[0]
if self.theme not in entry_points['mkdocs.themes']:
raise DistutilsOptionError("you must specify a valid theme name to work on")
theme = entry_points['mkdocs.themes'][self.theme]
return path.dirname(theme.resolve().__file__)
class compile_catalog(babel.compile_catalog, ThemeMixin):
user_options = babel.compile_catalog.user_options + [
("theme=", "t", "theme name to work on"),
]
def run(self):
# Possible bug in Babel - produces unused return value:
# https://github.com/python-babel/babel/blob/v2.10.3/babel/messages/frontend.py#L194
if super().run():
sys.exit(1)
def initialize_options(self):
super().initialize_options()
self.theme = None
def finalize_options(self):
if not self.directory:
theme_dir = self.get_theme_dir()
self.directory = f"{theme_dir}/locales"
super().finalize_options()
class extract_messages(babel.extract_messages, ThemeMixin):
user_options = babel.extract_messages.user_options + [
("domain=", "d", "domains of the POT output file"),
("theme=", "t", "theme name to work on"),
]
def initialize_options(self):
super().initialize_options()
self.domain = "messages"
self.theme = None
def finalize_options(self):
if not self.version:
version = self.distribution.get_version()
self.version = ".".join(i for i in version.split(".") if "dev" not in i)
if not self.mapping_file:
self.mapping_file = DEFAULT_MAPPING_FILE
if not self.input_paths or not self.output_file:
theme_dir = self.get_theme_dir()
if not self.input_paths:
self.input_paths = theme_dir
if not self.output_file:
self.output_file = f"{theme_dir}/{self.domain}.pot"
super().finalize_options()
class init_catalog(babel.init_catalog, ThemeMixin):
user_options = babel.init_catalog.user_options + [
("theme=", "t", "theme name to work on"),
]
def initialize_options(self):
super().initialize_options()
self.theme = None
def finalize_options(self):
if not self.input_file or not self.output_dir:
theme_dir = self.get_theme_dir()
if not self.input_file:
self.input_file = f"{theme_dir}/{self.domain}.pot"
if not self.output_dir:
self.output_dir = f"{theme_dir}/locales"
super().finalize_options()
class update_catalog(babel.update_catalog, ThemeMixin):
user_options = babel.update_catalog.user_options + [
("theme=", "t", "theme name to work on"),
]
def initialize_options(self):
super().initialize_options()
self.theme = None
def finalize_options(self):
if not self.input_file or not self.output_dir:
theme_dir = self.get_theme_dir()
if not self.input_file:
self.input_file = f"{theme_dir}/{self.domain}.pot"
if not self.output_dir:
self.output_dir = f"{theme_dir}/locales"
super().finalize_options()

View File

@ -0,0 +1,356 @@
from __future__ import annotations
import gzip
import logging
import os
import time
from typing import Any, Dict, Optional, Sequence, Set, Union
from urllib.parse import urlsplit
import jinja2
from jinja2.exceptions import TemplateNotFound
import mkdocs
from mkdocs import utils
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import Abort, BuildError
from mkdocs.structure.files import File, Files, get_files
from mkdocs.structure.nav import Navigation, get_navigation
from mkdocs.structure.pages import Page
class DuplicateFilter:
"""Avoid logging duplicate messages."""
def __init__(self) -> None:
self.msgs: Set[str] = set()
def __call__(self, record: logging.LogRecord) -> bool:
rv = record.msg not in self.msgs
self.msgs.add(record.msg)
return rv
log = logging.getLogger(__name__)
log.addFilter(DuplicateFilter())
def get_context(
nav: Navigation,
files: Union[Sequence[File], Files],
config: MkDocsConfig,
page: Optional[Page] = None,
base_url: str = '',
) -> Dict[str, Any]:
"""
Return the template context for a given page or template.
"""
if page is not None:
base_url = utils.get_relative_url('.', page.url)
extra_javascript = utils.create_media_urls(config.extra_javascript, page, base_url)
extra_css = utils.create_media_urls(config.extra_css, page, base_url)
if isinstance(files, Files):
files = files.documentation_pages()
return {
'nav': nav,
'pages': files,
'base_url': base_url,
'extra_css': extra_css,
'extra_javascript': extra_javascript,
'mkdocs_version': mkdocs.__version__,
'build_date_utc': utils.get_build_datetime(),
'config': config,
'page': page,
}
def _build_template(
name: str, template: jinja2.Template, files: Files, config: MkDocsConfig, nav: Navigation
) -> str:
"""
Return rendered output for given template as a string.
"""
# Run `pre_template` plugin events.
template = config.plugins.run_event('pre_template', template, template_name=name, config=config)
if utils.is_error_template(name):
# Force absolute URLs in the nav of error pages and account for the
# possibility that the docs root might be different than the server root.
# See https://github.com/mkdocs/mkdocs/issues/77.
# However, if site_url is not set, assume the docs root and server root
# are the same. See https://github.com/mkdocs/mkdocs/issues/1598.
base_url = urlsplit(config.site_url or '/').path
else:
base_url = utils.get_relative_url('.', name)
context = get_context(nav, files, config, base_url=base_url)
# Run `template_context` plugin events.
context = config.plugins.run_event(
'template_context', context, template_name=name, config=config
)
output = template.render(context)
# Run `post_template` plugin events.
output = config.plugins.run_event('post_template', output, template_name=name, config=config)
return output
def _build_theme_template(
template_name: str, env: jinja2.Environment, files: Files, config: MkDocsConfig, nav: Navigation
) -> None:
"""Build a template using the theme environment."""
log.debug(f"Building theme template: {template_name}")
try:
template = env.get_template(template_name)
except TemplateNotFound:
log.warning(f"Template skipped: '{template_name}' not found in theme directories.")
return
output = _build_template(template_name, template, files, config, nav)
if output.strip():
output_path = os.path.join(config.site_dir, template_name)
utils.write_file(output.encode('utf-8'), output_path)
if template_name == 'sitemap.xml':
log.debug(f"Gzipping template: {template_name}")
gz_filename = f'{output_path}.gz'
with open(gz_filename, 'wb') as f:
timestamp = utils.get_build_timestamp()
with gzip.GzipFile(
fileobj=f, filename=gz_filename, mode='wb', mtime=timestamp
) as gz_buf:
gz_buf.write(output.encode('utf-8'))
else:
log.info(f"Template skipped: '{template_name}' generated empty output.")
def _build_extra_template(template_name: str, files: Files, config: MkDocsConfig, nav: Navigation):
"""Build user templates which are not part of the theme."""
log.debug(f"Building extra template: {template_name}")
file = files.get_file_from_path(template_name)
if file is None:
log.warning(f"Template skipped: '{template_name}' not found in docs_dir.")
return
try:
with open(file.abs_src_path, encoding='utf-8', errors='strict') as f:
template = jinja2.Template(f.read())
except Exception as e:
log.warning(f"Error reading template '{template_name}': {e}")
return
output = _build_template(template_name, template, files, config, nav)
if output.strip():
utils.write_file(output.encode('utf-8'), file.abs_dest_path)
else:
log.info(f"Template skipped: '{template_name}' generated empty output.")
def _populate_page(page: Page, config: MkDocsConfig, files: Files, dirty: bool = False) -> None:
"""Read page content from docs_dir and render Markdown."""
try:
# When --dirty is used, only read the page if the file has been modified since the
# previous build of the output.
if dirty and not page.file.is_modified():
return
# Run the `pre_page` plugin event
page = config.plugins.run_event('pre_page', page, config=config, files=files)
page.read_source(config)
# Run `page_markdown` plugin events.
page.markdown = config.plugins.run_event(
'page_markdown', page.markdown, page=page, config=config, files=files
)
page.render(config, files)
# Run `page_content` plugin events.
page.content = config.plugins.run_event(
'page_content', page.content, page=page, config=config, files=files
)
except Exception as e:
message = f"Error reading page '{page.file.src_uri}':"
# Prevent duplicated the error message because it will be printed immediately afterwards.
if not isinstance(e, BuildError):
message += f" {e}"
log.error(message)
raise
def _build_page(
page: Page,
config: MkDocsConfig,
doc_files: Sequence[File],
nav: Navigation,
env: jinja2.Environment,
dirty: bool = False,
) -> None:
"""Pass a Page to theme template and write output to site_dir."""
try:
# When --dirty is used, only build the page if the file has been modified since the
# previous build of the output.
if dirty and not page.file.is_modified():
return
log.debug(f"Building page {page.file.src_uri}")
# Activate page. Signals to theme that this is the current page.
page.active = True
context = get_context(nav, doc_files, config, page)
# Allow 'template:' override in md source files.
if 'template' in page.meta:
template = env.get_template(page.meta['template'])
else:
template = env.get_template('main.html')
# Run `page_context` plugin events.
context = config.plugins.run_event(
'page_context', context, page=page, config=config, nav=nav
)
# Render the template.
output = template.render(context)
# Run `post_page` plugin events.
output = config.plugins.run_event('post_page', output, page=page, config=config)
# Write the output file.
if output.strip():
utils.write_file(
output.encode('utf-8', errors='xmlcharrefreplace'), page.file.abs_dest_path
)
else:
log.info(f"Page skipped: '{page.file.src_uri}'. Generated empty output.")
# Deactivate page
page.active = False
except Exception as e:
message = f"Error building page '{page.file.src_uri}':"
# Prevent duplicated the error message because it will be printed immediately afterwards.
if not isinstance(e, BuildError):
message += f" {e}"
log.error(message)
raise
def build(config: MkDocsConfig, live_server: bool = False, dirty: bool = False) -> None:
"""Perform a full site build."""
logger = logging.getLogger('mkdocs')
# Add CountHandler for strict mode
warning_counter = utils.CountHandler()
warning_counter.setLevel(logging.WARNING)
if config.strict:
logging.getLogger('mkdocs').addHandler(warning_counter)
try:
start = time.monotonic()
# Run `config` plugin events.
config = config.plugins.run_event('config', config)
# Run `pre_build` plugin events.
config.plugins.run_event('pre_build', config=config)
if not dirty:
log.info("Cleaning site directory")
utils.clean_directory(config.site_dir)
else: # pragma: no cover
# Warn user about problems that may occur with --dirty option
log.warning(
"A 'dirty' build is being performed, this will likely lead to inaccurate navigation and other"
" links within your site. This option is designed for site development purposes only."
)
if not live_server: # pragma: no cover
log.info(f"Building documentation to directory: {config.site_dir}")
if dirty and site_directory_contains_stale_files(config.site_dir):
log.info("The directory contains stale files. Use --clean to remove them.")
# First gather all data from all files/pages to ensure all data is consistent across all pages.
files = get_files(config)
env = config.theme.get_env()
files.add_files_from_theme(env, config)
# Run `files` plugin events.
files = config.plugins.run_event('files', files, config=config)
nav = get_navigation(files, config)
# Run `nav` plugin events.
nav = config.plugins.run_event('nav', nav, config=config, files=files)
log.debug("Reading markdown pages.")
for file in files.documentation_pages():
log.debug(f"Reading: {file.src_uri}")
assert file.page is not None
_populate_page(file.page, config, files, dirty)
# Run `env` plugin events.
env = config.plugins.run_event('env', env, config=config, files=files)
# Start writing files to site_dir now that all data is gathered. Note that order matters. Files
# with lower precedence get written first so that files with higher precedence can overwrite them.
log.debug("Copying static assets.")
files.copy_static_files(dirty=dirty)
for template in config.theme.static_templates:
_build_theme_template(template, env, files, config, nav)
for template in config.extra_templates:
_build_extra_template(template, files, config, nav)
log.debug("Building markdown pages.")
doc_files = files.documentation_pages()
for file in doc_files:
assert file.page is not None
_build_page(file.page, config, doc_files, nav, env, dirty)
# Run `post_build` plugin events.
config.plugins.run_event('post_build', config=config)
counts = warning_counter.get_counts()
if counts:
msg = ', '.join(f'{v} {k.lower()}s' for k, v in counts)
raise Abort(f'\nAborted with {msg} in strict mode!')
log.info('Documentation built in %.2f seconds', time.monotonic() - start)
except Exception as e:
# Run `build_error` plugin events.
config.plugins.run_event('build_error', error=e)
if isinstance(e, BuildError):
log.error(str(e))
raise Abort('\nAborted with a BuildError!')
raise
finally:
logger.removeHandler(warning_counter)
def site_directory_contains_stale_files(site_directory: str) -> bool:
"""Check if the site directory contains stale files from a previous build."""
return True if os.path.exists(site_directory) and os.listdir(site_directory) else False

View File

@ -0,0 +1,167 @@
from __future__ import annotations
import logging
import os
import re
import subprocess
from typing import Optional, Tuple, Union
import ghp_import
from packaging import version
import mkdocs
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import Abort
log = logging.getLogger(__name__)
default_message = """Deployed {sha} with MkDocs version: {version}"""
def _is_cwd_git_repo() -> bool:
try:
proc = subprocess.Popen(
['git', 'rev-parse', '--is-inside-work-tree'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except FileNotFoundError:
log.error("Could not find git - is it installed and on your path?")
raise Abort('Deployment Aborted!')
proc.communicate()
return proc.wait() == 0
def _get_current_sha(repo_path) -> str:
proc = subprocess.Popen(
['git', 'rev-parse', '--short', 'HEAD'],
cwd=repo_path or None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, _ = proc.communicate()
sha = stdout.decode('utf-8').strip()
return sha
def _get_remote_url(remote_name: str) -> Union[Tuple[str, str], Tuple[None, None]]:
# No CNAME found. We will use the origin URL to determine the GitHub
# pages location.
remote = f"remote.{remote_name}.url"
proc = subprocess.Popen(
["git", "config", "--get", remote],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, _ = proc.communicate()
url = stdout.decode('utf-8').strip()
if 'github.com/' in url:
host, path = url.split('github.com/', 1)
elif 'github.com:' in url:
host, path = url.split('github.com:', 1)
else:
return None, None
return host, path
def _check_version(branch: str) -> None:
proc = subprocess.Popen(
['git', 'show', '-s', '--format=%s', f'refs/heads/{branch}'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, _ = proc.communicate()
msg = stdout.decode('utf-8').strip()
m = re.search(r'\d+(\.\d+)+((a|b|rc)\d+)?(\.post\d+)?(\.dev\d+)?', msg, re.X | re.I)
previousv = version.parse(m.group()) if m else None
currentv = version.parse(mkdocs.__version__)
if not previousv:
log.warning('Version check skipped: No version specified in previous deployment.')
elif currentv > previousv:
log.info(
f'Previous deployment was done with MkDocs version {previousv}; '
f'you are deploying with a newer version ({currentv})'
)
elif currentv < previousv:
log.error(
f'Deployment terminated: Previous deployment was made with MkDocs version {previousv}; '
f'you are attempting to deploy with an older version ({currentv}). Use --ignore-version '
'to deploy anyway.'
)
raise Abort('Deployment Aborted!')
def gh_deploy(
config: MkDocsConfig,
message: Optional[str] = None,
force=False,
no_history=False,
ignore_version=False,
shell=False,
) -> None:
if not _is_cwd_git_repo():
log.error('Cannot deploy - this directory does not appear to be a git repository')
remote_branch = config.remote_branch
remote_name = config.remote_name
if not ignore_version:
_check_version(remote_branch)
if message is None:
message = default_message
sha = _get_current_sha(os.path.dirname(config.config_file_path))
message = message.format(version=mkdocs.__version__, sha=sha)
log.info(
"Copying '%s' to '%s' branch and pushing to GitHub.",
config.site_dir,
config.remote_branch,
)
try:
ghp_import.ghp_import(
config.site_dir,
mesg=message,
remote=remote_name,
branch=remote_branch,
push=True,
force=force,
use_shell=shell,
no_history=no_history,
nojekyll=True,
)
except ghp_import.GhpError as e:
log.error(f"Failed to deploy to GitHub with error: \n{e.message}")
raise Abort('Deployment Aborted!')
cname_file = os.path.join(config.site_dir, 'CNAME')
# Does this repository have a CNAME set for GitHub pages?
if os.path.isfile(cname_file):
# This GitHub pages repository has a CNAME configured.
with open(cname_file) as f:
cname_host = f.read().strip()
log.info(
f'Based on your CNAME file, your documentation should be '
f'available shortly at: http://{cname_host}'
)
log.info(
'NOTE: Your DNS records must be configured appropriately for your CNAME URL to work.'
)
return
host, path = _get_remote_url(remote_name)
if host is None or path is None:
# This could be a GitHub Enterprise deployment.
log.info('Your documentation should be available shortly.')
else:
username, repo = path.split('/', 1)
if repo.endswith('.git'):
repo = repo[: -len('.git')]
url = f'https://{username}.github.io/{repo}/'
log.info(f"Your documentation should shortly be available at: {url}")

View File

@ -0,0 +1,53 @@
from __future__ import annotations
import logging
import os
config_text = 'site_name: My Docs\n'
index_text = """# Welcome to MkDocs
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
## Commands
* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs -h` - Print help message and exit.
## Project layout
mkdocs.yml # The configuration file.
docs/
index.md # The documentation homepage.
... # Other markdown pages, images and other files.
"""
log = logging.getLogger(__name__)
def new(output_dir: str) -> None:
docs_dir = os.path.join(output_dir, 'docs')
config_path = os.path.join(output_dir, 'mkdocs.yml')
index_path = os.path.join(docs_dir, 'index.md')
if os.path.exists(config_path):
log.info('Project already exists.')
return
if not os.path.exists(output_dir):
log.info(f'Creating project directory: {output_dir}')
os.mkdir(output_dir)
log.info(f'Writing config file: {config_path}')
with open(config_path, 'w', encoding='utf-8') as f:
f.write(config_text)
if os.path.exists(index_path):
return
log.info(f'Writing initial docs: {index_path}')
if not os.path.exists(docs_dir):
os.mkdir(docs_dir)
with open(index_path, 'w', encoding='utf-8') as f:
f.write(index_text)

View File

@ -0,0 +1,130 @@
from __future__ import annotations
import functools
import logging
import shutil
import tempfile
from os.path import isdir, isfile, join
from typing import Optional
from urllib.parse import urlsplit
import jinja2.exceptions
from mkdocs.commands.build import build
from mkdocs.config import load_config
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.exceptions import Abort
from mkdocs.livereload import LiveReloadServer
log = logging.getLogger(__name__)
def serve(
config_file=None,
dev_addr=None,
strict=None,
theme=None,
theme_dir=None,
livereload='livereload',
watch_theme=False,
watch=[],
**kwargs,
):
"""
Start the MkDocs development server
By default it will serve the documentation on http://localhost:8000/ and
it will rebuild the documentation and refresh the page automatically
whenever a file is edited.
"""
# Create a temporary build directory, and set some options to serve it
# PY2 returns a byte string by default. The Unicode prefix ensures a Unicode
# string is returned. And it makes MkDocs temp dirs easier to identify.
site_dir = tempfile.mkdtemp(prefix='mkdocs_')
def mount_path(config: MkDocsConfig):
return urlsplit(config.site_url or '/').path
get_config = functools.partial(
load_config,
config_file=config_file,
dev_addr=dev_addr,
strict=strict,
theme=theme,
theme_dir=theme_dir,
site_dir=site_dir,
**kwargs,
)
live_server = livereload in ('dirty', 'livereload')
dirty = livereload == 'dirty'
def builder(config: Optional[MkDocsConfig] = None):
log.info("Building documentation...")
if config is None:
config = get_config()
# combine CLI watch arguments with config file values
if config.watch is None:
config.watch = watch
else:
config.watch.extend(watch)
# Override a few config settings after validation
config.site_url = f'http://{config.dev_addr}{mount_path(config)}'
build(config, live_server=live_server, dirty=dirty)
config = get_config()
config['plugins'].run_event('startup', command='serve', dirty=dirty)
try:
# Perform the initial build
builder(config)
host, port = config.dev_addr
server = LiveReloadServer(
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config)
)
def error_handler(code) -> Optional[bytes]:
if code in (404, 500):
error_page = join(site_dir, f'{code}.html')
if isfile(error_page):
with open(error_page, 'rb') as f:
return f.read()
return None
server.error_handler = error_handler
if live_server:
# Watch the documentation files, the config file and the theme files.
server.watch(config.docs_dir)
server.watch(config.config_file_path)
if watch_theme:
for d in config.theme.dirs:
server.watch(d)
# Run `serve` plugin events.
server = config.plugins.run_event('serve', server, config=config, builder=builder)
for item in config.watch:
server.watch(item)
try:
server.serve()
except KeyboardInterrupt:
log.info("Shutting down...")
finally:
server.shutdown()
except jinja2.exceptions.TemplateError:
# This is a subclass of OSError, but shouldn't be suppressed.
raise
except OSError as e: # pragma: no cover
# Avoid ugly, unhelpful traceback
raise Abort(f'{type(e).__name__}: {e}')
finally:
config['plugins'].run_event('shutdown')
if isdir(site_dir):
shutil.rmtree(site_dir)

View File

@ -0,0 +1,22 @@
import warnings
warnings.warn(
"mkdocs.commands.setup is never used in MkDocs and will be removed soon.", DeprecationWarning
)
try:
from mkdocs.commands.babel import (
compile_catalog,
extract_messages,
init_catalog,
update_catalog,
)
babel_cmdclass = {
'compile_catalog': compile_catalog,
'extract_messages': extract_messages,
'init_catalog': init_catalog,
'update_catalog': update_catalog,
}
except ImportError:
babel_cmdclass = {}