Skip to content

WIP: Trying out some refactoring ideas #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 31 additions & 95 deletions pydocstring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,31 @@

__version__ = "0.2.1"

import parso
from parso.python.tree import BaseNode, search_ancestor

import pydocstring.formatter
from pydocstring import exc

FORMATTER = {
"google": {
"start_args_block": "\n\nArgs:\n",
"param_placeholder": " {0} ({1}): {2}\n",
"param_placeholder_args": " *{0}: {1}\n",
"param_placeholder_kwargs": " **{0}: {1}\n",
"start_return_block": "\n\nReturns:\n",
"return_placeholder": " {0}: {1}\n",
"return_annotation_placeholder": " {0}: \n",
"start_yield_block": "\n\nYields:\n",
"yield_placeholder": " {0}: {1}\n",
"start_raise_block": "\n\nRaises:\n",
"raise_placeholder": " {0}: \n",
"start_attributes": "\n\nAttributes:\n",
"attribute_placeholder": " {0} ({1}): {2}\n",
},
"numpy": {
"start_args_block": "\n\n Parameters\n ----------\n",
"param_placeholder": " {0} : {1}\n {2}\n",
"param_placeholder_args": " *{0}\n {1}\n",
"param_placeholder_kwargs": " **{0}\n {1}\n",
"start_return_block": "\n\n Returns\n -------\n",
"return_placeholder": " {0}\n {1}\n",
"return_annotation_placeholder": " {0}\n \n",
"start_yield_block": "\n\n Yields\n ------\n",
"yield_placeholder": " {0}\n {1}\n",
"start_raise_block": "\n\n Raises\n ------\n",
"raise_placeholder": " {0}\n \n",
"start_attributes": "\n\n Attributes\n ----------\n",
"attribute_placeholder": " {0} : {1}\n {2}\n",
},
"reST": {
"start_args_block": "\n\n",
"param_placeholder": ":param {0}: {2}\n:type {0}: {1}\n",
"param_placeholder_args": ":param *{0}: {1}\n",
"param_placeholder_kwargs": ":param **{0}: {1}\n",
"start_return_block": "\n\n",
"return_placeholder": ":return: {1}\n:rtype: {0}\n",
"return_annotation_placeholder": ":return: \n:rtype: {0}\n",
"start_yield_block": "\n\n",
"yield_placeholder": ":yields: {1}\n:ytype: {0}\n",
"start_raise_block": "\n\n",
"raise_placeholder": ":raises {0}: \n",
"start_attributes": "\n\n",
"attribute_placeholder": ":var {0}: {2}\n:type {0}: {1}\n",
},
}


def generate_docstring(source, position=(1, 0), formatter="google", autocomplete=False):
from typing import Tuple
from pydocstring.format.docstring_styles import DocstringStyle
from pydocstring.format import format_docstring
from pydocstring.ingest import ingest_source


def remove_characters_from_source(source: str, position: Tuple[int, int], num_to_remove: int) -> str:
lines = source.splitlines(True)

# all full lines before the one the position is on
lines_before = lines[: position[0] - 1]

# position in buffer is length of all those lines + the column position (starting at 0)
bufferpos = sum(len(l) for l in lines_before) + position[1]

# Splice the desired bits of the source together
slice1 = source[: bufferpos - num_to_remove]
slice2 = source[bufferpos:]
return slice1 + slice2


def generate_docstring(
source: str, position: Tuple[int, int] = (1, 0), formatter: DocstringStyle = "google", autocomplete: bool = False
):
"""Generate a docstring

Args:
Expand All @@ -79,45 +48,12 @@ def generate_docstring(source, position=(1, 0), formatter="google", autocomplete
str or None: docstring, excluding quotation marks, or None, if one could not be generated
"""
if autocomplete:
lines = source.splitlines(True)
# all full lines before the one the position is on
lines_before = lines[: position[0] - 1]
# position in buffer is length of all those lines + the column position (starting at 0)
bufferpos = sum(len(l) for l in lines_before) + position[1]
# Splice the desired bits of the source together
slice1 = source[: bufferpos - 3]
slice2 = source[bufferpos:]
source = slice1 + slice2
# Shift the position to account for the removed quotes
position = (position[0], position[1] - 3)

