From b7cc0f774927f2a2def5dcd9413ada099661615a Mon Sep 17 00:00:00 2001 From: daizu Date: Mon, 15 Jun 2020 13:55:38 +0900 Subject: [PATCH] pytest OK --- docs/usage/library.py | 7 +- mkapi/__init__.py | 2 +- mkapi/core/attribute.py | 122 +++-- mkapi/core/base.py | 516 +++++++++++++++----- mkapi/core/docstring.py | 120 ++--- mkapi/core/inherit.py | 203 ++------ mkapi/core/linker.py | 2 +- mkapi/core/module.py | 33 +- mkapi/core/node.py | 20 +- mkapi/core/object.py | 5 +- mkapi/core/page.py | 6 +- mkapi/core/postprocess.py | 35 +- mkapi/core/renderer.py | 19 +- mkapi/core/signature.py | 80 ++- mkapi/core/{tree.py => structure.py} | 81 ++- mkapi/main.py | 21 + mkapi/plugins/api.py | 2 +- mkapi/templates/args.jinja2 | 6 +- mkapi/templates/docstring.jinja2 | 10 +- mkapi/templates/macros.jinja2 | 16 +- mkapi/templates/module.jinja2 | 6 +- mkapi/theme/css/mkapi-common.css | 8 +- mkapi/utils.py | 54 +- tests/core/test_core_attribute.py | 25 +- tests/core/test_core_docstring_from_text.py | 23 +- tests/core/test_core_inherit.py | 163 +------ tests/core/test_core_module.py | 4 +- tests/core/test_core_node.py | 4 +- tests/core/test_core_postprocess.py | 18 +- tests/core/test_core_signature.py | 14 +- 30 files changed, 897 insertions(+), 728 deletions(-) rename mkapi/core/{tree.py => structure.py} (57%) diff --git a/docs/usage/library.py b/docs/usage/library.py index 4fd025fd..4a210a1e 100644 --- a/docs/usage/library.py +++ b/docs/usage/library.py @@ -86,10 +86,9 @@ def to_str(self, x: int) -> str: # argument list: item = section.items[0] -print(f"name={item.name!r}") -print(f"markdown={item.markdown!r}, html={item.html!r}") +print(item) print(item.type) -print(item.desc) +print(item.description) # `Node.get_markdown()` creates a *joint* Markdown of this node. @@ -131,7 +130,7 @@ def to_str(self, x: int) -> str: # - section = child.docstring.sections[1] # type:ignore item = section.items[0] -item.desc.markdown, item.desc.html # A

tag is deleted. +item.description.markdown, item.description.html # A

tag is deleted. # ## Constructing HTML diff --git a/mkapi/__init__.py b/mkapi/__init__.py index 11f7321f..3569fe2b 100644 --- a/mkapi/__init__.py +++ b/mkapi/__init__.py @@ -2,6 +2,6 @@ from mkapi.core.module import get_module from mkapi.core.node import get_node -from mkapi.utils import display, get_html +from mkapi.main import display, get_html __all__ = ["get_node", "get_module", "get_html", "display"] diff --git a/mkapi/core/attribute.py b/mkapi/core/attribute.py index 9a36591b..6efb1fad 100644 --- a/mkapi/core/attribute.py +++ b/mkapi/core/attribute.py @@ -3,7 +3,7 @@ import inspect from dataclasses import InitVar, is_dataclass from functools import lru_cache -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, Iterable, List, Tuple import _ast from mkapi import utils @@ -42,6 +42,8 @@ def parse_node(x): return parse_subscript(x) elif isinstance(x, _ast.Tuple): return parse_tuple(x) + elif isinstance(x, _ast.Str): + return x.s else: raise NotImplementedError @@ -82,74 +84,67 @@ def get_description(lines: List[str], lineno: int) -> str: return "" -def get_class_attributes(cls) -> Dict[str, Tuple[Any, str]]: - """Returns a dictionary that maps attribute name to a tuple of - (type, description).""" +def get_source(obj) -> str: try: - source = inspect.getsource(cls.__init__) or "" + source = inspect.getsource(obj) or "" if not source: - return {} - except TypeError: - return {} - source = utils.join(source.split("\n")) - node = ast.parse(source) + return "" + except (OSError, TypeError): + return "" + else: + return source - attr_list: List[Tuple] = [] - module = importlib.import_module(cls.__module__) + +def get_attributes_list( + nodes: Iterable[ast.AST], module, is_module: bool = False +) -> List[Tuple[str, int, Any]]: + attr_list: List[Tuple[str, int, Any]] = [] globals = dict(inspect.getmembers(module)) - for x in ast.walk(node): + for x in nodes: if isinstance(x, _ast.AnnAssign): attr, lineno, type_str = parse_annotation_assign(x) type = eval(type_str, globals) attr_list.append((attr, lineno, type)) if isinstance(x, _ast.Attribute) and isinstance(x.ctx, _ast.Store): - attr_list.append(parse_attribute_with_lineno(x)) + attr, lineno = parse_attribute_with_lineno(x) + attr_list.append((attr, lineno, ())) + if is_module and isinstance(x, _ast.Assign): + attr, lineno = parse_attribute_with_lineno(x) + attr_list.append((attr, lineno, ())) + attr_list = [x for x in attr_list if not x[0].startswith('_')] attr_list = sorted(attr_list, key=lambda x: x[1]) + return attr_list + + +def get_attributes_dict( + attr_list: List[Tuple[str, int, Any]], source: str, prefix: str = "" +) -> Dict[str, Tuple[Any, str]]: attrs: Dict[str, Tuple[Any, str]] = {} lines = source.split("\n") - for name, lineno, *type in attr_list: - if name.startswith("self."): - name = name[5:] - desc = get_description(lines, lineno) + for name, lineno, type in attr_list: + if not prefix or name.startswith(prefix): + name = name[len(prefix) :] + description = get_description(lines, lineno) if type: - attrs[name] = type[0], desc # Assignment with type annotation wins. + attrs[name] = type, description # Assignment with type annotation wins. elif name not in attrs: - attrs[name] = None, desc + attrs[name] = None, description return attrs -def get_module_attributes(module) -> Dict[str, Tuple[Any, str]]: +def get_class_attributes(cls) -> Dict[str, Tuple[Any, str]]: """Returns a dictionary that maps attribute name to a tuple of (type, description).""" - try: - source = inspect.getsource(module) or "" - if not source: - return {} - except (OSError, TypeError): + source = get_source(cls) + if not source: return {} + source = utils.join(source.split("\n")) node = ast.parse(source) - - attr_list: List[Tuple] = [] - globals = dict(inspect.getmembers(module)) - for x in ast.iter_child_nodes(node): - if isinstance(x, _ast.AnnAssign): - attr, lineno, type_str = parse_annotation_assign(x) - type = eval(type_str, globals) - attr_list.append((attr, lineno, type)) - if isinstance(x, _ast.Assign): - attr_list.append(parse_attribute_with_lineno(x)) - attr_list = sorted(attr_list, key=lambda x: x[1]) - - attrs: Dict[str, Tuple[Any, str]] = {} - lines = source.split("\n") - for name, lineno, *type in attr_list: - desc = get_description(lines, lineno) - if type: - attrs[name] = type[0], desc # Assignment with type annotation wins. - elif name not in attrs: - attrs[name] = None, desc - return attrs + nodes = ast.walk(node) + module = importlib.import_module(cls.__module__) + attr_list = get_attributes_list(nodes, module) + return get_attributes_dict(attr_list, source, prefix="self.") def get_dataclass_attributes(cls) -> Dict[str, Tuple[Any, str]]: @@ -160,9 +155,42 @@ def get_dataclass_attributes(cls) -> Dict[str, Tuple[Any, str]]: for field in fields: if field.type != InitVar: attrs[field.name] = field.type, "" + + source = get_source(cls) + source = utils.join(source.split("\n")) + if not source: + return {} + node = ast.parse(source).body[0] + + def nodes(): + for x in ast.iter_child_nodes(node): + if isinstance(x, _ast.FunctionDef): + break + yield x + + module = importlib.import_module(cls.__module__) + attr_list = get_attributes_list(nodes(), module) + for name, (type, description) in get_attributes_dict(attr_list, source).items(): + if name in attrs: + attrs[name] = attrs[name][0], description + else: + attrs[name] = type, description + return attrs +def get_module_attributes(module) -> Dict[str, Tuple[Any, str]]: + """Returns a dictionary that maps attribute name to a tuple of + (type, description).""" + source = get_source(module) + if not source: + return {} + node = ast.parse(source) + nodes = ast.iter_child_nodes(node) + attr_list = get_attributes_list(nodes, module, is_module=True) + return get_attributes_dict(attr_list, source) + + @lru_cache(maxsize=1000) def get_attributes(obj) -> Dict[str, Tuple[Any, str]]: """Returns a dictionary that maps attribute name to diff --git a/mkapi/core/base.py b/mkapi/core/base.py index ac32ba85..62f8e744 100644 --- a/mkapi/core/base.py +++ b/mkapi/core/base.py @@ -1,38 +1,73 @@ """This module provides entity classes to represent docstring structure.""" from dataclasses import dataclass, field -from typing import Iterator, List, Optional +from typing import Iterator, List, Tuple -from mkapi.core import linker, preprocess +from mkapi.core import preprocess from mkapi.core.regex import LINK_PATTERN -from mkapi.core.signature import Signature @dataclass class Base: """Base class. - Args: - name: Name of self. - markdown: Markdown source. - - Attributes: - html: HTML string after conversion. + Examples: + >>> base = Base('x', 'markdown') + >>> base + Base('x') + >>> bool(base) + True + >>> list(base) + [Base('x')] + >>> base = Base() + >>> bool(base) + False + >>> list(base) + [] """ - name: str = "" - markdown: str = "" - html: str = field(default="", init=False) + name: str = "" #: Name of self. + markdown: str = "" #: Markdown source. + html: str = field(default="", init=False) #: HTML string after conversion. + + def __repr__(self): + class_name = self.__class__.__name__ + return f"{class_name}({self.name!r})" + + def __bool__(self) -> bool: + """Returns True if name is not empty.""" + return bool(self.name) + + def __iter__(self) -> Iterator["Base"]: + """Yields self if markdown is not empty.""" + if self.markdown: + yield self def set_html(self, html: str): - """Sets `html` attribute. + """Sets HTML string. Args: html: HTML string. """ self.html = html + def copy(self): + """Copys self. + + Examples: + >>> a = Base('x', 'text') + >>> b = a + >>> b.name = 'y' + >>> a.name + 'y' + >>> c = a.copy() + >>> c.name = 'z' + >>> a.name + 'y' + """ + return self.__class__(name=self.name, markdown=self.markdown) + -@dataclass +@dataclass(repr=False) class Inline(Base): """Inline class. @@ -43,6 +78,8 @@ class Inline(Base): >>> inline = Inline('markdown') >>> inline.name == inline.markdown True + >>> inline + Inline('markdown') >>> bool(inline) True >>> next(iter(inline)) is inline @@ -50,26 +87,25 @@ class Inline(Base): >>> inline.set_html("

