PK!Nmudkip/__init__.py__version__ = "0.1.3" PK!h;;mudkip/__main__.pyfrom mudkip.cli import mudkip mudkip(prog_name="mudkip") PK!#::mudkip/application.pyimport sys from io import StringIO from contextlib import contextmanager import shutil 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 Mudkip: def __init__(self, config=None): if config is None: config = Config() self.config = config 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 if self.config.project_name: conf.project = self.config.project_name conf.master_doc = "index" conf.exclude_patterns = [".*", "**/.*", "_*", "**/_*"] 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 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, *, 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") for event_batch in DirectoryWatcher(dirs, patterns, ignore_patterns): if build_manager: with build_manager(event_batch): self.build() else: self.build() def test(self): with self.sphinx_builder("doctest"): if self.config.verbose: self.build() else: with 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!骪Ƿ 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 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.""" @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 config_params(command): @click.option( "--source-dir", type=click.Path(file_okay=False), help="The source directory.", default=Config.default_source_dir, ) @click.option( "--output-dir", type=click.Path(file_okay=False), help="The output directory.", default=Config.default_output_dir, ) @click.option("--verbose", is_flag=True, help="Show Sphinx output.") @wraps(command) def wrapper(*args, **kwargs): return command(*args, **kwargs) return wrapper @mudkip.add_command @click.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.", ) @config_params def build(check, skip_broken_links, source_dir, output_dir, verbose): """Build documentation.""" padding = "\n" * verbose action = "Building and checking" if check else "Building" click.secho(f'{action} "{source_dir}"...{padding}', fg="blue") application = Mudkip(Config(source_dir, output_dir, verbose)) 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.add_command @click.command() @config_params def develop(source_dir, output_dir, verbose): """Start development server.""" padding = "\n" * verbose click.secho(f'Watching "{source_dir}"...{padding}', fg="blue") application = Mudkip(Config(source_dir, output_dir, verbose)) with exception_handler(): application.build() @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(build_manager=build_manager) except KeyboardInterrupt: click.secho("\nExit.", fg="yellow") @mudkip.add_command @click.command() @config_params def test(source_dir, output_dir, verbose): """Test documentation.""" padding = "\n" * verbose click.secho(f'Testing "{source_dir}"...{padding}', fg="blue") application = Mudkip(Config(source_dir, output_dir, verbose)) with exception_handler(exit=True): passed, summary = application.test() if not verbose: click.echo("\n" + summary) if passed: click.secho("\nPassed.", fg="yellow") else: click.secho("\nFailed.", fg="red", bold=True) sys.exit(1) @mudkip.add_command @click.command() @config_params def clean(source_dir, output_dir, verbose): """Remove output directory.""" padding = "\n" * verbose click.secho(f'Cleaning "{source_dir}"...{padding}', fg="blue") application = Mudkip(Config(source_dir, output_dir, verbose)) with exception_handler(exit=True): application.clean() click.secho("\nDone.", fg="yellow") PK![T,mudkip/config.pyfrom pathlib import Path import toml class Config: default_source_dir = "docs" default_output_dir = "docs/_build" def __init__( self, source_dir=None, output_dir=None, verbose=False, project_name=None, project_dir=None, ): 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 if project_name: self.project_name = project_name else: self.try_set_project_name() if project_dir: self.project_dir = project_dir else: self.try_set_project_dir() self.mkdir += self.source_dir, self.output_dir self.set_sphinx_arguments() for directory in self.mkdir: directory.mkdir(parents=True, exist_ok=True) def set_sphinx_arguments(self): self.sphinx_srcdir = self.source_dir self.sphinx_outdir = self.output_dir / "sphinx" self.sphinx_doctreedir = self.sphinx_outdir / ".doctrees" self.sphinx_buildername = "xml" self.sphinx_confdir = None self.sphinx_confoverrides = {} def try_set_project_name(self): try: with open("pyproject.toml") as pyproject: package_info = toml.load(pyproject)["tool"]["poetry"] except FileNotFoundError: self.project_name = None else: self.project_name = package_info["name"] 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!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!HJZ|%,'mudkip-0.1.3.dist-info/entry_points.txtN+I/N.,()-M,Pz9V&PK!11mudkip-0.1.3.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.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H,mudkip-0.1.3.dist-info/METADATAr0ze4Xh&I rpPm,!N\.=z}$)2~ՒYRKs %CT0JB#m*!nLJsVE*4z/ \ZeHf%NK t_ƱV69-Xiy Z LP^Rm|^f* VC]! ƯpN j r7s*eR*؅HVZf *כ['5|.Gϸ'p09h0>Q_B3ק0¯==ia1>EZY$bVt_d(Z8jx6S蛎<%6^ɤu;6xʾ<['ig$+:̵b76MD%~V.^\΃ "5P5uMQRgj 侮 n1-f&?ZPw|nS~1O}~PK!H'yamudkip-0.1.3.dist-info/RECORDuI@@}l@E/TBD&tC<$2{C/~rs}XAfP ד'RrX^kR:Imo$F}uw#M*~AJ73IEQ[E=*SȎ=Dr$#hzP.E:5g[Ӑ!bԉQgH0U \LXDx}[̷]t.H6;L4FSOSPW1L Hٵ9 TnbAfpowcɱ E]Ww?d3k7w;S]&UCk4my<-@\6oǂG9nWj(Hߘs:tUaa ^=mqW%-K}`[]GĆl7!U7hBRPㅿja^\ڣJMAҊh w.~g'֥DSEh˞hIpO-I}=AoET>4aQ+/^ eD{rg>t݉nU%~ZsrzdYfW%tKİPK!Nmudkip/__init__.pyPK!h;;Fmudkip/__main__.pyPK!#::mudkip/application.pyPK!骪Ƿ mudkip/cli.pyPK![T,&mudkip/config.pyPK!ۅ''-mudkip/errors.pyPK!33i-mudkip/extension.pyPK!h'*/mudkip/watch.pyPK!HJZ|%,'>mudkip-0.1.3.dist-info/entry_points.txtPK!11?mudkip-0.1.3.dist-info/LICENSEPK!HڽTUCmudkip-0.1.3.dist-info/WHEELPK!H,Dmudkip-0.1.3.dist-info/METADATAPK!H'yaSFmudkip-0.1.3.dist-info/RECORDPK {H