PK!^`docparse/__init__.pyfrom enum import Enum import inspect from pkg_resources import iter_entry_points from typing import Callable, Dict, Optional, Sequence, cast class DocStyle(Enum): """Enumeration of supported DocTypes. """ GOOGLE = "google" class Paragraphs: def __init__(self, paragraphs: Sequence[str]): self.paragraphs = paragraphs def __len__(self): return len(self.paragraphs) def __getitem__(self, item): return self.paragraphs[item] def __str__(self): return "\n".join(self.paragraphs) def __eq__(self, other: "Paragraphs"): return str(self.paragraphs) == str(other.paragraphs) @staticmethod def from_lines(lines: Sequence[str]) -> "Paragraphs": """Join consecutive non-empty lines. """ paragraphs = [] start = 0 n = len(lines) while start < n: try: end = lines.index("", start) paragraphs.append(" ".join(lines[start: end])) start = end + 1 except ValueError: paragraphs.append(" ".join(lines[start:])) break return Paragraphs(paragraphs) class Named: def __init__(self, names: str, description: Optional[Paragraphs] = None): self.names = names self.description = description def __eq__(self, other: "Named"): return self.names == other.names and self.description == other.description class Typed: def __init__(self, datatype: str, description: Optional[Paragraphs] = None): self.datatype = datatype self.description = description def __eq__(self, other: "Typed"): return self.datatype == other.names and self.description == other.description class Field: def __init__( self, name: str, datatype: Optional[str] = None, description: Optional[Paragraphs] = None ): self.name = name self.datatype = datatype self.description = description def __eq__(self, other: "Field"): return ( self.name == other.name and self.datatype == other.names and self.description == other.description ) class DocString: def __init__( self, description: Optional[Paragraphs] = None, parameters: Optional[Dict[str, Field]] = None, keyword_arguments: Optional[Dict[str, Field]] = None, returns: Optional[Typed] = None, yields: Optional[Typed] = None, raises: Optional[Dict[str, Field]] = None, directives: Optional[Dict[str, Paragraphs]] = None, **kwargs ): self.sections = dict( description=description, parameters=parameters, keyword_arguments=keyword_arguments, returns=returns, yields=yields, raises=raises, ) self.sections.update(kwargs) self.directives = directives def __contains__(self, section): return section in self.sections def __getattr__(self, section): return self.sections[section] def __getitem__(self, section): return self.sections[section] @property def summary(self) -> Optional[str]: if self.description and len(self.description) > 0: return self.description[0] return None def has_directive(self, name): return self.directives and name in self.directives def get_directive(self, name) -> Paragraphs: if not self.has_directive(name): raise ValueError(f"Directive not found: '{name}'") return self.directives[name] REGISTRY: Dict[DocStyle, Callable[[str], DocString]] = None def parser(docstyle: DocStyle): """Decorator for a parser function. """ def decorator(f): REGISTRY[docstyle] = f return f return decorator def parse_docs(obj, docstyle: DocStyle) -> Optional[DocString]: """Parses a docstring of the specified style. Args: obj: Either a docstring or an object that has an associated docstring. docstyle: The docstring style. Returns: A DocString object, or None if `obj` is None or has no docstring. """ parse_func = REGISTRY[docstyle] docstring = get_docstring(obj) if docstring: return parse_func(docstring) def get_docstring(obj) -> str: if isinstance(obj, str): return inspect.cleandoc(cast(str, obj)) else: return inspect.getdoc(obj) if REGISTRY is None: REGISTRY = {} defaults = {} # process entry points, defer loading default parsers for entry_point in iter_entry_points(group="docparse.parsers"): if entry_point.name.endswith("_default"): defaults[entry_point.name[:-8]] = entry_point else: entry_point.load() # load default parsers for doc styles that haven't been overridden for name, entry_point in defaults.items(): if name not in REGISTRY: entry_point.load() PK!cqdocparse/google.py"""Google-style docstring parser. Code adapted from Napoleon: https://github.com/sphinx-contrib/napoleon/blob/master/sphinxcontrib/napoleon/docstring.py """ from functools import partial import re from typing import Tuple, Dict, Sequence, Optional, cast from docparse import DocString, DocStyle, Paragraphs, Typed, Field, parser SECTION_RE = re.compile(r"^(\w[\w ]+):$") DIRECTIVE_RE = re.compile(r"^.. ([\w ]+):$") XREF_RE = re.compile(r"(:(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)") SINGLE_COLON_RE = re.compile(r"(?\w+):`(?P[a-zA-Z0-9_.-]+)`| (?P[a-zA-Z0-9_.-]+))\s*", re.X ) SECTION_PARSERS = {} ALIASES = {} @parser(DocStyle.GOOGLE) def parse_google_docstring(docstring, allow_directives=True) -> DocString: lines = docstring.splitlines() num_lines = len(lines) if num_lines == 0: return DocString() def check_section_header(_line) -> Tuple[Optional[str], bool]: match = SECTION_RE.match(_line) if match: name = match.group(1) if name in ALIASES: name = ALIASES[name] if name in SECTION_PARSERS: return name, False if allow_directives: match = DIRECTIVE_RE.match(_line) if match: return match.group(1), True return None, False def add_section(_name, _is_directive, _lines, _sections, _directives): if _is_directive: _directives[_name] = _parse_generic_section(_lines) else: parser_func = SECTION_PARSERS.get(_name, _parse_generic_section) _sections[KEYWORD_RE.sub("_", _name.lower())] = parser_func(_lines) sections = {} directives = {} cur_section = "Description" cur_directive = False cur_lines = [] for line in lines: section, is_directive = check_section_header(line) if section: add_section(cur_section, cur_directive, cur_lines, sections, directives) cur_section = section cur_directive = is_directive cur_lines = [] else: cur_lines.append(line) if cur_lines: add_section(cur_section, cur_directive, cur_lines, sections, directives) return DocString(directives=directives, **sections) def add_aliases(name: str, *aliases: str): for alias in aliases: ALIASES[alias] = name add_aliases("Examples", "Example") add_aliases("Keyword Arguments", "Keyword Args") add_aliases("Notes", "Note") add_aliases("Parameters", "Args", "Arguments") add_aliases("Returns", "Return") add_aliases("Warning", "Warnings") add_aliases("Yields", "Yield") def section_parser(*sections: str): """Decorator that registers a function as a section paraser. """ def decorator(f): for section in sections: if isinstance(section, str): SECTION_PARSERS[section] = f else: name, kwargs = cast(Tuple[str, dict], section) SECTION_PARSERS[name] = partial(f, **kwargs) return f return decorator @section_parser( "Attention", "Caution", "Danger", "Error", "Hint", "Important", "Note", "References", "See also", "Tip", "Todo", "Warning" ) def _parse_generic_section(lines: Sequence[str]) -> Paragraphs: """Combine lines and remove indents. Lines separated by blank lines are grouped into paragraphs. Args: lines: A sequence of line strings. Returns: A tuple of paragraph strings. """ return Paragraphs.from_lines(tuple(line.strip() for line in lines)) @section_parser("Examples") def _parse_verbatim_section(lines: Sequence[str]) -> Sequence[str]: return _dedent(lines) @section_parser( ("Parameters", dict(parse_type=True)), ("Keyword Arguments", dict(parse_type=True)), ("Other Parameters", dict(parse_type=True)), "Methods", "Warns" ) def _parse_fields_section( lines: Sequence[str], parse_type: bool = False, prefer_type=False ) -> Dict[str, Field]: cur_indent = None fields = [] for line in lines: indent, line = INDENT_RE.match(line).groups() indent_size = len(indent) if line and (cur_indent is None or indent_size <= cur_indent): # New parameter cur_indent = indent_size before, colon, after = _partition_field_on_colon(line) field_type = None if parse_type: field_name, field_type = _parse_parameter_type(before) else: field_name = before if prefer_type and not field_type: field_type, field_name = field_name, field_type fields.append(( _escape_args_and_kwargs(field_name), field_type, [after.lstrip()] )) elif fields: # Add additional lines to current parameter fields[-1][2].append(line.lstrip()) else: raise ValueError(f"Unexpected line in Args block: {line}") return dict( (field[0], Field(field[0], field[1], Paragraphs.from_lines(field[2]))) for field in fields ) @section_parser( "Returns", "Yields" ) def _parse_returns_section(lines: Sequence[str]) -> Typed: before, colon, after = _partition_field_on_colon(lines[0]) return_type = None if colon: if after: return_desc = [after] + list(lines[1:]) else: return_desc = lines[1:] return_type = before else: return_desc = lines return Typed(return_type, Paragraphs.from_lines(return_desc)) @section_parser("Raises") def _parse_raises_section(lines: Sequence[str]) -> Dict[str, Field]: fields = _parse_fields_section(lines, prefer_type=True) for field in fields.values(): match = NAME_RE.match(field.datatype).groupdict() if match["role"]: field.datatype = match["name"] return fields def _partition_field_on_colon(line: str) -> Tuple[str, str, str]: before_colon = [] after_colon = [] colon = "" found_colon = False for i, source in enumerate(XREF_RE.split(line)): if found_colon: after_colon.append(source) else: m = SINGLE_COLON_RE.search(source) if (i % 2) == 0 and m: found_colon = True colon = source[m.start(): m.end()] before_colon.append(source[:m.start()]) after_colon.append(source[m.end():]) else: before_colon.append(source) return ( "".join(before_colon).strip(), colon, "".join(after_colon).strip() ) def _parse_parameter_type(name_type_str: str) -> Tuple[str, Optional[str]]: match = TYPED_PARAM_RE.match(name_type_str) # type: ignore if match: return match.group(1), match.group(2) else: return name_type_str, None def _escape_args_and_kwargs(name: str) -> str: if name.startswith("**"): return r"\*\*" + name[2:] elif name.startswith("*"): return r"\*" + name[1:] else: return name def _dedent(lines: Sequence[str]) -> Sequence[str]: lens = [ len(INDENT_RE.match(line).group(1)) for line in lines if line ] min_indent = min(lens) if lens else 0 return [line[min_indent:] for line in lines] PK!HqOP&3)docparse-0.2.0.dist-info/entry_points.txtNO.H,*NEű\9)i9%pq..PK!u22 docparse-0.2.0.dist-info/LICENSECopyright (c) 2017 John Didion 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!HnHTUdocparse-0.2.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HEtvB!docparse-0.2.0.dist-info/METADATANK1W++i"5^="n3{*yF4J^S #rmmZk$gs1A%XCtBi $VTϟI(\q6xget<ꉼ5 3a+U&i ٞW_v PK!HI־]!docparse-0.2.0.dist-info/RECORDuvC@}eH.& "8b a|}Wz܋IEQQ}q 4Ei>Eس2d@ ͥ*L}׌"+=rc^~Gv$UN}hzUIt.`隝B7C@ 99\~])y}ǢuO~'Yc3ܯ|\e60]H;]C{]^oRu|؞'A*H"FuŽ zdwfDֽģ=tfᤦ ؈kE~PK!^`docparse/__init__.pyPK!cqdocparse/google.pyPK!HqOP&3)1docparse-0.2.0.dist-info/entry_points.txtPK!u22 2docparse-0.2.0.dist-info/LICENSEPK!HnHTU6docparse-0.2.0.dist-info/WHEELPK!HEtvB! 7docparse-0.2.0.dist-info/METADATAPK!HI־]!8docparse-0.2.0.dist-info/RECORDPK9