PK!Xmudkip/__init__.py__version__ = "0.1.4" PK!6%%mudkip/__main__.pyfrom mudkip.cli import main main() PK!.mudkip/application.pyimport sys import time import shutil from io import StringIO from contextlib import contextmanager, nullcontext from sphinx.application import Sphinx from sphinx.errors import SphinxError from sphinx.util import logging from recommonmark.transform import AutoStructify from tomlkit.toml_file import TOMLFile from .config import Config from .errors import MudkipError from .server import dev_server from .watch import DirectoryWatcher class Mudkip: def __init__(self, *args, config=None, pyproject="pyproject.toml", **kwargs): if pyproject and not isinstance(pyproject, TOMLFile): pyproject = TOMLFile(pyproject) if config is None: params = {} if pyproject: try: tool = pyproject.read()["tool"] params.update(tool.get("mudkip", {}), poetry=tool.get("poetry")) except FileNotFoundError: pass params.update(kwargs) config = Config(*args, **params) self.config = config self.pyproject = pyproject 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.copyright = time.strftime("%Y") if self.config.project_author: conf.author = self.config.project_author conf.copyright += ", " + conf.author 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, 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") if self.config.dev_server: server = dev_server(self.sphinx.outdir, host, port) else: server = nullcontext() with server: 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!鑈 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 with_application(command): @click.option("--rtd", is_flag=True, help="Use the Read the Docs theme.") @click.option( "--source-dir", type=click.Path(file_okay=False), help="The source directory." ) @click.option( "--output-dir", type=click.Path(file_okay=False), help="The output directory." ) @click.option("--verbose", is_flag=True, help="Show Sphinx output.") @wraps(command) def wrapper(rtd, source_dir, output_dir, verbose, *args, **kwargs): params = dict( preset="rtd" if rtd else None, 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("--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() @with_application @click.option("--host", help="Development server host.", default="127.0.0.1") @click.option("--port", help="Development server port.", default=5500) 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: 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!mudkip/config.pyimport re from pathlib import Path from .preset import Preset 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/_build" def __init__( self, preset="default", source_dir=None, output_dir=None, verbose=False, project_name=None, project_author=None, project_dir=None, dev_server=False, poetry=None, ): self.preset = preset if isinstance(preset, Preset) else Preset.get(preset) self.dev_server = 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") self.project_author = project_author or join_authors(self.poetry.get("authors")) 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) self.preset.execute(self) 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_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!mudkip/preset.pydef 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 = True config.sphinx_buildername = "dirhtml" config.sphinx_confoverrides.update(html_theme="sphinx_rtd_theme") PK!gʾmudkip/server.pyfrom os import path import logging from contextlib import contextmanager from multiprocessing import Process from livereload import Server @contextmanager def dev_server(directory, host, port): try: process = Process(target=serve_directory, args=(directory, host, port)) process.start() yield finally: process.terminate() def serve_directory(directory, host, port): server = DevServer() server.watch(path.join(directory, "**", "*.html")) server.serve(port=port, host=host, root=directory) class DevServer(Server): def _setup_logging(self): super()._setup_logging() logger = logging.getLogger("livereload") logger.setLevel(100) 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.4.dist-info/entry_points.txtN+I/N.,()-M,Pz9Vy\\PK!11mudkip-0.1.4.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.4.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HK"mudkip-0.1.4.dist-info/METADATAN0vCIвTbZV] xıg;)yLr#^!]> (D3!ļoD*ZSUxo<*)FQ)A'ib{H~LZ\Hp1P~"ȵes4-L*T/ha9PMNY)'nE+^0[Vy'}2$@'՚XI?zR*QBmmdD(,OIa꿖'LGnxD 4vX ;L=.r{Mu p뫳XuƽG #by`EDWW&u ;F얙mj4I w iDTD3DK|Rhf^=)&w"rcs{Pv_pg|w?~E-Co.r`xBt 5a—d~ٸ]&GՈP/ 巶] ?o#kdA~v퓖uحPK!H=)AYmudkip-0.1.4.dist-info/RECORDuǎJ\ xa1 M4M c)`jO/-@ "3ˌ?Ő?&,6l?lJ8% ()2Cqܯ jϷC$e2u0ۀFvNv-O({<*g͛eXL+8ȗf~MX