tree = parso.parse(source)
assert isinstance(tree, BaseNode)
try:
leaf = tree.get_leaf_for_position(position, include_prefixes=True)
except ValueError as e:
leaf = tree
if not leaf: # pragma: no cover
raise exc.FailedToGenerateDocstringError(
"Could not find leaf at cursor position {}".format(position)
)
scopes = ("classdef", "funcdef", "file_input")
scope = search_ancestor(leaf, *scopes)
if not scope:
if leaf.type == "file_input":
scope = leaf
else: # pragma: no cover
raise exc.FailedToGenerateDocstringError(
"Could not find scope of leaf {} ".format(leaf)
)
num_remove = 3
source = remove_characters_from_source(source, position, num_remove)

if scope.type == "classdef":
return pydocstring.formatter.class_docstring(scope, FORMATTER[formatter])
elif scope.type == "funcdef":
return pydocstring.formatter.function_docstring(scope, FORMATTER[formatter])
elif scope.type == "file_input":
return pydocstring.formatter.module_docstring(scope, FORMATTER[formatter])
# Shift the position to account for the removed quotes
# ToDo: what happens if position[1] becomes negative?
position = (position[0], position[1] - num_remove)

raise exc.FailedToGenerateDocstringError(
"Failed to generate Docstring for: {}".format(scope)
) # pragma: no cover
structured_input = ingest_source.run(source, position)
return format_docstring.run(structured_input, formatter)
49 changes: 49 additions & 0 deletions pydocstring/data_structures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from dataclasses import dataclass, field
from typing import List, Optional, Union


@dataclass
class ParamDetails:
name: str
type: str = "TYPE"
default: Optional[str] = None


@dataclass
class ReturnDetails:
type: str = "TYPE"
expression: str = ""


@dataclass
class FunctionDetails:
params: List[ParamDetails] = field(default_factory=list)
args: Optional[str] = None
kwargs: Optional[str] = None
returns: List[ReturnDetails] = field(default_factory=list)
yields: List[ReturnDetails] = field(default_factory=list)
raises: List[str] = field(default_factory=list)
annotation: Optional[str] = None

def has_parameters(self) -> bool:
return self.params or self.args or self.kwargs


@dataclass
class AttrDetails:
name: str
code: str
type: str


@dataclass
class ClassDetails:
attrs: List[AttrDetails] = field(default_factory=list)


@dataclass
class ModuleDetails:
attrs: List[AttrDetails] = field(default_factory=list)


