Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
krassowski committed Feb 19, 2024
1 parent 35f8c7a commit 3119cd6
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 1 deletion.
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-added-large-files
- id: check-case-conflict
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: check-builtin-literals
- id: trailing-whitespace
- id: check-merge-conflict
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# ipython-markdown-inspector
# ipython-markdown-inspector


## Installation

```
pip install ipython-markdown-inspector
```

## Usage

To load an extension while IPython is running, use the `%load_ext` magic:

```
%load_ext ipython_markdown_inspector
```

To load it each time IPython starts, list it in your [configuration file](https://ipython.readthedocs.io/en/stable/config/intro.html):

```python
c.InteractiveShellApp.extensions = [
'ipython_markdown_inspector'
]
```
53 changes: 53 additions & 0 deletions ipython_markdown_inspector/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from functools import partial
from typing import Any, Optional, Union

from IPython.core.interactiveshell import InteractiveShell
from IPython.core.oinspect import OInfo

from ._hook_data import InspectorHookData
from .formatter import as_markdown


def hook(
obj_or_data: Union[InspectorHookData, Any],
info: Optional[OInfo] = None,
*_,
ipython: InteractiveShell,
) -> str:
if isinstance(obj_or_data, InspectorHookData):
data = obj_or_data
else:
# fallback for IPython 8.21
obj = obj_or_data
detail_level = 0
omit_sections = []
info_dict = ipython.inspector.info(
obj, "", info=info, detail_level=detail_level
)
data = InspectorHookData(
obj=obj,
info=info,
info_dict=info_dict,
detail_level=detail_level,
omit_sections=omit_sections,
)
return as_markdown(data)


ORIGINAL_HOOK = None


def load_ipython_extension(ipython: InteractiveShell):
global ORIGINAL_HOOK
ORIGINAL_HOOK = ipython.inspector.mime_hooks.get("text/markdown", None)
ipython.inspector.mime_hooks["text/markdown"] = partial(hook, ipython=ipython)


def unload_ipython_extension(ipython: InteractiveShell):
if ORIGINAL_HOOK is None:
del ipython.inspector.mime_hooks["text/markdown"]
else:
ipython.inspector.mime_hooks["text/markdown"] = ORIGINAL_HOOK


__all__ = []
9 changes: 9 additions & 0 deletions ipython_markdown_inspector/_hook_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
try:
from IPython.core.oinspect import InspectorHookData
except ImportError:
# TODO: remove once we require a version which includes
# https://github.com/ipython/ipython/pull/14342
from ._ipython_patch import InspectorHookData


__all__ = ["InspectorHookData"]
40 changes: 40 additions & 0 deletions ipython_markdown_inspector/_ipython_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Any, TypedDict, Optional
from dataclasses import dataclass

from IPython.core.oinspect import OInfo


class InfoDict(TypedDict):
type_name: Optional[str]
base_class: Optional[str]
string_form: Optional[str]
namespace: Optional[str]
length: Optional[str]
file: Optional[str]
definition: Optional[str]
docstring: Optional[str]
source: Optional[str]
init_definition: Optional[str]
class_docstring: Optional[str]
init_docstring: Optional[str]
call_def: Optional[str]
call_docstring: Optional[str]
subclasses: Optional[str]
# These won't be printed but will be used to determine how to
# format the object
ismagic: bool
isalias: bool
isclass: bool
found: bool
name: str


@dataclass
class InspectorHookData:
"""Data passed to the mime hook"""

obj: Any
info: Optional[OInfo]
info_dict: InfoDict
detail_level: int
omit_sections: list[str]
104 changes: 104 additions & 0 deletions ipython_markdown_inspector/formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from typing import Dict, List

import docstring_to_markdown
from IPython.core.oinspect import is_simple_callable

from ._hook_data import InspectorHookData
from .models import Field, CodeField, DocField, RowField


FIELDS: Dict[str, List[Field]] = {
"alias": [
CodeField(label="Repr", key="string_form"),
],
"magic": [
DocField(label="Docstring", key="docstring"),
CodeField(label="Source", key="source", min_level=1),
RowField(label="File", key="file"),
],
"class_or_callable": [
# Functions, methods, classes
CodeField(label="Signature", key="definition"),
CodeField(label="Init signature", key="init_definition"),
DocField(label="Docstring", key="docstring"),
CodeField(label="Source", key="source", min_level=1),
DocField(label="Init docstring", key="init_docstring"),
RowField(label="File", key="file"),
RowField(label="Type", key="type_name"),
RowField(label="Subclasses", key="subclasses"),
],
"default": [
# General Python objects
CodeField(label="Signature", key="definition"),
CodeField(label="Call signature", key="call_def"),
RowField(label="Type", key="type_name"),
RowField(label="String form", key="string_form"),
RowField(label="Namespace", key="namespace"),
RowField(label="Length", key="length"),
RowField(label="File", key="file"),
DocField(label="Docstring", key="docstring"),
CodeField(label="Source", key="source", min_level=1),
DocField(label="Class docstring", key="class_docstring"),
DocField(label="Init docstring", key="init_docstring"),
DocField(label="Call docstring", key="call_docstring"),
],
}


TABLE_STARTER = """\
| <!-- --> | <!-- --> |
|----------|----------|\
"""


def markdown_formatter(text: str):
try:
converted = docstring_to_markdown.convert(text)
return converted
except docstring_to_markdown.UnknownFormatError:
return f"<pre>{text}</pre>"


def code_formatter(code, language="python"):
return f"```{language}\n{code}\n```"


def as_markdown(data: InspectorHookData) -> str:
if data.info and not data.info.found:
return str(data.info)

info_dict = data.info_dict

if info_dict["namespace"] == "Interactive":
info_dict["namespace"] = None

# TODO: maybe remove docstring from source?
# info_dict["source"] = remove_docstring(source)

if info_dict["isalias"]:
fields = FIELDS["alias"]
elif info_dict["ismagic"]:
fields = FIELDS["magic"]
if info_dict["isclass"] or is_simple_callable(data.obj):
fields = FIELDS["class_or_callable"]
else:
fields = FIELDS["default"]

chunks = []

in_table = False
for field in fields:
value = info_dict.get(field.key)
if value is None:
continue
if field.kind == "row":
if not in_table:
in_table = True
chunks.append(TABLE_STARTER)
chunks[-1] += f"\n| {field.label} | `{value}` |"
if field.kind == "code":
chunks.append(f"#### {field.label}\n\n" + code_formatter(value))
if field.kind == "doc":
chunks.append(f"#### {field.label}\n\n" + markdown_formatter(value))

return "\n\n".join(chunks)
25 changes: 25 additions & 0 deletions ipython_markdown_inspector/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from dataclasses import dataclass
from typing import Literal


@dataclass
class Field:
key: str
label: str
kind: str
min_level: int = 0


@dataclass
class RowField(Field):
kind: Literal["row"] = "row"


@dataclass
class CodeField(Field):
kind: Literal["code"] = "code"


@dataclass
class DocField(Field):
kind: Literal["doc"] = "doc"
50 changes: 50 additions & 0 deletions ipython_markdown_inspector/tests/test_as_markdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from IPython.core.interactiveshell import InteractiveShell
from IPython import get_ipython
import pytest

from ipython_markdown_inspector.formatter import as_markdown
from ipython_markdown_inspector._hook_data import InspectorHookData


def simple_func(arg):
"""Calls :py:func:`bool` on ``arg``"""
return bool(arg)


class SimpleClass:
pass


@pytest.mark.parametrize(
"object_name, part",
[
["%%python", "Run cells with python"],
["simple_func", "Calls `bool` on ``arg``"],
["simple_func", "| Type | `function` |"],
["test_int", "| Type | `int` |"],
["test_int", "| String form | `1` |"],
["test_str", "| Type | `str` |"],
["test_str", "| String form | `a` |"],
["test_str", "| Length | `1` |"],
["simple_cls", "| Type | `type` |"],
["simple_instance", "| Type | `SimpleClass` |"],
],
)
def test_result_contains(object_name, part):
ip: InteractiveShell = get_ipython() # type: ignore
ip.user_ns["test_str"] = "a"
ip.user_ns["test_int"] = 1
ip.user_ns["simple_func"] = simple_func
ip.user_ns["simple_cls"] = SimpleClass
ip.user_ns["simple_instance"] = SimpleClass()
oinfo = ip._object_find(object_name)
detail_level = 0
info_dict = ip.inspector.info(oinfo.obj, object_name)
data = InspectorHookData(
obj=oinfo.obj,
info=oinfo,
info_dict=info_dict,
detail_level=detail_level,
omit_sections=[],
)
assert part in as_markdown(data)
45 changes: 45 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "ipython-markdown-inspector"
version = "0.0.0"
dependencies = [
"ipython>=8.21.0",
"docstring-to-markdown>=0.13,<1.0"
]
requires-python = ">=3.8"
authors = [
{name = "Michał Krassowski"}
]
description = ""
readme = "README.md"
license = {file = "LICENSE"}
keywords = ["IPython", "markdown", "inpsector", "docstring"]
classifiers = [
"Framework :: IPython",
"Framework :: Jupyter",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
]

[project.urls]
Repository = "https://github.com/krassowski/ipython-markdown-inspector.git"
"Bug Tracker" = "https://github.com/krassowski/ipython-markdown-inspector/issues"
Changelog = "https://github.com/krassowski/ipython-markdown-inspector/blob/main/CHANGELOG.md"

[project.optional-dependencies]
test = [
"pytest"
]
dev = [
"build",
"pre-commit",
"ruff==0.2.0"
]

0 comments on commit 3119cd6

Please sign in to comment.