PK!&mudkip/__init__.py__version__ = "0.1.7" PK!6%%mudkip/__main__.pyfrom mudkip.cli import main main() PK!uumudkip/application.pyimport sys import time import shutil from os import path from io import StringIO from contextlib import contextmanager, nullcontext import tomlkit from tomlkit.toml_file import TOMLFile as BaseTOMLFile from sphinx.application import Sphinx from sphinx.errors import SphinxError from sphinx.util import logging from recommonmark.transform import AutoStructify from .config import Config from .errors import MudkipError from .watch import DirectoryWatcher class TOMLFile(BaseTOMLFile): def exists(self): return path.isfile(self._path) class Mudkip: def __init__( self, *args, config=None, pyproject_file="pyproject.toml", mudkip_file="mudkip.toml", **kwargs, ): pyproject = TOMLFile(pyproject_file) mudkip = TOMLFile(mudkip_file) if config is None: params = {} if pyproject.exists(): tool = pyproject.read().get("tool", {}) params.update(tool.get("mudkip", {}), poetry=tool.get("poetry")) if mudkip.exists(): params.update(mudkip.read().get("mudkip", {})) params.update(kwargs) config = Config(*args, **params) self.config = config self.pyproject = pyproject self.mudkip = mudkip self.create_sphinx_application() self.configure_sphinx() def create_sphinx_application(self): extra_args = {} if not self.config.verbose: extra_args["status"] = None self.sphinx = Sphinx( self.config.sphinx_srcdir, self.config.sphinx_confdir, self.config.sphinx_outdir, self.config.sphinx_doctreedir, self.config.sphinx_buildername, self.config.sphinx_confoverrides, **extra_args, ) def configure_sphinx(self): conf = self.sphinx.config conf.project = self.config.sphinx_project conf.copyright = time.strftime("%Y") if self.config.author: conf.author = self.config.author conf.copyright += ", " + conf.author if self.config.copyright: conf.copyright = self.config.copyright conf.version = self.config.version conf.release = self.config.release conf.master_doc = "index" conf.exclude_patterns = [".*", "**/.*", "_*", "**/_*"] conf.nitpicky = True self.sphinx.setup_extension("mudkip.extension") self.sphinx.setup_extension("recommonmark") recommonmark_config = { "enable_auto_toc_tree": True, "enable_math": True, "enable_inline_math": True, "enable_eval_rst": True, } self.sphinx.add_config_value("recommonmark_config", recommonmark_config, "env") self.sphinx.add_transform(AutoStructify) self.sphinx.setup_extension("sphinx.ext.autodoc") self.sphinx.setup_extension("sphinx.ext.napoleon") self.sphinx.setup_extension("sphinx.ext.doctest") self.sphinx.setup_extension("sphinx_autodoc_typehints") @contextmanager def sphinx_warning_is_error(self): try: original_value = self.sphinx.warningiserror self.sphinx.warningiserror = True yield finally: self.sphinx.warningiserror = original_value @contextmanager def sphinx_builder(self, buildername): try: original_builder = self.sphinx.builder self.sphinx.preload_builder(buildername) self.sphinx.builder = self.sphinx.create_builder(buildername) self.sphinx._init_builder() yield finally: self.sphinx.builder = original_builder @contextmanager def sphinx_mute(self): try: original_status = self.sphinx._status original_warning = self.sphinx._warning self.sphinx._status = StringIO() self.sphinx._warning = StringIO() logging.setup(self.sphinx, self.sphinx._status, self.sphinx._warning) yield finally: self.sphinx._status = original_status self.sphinx._warning = original_warning logging.setup(self.sphinx, self.sphinx._status, self.sphinx._warning) def init(self, title=None): table = tomlkit.table() table["title"] = title = ( title or self.config.title or path.basename(path.abspath(".")) ) table["preset"] = self.config.preset.name source_dir = str(self.config.source_dir) output_dir = str(self.config.output_dir) if source_dir != self.config.default_source_dir: table["source_dir"] = source_dir if output_dir != self.config.default_output_dir: table["output_dir"] = output_dir table.add(tomlkit.nl()) if self.mudkip.exists(): doc = self.mudkip.read() if "mudkip" not in doc: doc["mudkip"] = table else: doc["mudkip"].update(table) self.mudkip.write(doc) elif self.pyproject.exists(): doc = self.pyproject.read() tool = None try: tool = doc["tool"] if "mudkip" not in tool: tool._insert_after("poetry", "mudkip", table) else: tool["mudkip"].update(table) except KeyError: if tool is None: doc["tool"] = {"mudkip": table} else: tool["mudkip"] = table self.pyproject.write(doc) else: self.mudkip.write(tomlkit.document().add("mudkip", table)) index_rst = self.config.source_dir / "index.rst" index_md = self.config.source_dir / "index.md" if not index_rst.is_file() and not index_md.is_file(): index_rst.write_text(f"{title}\n{'=' * len(title)}\n") def build(self, *, check=False, skip_broken_links=False): try: self.delete_autodoc_cache() if check: self.clean() with self.sphinx_warning_is_error(): self.sphinx.build() if not skip_broken_links: with self.sphinx_builder("linkcheck"): self.sphinx.build() else: self.sphinx.build() except SphinxError as exc: raise MudkipError(exc.args[0]) from exc def delete_autodoc_cache(self): if not self.config.project_name: return modules = [ mod for mod in sys.modules if mod == self.config.project_name or mod.startswith(self.config.project_name + ".") ] for mod in modules: del sys.modules[mod] def develop(self, host="127.0.0.1", port=5500, build_manager=None): patterns = [f"*{suff}" for suff in self.sphinx.config.source_suffix] ignore_patterns = self.sphinx.config.exclude_patterns dirs = [self.config.source_dir] if self.config.project_dir: dirs.append(self.config.project_dir) patterns.append("*.py") with self.config.dev_server(self.sphinx.outdir, host, port): for event_batch in DirectoryWatcher(dirs, patterns, ignore_patterns): with build_manager(event_batch) if build_manager else nullcontext(): self.build() def test(self): with self.sphinx_builder("doctest"): with nullcontext() if self.config.verbose else self.sphinx_mute(): self.build() output = self.config.sphinx_outdir / "output.txt" content = output.read_text() if output.is_file() else "" _, _, result = content.partition("\n\n") return self.sphinx.statuscode == 0, result.strip() def clean(self): shutil.rmtree(self.config.output_dir) PK!ȴ9qq mudkip/cli.pyimport sys import time from os import path from functools import wraps from contextlib import contextmanager from traceback import format_exc import click from . import __version__ from .application import Mudkip from .config import Config from .errors import MudkipError from .preset import Preset from .server import noop_dev_server DIRECTORY = click.Path(file_okay=False) @contextmanager def exception_handler(exit=False): try: yield except Exception as exc: error = exc.args[0] if isinstance(exc, MudkipError) else format_exc() click.secho(error, fg="red", bold=True) if exit: sys.exit(1) def print_version(ctx, _param, value): if not value or ctx.resilient_parsing: return click.secho(f"Mudkip v{__version__}", fg="blue") ctx.exit() @click.group() @click.option( "--version", is_flag=True, is_eager=True, expose_value=False, callback=print_version, help="Show the version and exit.", ) def mudkip(): """A friendly Sphinx wrapper.""" def with_application(command): @click.option( "--preset", type=click.Choice(list(Preset.registry)), help="Documentation preset.", ) @click.option("--source-dir", type=DIRECTORY, help="The source directory.") @click.option("--output-dir", type=DIRECTORY, help="The output directory.") @click.option("--verbose", is_flag=True, help="Show Sphinx output.") @wraps(command) def wrapper(preset, source_dir, output_dir, verbose, *args, **kwargs): params = dict( preset=preset, source_dir=source_dir, output_dir=output_dir, verbose=verbose ) for key, value in tuple(params.items()): if not value: del params[key] return command(*args, application=Mudkip(**params), **kwargs) return wrapper @mudkip.command() @click.option("--title", help="Documentation title.") @with_application def init(application, title): """Initialize documentation.""" padding = "\n" * application.config.verbose click.secho( f'{padding}Initializing "{application.config.source_dir}"...', fg="blue" ) with exception_handler(exit=True): application.init(title) click.secho("\nDone.", fg="yellow") @mudkip.command() @click.option("--check", is_flag=True, help="Check documentation.") @click.option( "--skip-broken-links", is_flag=True, help="Do not check external links for integrity.", ) @with_application def build(application, check, skip_broken_links): """Build documentation.""" padding = "\n" * application.config.verbose action = "Building and checking" if check else "Building" click.secho( f'{padding}{action} "{application.config.source_dir}"...{padding}', fg="blue" ) with exception_handler(exit=True): application.build(check=check, skip_broken_links=skip_broken_links) message = "All good" if check else "Done" click.secho(f"\n{message}.", fg="yellow") @mudkip.command() @click.option("--host", help="Development server host.", default="127.0.0.1") @click.option("--port", help="Development server port.", default=5500) @with_application def develop(application, host, port): """Start development server.""" padding = "\n" * application.config.verbose click.secho( f'{padding}Watching "{application.config.source_dir}"...{padding}', fg="blue" ) with exception_handler(): application.build() if application.config.dev_server != noop_dev_server: click.secho(f"{padding}Server running on http://{host}:{port}", fg="blue") @contextmanager def build_manager(event_batch): now = time.strftime("%H:%M:%S") click.secho(f"{padding}{now}", fg="black", bold=True, nl=False) events = event_batch.all_events if len(events) == 1: event = events[0] filename = path.basename(event.src_path) click.echo(f" {event.event_type} {filename}{padding}") else: click.echo(f" {len(events)} changes{padding}") with exception_handler(): yield try: application.develop(host, port, build_manager) except KeyboardInterrupt: click.secho("\nExit.", fg="yellow") @mudkip.command() @with_application def test(application): """Test documentation.""" padding = "\n" * application.config.verbose click.secho( f'{padding}Testing "{application.config.source_dir}"...{padding}', fg="blue" ) with exception_handler(exit=True): passed, summary = application.test() if not application.config.verbose: click.echo("\n" + summary) if passed: click.secho("\nPassed.", fg="yellow") else: click.secho("\nFailed.", fg="red", bold=True) sys.exit(1) @mudkip.command() @with_application def clean(application): """Remove output directory.""" padding = "\n" * application.config.verbose click.secho(f'{padding}Removing "{application.config.output_dir}"...', fg="blue") with exception_handler(exit=True): application.clean() click.secho("\nDone.", fg="yellow") def main(): mudkip(prog_name="mudkip") PK!\6L L mudkip/config.pyimport re from pathlib import Path from .preset import Preset from .server import noop_dev_server AUTHOR_EXTRA = re.compile(r"<.*?>|\(.*?\)|\[.*?\]") SPACES = re.compile(r"\s+") def join_authors(authors): if not authors: return if isinstance(authors, str): string = authors elif len(authors) < 2: string = "".join(authors) else: string = ", ".join(authors[:-1]) + f" and {authors[-1]}" return SPACES.sub(" ", AUTHOR_EXTRA.sub("", string)).strip().replace(" ,", ",") class Config: default_source_dir = "docs" default_output_dir = "docs/.mudkip/dist" def __init__( self, preset="rtd", source_dir=None, output_dir=None, verbose=False, project_name=None, project_dir=None, title=None, copyright=None, author=None, version=None, release=None, dev_server=None, poetry=None, ): self.preset = preset if isinstance(preset, Preset) else Preset.get(preset) self.dev_server = dev_server or noop_dev_server self.poetry = {} if poetry is None else poetry self.mkdir = [] self.source_dir = Path(source_dir or self.default_source_dir) self.output_dir = Path(output_dir or self.default_output_dir) self.verbose = verbose self.project_name = project_name or self.poetry.get("name") if project_dir: self.project_dir = project_dir else: self.try_set_project_dir() self.title = title or self.project_name self.copyright = copyright self.author = author or join_authors(self.poetry.get("authors")) self.release = release or self.poetry.get("version") self.version = version or self.release and ".".join(self.release.split(".")[:2]) self.mkdir += self.source_dir, self.output_dir self.set_sphinx_arguments() for directory in self.mkdir: directory.mkdir(parents=True, exist_ok=True) self.preset.execute(self) def set_sphinx_arguments(self): self.sphinx_project = self.title self.sphinx_srcdir = self.source_dir self.sphinx_outdir = self.output_dir self.sphinx_doctreedir = self.sphinx_outdir / ".doctrees" self.sphinx_buildername = "xml" self.sphinx_confdir = None self.sphinx_confoverrides = {} def try_set_project_dir(self): self.project_dir = None if self.project_name: path = Path(self.project_name) if path.is_dir(): self.project_dir = path PK!ۅ''mudkip/errors.pyclass MudkipError(Exception): pass PK!33mudkip/extension.pyfrom . import __version__ def process_doctree(app, doctree, docname): relations = app.env.collect_relations() parent, prev, next = relations.get(docname, (None,) * 3) attributes = {"name": docname, "parent": parent, "prev": prev, "next": next} for name, value in attributes.items(): if value is not None: doctree[name] = value def setup(app): app.connect("doctree-resolved", process_doctree) return { "version": __version__, "parallel_read_safe": True, "parallel_write_safe": True, } PK!uumudkip/preset.pyfrom .server import livereload_dev_server def preset(func): Preset.register(Preset(func.__name__, func)) return func class Preset: registry = {} def __init__(self, name, callback=None): self.name = name self.callback = callback def execute(self, config): if self.callback: self.callback(config) @classmethod def register(cls, preset): cls.registry[preset.name] = preset return preset @classmethod def get(cls, name): return cls.registry[name] @preset def default(config): pass @preset def rtd(config): config.dev_server = livereload_dev_server config.sphinx_buildername = "dirhtml" config.sphinx_confoverrides.update(html_theme="sphinx_rtd_theme") PK!Wjjmudkip/server.pyfrom os import path import logging from contextlib import contextmanager from multiprocessing import Process from livereload import Server @contextmanager def noop_dev_server(directory, host, port): yield @contextmanager def livereload_dev_server(directory, host, port): try: process = Process( target=LivereloadServer.serve_directory, args=(directory, host, port) ) process.start() yield finally: process.terminate() class LivereloadServer(Server): def _setup_logging(self): super()._setup_logging() logger = logging.getLogger("livereload") logger.setLevel(100) @classmethod def serve_directory(cls, directory, host, port): server = cls() server.watch(path.join(directory, "**", "*.html")) server.serve(port=port, host=host, root=directory) PK!h'*mudkip/watch.pyfrom queue import Queue from threading import Timer from pathlib import Path from functools import partial from itertools import chain from typing import NamedTuple from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler class EventBatch(NamedTuple): moved: list created: list modified: list deleted: list @property def all_events(self): return list(chain(self.moved, self.created, self.modified, self.deleted)) class DirectoryWatcher: def __init__( self, directories=(), patterns=None, ignore_patterns=None, ignore_directories=False, case_sensitive=False, recursive=True, debounce_time=0.25, ): self.directories = set() self.patterns = patterns self.ignore_patterns = ignore_patterns self.ignore_directories = ignore_directories self.case_sensitive = case_sensitive self.recursive = recursive self.debounce_time = debounce_time for directory in directories: self.watch(directory) self.queue = Queue() self.timer = None self.moved, self.created, self.modified, self.deleted = [], [], [], [] self.created_paths = set() def watch(self, directory): directory = Path(directory).absolute() subdirs = set() for watched in self.directories: if directory in watched.parents: subdirs.add(watched) elif watched in directory.parents: return self.directories.add(directory) self.directories -= subdirs def __iter__(self): observer = Observer() for directory in self.directories: handler = PatternMatchingEventHandler( self.patterns, self.ignore_patterns, self.ignore_directories, self.case_sensitive, ) handler.on_moved = partial(self.callback, self.moved) handler.on_created = partial(self.callback, self.created) handler.on_modified = partial(self.callback, self.modified) handler.on_deleted = partial(self.callback, self.deleted) observer.schedule(handler, str(directory), self.recursive) observer.start() try: while True: yield self.queue.get() finally: observer.stop() observer.join() def callback(self, category, event): if self.timer: self.timer.cancel() if all(e.src_path != event.src_path for e in category): category.append(event) if category is self.created: self.created_paths.add(event.src_path) if category in (self.modified, self.deleted): self.created[:] = [ e for e in self.created if e.src_path != event.src_path ] if category is self.deleted: self.modified[:] = [ e for e in self.modified if e.src_path != event.src_path ] if event.src_path in self.created_paths: category.remove(event) self.timer = Timer(self.debounce_time, self.debounced_callback) self.timer.start() def debounced_callback(self): self.timer = None event_batch = EventBatch( list(self.moved), list(self.created), list(self.modified), list(self.deleted), ) self.moved.clear() self.created.clear() self.modified.clear() self.deleted.clear() self.created_paths.clear() self.queue.put(event_batch) PK!HL'*'mudkip-0.1.7.dist-info/entry_points.txtN+I/N.,()-M,Pz9Vy\\PK!11mudkip-0.1.7.dist-info/LICENSEMIT License Copyright (c) 2019 Valentin Berlier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUmudkip-0.1.7.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HtM"mudkip-0.1.7.dist-info/METADATAn0vSqECA]TS&G{ʹ^ЩT15ԻC#]tN9IX H Ѫ*|qVLY~7J }q /Z)ޢ;>agtHtQ٭a?x"%&ؖHJ$rhvoܯky{Ϙ6!2dH 4vX :L=.r{Mu `뫳XwFG #by`ED&uCwAvٚmj4I w iDLD3DK| Rhf^=)&w*rc {Pv_pg|w?~y-CWob`xBt 5a—d~ٸ]&_ombo;q~FVePp7"<'-[mPK!HݳQYmudkip-0.1.7.dist-info/RECORDuɖJy} X͠ 4H(M ~}ܽ=4c$$t ?Qgn"jA?28x[f rc,a~5+Ԥ%<܏aLuvMLMr[zٷ+Pu5R Zf5ts?{R\I ޝ̸15z|D!QaTvXfPA6TU"69BG0ÛWd~<jD|5U: ʁ IVgNqVWfWX(FgWR\O[/P* Im荸$exX ~շ},⨞{-mD{mHo I?5+z~]U%[AӼn _PK!&mudkip/__init__.pyPK!6%%Fmudkip/__main__.pyPK!uumudkip/application.pyPK!ȴ9qq C mudkip/cli.pyPK!\6L L 4mudkip/config.pyPK!ۅ''Y?mudkip/errors.pyPK!33?mudkip/extension.pyPK!uuBmudkip/preset.pyPK!WjjDEmudkip/server.pyPK!h'*Hmudkip/watch.pyPK!HL'*'Wmudkip-0.1.7.dist-info/entry_points.txtPK!11-Xmudkip-0.1.7.dist-info/LICENSEPK!HڽTU\mudkip-0.1.7.dist-info/WHEELPK!HtM"(]mudkip-0.1.7.dist-info/METADATAPK!HݳQY_mudkip-0.1.7.dist-info/RECORDPKb