IngestedDetails = Union[FunctionDetails, ClassDetails, ModuleDetails]
8 changes: 8 additions & 0 deletions pydocstring/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ class FailedToGenerateDocstringError(Exception):
"""

pass


class FailedToIngestError(Exception):
"""
Could not ingest the provided source text
"""

pass
98 changes: 98 additions & 0 deletions pydocstring/format/docstring_styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from dataclasses import dataclass
from strenum import StrEnum

from pydocstring import exc

class DocstringStyle(StrEnum):
GOOGLE = "google"
NUMPY = "numpy"
REST = "reST"

@classmethod
def list_values(cls):
return [x.value for x in cls]


@dataclass
class FormatTemplate:
start_args_block: str
param_placeholder: str
param_placeholder_args: str
param_placeholder_kwargs: str
start_return_block: str
return_placeholder: str
return_annotation_placeholder: str
start_yield_block: str
yield_placeholder: str
start_raise_block: str
raise_placeholder: str
start_attributes: str
attribute_placeholder: str


TEMPLATE_MAP = {
DocstringStyle.GOOGLE: FormatTemplate(
start_args_block="\n\nArgs:\n",
param_placeholder=" {0} ({1}): {2}\n",
param_placeholder_args=" *{0}: {1}\n",
param_placeholder_kwargs=" **{0}: {1}\n",
start_return_block="\n\nReturns:\n",
return_placeholder=" {0}: {1}\n",
return_annotation_placeholder=" {0}: \n",
start_yield_block="\n\nYields:\n",
yield_placeholder=" {0}: {1}\n",
start_raise_block="\n\nRaises:\n",
raise_placeholder=" {0}: \n",
start_attributes="\n\nAttributes:\n",
attribute_placeholder=" {0} ({1}): {2}\n",
),
DocstringStyle.NUMPY: FormatTemplate(
start_args_block="\n\n Parameters\n ----------\n",
param_placeholder=" {0} : {1}\n {2}\n",
param_placeholder_args=" *{0}\n {1}\n",
param_placeholder_kwargs=" **{0}\n {1}\n",
start_return_block="\n\n Returns\n -------\n",
return_placeholder=" {0}\n {1}\n",
return_annotation_placeholder=" {0}\n \n",
start_yield_block="\n\n Yields\n ------\n",
yield_placeholder=" {0}\n {1}\n",
start_raise_block="\n\n Raises\n ------\n",
raise_placeholder=" {0}\n \n",
start_attributes="\n\n Attributes\n ----------\n",
attribute_placeholder=" {0} : {1}\n {2}\n",
),
DocstringStyle.REST: FormatTemplate(
start_args_block="\n\n",
param_placeholder=":param {0}: {2}\n:type {0}: {1}\n",
param_placeholder_args=":param *{0}: {1}\n",
param_placeholder_kwargs=":param **{0}: {1}\n",
start_return_block="\n\n",
return_placeholder=":return: {1}\n:rtype: {0}\n",
return_annotation_placeholder=":return: \n:rtype: {0}\n",
start_yield_block="\n\n",
yield_placeholder=":yields: {1}\n:ytype: {0}\n",
start_raise_block="\n\n",
raise_placeholder=":raises {0}: \n",
start_attributes="\n\n",
attribute_placeholder=":var {0}: {2}\n:type {0}: {1}\n",
),
}


def get_style_template(style: DocstringStyle) -> FormatTemplate:
"""
Args:
style (DocstringStyle): the format of the docstring choose from google, numpy, reST.

Raises:
exc.InvalidFormatter: If the value provided to `formatter` is not a supported
formatter name

Returns:
FormatTemplate: the template for generating a docstring in a chosen style
"""

if style not in TEMPLATE_MAP:
raise exc.InvalidFormatterError("Failed to find template for: {}".format(style))

return TEMPLATE_MAP[style]
86 changes: 86 additions & 0 deletions pydocstring/format/format_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Docstring Formatter
"""

from pydocstring import exc
from pydocstring.data_structures import (
ClassDetails,
FunctionDetails,
IngestedDetails,
ModuleDetails
)
from pydocstring.format.docstring_styles import (
DocstringStyle,
FormatTemplate,
get_style_template
)
from pydocstring.format.format_utils import (
format_annotation,
format_attributes,
format_params,
format_raises,
format_returns,
format_yields,
)


def format_function(function_details: FunctionDetails, format_template: FormatTemplate) -> str:
docstring = "\n"

if function_details.has_parameters():
docstring += format_params(function_details, format_template)

if function_details.returns:
docstring += format_returns(function_details.returns, format_template)
elif function_details.annotation:
docstring += format_annotation(function_details.annotation, format_template)

if function_details.yields:
docstring += format_yields(function_details.yields, format_template)

if function_details.raises:
docstring += format_raises(function_details.raises, format_template)

docstring += "\n"
return docstring


def format_class(class_details: ClassDetails, format_template: FormatTemplate) -> str:
docstring = "\n"

if class_details.attrs:
docstring += format_attributes(class_details.attrs, format_template)

docstring += "\n"
return docstring


def format_module(module_details: ModuleDetails, format_template: FormatTemplate) -> str:
docstring = "\n"

if module_details.attrs:
docstring += format_attributes(module_details.attrs, format_template)

docstring += "\n"

if not docstring.strip():
return "\n\nEmpty Module\n\n"

return docstring


def run(structured_data: IngestedDetails, style: DocstringStyle) -> str:
template = get_style_template(style)

if isinstance(structured_data, FunctionDetails):
return format_function(structured_data, template)

elif isinstance(structured_data, ClassDetails):
return format_class(structured_data, template)

elif isinstance(structured_data, ModuleDetails):
return format_module(structured_data, template)

raise exc.FailedToGenerateDocstringError(
"Failed to generate Docstring for: {}".format(structured_data)
) # pragma: no cover
Loading