p1

p2

") >>> inline.html 'p1
p2' + >>> inline.copy() + Inline('markdown') """ + markdown: str = field(init=False) + def __post_init__(self): self.markdown = self.name - def __bool__(self) -> bool: - """Returns True if name is not empty.""" - return self.name != "" - - def __iter__(self) -> Iterator[Base]: - """Yields self if the markdown attribute has link form.""" - if self.markdown: - yield self - def set_html(self, html: str): """Sets `html` attribute cleaning `p` tags.""" self.html = preprocess.strip_ptags(html) + def copy(self): + """Copys self.""" + return self.__class__(name=self.name) -@dataclass + +@dataclass(repr=False) class Type(Inline): """Type class represents type of [Item](), [Section](), [Docstring](), or [Object](). @@ -77,16 +113,20 @@ class Type(Inline): Examples: >>> a = Type('str') >>> a - Type(name='str', markdown='', html='str') + Type('str') >>> list(a) [] >>> b = Type('[Object](base.Object)') - >>> b - Type(name='[Object](base.Object)', markdown='[Object](base.Object)', html='') - >>> list(b) == [b] - True + >>> b.markdown + '[Object](base.Object)' + >>> list(b) + [Type('[Object](base.Object)')] + >>> a.copy() + Type('str') """ + markdown: str = field(default="", init=False) + def __post_init__(self): if LINK_PATTERN.search(self.name): self.markdown = self.name @@ -101,49 +141,144 @@ class Item(Type): Args: type: Type of self. + description: Description of self. kind: Kind of self, for example `readonly_property`. This value is rendered as a class attribute in HTML. - Attributes: - desc: Description of self. - Examples: - >>> item = Item('[x](x)', 'A parameter.', Type('int')) + >>> item = Item('[x](x)', Type('int'), Inline('A parameter.')) + >>> item + Item('[x](x)', 'int') >>> item.name, item.markdown, item.html ('[x](x)', '[x](x)', '') >>> item.type - Type(name='int', markdown='', html='int') - >>> item.desc - Inline(name='A parameter.', markdown='A parameter.', html='') + Type('int') + >>> item.description + Inline('A parameter.') + >>> item = Item('[x](x)', 'str', 'A parameter.') + >>> item.type + Type('str') >>> it = iter(item) >>> next(it) is item True - >>> next(it) is item.desc + >>> next(it) is item.description True >>> item.set_html('

init

') >>> item.html '__init__' """ + markdown: str = field(default="", init=False) type: Type = field(default_factory=Type) - desc: Inline = field(init=False) + description: Inline = field(default_factory=Inline) kind: str = "" def __post_init__(self): - self.desc = Inline(self.markdown) - self.markdown = "" + if isinstance(self.type, str): + self.type = Type(self.type) + if isinstance(self.description, str): + self.description = Inline(self.description) super().__post_init__() + def __repr__(self): + class_name = self.__class__.__name__ + return f"{class_name}({self.name!r}, {self.type.name!r})" + def __iter__(self) -> Iterator[Base]: if self.markdown: yield self yield from self.type - yield from self.desc + yield from self.description def set_html(self, html: str): html = html.replace("", "__").replace("", "__") super().set_html(html) + def to_tuple(self) -> Tuple[str, str, str]: + """Returns a tuple of (name, type, description). + + Examples: + >>> item = Item('[x](x)', 'int', 'A parameter.') + >>> item.to_tuple() + ('[x](x)', 'int', 'A parameter.') + """ + return self.name, self.type.name, self.description.name + + def set_type(self, type: Type, force: bool = False): + """Sets type. + + Args: + item: Type instance. + force: If True, overwrite self regardless of existing type and + description. + + See Also: + * [Item.update]() + """ + if not force and self.type.name: + return + if type.name: + self.type = type.copy() + + def set_description(self, description: Inline, force: bool = False): + """Sets description. + + Args: + description: Inline instance. + force: If True, overwrite self regardless of existing type and + description. + + See Also: + * [Item.update]() + """ + if not force and self.description.name: + return + if description.name: + self.description = description.copy() + + def update(self, item: "Item", force: bool = False): + """Updates type and description. + + Args: + item: Item instance. + force: If True, overwrite self regardless of existing type and + description. + + Examples: + >>> item = Item('x') + >>> item2 = Item('x', 'int', 'description') + >>> item.update(item2) + >>> item.to_tuple() + ('x', 'int', 'description') + >>> item2 = Item('x', 'str', 'new description') + >>> item.update(item2) + >>> item.to_tuple() + ('x', 'int', 'description') + >>> item.update(item2, force=True) + >>> item.to_tuple() + ('x', 'str', 'new description') + >>> item.update(Item('x'), force=True) + >>> item.to_tuple() + ('x', 'str', 'new description') + """ + if item.name != self.name: + raise ValueError(f"Different name: {self.name} != {item.name}.") + self.set_description(item.description, force) + self.set_type(item.type, force) + + def copy(self): + """Copys self. + + Examples: + >>> item = Item('x', 'str', 'description', kind='rw') + >>> new = item.copy() + >>> new.description + Inline('description') + >>> new.kind + 'rw' + """ + return Item(*self.to_tuple(), kind=self.kind) + @dataclass class Section(Base): @@ -154,25 +289,13 @@ class Section(Base): type: Type of self. Examples: - `Section` is iterable: - >>> section = Section('Returns', markdown='An integer.') - >>> for x in section: - ... assert x is section - >>> items = [Item('x'), Item('[y](a)'), Item('z')] >>> section = Section('Parameters', items=items) - >>> [item.name for item in section] - ['[y](a)'] - - Indexing: - >>> isinstance(section['x'], Item) - True - >>> section['z'].name - 'z' + >>> section + Section('Parameters', num_items=3) + >>> list(section) + [Item('[y](a)', '')] - Contains: - >>> 'x' in section - True """ items: List[Item] = field(default_factory=list) @@ -182,31 +305,47 @@ def __post_init__(self): if self.markdown: self.markdown = preprocess.convert(self.markdown) - def __iter__(self) -> Iterator[Base]: - """Yields a [Base]() instance that has non empty Markdown. + def __repr__(self): + class_name = self.__class__.__name__ + return f"{class_name}({self.name!r}, num_items={len(self.items)})" - Args: - name: Item name. - """ + def __bool__(self): + """Returns True if the number of items is larger than 0.""" + return len(self.items) > 0 + + def __iter__(self) -> Iterator[Base]: + """Yields a [Base]() instance that has non empty Markdown.""" yield from self.type if self.markdown: yield self for item in self.items: yield from item - def __getitem__(self, name) -> Optional[Item]: - """Returns an [Item]() instance whose name is equal to `name`. If not found, - returns None. + def __getitem__(self, name: str) -> Item: + """Returns an [Item]() instance whose name is equal to `name`. + + If there is no Item instance, a Item instance is newly created. Args: name: Item name. + + Examples: + >>> section = Section("", items=[Item('x')]) + >>> section['x'] + Item('x', '') + >>> section['y'] + Item('y', '') + >>> section.items + [Item('x', ''), Item('y', '')] """ for item in self.items: if item.name == name: return item - return None + item = Item(name) + self.items.append(item) + return item - def __delitem__(self, name): + def __delitem__(self, name: str): """Delete an [Item]() instance whose name is equal to `name`. Args: @@ -215,14 +354,110 @@ def __delitem__(self, name): for k, item in enumerate(self.items): if item.name == name: del self.items[k] + return + raise KeyError(f"name not found: {name}") - def __contains__(self, name) -> bool: - """Returns True if there is an [Item]() instance whose name is `name`. + def __contains__(self, name: str) -> bool: + """Returns True if there is an [Item]() instance whose name is equal to `name`. Args: name: Item name. """ - return self[name] is not None + for item in self.items: + if item.name == name: + return True + return False + + def set_item(self, item: Item, force: bool = False): + """Sets an [Item](). + + Args: + item: Item instance. + force: If True, overwrite self regardless of existing item. + + Examples: + >>> items = [Item('x', 'int'), Item('y', 'str', 'y')] + >>> section = Section('Parameters', items=items) + >>> section.set_item(Item('x', 'float', 'X')) + >>> section['x'].to_tuple() + ('x', 'int', 'X') + >>> section.set_item(Item('y', 'int', 'Y'), force=True) + >>> section['y'].to_tuple() + ('y', 'int', 'Y') + >>> section.set_item(Item('z', 'float', 'Z')) + >>> [item.name for item in section.items] + ['x', 'y', 'z'] + + See Also: + * [Section.update] + """ + for k, x in enumerate(self.items): + if x.name == item.name: + self.items[k].update(item, force) + return + self.items.append(item.copy()) + + def update(self, section: "Section", force: bool = False): + """Updates items. + + Args: + section: Section instance. + force: If True, overwrite items of self regardless of existing value. + + Examples: + >>> s1 = Section('Parameters', items=[Item('a', 's'), Item('b', 'f')]) + >>> s2 = Section('Parameters', items=[Item('a', 'i', 'A'), Item('x', 'd')]) + >>> s1.update(s2) + >>> s1['a'].to_tuple() + ('a', 's', 'A') + >>> s1['x'].to_tuple() + ('x', 'd', '') + >>> s1.update(s2, force=True) + >>> s1['a'].to_tuple() + ('a', 'i', 'A') + >>> s1.items + [Item('a', 'i'), Item('b', 'f'), Item('x', 'd')] + """ + if section.name != self.name: + raise ValueError(f"Different name: {self.name} != {section.name}.") + for item in section.items: + self.set_item(item, force) + + def merge(self, section: "Section", force: bool = False) -> "Section": + """Returns a merged Section + + Examples: + >>> s1 = Section('Parameters', items=[Item('a', 's'), Item('b', 'f')]) + >>> s2 = Section('Parameters', items=[Item('a', 'i'), Item('c', 'd')]) + >>> s3 = s1.merge(s2) + >>> s3.items + [Item('a', 's'), Item('b', 'f'), Item('c', 'd')] + >>> s3 = s1.merge(s2, force=True) + >>> s3.items + [Item('a', 'i'), Item('b', 'f'), Item('c', 'd')] + >>> s3 = s2.merge(s1) + >>> s3.items + [Item('a', 'i'), Item('c', 'd'), Item('b', 'f')] + """ + if section.name != self.name: + raise ValueError(f"Different name: {self.name} != {section.name}.") + merged = Section(self.name) + for item in self.items: + merged.set_item(item) + for item in section.items: + merged.set_item(item, force=force) + return merged + + def copy(self): + """Copys self. + + Examples: + >>> s = Section('E', 'markdown', [Item('a', 's'), Item('b', 'i')]) + >>> s.copy() + Section('E', num_items=2) + """ + items = [item.copy() for item in self.items] + return self.__class__(self.name, self.markdown, items=items) SECTION_ORDER = ["Bases", "", "Parameters", "Attributes", "Returns", "Yields", "Raises"] @@ -246,89 +481,126 @@ class Docstring: >>> parameters = Section("Parameters", items=[Item("a"), Item("[b](!a)")]) >>> returns = Section("Returns", markdown="Results") >>> docstring = Docstring([default, parameters, returns]) + >>> docstring + Docstring(num_sections=3) `Docstring` is iterable: - >>> [base.name for base in docstring] - ['', '[b](!a)', 'Returns'] + >>> list(docstring) + [Section('', num_items=0), Item('[b](!a)', ''), Section('Returns', num_items=0)] Indexing: >>> docstring["Parameters"].items[0].name 'a' + + Section ordering: + >>> docstring = Docstring() + >>> _ = docstring[''] + >>> _ = docstring['Todo'] + >>> _ = docstring['Attributes'] + >>> _ = docstring['Parameters'] + >>> [section.name for section in docstring.sections] + ['', 'Parameters', 'Attributes', 'Todo'] """ sections: List[Section] = field(default_factory=list) type: Type = field(default_factory=Type) + def __repr__(self): + class_name = self.__class__.__name__ + num_sections = len(self.sections) + return f"{class_name}(num_sections={num_sections})" + def __bool__(self): + """Returns True if the number of sections is larger than 0.""" return len(self.sections) > 0 def __iter__(self) -> Iterator[Base]: + """Yields [Base]() instance.""" yield from self.type for section in self.sections: yield from section - def __getitem__(self, name: str) -> Optional[Section]: + def __getitem__(self, name: str) -> Section: + """Returns a [Section]() instance whose name is equal to `name`. + + If there is no Section instance, a Section instance is newly created. + + Args: + name: Section name. + """ for section in self.sections: if section.name == name: return section - return None + section = Section(name) + self.set_section(section) + return section + + def __contains__(self, name) -> bool: + """Returns True if there is a [Section]() instance whose name is + equal to `name`. - def __setitem__(self, name: str, section: Section): - for k, section_ in enumerate(self.sections): - if section_.name == name: - self.sections[k] = section + Args: + name: Section name. + """ + for section in self.sections: + if section.name == name: + return True + return False + + def set_section( + self, + section: Section, + force: bool = False, + copy: bool = False, + replace: bool = False, + ): + """Sets a [Section](). + + Args: + section: Section instance. + force: If True, overwrite self regardless of existing seciton. + + Examples: + >>> items = [Item('x', 'int'), Item('y', 'str', 'y')] + >>> s1 = Section('Attributes', items=items) + >>> items = [Item('x', 'str', 'X'), Item('z', 'str', 'z')] + >>> s2 = Section('Attributes', items=items) + >>> doc = Docstring([s1]) + >>> doc.set_section(s2) + >>> doc['Attributes']['x'].to_tuple() + ('x', 'int', 'X') + >>> doc['Attributes']['z'].to_tuple() + ('z', 'str', 'z') + >>> doc.set_section(s2, force=True) + >>> doc['Attributes']['x'].to_tuple() + ('x', 'str', 'X') + + >>> items = [Item('x', 'X', 'str'), Item('z', 'z', 'str')] + >>> s3 = Section('Parameters', items=items) + >>> doc.set_section(s3) + >>> doc.sections + [Section('Parameters', num_items=2), Section('Attributes', num_items=3)] + """ + name = section.name + for k, x in enumerate(self.sections): + if x.name == name: + if replace: + self.sections[k] = section + else: + self.sections[k].update(section, force=force) return + if copy: + section = section.copy() if name not in SECTION_ORDER: self.sections.append(section) return order = SECTION_ORDER.index(name) - for k, section_ in enumerate(self.sections): - if section_.name not in SECTION_ORDER: + for k, x in enumerate(self.sections): + if x.name not in SECTION_ORDER: self.sections.insert(k, section) return - order_ = SECTION_ORDER.index(section_.name) + order_ = SECTION_ORDER.index(x.name) if order < order_: self.sections.insert(k, section) return self.sections.append(section) - - -@dataclass -class Object(Base): - """Object class represents an object. - - Args: - name: Object name. - prefix: Object prefix. - qualname: Qualified name. - kind: Object kind such as 'class', 'function', *etc.* - signature: Signature if object is module or callable. - - Attributes: - id: ID attribute of HTML. - type: Type for missing Returns and Yields sections. - """ - - prefix: str = "" - qualname: str = "" - markdown: str = field(init=False) - id: str = field(init=False) - kind: str = "" - type: Type = field(default_factory=Type, init=False) - signature: Signature = field(default_factory=Signature) - - def __post_init__(self): - self.id = self.name - if self.prefix: - self.id = ".".join([self.prefix, self.name]) - if not self.markdown: - name = linker.link(self.name, self.id) - if self.prefix: - prefix = linker.link(self.prefix, self.prefix) - self.markdown = ".".join([prefix, name]) - else: - self.markdown = name - - def __iter__(self) -> Iterator[Base]: - yield from self.type - yield self diff --git a/mkapi/core/docstring.py b/mkapi/core/docstring.py index 8c84f31d..512fce24 100644 --- a/mkapi/core/docstring.py +++ b/mkapi/core/docstring.py @@ -4,9 +4,9 @@ import re from typing import Any, Iterator, List, Tuple -from mkapi.core.base import Docstring, Item, Section, Type -from mkapi.core.linker import get_link, replace_link from mkapi.core import preprocess +from mkapi.core.base import Docstring, Inline, Item, Section, Type +from mkapi.core.linker import get_link, replace_link from mkapi.core.signature import get_signature from mkapi.utils import get_indent, join @@ -123,8 +123,8 @@ def split_parameter(doc: str) -> Iterator[List[str]]: start = stop -def parse_parameter(lines: List[str], style: str) -> Tuple[str, str, str]: - """Returns a tuple of (name, markdown, type). +def parse_parameter(lines: List[str], style: str) -> Item: + """Returns a Item instance that represents a parameter. Args: lines: Splitted parameter docstring lines. @@ -148,16 +148,16 @@ def parse_parameter(lines: List[str], style: str) -> Tuple[str, str, str]: name, type = m.group(1), m.group(2) else: type = "" - return name, "\n".join(parsed), type + return Item(name, Type(type), Inline("\n".join(parsed))) -def parse_parameters(doc: str, style: str) -> List[Tuple[str, str, str]]: - """Returns a list of (name, markdown, type).""" +def parse_parameters(doc: str, style: str) -> List[Item]: + """Returns a list of Item.""" return [parse_parameter(lines, style) for lines in split_parameter(doc)] def parse_returns(doc: str, style: str) -> Tuple[str, str]: - """Returns a tuple of (markdown, type).""" + """Returns a tuple of (type, markdown).""" lines = doc.split("\n") if style == "google": if ":" in lines[0]: @@ -169,7 +169,7 @@ def parse_returns(doc: str, style: str) -> Tuple[str, str]: else: type = lines[0].strip() lines = lines[1:] - return join(lines), type + return type, join(lines) def get_section(name: str, doc: str, style: str) -> Section: @@ -178,9 +178,9 @@ def get_section(name: str, doc: str, style: str) -> Section: markdown = "" items = [] if name in ["Parameters", "Attributes", "Raises"]: - items = [Item(n, m, Type(t)) for n, m, t in parse_parameters(doc, style)] + items = parse_parameters(doc, style) elif name in ["Returns", "Yields"]: - markdown, type = parse_returns(doc, style) + type, markdown = parse_returns(doc, style) else: markdown = doc return Section(name, markdown, items, Type(type)) @@ -195,7 +195,7 @@ def parse_bases(doc: Docstring, obj: Any): return types = [get_link(obj, include_module=True) for obj in objs] items = [Item(type=Type(type)) for type in types if type] - doc["Bases"] = Section("Bases", items=items) + doc.set_section(Section("Bases", items=items)) def parse_property(doc: Docstring, obj: Any): @@ -210,43 +210,43 @@ def parse_property(doc: Docstring, obj: Any): section.markdown = markdown -def parse_attribute(doc: Docstring, obj: Any): - """Parses attributes' docstring to inspect type and description from source.""" - signature = get_signature(obj) - attrs = signature.attributes - attrs_desc = signature.attributes_desc +def parse_source(doc: Docstring, obj: Any): + """Parses parameters' docstring to inspect type and description from source. - if not attrs: + Examples: + >>> from mkapi.core.base import Base + >>> doc = Docstring() + >>> parse_source(doc, Base) + >>> section = doc['Parameters'] + >>> section['name'].to_tuple() + ('name', 'str, optional', 'Name of self.') + >>> section = doc['Attributes'] + >>> section['html'].to_tuple() + ('html', 'str', 'HTML string after conversion.') + + s = get_signature(Base) + s['Parameters'] + """ + signature = get_signature(obj) + if not hasattr(signature, "parameters"): return - section = doc["Attributes"] - if section is None: - if any(x for x in attrs_desc.values()): - section = Section("Attributes") - doc["Attributes"] = section - else: - return - items = [] - for name in attrs: - item = section[name] - if item: - if not item.type.markdown: - item.type = Type(attrs[name]) - if not item.markdown: - item.markdown = attrs_desc[name] - del section[name] - elif attrs_desc[name]: - item = Item(name, attrs_desc[name], type=Type(attrs[name])) - else: - continue - items.append(item) - items.extend(section.items) - section.items = items + name = "Parameters" + section = signature[name] + if name in doc: + section = section.merge(doc[name], force=True) + if section: + doc.set_section(section, replace=True) + + name = "Attributes" + section = signature[name] + if name not in doc and not section: + return + doc[name].update(section) def postprocess(doc: Docstring, obj: Any): parse_bases(doc, obj) - if inspect.ismodule(obj) or inspect.isclass(obj): - parse_attribute(doc, obj) + parse_source(doc, obj) if isinstance(obj, property): parse_property(doc, obj) @@ -257,30 +257,21 @@ def postprocess(doc: Docstring, obj: Any): if signature.signature is None: return - def get_type(type: str) -> Type: - if type.startswith("("): # tuple - type = type[1:-1] - return Type(type) - - if doc["Parameters"] is not None: + if "Parameters" in doc: for item in doc["Parameters"].items: - if not item.type and item.name in signature.parameters: - item.type = get_type(signature.parameters[item.name]) - if "{default}" in item.desc.markdown and item.name in signature: + description = item.description.name + if "{default}" in description and item.name in signature: default = signature.defaults[item.name] - item.desc.markdown = item.desc.markdown.replace("{default}", default) - - if doc["Attributes"] is not None and signature.attributes: - for item in doc["Attributes"].items: - if not item.type and item.name in signature.attributes: - item.type = get_type(signature.attributes[item.name]) + description = description.replace("{default}", default) + item.set_description(Inline(description), force=True) for name in ["Returns", "Yields"]: - section = doc[name] - if section is not None and not section.type: - section.type = Type(getattr(signature, name.lower())) + if name in doc: + section = doc[name] + if not section.type: + section.type = Type(getattr(signature, name.lower())) - if doc["Returns"] is None and doc["Yields"] is None: + if "Returns" not in doc and "Yields" not in doc: from mkapi.core.node import get_kind kind = get_kind(obj) @@ -308,11 +299,6 @@ def get_docstring(obj: Any) -> Docstring: for section in split_section(doc): sections.append(get_section(*section)) docstring = Docstring(sections) - elif inspect.isclass(obj) and hasattr(obj, "mro"): - bases = obj.mro()[1:-1] - if not bases: - return Docstring() - docstring = Docstring([Section()]) else: return Docstring() postprocess(docstring, obj) diff --git a/mkapi/core/inherit.py b/mkapi/core/inherit.py index daaf3b21..f3927d35 100644 --- a/mkapi/core/inherit.py +++ b/mkapi/core/inherit.py @@ -1,43 +1,45 @@ """This module implements the functionality of docstring inheritance.""" -from typing import Dict, Iterator, List, Tuple +from typing import Iterator, Tuple -from mkapi.core.base import Inline, Item, Section, Type +from mkapi.core.base import Section from mkapi.core.node import Node, get_node -def get_params(node: Node, name: str) -> Tuple[Dict[str, str], Dict[str, str]]: - """Returns a tuple of (docstring params, signature params). - - Each params is a dictionary of name-type mapping. +def get_section(node: Node, name: str, mode: str) -> Section: + """Returns a tuple of (docstring section, signature section). Args: node: Node instance. - name: Section name: `Parameters` or `Attributes`. + name: Section name: `Parameters` or `Attributes`. + mode: Mode name: `Docstring` or `Signature`. Examples: >>> node = get_node('mkapi.core.base.Type') - >>> doc_params, sig_params = get_params(node, 'Parameters') - >>> doc_params - {} - >>> sig_params - {'name': 'str, optional', 'markdown': 'str, optional'} + >>> section = get_section(node, 'Parameters', 'Docstring') + >>> 'name' in section + True + >>> section['name'].to_tuple() + ('name', 'str, optional', '') """ - section = node.docstring[name] - if section is None: - docstring_params = {} + if mode == "Docstring": + if name in node.docstring: + return node.docstring[name] + else: + return Section(name) else: - docstring_params = {item.name: item.type.name for item in section.items} - signature_params = node.object.signature[name] - return docstring_params, signature_params + if hasattr(node.object.signature, name): + return node.object.signature[name] + else: + return Section(name) -def is_complete(node: Node, name: str = "") -> bool: +def is_complete(node: Node, name: str = "both") -> bool: """Returns True if docstring is complete. Args: node: Node instance. - name: Section name: 'Parameters' or 'Attributes', or ''. - If name is '', both sections are checked. + name: Section name: 'Parameters' or 'Attributes', or 'both'. + If name is 'both', both sections are checked. Examples: >>> from mkapi.core.object import get_object @@ -48,139 +50,51 @@ def is_complete(node: Node, name: str = "") -> bool: >>> is_complete(node) False """ - if not name: + if name == "both": return all(is_complete(node, name) for name in ["Parameters", "Attributes"]) - docstring_params, signature_params = get_params(node, name) - for param in signature_params: - if param not in docstring_params: + doc_section = get_section(node, name, "Docstring") + sig_section = get_section(node, name, "Signature") + for item in sig_section.items: + if item.name not in doc_section: return False - section = node.docstring[name] - if section is None: + if not doc_section: return True - for item in section.items: - if not item.desc.markdown: + for item in doc_section.items: + if not item.description.name: return False return True -def inherit_base(node: Node, base: Node, name: str = ""): +def inherit_base(node: Node, base: Node, name: str = "both"): """Inherits Parameters or Attributes section from base class. Args: node: Node instance. base: Node instance of a super class. - name: Section name: 'Parameters' or 'Attributes', or ''. - If name is '', both sections are inherited. + name: Section name: 'Parameters' or 'Attributes', or 'both'. + If name is 'both', both sections are inherited. Examples: >>> from mkapi.core.object import get_object >>> base = Node(get_object('mkapi.core.base.Base')) >>> node = Node(get_object('mkapi.core.base.Type')) - >>> [item.name for item in base.docstring['Parameters'].items] - ['name', 'markdown'] - >>> node.docstring['Parameters'] is None - True + >>> node.docstring['Parameters']['name'].to_tuple() + ('name', 'str, optional', '') >>> inherit_base(node, base) - >>> [item.name for item in node.docstring['Parameters'].items] - ['name', 'markdown'] + >>> node.docstring['Parameters']['name'].to_tuple() + ('name', 'str, optional', 'Name of self.') """ - if not name: + if name == "both": for name in ["Parameters", "Attributes"]: inherit_base(node, base, name) return - base_section = base.docstring[name] - if base_section is None: - return - _, node_params = get_params(node, name) - _, base_params = get_params(base, name) - node_section = node.docstring[name] - items = [] - for item in base_section.items: - if node_section is None or item.name not in node_section: - if ( - item.name in node_params - and node_params[item.name] == base_params[item.name] - ): - items.append(item) - if node_section is not None: - for item in node_section.items: - if item not in items: - items.append(item) - node.docstring[name] = Section(name, items=items) # type:ignore - - -def inherit_signature(node: Node, name: str = ""): - """Inherits Parameters or Attributes section from signature. - - Args: - node: Node instance. - name: Section name: 'Parameters' or 'Attributes', or ''. - If name is '', both sections are inherited. - - Examples: - >>> from mkapi.core.object import get_object - >>> base = Node(get_object('mkapi.core.base.Base')) - >>> [item.name for item in base.docstring['Attributes'].items] - ['html'] - >>> inherit_signature(base) - >>> [item.name for item in base.docstring['Attributes'].items] - ['name', 'markdown', 'html'] - """ - if not name: - for name in ["Parameters", "Attributes"]: - inherit_signature(node, name) - return - - _, params = get_params(node, name) - if not params: - return - - node_section = node.docstring[name] - items = [] - for item_name, type in params.items(): - if node_section is None or item_name not in node_section: - item = Item(item_name, markdown="", type=Type(type)) - else: - item = node_section[item_name] # type:ignore - items.append(item) - node.docstring[name] = Section(name, items=items) - - -def inherit_parameters(node: Node): - """Attributes section inherits items' markdown from Parameters section. - - Args: - node: Node instance. - - Note: - This function does not create any items. Call [inherit_signature]()() first. - - Examples: - >>> from mkapi.core.object import get_object - >>> base = Node(get_object('mkapi.core.base.Base')) - >>> node = Node(get_object('mkapi.core.base.Type')) - >>> [item.name for item in base.docstring['Parameters'].items] - ['name', 'markdown'] - >>> inherit_signature(base) - >>> section = base.docstring['Attributes'] - >>> [item.name for item in section.items] - ['name', 'markdown', 'html'] - >>> section['name'].desc.html - '' - >>> inherit_parameters(base) - >>> section['name'].desc.markdown != '' - True - """ - param_section = node.docstring["Parameters"] - attr_section = node.docstring["Attributes"] - if param_section is None or attr_section is None: - return - for item in attr_section.items: - if not item.desc.markdown and item.name in param_section: - desc = param_section[item.name].desc # type:ignore - item.desc = Inline(desc.name) + base_section = get_section(base, name, "Docstring") + node_section = get_section(node, name, "Docstring") + section = base_section.merge(node_section, force=True) + if section: + node.docstring.set_section(section, replace=True) def get_bases(node: Node) -> Iterator[Tuple[Node, Iterator[Node]]]: @@ -219,16 +133,14 @@ def gen(name=name): yield member, gen() -def inherit(node: Node, strict: bool = False): +def inherit(node: Node): """Inherits Parameters and Attributes from superclasses. - This function calls [inherit_base]()(), [inherit_signature]()(), - [inherit_parameters]()(). - Args: node: Node instance. - strict: If True, inherits from signature, too. """ + if node.object.kind not in ["class", "dataclass"]: + return for node, bases in get_bases(node): if is_complete(node): continue @@ -236,24 +148,3 @@ def inherit(node: Node, strict: bool = False): inherit_base(node, base) if is_complete(node): break - if strict: - inherit_signature(node) - if node.object.kind == "dataclass": - inherit_parameters(node) - - -def inherit_by_filters(node: Node, filters: List[str]): - """Inherits Parameters and Attributes from superclasses. - - Args: - node: Node instance. - filters: Chose fileters. 'inherit' for superclass inheritance or 'strict' - for signature inheritance. - """ - if node.object.kind in ["class", "dataclass"]: - if "inherit" in filters: - inherit(node) - elif "strict" in filters: - inherit(node, strict=True) - elif "strict" in filters and node.object.signature.signature: - inherit_signature(node, "Parameters") diff --git a/mkapi/core/linker.py b/mkapi/core/linker.py index f36eb70f..1c3dd1fe 100644 --- a/mkapi/core/linker.py +++ b/mkapi/core/linker.py @@ -179,7 +179,7 @@ def replace_link(obj: Any, markdown: str) -> str: Examples: >>> from mkapi.core.object import get_object - >>> obj = get_object('mkapi.core.base.Object') + >>> obj = get_object('mkapi.core.structure.Object') >>> replace_link(obj, '[Signature]()') '[Signature](!mkapi.core.signature.Signature)' >>> replace_link(obj, '[](Signature)') diff --git a/mkapi/core/module.py b/mkapi/core/module.py index a3268c63..48e5290f 100644 --- a/mkapi/core/module.py +++ b/mkapi/core/module.py @@ -2,11 +2,11 @@ import inspect import os from dataclasses import dataclass, field -from typing import Iterator, List, Optional +from typing import Dict, Iterator, List, Optional -from mkapi.core.node import get_kind +from mkapi.core.node import Node, get_kind, get_node from mkapi.core.object import get_object, get_sourcefile_and_lineno -from mkapi.core.tree import Tree +from mkapi.core.structure import Tree @dataclass(repr=False) @@ -16,24 +16,16 @@ class Module(Tree): Attributes: parent: Parent Module instance. members: Member Module instances. - objects: If self is module, object member names are - collected in this list. + node: Node inspect of self. """ parent: Optional["Module"] = field(default=None, init=False) members: List["Module"] = field(init=False) - objects: List[str] = field(default_factory=list, init=False) + node: Node = field(init=False) def __post_init__(self): super().__post_init__() - if self.object.kind == "module": - objects = get_objects(self.obj) - self.objects = [".".join([self.object.id, obj]) for obj in objects] - - def __repr__(self): - s = super().__repr__()[:-1] - objects = len(self.objects) - return f"{s}, num_objects={objects})" + self.node = get_node(self.obj, use_cache=False) def __iter__(self) -> Iterator["Module"]: if self.docstring: @@ -70,7 +62,7 @@ def get_source(self, filters: List[str]) -> str: """Returns a source for module.""" from mkapi.core.source import get_source - return get_source(self, filters) + return get_source(self, filters) # type:ignore def get_objects(obj) -> List[str]: @@ -112,6 +104,9 @@ def get_members(obj) -> List[Module]: return members +modules: Dict[str, Module] = {} + + def get_module(name) -> Module: """Returns a Module instace by name or object. @@ -123,4 +118,10 @@ def get_module(name) -> Module: else: obj = name - return Module(obj) + name = obj.__name__ + if name in modules: + return modules[name] + else: + module = Module(obj) + modules[name] = module + return module diff --git a/mkapi/core/node.py b/mkapi/core/node.py index 672d8b73..78272b30 100644 --- a/mkapi/core/node.py +++ b/mkapi/core/node.py @@ -3,9 +3,9 @@ from dataclasses import dataclass, field from typing import Any, Iterator, List, Optional -from mkapi.core.base import Base, Object +from mkapi.core.base import Base from mkapi.core.object import from_object, get_object, get_sourcefiles -from mkapi.core.tree import Tree +from mkapi.core.structure import Object, Tree @dataclass(repr=False) @@ -174,30 +174,36 @@ def get_members(obj: Any) -> List[Node]: if isinstance(obj, property): return [] - recursive = not inspect.ismodule(obj) sourcefiles = get_sourcefiles(obj) members = [] for name, obj in inspect.getmembers(obj): sourcefile_index = is_member(obj, name, sourcefiles) if sourcefile_index != -1 and not from_object(obj): - member = get_node(obj, recursive, sourcefile_index) + member = get_node(obj, sourcefile_index) if member.docstring: members.append(member) return sorted(members, key=lambda x: (-x.sourcefile_index, x.lineno)) -def get_node(name, recursive: bool = True, sourcefile_index: int = 0) -> Node: +def get_node(name, sourcefile_index: int = 0, use_cache: bool = True) -> Node: """Returns a Node instace by name or object. Args: name: Object name or object itself. - recursive: If True, member objects are collected recursively. sourcefile_index: If `obj` is a member of class, this value is the index of unique source files given by `mro()` of the class. Otherwise, 0. """ + from mkapi.core.module import modules + if isinstance(name, str): obj = get_object(name) else: obj = name - return Node(obj, recursive, sourcefile_index) + if use_cache: + if hasattr(obj, "__module__") and obj.__module__ in modules: + node = modules[obj.__module__] + if obj.__qualname__ in node: + return node[obj.__qualname__] + + return Node(obj, sourcefile_index) diff --git a/mkapi/core/object.py b/mkapi/core/object.py index 62b3c093..de74c4cf 100644 --- a/mkapi/core/object.py +++ b/mkapi/core/object.py @@ -46,12 +46,11 @@ def get_fullname(obj: Any, name: str) -> str: name: Object name in the module. Examples: - >>> import inspect >>> obj = get_object('mkapi.core.base.Item') >>> get_fullname(obj, 'Section') 'mkapi.core.base.Section' - >>> get_fullname(obj, 'linker.link') - 'mkapi.core.linker.link' + >>> get_fullname(obj, 'preprocess') + 'mkapi.core.preprocess' >>> get_fullname(obj, 'abc') '' """ diff --git a/mkapi/core/page.py b/mkapi/core/page.py index 97f42a82..dc2951d4 100644 --- a/mkapi/core/page.py +++ b/mkapi/core/page.py @@ -6,7 +6,7 @@ from mkapi import utils from mkapi.core import postprocess from mkapi.core.base import Base, Section -from mkapi.core.inherit import inherit_by_filters +from mkapi.core.inherit import inherit from mkapi.core.linker import resolve_link from mkapi.core.node import Node, get_node from mkapi.core.regex import MKAPI_PATTERN, NODE_PATTERN, node_markdown @@ -53,9 +53,9 @@ def split(self, source: str) -> Iterator[str]: if markdown: yield self.resolve_link(markdown) heading, name = match.groups() - name, filters = utils.filter(name) + name, filters = utils.split_filters(name) node = get_node(name) - inherit_by_filters(node, filters) + inherit(node) postprocess.transform(node, filters) self.nodes.append(node) markdown = node.get_markdown(level=len(heading), callback=callback) diff --git a/mkapi/core/postprocess.py b/mkapi/core/postprocess.py index b81bac7b..a10c0572 100644 --- a/mkapi/core/postprocess.py +++ b/mkapi/core/postprocess.py @@ -1,6 +1,6 @@ from typing import List, Optional -from mkapi.core.base import Item, Section, Type +from mkapi.core.base import Inline, Item, Section, Type from mkapi.core.node import Node from mkapi.core.renderer import renderer @@ -16,8 +16,8 @@ def transform_property(node: Node): name = member.object.name kind = member.object.kind type = member.object.type - markdown = member.docstring.sections[0].markdown - item = Item(name, markdown, type=type, kind=kind) + description = member.docstring.sections[0].markdown + item = Item(name, type, Inline(description), kind=kind) section.items.append(item) else: members.append(member) @@ -30,14 +30,13 @@ def get_type(node: Node) -> Type: name = type.name else: for name in ["Returns", "Yields"]: - section = node.docstring[name] - if section and section.type: - name = section.type.name - break + if name in node.docstring: + section = node.docstring[name] + if section.type: + name = section.type.name + break else: name = "" - if name.startswith("("): - name = name[1:-1] return Type(name) @@ -58,22 +57,24 @@ def is_member(kind): object = member.object kind = object.kind type = get_type(member) - section_ = member.docstring[""] - if section_: - markdown = section_.markdown - if "\n\n" in markdown: - markdown = markdown.split("\n\n")[0] - item = Item(object.name, markdown, type=type, kind=kind) + if member.docstring and "" in member.docstring: + description = member.docstring[""].markdown + if "\n\n" in description: + description = description.split("\n\n")[0] + else: + description = "" + item = Item(object.name, type, Inline(description), kind) item.markdown, url, signature = "", "", "" if filters and "link" in filters: url = "#" + object.id elif filters and "apilink" in filters: url = "../" + node.object.id + "#" + object.id if object.kind not in ["class", "dataclass"]: - signature = "(" + ",".join(object.signature.parameters.keys()) + ")" + args = [item.name for item in object.signature.parameters.items] + signature = "(" + ",".join(args) + ")" item.html = renderer.render_object_member(object.name, url, signature) section.items.append(item) - node.docstring[name] = section + node.docstring.set_section(section) def transform_class(node: Node): diff --git a/mkapi/core/renderer.py b/mkapi/core/renderer.py index 68526765..6eb771d0 100644 --- a/mkapi/core/renderer.py +++ b/mkapi/core/renderer.py @@ -10,9 +10,10 @@ import mkapi from mkapi.core import linker -from mkapi.core.base import Docstring, Object, Section +from mkapi.core.base import Docstring, Section from mkapi.core.module import Module from mkapi.core.node import Node +from mkapi.core.structure import Object @dataclass @@ -42,8 +43,8 @@ def render(self, node: Node, filters: List[str] = None) -> str: node: Node instance. """ object = self.render_object(node.object, filters=filters) - docstring = self.render_docstring(node.docstring) - members = [self.render(member) for member in node.members] + docstring = self.render_docstring(node.docstring, filters=filters) + members = [self.render(member, filters) for member in node.members] return self.render_node(node, object, docstring, members) def render_node( @@ -91,7 +92,7 @@ def render_object_member(self, name: str, url: str, signature: str) -> str: template = self.templates["object_member"] return template.render(name=name, url=url, signature=signature) - def render_docstring(self, docstring: Docstring) -> str: + def render_docstring(self, docstring: Docstring, filters: List[str] = None) -> str: """Returns a rendered HTML for Docstring. Args: @@ -102,19 +103,23 @@ def render_docstring(self, docstring: Docstring) -> str: template = self.templates["docstring"] for section in docstring.sections: if section.items: - section.html = self.render_section(section) + valid = any(item.description for item in section.items) + if filters and 'strict' in filters or valid: + section.html = self.render_section(section, filters) return template.render(docstring=docstring) - def render_section(self, section: Section) -> str: + def render_section(self, section: Section, filters: List[str] = None) -> str: """Returns a rendered HTML for Section. Args: section: Section instance. """ + if filters is None: + filters = [] if section.name == "Bases": return self.templates["bases"].render(section=section) else: - return self.templates["args"].render(section=section) + return self.templates["args"].render(section=section, filters=filters) def render_module(self, module: Module, filters: List[str]) -> str: """Returns a rendered Markdown for Module. diff --git a/mkapi/core/signature.py b/mkapi/core/signature.py index d8e661eb..4a0c23c4 100644 --- a/mkapi/core/signature.py +++ b/mkapi/core/signature.py @@ -1,12 +1,14 @@ """This module provides Signature class that inspects object and creates signature and types.""" +import importlib import inspect -from dataclasses import dataclass, field +from dataclasses import InitVar, dataclass, field, is_dataclass from functools import lru_cache from typing import Any, Dict, Optional, TypeVar, Union from mkapi.core import linker, preprocess from mkapi.core.attribute import get_attributes +from mkapi.core.base import Inline, Item, Section, Type @dataclass @@ -18,29 +20,25 @@ class Signature: Attributes: signature: `inspect.Signature` instance. - parameters: Parameter dictionary. Key is parameter name - and value is type string. - defaults: Default value dictionary. Key is parameter name - and value is default value. - attributes: Attribute dictionary for dataclass. Key is attribute name - and value is type string. + parameters: Parameters section. + defaults: Default value dictionary. Key is parameter name and + value is default value. + attributes: Attributes section. returns: Returned type string. Used in Returns section. yields: Yielded type string. Used in Yields section. """ obj: Any = field(default=None, repr=False) signature: Optional[inspect.Signature] = field(default=None, init=False) - parameters: Dict[str, str] = field(default_factory=dict, init=False) + parameters: Section = field(init=False) defaults: Dict[str, Any] = field(default_factory=dict, init=False) - attributes: Dict[str, str] = field(default_factory=dict, init=False) - attributes_desc: Dict[str, str] = field(default_factory=dict, init=False) + attributes: Section = field(init=False) returns: str = field(default="", init=False) yields: str = field(default="", init=False) def __post_init__(self): if self.obj is None: return - self.get_attributes() if not callable(self.obj): return try: @@ -48,10 +46,11 @@ def __post_init__(self): except (TypeError, ValueError): pass + items = [] for name, parameter in self.signature.parameters.items(): if name == "self": continue - type = to_string(parameter.annotation) + type = to_string(parameter.annotation, obj=self.obj) default = parameter.default if default == inspect.Parameter.empty: self.defaults[name] = default @@ -59,11 +58,12 @@ def __post_init__(self): self.defaults[name] = f"{default!r}" if not type.endswith(", optional"): type += ", optional" - self.parameters[name] = type - + items.append(Item(name, Type(type))) + self.parameters = Section("Parameters", items=items) + self.set_attributes() return_annotation = self.signature.return_annotation - self.returns = to_string(return_annotation, "returns") - self.yields = to_string(return_annotation, "yields") + self.returns = to_string(return_annotation, "returns", obj=self.obj) + self.yields = to_string(return_annotation, "yields", obj=self.obj) def __contains__(self, name): return name in self.parameters @@ -76,25 +76,44 @@ def __str__(self): return "" args = [] - for arg in self.parameters: + for item in self.parameters.items: + arg = item.name if self.defaults[arg] != inspect.Parameter.empty: arg += "=" + self.defaults[arg] args.append(arg) return "(" + ", ".join(args) + ")" - def get_attributes(self): - for name, (type, desc) in get_attributes(self.obj).items(): - type = to_string(type) if type else "" + def set_attributes(self): + """ + Examples: + >>> from mkapi.core.base import Base + >>> s = Signature(Base) + >>> s.parameters['name'].to_tuple() + ('name', 'str, optional', 'Name of self.') + >>> s.attributes['html'].to_tuple() + ('html', 'str', 'HTML string after conversion.') + """ + items = [] + for name, (type, description) in get_attributes(self.obj).items(): + type = to_string(type, obj=self.obj) if type else "" if not type: - type, desc = preprocess.split_type(desc) - self.attributes[name] = type - self.attributes_desc[name] = desc + type, description = preprocess.split_type(description) + + item = Item(name, Type(type), Inline(description)) + if is_dataclass(self.obj): + if name in self.parameters: + self.parameters[name].set_description(item.description) + if self.obj.__dataclass_fields__[name].type != InitVar: + items.append(item) + else: + items.append(item) + self.attributes = Section("Attributes", items=items) - def split(self, sep=','): + def split(self, sep=","): return str(self).split(sep) -def to_string(annotation, kind: str = "returns") -> str: +def to_string(annotation, kind: str = "returns", obj=None) -> str: """Returns string expression of annotation. If possible, type string includes link. @@ -129,7 +148,7 @@ def to_string(annotation, kind: str = "returns") -> str: if annotation == ...: return "..." if hasattr(annotation, "__forward_arg__"): - return annotation.__forward_arg__ + return resolve_forward_arg(obj, annotation.__forward_arg__) if annotation == inspect.Parameter.empty or annotation is None: return "" name = linker.get_link(annotation) @@ -282,6 +301,15 @@ def to_string_with_prefix(annotation, prefix=","): return "" +def resolve_forward_arg(obj: Any, name: str) -> str: + if obj is None or not hasattr(obj, "__module__"): + return name + module = importlib.import_module(obj.__module__) + globals = dict(inspect.getmembers(module)) + type = eval(name, globals) + return to_string(type) + + @lru_cache(maxsize=1000) def get_signature(obj: Any) -> Signature: return Signature(obj) diff --git a/mkapi/core/tree.py b/mkapi/core/structure.py similarity index 57% rename from mkapi/core/tree.py rename to mkapi/core/structure.py index 882734a7..0a3f2f72 100644 --- a/mkapi/core/tree.py +++ b/mkapi/core/structure.py @@ -1,13 +1,56 @@ """This module provides base class of [Node](mkapi.core.node.Node) and [Module](mkapi.core.module.Module).""" from dataclasses import dataclass, field -from typing import Any, List, Union +from typing import Any, Iterator, List, Union -from mkapi.core.base import Object +from mkapi.core.base import Base, Type from mkapi.core.docstring import Docstring, get_docstring from mkapi.core.object import (get_qualname, get_sourcefile_and_lineno, split_prefix_and_name) -from mkapi.core.signature import get_signature +from mkapi.core.signature import Signature, get_signature + + +@dataclass +class Object(Base): + """Object class represents an object. + + Args: + name: Object name. + prefix: Object prefix. + qualname: Qualified name. + kind: Object kind such as 'class', 'function', *etc.* + signature: Signature if object is module or callable. + + Attributes: + id: ID attribute of HTML. + type: Type for missing Returns and Yields sections. + """ + + prefix: str = "" + qualname: str = "" + markdown: str = field(init=False) + id: str = field(init=False) + kind: str = "" + type: Type = field(default_factory=Type, init=False) + signature: Signature = field(default_factory=Signature) + + def __post_init__(self): + from mkapi.core import linker + + self.id = self.name + if self.prefix: + self.id = ".".join([self.prefix, self.name]) + if not self.markdown: + name = linker.link(self.name, self.id) + if self.prefix: + prefix = linker.link(self.prefix, self.prefix) + self.markdown = ".".join([prefix, name]) + else: + self.markdown = name + + def __iter__(self) -> Iterator[Base]: + yield from self.type + yield self @dataclass @@ -25,7 +68,6 @@ class Tree: docstring: Docstring instance. parent: Parent instance. members: Member instances. - recursive: If True, member objects are collected recursively. """ obj: Any = field() @@ -35,7 +77,6 @@ class Tree: docstring: Docstring = field(init=False) parent: Any = field(default=None, init=False) members: List[Any] = field(init=False) - recursive: bool = field(default=True) def __post_init__(self): obj = self.obj @@ -48,10 +89,7 @@ def __post_init__(self): prefix=prefix, name=name, qualname=qualname, kind=kind, signature=signature, ) self.docstring = get_docstring(obj) - if self.recursive: - self.members = self.get_members() - else: - self.members = [] + self.members = self.get_members() for member in self.members: member.parent = self @@ -60,25 +98,36 @@ def __repr__(self): id = self.object.id sections = len(self.docstring.sections) numbers = len(self.members) - return f"{class_name}({id!r}, num_sections={sections}, num_numbers={numbers})" + return f"{class_name}({id!r}, num_sections={sections}, num_members={numbers})" - def __getitem__(self, index: Union[int, str]): + def __getitem__(self, index: Union[int, str, List[str]]): """Returns a member Tree instance. If `index` is str, a member Tree instance whose name is equal to `index` is returned. """ + if isinstance(index, list): + node = self + for name in index: + node = node[name] + return node if isinstance(index, int): return self.members[index] - else: - for member in self.members: - if member.object.name == index: - return member + if isinstance(index, str) and "." in index: + names = index.split(".") + return self[names] + for member in self.members: + if member.object.name == index: + return member + raise IndexError def __getattr__(self, name: str): """Returns a member Tree instance whose name is equal to `name`. """ - return self[name] + try: + return self[name] + except IndexError: + raise AttributeError def __len__(self): return len(self.members) diff --git a/mkapi/main.py b/mkapi/main.py index d6bbb6e5..692aeb85 100644 --- a/mkapi/main.py +++ b/mkapi/main.py @@ -2,6 +2,7 @@ import sys import click +from markdown import Markdown from mkapi import __version__ @@ -14,3 +15,23 @@ @click.version_option(version_msg, "-V", "--version") def cli(ctx): pass + + +converter = Markdown() + + +def get_html(node): + from mkapi.core.node import Node, get_node + + if not isinstance(node, Node): + node = get_node(node) + markdown = node.get_markdown() + html = converter.convert(markdown) + node.set_html(html) + return node.get_html() + + +def display(name): + from IPython.display import HTML + + return HTML(get_html(name)) diff --git a/mkapi/plugins/api.py b/mkapi/plugins/api.py index 1ea455b0..502ace47 100644 --- a/mkapi/plugins/api.py +++ b/mkapi/plugins/api.py @@ -39,7 +39,7 @@ def collect(path: str, docs_dir: str, config_dir) -> Tuple[list, list]: if root not in sys.path: sys.path.insert(0, root) - package_path, filters = utils.filter(package_path) + package_path, filters = utils.split_filters(package_path) module = get_module(package_path) nav = [] diff --git a/mkapi/templates/args.jinja2 b/mkapi/templates/args.jinja2 index dd5ea7ee..9427a30c 100644 --- a/mkapi/templates/args.jinja2 +++ b/mkapi/templates/args.jinja2 @@ -1,9 +1,11 @@ {% from 'macros.jinja2' import base_type -%} diff --git a/mkapi/templates/docstring.jinja2 b/mkapi/templates/docstring.jinja2 index b39c76f3..97710bd9 100644 --- a/mkapi/templates/docstring.jinja2 +++ b/mkapi/templates/docstring.jinja2 @@ -1,15 +1,17 @@ {% from 'macros.jinja2' import base_type -%}
-{% for section in docstring.sections %} +{% for section in docstring.sections -%} +{% if section.html -%}
-{% if section.name %} +{% if section.name -%}
{{ section.name }} {{ base_type(section) }}
-{% endif %} +{% endif -%}
{{ section.html|safe }}
-{% endfor %} +{% endif -%} +{% endfor -%}
diff --git a/mkapi/templates/macros.jinja2 b/mkapi/templates/macros.jinja2 index e9e46fe0..b5491613 100644 --- a/mkapi/templates/macros.jinja2 +++ b/mkapi/templates/macros.jinja2 @@ -3,12 +3,13 @@ {%- endmacro %} {% macro base_type(base) -%} - {% if base.type %}({{ base.type.html|safe }}){% endif %} + {% if base.type %}{% if not base.type.html.startswith("(") -%} + ({% endif %}{{ base.type.html|safe }}{% if not base.type.html.startswith("(") %}){% endif %}{% endif %} {%- endmacro %} -{% macro object_type(object) -%} - {% if object.type %} → {{ select_type(object) }}{% endif %} -{%- endmacro %} +{%- macro object_type(object) -%} + {% if object.type %} → {{ object.type.html|safe }}{% endif -%} +{%- endmacro -%} {% macro object_prefix(object, url, upper) -%} {% if url %}{% endif -%} @@ -32,13 +33,12 @@ {%- endif %} {%- endmacro %} -{% macro object_body(object, prefix_url, name_url, tag, upper) -%} +{%- macro object_body(object, prefix_url, name_url, tag, upper) -%} {% if object.prefix and '.' not in object.qualname -%} <{{ tag }} class="mkapi-object-prefix">{{ object_prefix(object, prefix_url, upper) }} {%- endif -%} - <{{ tag }} class="mkapi-object-name">{{ object_name(object, name_url, upper) }}{{ object_signature(object.signature, tag) }} - {{ object_type(object) }} -{%- endmacro %} + <{{ tag }} class="mkapi-object-name">{{ object_name(object, name_url, upper) }}{{ object_signature(object.signature, tag) }} {{ object_type(object) }} +{%- endmacro -%} {% macro object_member(name, url, signature) -%} {% if url %}{% endif -%}{{ name }}{% if url %}{% endif %}{{ object_signature(signature, tag='code', in_code=True) }} diff --git a/mkapi/templates/module.jinja2 b/mkapi/templates/module.jinja2 index ba1eb216..973025b3 100644 --- a/mkapi/templates/module.jinja2 +++ b/mkapi/templates/module.jinja2 @@ -1,11 +1,11 @@ # ![mkapi]({{ module.object.id }}{{ module_filter }}|link|plain) {% if module.object.kind == 'module' -%} -{% for object in module.objects -%} +{% for node in module.node.members -%} {% if 'noheading' in object_filter -%} -![mkapi]({{ object }}{{ object_filter }}) +![mkapi]({{ node.object.id }}{{ object_filter }}) {% else -%} -## ![mkapi]({{ object }}{{ object_filter }}) +## ![mkapi]({{ node.object.id }}{{ object_filter }}) {% endif -%} {% endfor -%} {% else -%} diff --git a/mkapi/theme/css/mkapi-common.css b/mkapi/theme/css/mkapi-common.css index 7fc878ae..46a08e41 100644 --- a/mkapi/theme/css/mkapi-common.css +++ b/mkapi/theme/css/mkapi-common.css @@ -224,9 +224,7 @@ h3.mkapi-object.plain ~ .mkapi-docstring { } .mkapi-section-body-example pre code, -.mkapi-section-body-example code, -.mkapi-section-body-examples pre code, -.mkapi-section-body-examples code { +.mkapi-section-body-examples pre code { background: none; border: none; padding: 0px 0px; @@ -294,8 +292,8 @@ h3.mkapi-object.plain ~ .mkapi-docstring { .mkapi-node ul.mkapi-args-list li { margin: 0px 0px 2px 0px; - padding-left: 20px; - text-indent: -20px; + padding-left: 40px; + text-indent: -40px; list-style: none; line-height: 1.25em; } diff --git a/mkapi/utils.py b/mkapi/utils.py index 517ab778..28f63b80 100644 --- a/mkapi/utils.py +++ b/mkapi/utils.py @@ -1,6 +1,5 @@ -from markdown import Markdown - -converter = Markdown() +import importlib +from typing import Any def get_indent(line: str) -> int: @@ -17,29 +16,46 @@ def join(lines): return "\n".join(line[indent:] for line in lines).strip() -def get_html(node): - from mkapi.core.node import Node, get_node - - if not isinstance(node, Node): - node = get_node(node) - markdown = node.get_markdown() - html = converter.convert(markdown) - node.set_html(html) - return node.get_html() - +def get_object(name: str) -> Any: + """Reutrns an object specified by `name`. -def display(name): - from IPython.display import HTML + Args: + name: Object name. - return HTML(get_html(name)) + Examples: + >>> import inspect + >>> obj = get_object('mkapi.core') + >>> inspect.ismodule(obj) + True + >>> obj = get_object('mkapi.core.base') + >>> inspect.ismodule(obj) + True + >>> obj = get_object('mkapi.core.node.Node') + >>> inspect.isclass(obj) + True + >>> obj = get_object('mkapi.core.node.Node.get_markdown') + >>> inspect.isfunction(obj) + True + """ + names = name.split(".") + for k in range(len(names), 0, -1): + module_name = ".".join(names[:k]) + try: + obj = importlib.import_module(module_name) + except ModuleNotFoundError: + continue + for attr in names[k:]: + obj = getattr(obj, attr) + return obj + raise ValueError(f"Could not find object: {name}") -def filter(name): +def split_filters(name): """ Examples: - >>> filter("a.b.c") + >>> split_filters("a.b.c") ('a.b.c', []) - >>> filter("a.b.c|upper|strict") + >>> split_filters("a.b.c|upper|strict") ('a.b.c', ['upper', 'strict']) """ index = name.find("|") diff --git a/tests/core/test_core_attribute.py b/tests/core/test_core_attribute.py index 80e29efc..4b811c1d 100644 --- a/tests/core/test_core_attribute.py +++ b/tests/core/test_core_attribute.py @@ -51,12 +51,33 @@ def test_class_attribute_without_desc(): @dataclass class C: + x: int #: int + #: A + y: A + z: B + """B + + end. + """ + + +def test_dataclass_attribute(): + attrs = get_attributes(C) + for k, (name, (type, markdown)) in enumerate(attrs.items()): + assert name == ["x", "y", "z"][k] + assert markdown == ["int", "A", "B\n\nend."][k] + if k == 0: + assert type is int + + +@dataclass +class D: x: int y: List[str] def test_dataclass_attribute_without_desc(): - attrs = get_attributes(C) + attrs = get_attributes(D) for k, (name, (type, markdown)) in enumerate(attrs.items()): assert name == ["x", "y"][k] assert markdown == "" @@ -67,7 +88,7 @@ def test_dataclass_attribute_without_desc(): assert x == "list of str" -def test_module_attribute_without_desc(): +def test_module_attribute(): attrs = get_attributes(google_style) for k, (name, (type, markdown)) in enumerate(attrs.items()): if k == 0: diff --git a/tests/core/test_core_docstring_from_text.py b/tests/core/test_core_docstring_from_text.py index cdb83cad..1114b6e4 100644 --- a/tests/core/test_core_docstring_from_text.py +++ b/tests/core/test_core_docstring_from_text.py @@ -103,16 +103,16 @@ def test_parse_parameter(style): it = split_parameter(body) lines = next(it) assert len(lines) == 4 if style == "goole" else 5 - name, markdown, type = parse_parameter(lines, style) - assert name == "x" - assert markdown == "The first\nparameter\n\nwith type." - assert type == "int" + item = parse_parameter(lines, style) + assert item.name == "x" + assert item.description.markdown == "The first\nparameter\n\nwith type." + assert item.type.name == "int" lines = next(it) assert len(lines) == 4 if style == "goole" else 5 - name, markdown, type = parse_parameter(lines, style) - assert name == "y" - assert markdown == "The second\nparameter\n\nwithout type." - assert type == "" + item = parse_parameter(lines, style) + assert item.name == "y" + assert item.description.markdown == "The second\nparameter\n\nwithout type." + assert item.type.name == "" @pytest.mark.parametrize("style", ["google", "numpy"]) @@ -122,11 +122,6 @@ def test_parse_returns(style): next(it) next(it) section, body, style = next(it) - markdown, type = parse_returns(body, style) + type, markdown = parse_returns(body, style) assert markdown == "e\n\nf" assert type == "int" - # lines = next(it) - # assert len(lines) == 2 if style == "goole" else 3 - # type, markdown = parse_raise(lines, style) - # assert type == "TypeError" - # assert markdown == "c\nd" diff --git a/tests/core/test_core_inherit.py b/tests/core/test_core_inherit.py index bf8db5c8..42736128 100644 --- a/tests/core/test_core_inherit.py +++ b/tests/core/test_core_inherit.py @@ -1,159 +1,8 @@ -from dataclasses import dataclass +from mkapi.core.base import Base, Inline +from mkapi.core.inherit import is_complete +from mkapi.core.node import Node -import pytest -import mkapi -from mkapi.core.inherit import (get_params, inherit, inherit_by_filters, - is_complete) - - -@dataclass -class A: - """Base class. - - Parameters: - name: Object name. - type: Object type - - Attributes: - name: Object name. - type: Object type - """ - - name: str - type: str = "" - - def set_name(self, name: str): - """Sets name. - - Args: - name: A New name - """ - self.name = name - - -@dataclass -class B(A): - """Item class. - - Parameters: - markdown: Object markdown - - Attributes: - markdown: Object markdown - """ - - markdown: str = "" - - def set_name(self, name: str): - """Sets name in upper case.""" - self.name = name.upper() - - -@dataclass -class C(A): - """Item class. - - Parameters: - markdown: Object markdown - """ - - markdown: str = "" - - def set_name(self, name: str): - """Sets name in upper case.""" - self.name = name.upper() - - -@dataclass -class D(A): - """Item class. - - Parameters: - markdown: Object markdown - """ - - markdown: str = "" - - def set_name(self, name: str): - """Sets name in upper case.""" - self.name = name.upper() - - -@pytest.fixture(scope='module') -def a(): - return mkapi.get_node(A) - - -@pytest.fixture(scope='module') -def b(): - return mkapi.get_node(B) - - -@pytest.fixture(scope='module') -def c(): - return mkapi.get_node(C) - - -@pytest.fixture(scope='module') -def d(): - return mkapi.get_node(D) - - -def test_is_complete(a, b): - assert is_complete(a) - assert not is_complete(b) - - -@pytest.mark.parametrize("name", ["Parameters", "Attributes"]) -def test_get_params(a, b, name): - a_doc_params, a_sig_params = get_params(a, name) - assert len(a_doc_params) == 2 - assert len(a_sig_params) == 2 - - b_doc_params, b_sig_params = get_params(b, name) - assert len(b_doc_params) == 1 if name == 'Parameters' else 3 - assert len(b_sig_params) == 3 - - -def test_inherit(b): - inherit(b) - assert is_complete(b) - - -@pytest.mark.parametrize("name", ["Parameters", "Attributes"]) -def test_get_params_after(b, name): - b_doc_params, b_sig_params = get_params(b, name) - assert len(b_doc_params) == 3 - assert len(b_sig_params) == 3 - - -def test_inheritance_members(b): - item = b.members[0].docstring["Parameters"].items[0] - assert item.name == "name" - assert item.type.name == "str" - - -def test_inheritance_parameters(c): - doc_params, sig_params = get_params(c, "Attributes") - assert len(doc_params) == 0 - assert len(sig_params) == 3 - inherit(c, strict=True) - doc_params, sig_params = get_params(c, "Attributes") - assert len(doc_params) == 3 - - -def test_inheritance_by_filters(d): - doc_params, sig_params = get_params(d, "Attributes") - assert len(doc_params) == 0 - assert len(sig_params) == 3 - inherit_by_filters(d, ['inherit']) - doc_params, sig_params = get_params(d, "Parameters") - assert len(doc_params) == 3 - doc_params, sig_params = get_params(d, "Attributes") - assert len(doc_params) == 2 - inherit_by_filters(d, ['strict']) - doc_params, sig_params = get_params(d, "Parameters") - assert len(doc_params) == 3 - doc_params, sig_params = get_params(d, "Attributes") - assert len(doc_params) == 3 +def test_is_complete(): + assert is_complete(Node(Base)) + assert not is_complete(Node(Inline)) diff --git a/tests/core/test_core_module.py b/tests/core/test_core_module.py index d06b9bbe..d32f3615 100644 --- a/tests/core/test_core_module.py +++ b/tests/core/test_core_module.py @@ -15,10 +15,10 @@ def test_get_module(): assert base.parent is core assert base.object.markdown == "[mkapi.core](!mkapi.core).[base](!mkapi.core.base)" assert base.object.kind == "module" - assert len(base.objects) == 7 + assert len(base.node.members) == 6 def test_repr(): module = get_module("mkapi.core.base") - s = "Module('mkapi.core.base', num_sections=1, num_numbers=0, num_objects=7)" + s = "Module('mkapi.core.base', num_sections=1, num_members=0)" assert repr(module) == s diff --git a/tests/core/test_core_node.py b/tests/core/test_core_node.py index c00614d6..f460d44d 100644 --- a/tests/core/test_core_node.py +++ b/tests/core/test_core_node.py @@ -57,8 +57,6 @@ def test_get_markdown(): x = "[mkapi.core.base](!mkapi.core.base).[Base](!mkapi.core.base.Base)" assert parts[0] == x assert parts[1] == "Base class." - assert parts[6] == "Sets `html` attribute." - markdown = node.get_markdown(level=2) parts = [x.strip() for x in markdown.split("")] x = "[mkapi.core.base](!mkapi.core.base).[Base](!mkapi.core.base.Base)" @@ -99,4 +97,4 @@ def test_package(): def test_repr(): node = get_node("mkapi.core.base") - assert repr(node) == "Node('mkapi.core.base', num_sections=1, num_numbers=7)" + assert repr(node) == "Node('mkapi.core.base', num_sections=1, num_members=6)" diff --git a/tests/core/test_core_postprocess.py b/tests/core/test_core_postprocess.py index 473253b4..6faf34ea 100644 --- a/tests/core/test_core_postprocess.py +++ b/tests/core/test_core_postprocess.py @@ -38,7 +38,8 @@ class B: @pytest.fixture def node(): - return Node(A) + node = Node(A) + return node def test_transform_property(node): @@ -52,8 +53,9 @@ def test_get_type(node): assert P.get_type(node).name == "" assert P.get_type(node.f).name == "str" assert P.get_type(node.g).name == "int" - assert P.get_type(node.a).name == "int, str" + assert P.get_type(node.a).name == "(int, str)" assert P.get_type(node.b).name == "str" + node.g.docstring.sections[1] def test_transform_class(node): @@ -77,11 +79,11 @@ def test_transform_module(module): P.transform(node, ["link"]) q = module.__name__ section = node.docstring["Functions"] - assert 'add' in section - item = section['add'] - item.markdown.startswith('Returns') + assert "add" in section + item = section["add"] + item.markdown.startswith("Returns") item.html.startswith(f'add') - assert 'gen' in section + assert "gen" in section section = node.docstring["Classes"] - assert 'ExampleClass' in section - assert 'ExampleDataClass' in section + assert "ExampleClass" in section + assert "ExampleDataClass" in section diff --git a/tests/core/test_core_signature.py b/tests/core/test_core_signature.py index 6413bed0..331827fd 100644 --- a/tests/core/test_core_signature.py +++ b/tests/core/test_core_signature.py @@ -8,28 +8,28 @@ def test_function(add): assert str(s) == "(x, y=1)" assert "x" in s - assert s.parameters["x"] == "int" - assert s.parameters["y"] == "int, optional" + assert s.parameters["x"].to_tuple()[1] == "int" + assert s.parameters["y"].to_tuple()[1] == "int, optional" assert s.returns == "int" def test_generator(gen): s = Signature(gen) assert "n" in s - assert s.parameters["n"] == "" + assert s.parameters["n"].to_tuple()[1] == "" assert s.yields == "str" def test_class(ExampleClass): s = Signature(ExampleClass) - assert s.parameters["x"] == "list of int" - assert s.parameters["y"] == "(str, int)" + assert s.parameters["x"].to_tuple()[1] == "list of int" + assert s.parameters["y"].to_tuple()[1] == "(str, int)" def test_dataclass(ExampleDataClass): s = Signature(ExampleDataClass) - assert s.attributes["x"] == "int" - assert s.attributes["y"] == "int" + assert s.attributes["x"].to_tuple()[1] == "int" + assert s.attributes["y"].to_tuple()[1] == "int" def test_to_string():