diff --git a/Contributors.md b/Contributors.md
index 4856d2c..b3fd344 100644
--- a/Contributors.md
+++ b/Contributors.md
@@ -9,3 +9,5 @@
* [RCristiano](https://github.com/RCristiano)
* [Hildebrando Pedroni](https://github.com/HildePedroni)
* [Saran Balaji](https://github.com/csaranbalaji)
+* [Ashir Gupta](https://github.com/AshirGuptash)
+* [Calvin Kerns](https://github.com/calkerns)
diff --git a/README.md b/README.md
index 531be3d..d26483f 100644
--- a/README.md
+++ b/README.md
@@ -51,13 +51,16 @@ To run on a Git Diff patch. Run
$ dyc diff
```
-
To have Docstrings prepended on a method while development.
Run the following command
```sh
$ dyc diff --watch
```
+In order to bypass the text editor pop-up in the confirmation stage. Run
+```sh
+$ dyc start --skip-confirm
+```
## Method Docstring Options
*You can also Setup your own customized Docstring Method Formatting in `dyc.yaml` within `formats` key*
@@ -93,15 +96,47 @@ $ dyc diff --watch
| `ignore` | Arguments to ignore. | list |
| `prefix` | A prefix like "@param". | str |
+
+
## Classes Docstring Options
-// TODO
+*You can also Setup your own customized Docstring Class Formatting in `dyc.yaml` within `formats` key*
+
+
+*Class*
+
+| Key | Description | Type |
+|:-----------------------: |:-----------------------------------------------------------------------------------------------------------------------: |------|
+| `keywords` | The necessary keyword to search for in a line that triggers actions (default = ["class"]) | list |
+| `enabled` | Determine if formatting is enabled for the extension | bool |
+| `indent` | Indentation in a class. Limited options ['tab', '2 spaces', '4 spaces'] | str |
+| `indent_content` | Confirm if the content of a docstring has to be indented as well | bool |
+| `open` | Starting opener text of a method | str |
+| `close` | Close text of a class. This could be the same as opened, but not all languages opening and closing docstrings are same | str |
+| `comments` | Comments symbols | str |
+| `break_after_open` | Do we add a new line after adding the open strings of a class? | bool |
+| `break_after_docstring` | Do we add a new line after adding the docstring? | bool |
+| `break_before_close` | Add a new line before closing docstrings on a class | bool |
+| `words_per_line` | How many words do we add per docstring? | int |
+| `within_scope` | Should the docstring be within the scope of the class or out? Just like JS Method docstrings | bool |
+
+
+*Parents*
+
+| Key | Description | Type |
+|:-----------: |:---------------------------------------------: |:----: |
+| `title` | A title for arguments. i.e: "Inheritance" | str |
+| `underline` | Underline the title | bool |
+| `inline` | Add docstrings all inline or break within. | bool |
+| `prefix` | A prefix like "@parent". | str |
+
+
## Top of file Options
// TODO
-### Example
+## Example
```sh
$ cd myapp/
@@ -260,6 +295,8 @@ def hello(name):
## Development
+### Setup
+
Thank you for taking the time to contribute into this project. It is simple but could be really helpful moving forward to all developers.
To get started.
@@ -273,7 +310,6 @@ To get started.
$ pip install --editable .
```
-
Before commiting:
Install the pre-commit hooks
diff --git a/dyc/base.py b/dyc/base.py
index 704b972..6ea064c 100644
--- a/dyc/base.py
+++ b/dyc/base.py
@@ -6,10 +6,11 @@
class Builder(object):
- def __init__(self, filename, config, placeholders=False):
+ def __init__(self, filename, config, placeholders=False, skip_confirm=False):
self.filename = filename
self.config = config
self.placeholders = placeholders
+ self.skip_confirm = skip_confirm
details = dict()
@@ -35,7 +36,7 @@ def initialize(self, change=None):
word.lstrip() for word in line.split(" ") if word.lstrip() in keywords
]
found = len(foundList) > 0 and not is_comment(
- line, self.config.get('comments')
+ line, self.config.get("comments")
)
# Checking an unusual format in method declaration
if foundList:
diff --git a/dyc/classes.py b/dyc/classes.py
new file mode 100644
index 0000000..c0e90ef
--- /dev/null
+++ b/dyc/classes.py
@@ -0,0 +1,528 @@
+import sys
+import fileinput
+import linecache
+import click
+import os
+import re
+import copy
+from .utils import (
+ get_leading_whitespace,
+ add_start_end,
+ is_comment,
+ is_one_line_method,
+ BlankFormatter,
+ get_indent,
+)
+from .base import Builder
+
+
+class ClassBuilder(Builder):
+ already_printed_filepaths = [] # list of already printed files
+
+ def validate(self, result):
+ """
+ An abstract validator method that checks if the class is
+ still valid and gives the final decision
+ Parameters
+ ----------
+ ClassInterface result: The Class Interface result
+ """
+ if not result:
+ return False
+ name = result.name
+ if name not in self.config.get(
+ "ignore", []
+ ) and not self.is_first_line_documented(result):
+ if (
+ self.filename not in self.already_printed_filepaths
+ ): # Print file of class to document
+ click.echo(
+ "\n\nIn file {} :\n".format(
+ click.style(
+ os.path.join(*self.filename.split(os.sep)[-3:]), fg="red"
+ )
+ )
+ )
+ self.already_printed_filepaths.append(self.filename)
+ confirmed = (
+ True
+ if self.placeholders
+ else click.confirm(
+ "Do you want to document class {}?".format(
+ click.style(name, fg="green")
+ )
+ )
+ )
+ if confirmed:
+ return True
+
+ return False
+
+ def is_first_line_documented(self, result):
+ """
+ A boolean function that determines weather the first line has
+ a docstring or not
+ Parameters
+ ----------
+ ClassInterface result: Is a class interface class that could be
+ subject to be taking a docstring
+ str line: The line of the found class
+ """
+ returned = False
+ # copied from pull request related to issue #63
+ read_first_line = linecache.getline(result.filename, result.start)
+ read_second_line = linecache.getline(result.filename, result.start + 1)
+ finalTwoLines = read_first_line + "\n" + read_second_line
+ # The open_brace_pattern is """ by default, but can be configured in the yml files
+ open_brace_pattern = self.config.get("open", None)
+ pattern = r":\n\s*?({})".format(open_brace_pattern)
+ # Other experimental regexe patterns below
+ #:\n\s{4}(?:""") #r':[\s\S]?[\n][\s]*(""")'
+ match = re.search(pattern, finalTwoLines)
+ returned = True if match else False
+
+ linecache.clearcache()
+ return returned
+
+ def extract_and_set_information(self, filename, start, line, length):
+ """
+ This is a main abstract method tin the builder base
+ to add result into details. Used in Class Builder to
+ pull the candidates that are subject to docstrings
+ Parameters
+ ----------
+ str filename: The file's name
+ int start: Starting line
+ str line: Full line text
+ int length: The length of the extracted data
+ """
+ # ------------------------------#
+ # CAN GENERALIZE THIS FUNCTION #
+ # ------------------------------#
+ start_line = linecache.getline(filename, start)
+ initial_line = line
+ start_leading_space = get_leading_whitespace(
+ start_line
+ ) # Where function started
+ class_string = start_line
+ if not is_one_line_method(start_line, self.config.get("keywords")):
+ class_string = line
+ linesBackwards = class_string.count("\n") - 1
+ start_leading_space = get_leading_whitespace(
+ linecache.getline(filename, start - linesBackwards)
+ )
+ line_within_scope = True
+ lineno = start + 1
+ line = linecache.getline(filename, lineno)
+ end_of_file = False
+ end = None
+ while line_within_scope and not end_of_file:
+ current_leading_space = get_leading_whitespace(line)
+ if len(current_leading_space) <= len(start_leading_space) and line.strip():
+ end = lineno - 1
+ break
+ class_string += line
+ lineno = lineno + 1
+ line = linecache.getline(filename, int(lineno))
+ end_of_file = True if lineno > length else False
+
+ if not end:
+ end = length
+
+ linecache.clearcache()
+ return ClassInterface(
+ plain=class_string,
+ name=self._get_name(initial_line),
+ start=start,
+ end=end,
+ filename=filename,
+ classes=self.extract_classes(initial_line.strip("\n")),
+ config=self.config,
+ leading_space=get_leading_whitespace(initial_line),
+ placeholders=self.placeholders,
+ skip_confirm=self.skip_confirm,
+ )
+
+ def extract_classes(self, line):
+ """
+ Public method to extract parents that calls ClassDetails
+ class to extract parents
+ Parameters
+ ----------
+ str line: line that contains the parents of the class
+ """
+ parents = ClassDetails(line, self.config.get("parents", {}))
+ parents.extract()
+ return parents.sanitize()
+
+ def prompts(self):
+ """
+ Abstract prompt method in builder to execute prompts over candidates
+ """
+ for class_interface in self._class_interface_gen():
+ class_interface.prompt() if class_interface else None
+
+ def _class_interface_gen(self):
+ """
+ A generator that yields the class interfaces
+ """
+ if not self.details:
+ yield None
+ for filename, func_pack in self.details.items():
+ for class_interface in func_pack.values():
+ yield class_interface
+
+ def apply(self):
+ """
+ Over here we are looping over the result of the
+ chosen classes to document and applying the changes to the
+ files as confirmed
+ """
+ for class_interface in self._class_interface_gen():
+ if not class_interface:
+ continue
+ fileInput = fileinput.input(class_interface.filename, inplace=True)
+
+ for line in fileInput:
+ tmpLine = line
+ if self._is_class(line) and ":" not in line:
+ openedP = line.count("(")
+ closedP = line.count(")")
+ pos = 1
+ if openedP == closedP:
+ continue
+ else:
+ while openedP != closedP:
+ tmpLine += fileInput.readline()
+ openedP = tmpLine.count("(")
+ closedP = tmpLine.count(")")
+ pos += 1
+ line = tmpLine
+
+ if self._get_name(line) == class_interface.name:
+ if self.config.get("within_scope"):
+ sys.stdout.write(line + class_interface.result + "\n")
+ else:
+ sys.stdout.write(class_interface.result + "\n" + line)
+ else:
+ sys.stdout.write(line)
+
+ def _get_name(self, line):
+ """
+ Grabs the name of the class from the given line
+ Parameters
+ ----------
+ str line: String line that has the class' name
+ """
+ for keyword in self.config.get("keywords", []):
+ clear_defs = re.sub("{} ".format(keyword), "", line.strip())
+ name = re.sub(r"\([^)]*\)\:", "", clear_defs).strip()
+ if re.search(r"\(([\s\S]*)\)", name):
+ try:
+ name = re.match(r"^[^\(]+", name).group()
+ except:
+ pass
+ if name:
+ return name
+
+ def _is_class(self, line):
+ """
+ A predicate method that checks if a line is a
+ class
+
+ Parameters
+ ----------
+ str line: Text string of a line in a file
+ """
+ return line.strip().split(" ")[0] in self.config.get("keywords")
+
+
+class ClassFormatter:
+
+ formatted_string = "{open}{break_after_open}{class_docstring}{break_after_docstring}{empty_line}{parents_format}{break_before_close}{close}"
+ fmt = BlankFormatter()
+
+ def format(self, skip_confirm):
+ """
+ Public formatting method that executes a pattern of methods to
+ complete the process
+ """
+ self.pre()
+ self.build_docstrings()
+ self.build_parents()
+ self.result = self.fmt.format(self.formatted_string, **self.class_format)
+ self.add_indentation()
+ self.polish()
+ self.skip_confirm = skip_confirm
+
+ def wrap_strings(self, words):
+ """
+ Compact how many words should be in a line
+ Parameters
+ ----------
+ str words: docstring given
+ """
+ subs = []
+ n = self.config.get("words_per_line")
+ for i in range(0, len(words), n):
+ subs.append(" ".join(words[i : i + n]))
+ return "\n".join(subs)
+
+ def pre(self):
+ """
+ In the formatter, this method sets up the object that
+ will be used in a formatted way,. Also translates configs
+ into consumable values
+ """
+ class_format = copy.deepcopy(self.config)
+ class_format["indent"] = (
+ get_indent(class_format["indent"]) if class_format["indent"] else " "
+ )
+ class_format["indent_content"] = (
+ get_indent(class_format["indent"])
+ if get_indent(class_format["indent_content"])
+ else ""
+ )
+ class_format["break_after_open"] = (
+ "\n" if class_format["break_after_open"] else ""
+ )
+ class_format["break_after_docstring"] = (
+ "\n" if class_format["break_after_docstring"] else ""
+ )
+ class_format["break_before_close"] = (
+ "\n" if class_format["break_before_close"] else ""
+ )
+ class_format["empty_line"] = "\n"
+
+ parents_format = copy.deepcopy(self.config.get("parents"))
+ parents_format["inline"] = "" if parents_format["inline"] else "\n"
+
+ self.class_format = class_format
+ self.parents_format = parents_format
+
+ def build_docstrings(self):
+ """
+ Mainly adds docstrings of the class after cleaning up text
+ into reasonable chunks
+ """
+ text = self.class_docstring or "Missing Docstring!"
+ self.class_format["class_docstring"] = self.wrap_strings(text.split(" "))
+
+ def build_parents(self):
+ """
+ Main function for wrapping up parent docstrings
+ """
+ if not self.classes: # if len(self.classes) == 0
+ self.class_format["parents_format"] = ""
+ self.class_format["break_before_close"] = ""
+ self.class_format["empty_line"] = ""
+ return
+
+ config = self.config.get("parents")
+ formatted_parents = "{prefix} {name}: {doc}"
+
+ title = self.parents_format.get("title")
+ if title:
+ underline = "-" * len(title)
+ self.parents_format["title"] = (
+ "{}\n{}\n".format(title, underline)
+ if config.get("underline")
+ else "{}\n".format(title)
+ )
+
+ result = []
+
+ if self.classes: # if len(self.classes) > 0
+ for parent_details in self.class_parent_list:
+ parent_details["prefix"] = self.parents_format.get("prefix")
+
+ result.append(
+ self.fmt.format(formatted_parents, **parent_details).strip()
+ )
+
+ self.parents_format["body"] = "\n".join(result)
+ self.class_format["parents_format"] = self.fmt.format(
+ "{title}{body}", **self.parents_format
+ )
+
+ def add_indentation(self):
+ """
+ Translates indent params to actual indents
+ """
+ temp = self.result.split("\n")
+ space = self.class_format.get("indent")
+ indent_content = self.class_format.get("indent_content")
+ if indent_content:
+ content = temp[1:-1]
+ content = [indent_content + docline for docline in temp][1:-1]
+ temp[1:-1] = content
+ self.result = "\n".join([space + docline for docline in temp])
+
+ def confirm(self, polished):
+ """
+ Pop up editor function to finally confirm if the documented
+ format is accepted
+ Parameters
+ ----------
+ str polished: complete polished string before popping up
+ """
+ polished = add_start_end(polished)
+ class_split = self.plain.split("\n")
+ if self.config.get("within_scope"):
+ # Check if class comes in an unusual format
+ keywords = self.config.get("keywords")
+ firstLine = class_split[0]
+ pos = 1
+ while not is_one_line_method(firstLine, keywords):
+ firstLine += class_split[pos]
+ pos += 1
+ class_split.insert(pos, polished)
+ else:
+ class_split.insert(0, polished)
+
+ try:
+ result = "\n".join(class_split)
+
+ # If running an automated test, skip the editor confirmation
+ if self.skip_confirm:
+ message = result
+ else:
+ message = click.edit(
+ "## CONFIRM: MODIFY DOCSTRING BETWEEN START AND END LINES ONLY\n\n"
+ + result
+ )
+ message = (
+ result if message == None else "\n".join(message.split("\n")[2:])
+ )
+ except:
+ print("Quitting the program in the editor terminates the process. Thanks")
+ sys.exit()
+
+ final = []
+ start = False
+ end = False
+
+ for x in message.split("\n"):
+ stripped = x.strip()
+ if stripped == "## END":
+ end = True
+ if start and not end:
+ final.append(x)
+ if stripped == "## START":
+ start = True
+
+ self.result = "\n".join(final)
+
+ def polish(self):
+ """
+ Editor wrapper to confirm result
+ """
+ docstring = self.result.split("\n")
+ polished = "\n".join([self.leading_space + docline for docline in docstring])
+ if self.placeholders:
+ self.result = polished
+ else:
+ self.confirm(polished)
+
+
+class ClassInterface(ClassFormatter):
+ def __init__(
+ self,
+ plain,
+ name,
+ start,
+ end,
+ filename,
+ classes,
+ config,
+ leading_space,
+ placeholders,
+ skip_confirm,
+ ):
+ self.plain = plain
+ self.name = name
+ self.start = start
+ self.end = end
+ self.filename = filename
+ self.classes = classes
+ self.class_docstring = ""
+ self.class_parent_list = []
+ self.config = config
+ self.leading_space = leading_space
+ self.placeholders = placeholders
+ self.skip_confirm = skip_confirm
+
+ def prompt(self):
+ self._prompt_docstring()
+ self._prompt_parents()
+ self.format(skip_confirm=self.skip_confirm)
+
+ def _prompt_docstring(self):
+ """
+ Simple prompt for a class' docstring
+ """
+ if self.placeholders:
+ self.class_docstring = ""
+ else:
+ echo_name = click.style(self.name, fg="green")
+ self.class_docstring = click.prompt(
+ "\n({}) Class docstring ".format(echo_name)
+ )
+
+ def _prompt_parents(self):
+ """
+ Wrapper for classes
+ """
+
+ def _echo_parent_style(parent):
+ """
+ Just a small wrapper for echoing parentss
+ Parameters
+ -----------
+ str parent: parent name
+ """
+ return click.style("{}".format(parent), fg="red")
+
+ for parent in self.classes:
+ doc_placeholder = ""
+ parent_doc = (
+ click.prompt(
+ "\n({}) Inherited class docstring ".format(
+ _echo_parent_style(parent)
+ )
+ )
+ if not self.placeholders
+ else doc_placeholder
+ )
+ self.class_parent_list.append(dict(doc=parent_doc, name=parent))
+
+
+class ClassDetails(object):
+ def __init__(self, line, config):
+ self.line = line
+ self.config = config
+ self.parents = []
+
+ def extract(self):
+ """
+ Retrieves class parents from a line and cleans them
+ """
+ try:
+ parents = re.search(r"\(([\s\S]*)\)", self.line).group(1).split(",")
+ self.parents = [
+ parent.replace("\n", "").replace("\t", "").strip() for parent in parents
+ ]
+ except:
+ pass
+ self.parents = [parent for parent in self.parents if parent != ""]
+
+ def sanitize(self):
+ """
+ Sanitizes classes to validate all classes are correct
+ """
+ # Updated filter function to remove invalid parent names due to bad syntax
+ return list(
+ filter(
+ lambda parent: not re.findall(r"[^a-zA-Z0-9_]", parent), self.parents
+ )
+ )
diff --git a/dyc/configs/__init__.py b/dyc/configs/__init__.py
index 605ff48..b7d5554 100644
--- a/dyc/configs/__init__.py
+++ b/dyc/configs/__init__.py
@@ -2,8 +2,8 @@
from ..utils import read_yaml
ROOT_PATH = os.getcwd()
-DEFAULT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'defaults.yaml')
-CUSTOM = os.path.join(ROOT_PATH, 'dyc.yaml')
+DEFAULT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "defaults.yaml")
+CUSTOM = os.path.join(ROOT_PATH, "dyc.yaml")
class Config(object):
@@ -31,27 +31,27 @@ def _override_formats(self):
Loops over the formats i.e `py`, `js`. And assigns the given customized values
in Root dyc.yaml file. Otherwise it will use the default values
"""
- formats = self.custom.get('formats', [])
+ formats = self.custom.get("formats", [])
for index, value in enumerate(formats):
- extension = value.get('extension')
+ extension = value.get("extension")
cnf_index = self._get_custom_extension_index(extension)
try:
for nested_key, nested_obj in value.iteritems():
try:
- self.plain.get('formats')[cnf_index][nested_key].update(
+ self.plain.get("formats")[cnf_index][nested_key].update(
**nested_obj
) if nested_obj else None
except AttributeError:
continue
except (IndexError, TypeError):
- self.plain.get('formats').append(value)
+ self.plain.get("formats").append(value)
def _get_custom_extension_index(self, extension):
"""
If a customised extension is defined. Add that to the config
"""
- for index, value in enumerate(self.plain.get('formats')):
- if value.get('extension') == extension:
+ for index, value in enumerate(self.plain.get("formats")):
+ if value.get("extension") == extension:
return index
def _is_mutated(self, value):
diff --git a/dyc/configs/defaults.yaml b/dyc/configs/defaults.yaml
index a119aec..f99e8a5 100644
--- a/dyc/configs/defaults.yaml
+++ b/dyc/configs/defaults.yaml
@@ -36,3 +36,20 @@ formats:
- self
class:
keywords: ['class']
+ enabled: true
+ indent: '4 spaces'
+ indent_content: false
+ open: '"""'
+ close: '"""'
+ comments:
+ - '#'
+ break_after_open: true
+ break_after_docstring: true
+ break_before_close: true
+ words_per_line: 10
+ within_scope: true
+ parents:
+ title: 'Inheritance'
+ underline: true
+ inline: false
+ prefix: ''
diff --git a/dyc/diff.py b/dyc/diff.py
index 6513ec0..ba5f9cc 100644
--- a/dyc/diff.py
+++ b/dyc/diff.py
@@ -15,7 +15,7 @@
class DiffParser:
- PREFIX = 'diff --git'
+ PREFIX = "diff --git"
def parse(self, staged=False):
"""
@@ -25,8 +25,8 @@ def parse(self, staged=False):
----------
bool staged: Only using the staged files
"""
- self.diffs = self.repo.index.diff('HEAD' if staged else None)
- self.plain = self.repo.git.diff('HEAD').split('\n')
+ self.diffs = self.repo.index.diff("HEAD" if staged else None)
+ self.plain = self.repo.git.diff("HEAD").split("\n")
return self._pack()
def _pack(self):
@@ -36,9 +36,9 @@ def _pack(self):
patches = []
for diff in self.diffs:
if not self.is_candidate(diff.a_path):
- print('File {} is not a candidate to apply DYC'.format(diff.a_path))
+ print("File {} is not a candidate to apply DYC".format(diff.a_path))
continue
- sep = '{} a/{} b/{}'.format(self.PREFIX, diff.a_path, diff.b_path)
+ sep = "{} a/{} b/{}".format(self.PREFIX, diff.a_path, diff.b_path)
patch = self.__clean(self.__patch(sep), diff)
patches.append(patch)
return patches
@@ -72,7 +72,7 @@ def __patch(self, separator):
break
elif hit:
patch.append(line)
- return '\n'.join(patch)
+ return "\n".join(patch)
def __pack(self, patch):
"""
@@ -90,14 +90,14 @@ def __pack(self, patch):
_hunk = get_hunk(line)
if (len(patch) - 1) == index:
- final.append(dict(patch='\n'.join(result), hunk=(start, end)))
+ final.append(dict(patch="\n".join(result), hunk=(start, end)))
if len(_hunk) and not hit:
start, end = get_additions_in_first_hunk(get_hunk(line))
hit = True
continue
elif len(_hunk) and hit:
- final.append(dict(patch='\n'.join(result), hunk=(start, end)))
+ final.append(dict(patch="\n".join(result), hunk=(start, end)))
start, end = get_additions_in_first_hunk(get_hunk(line))
result = []
hit = True
@@ -109,13 +109,13 @@ def __pack(self, patch):
def __clean(self, patch, diff):
"""Returns a clean dict of a path"""
result = {}
- result['additions'] = self.__additions(
- self.__pack(patch.split('\n')), diff.a_path
+ result["additions"] = self.__additions(
+ self.__pack(patch.split("\n")), diff.a_path
) # [{hunk: (start, end), patch:}]
- result['plain'] = patch
- result['diff'] = diff
- result['name'] = ntpath.basename(diff.a_path)
- result['path'] = diff.a_path
+ result["plain"] = patch
+ result["diff"] = diff
+ result["name"] = ntpath.basename(diff.a_path)
+ result["path"] = diff.a_path
return result
def __additions(self, hunks, path):
@@ -127,17 +127,17 @@ def __additions(self, hunks, path):
str path: path of a file
"""
for hunk in hunks:
- patch = hunk.get('patch')
+ patch = hunk.get("patch")
result = []
- for line in patch.split('\n'):
+ for line in patch.split("\n"):
try:
- if line[0] == '+' and not line.startswith('+++'):
+ if line[0] == "+" and not line.startswith("+++"):
l = line[1:]
result.append(l)
except IndexError as e:
print(e.message)
continue
- hunk['patch'] = '\n'.join(result)
+ hunk["patch"] = "\n".join(result)
return hunks
diff --git a/dyc/dyc.py b/dyc/dyc.py
index 5e97a6b..1ff4d6a 100644
--- a/dyc/dyc.py
+++ b/dyc/dyc.py
@@ -32,10 +32,11 @@ def main(config):
@main.command()
-@click.option('--placeholders', is_flag=True, default=False)
-@click.argument('files', nargs=-1, type=click.Path(exists=True), required=False)
+@click.option("--placeholders", is_flag=True, default=False)
+@click.argument("files", nargs=-1, type=click.Path(exists=True), required=False)
+@click.option("--skip-confirm", required=False, default=False, is_flag=True)
@config
-def start(config, files, placeholders):
+def start(config, files, placeholders, skip_confirm):
"""
This is the entry point of starting DYC for the whole project.
When you run `dyc start`. ParsedConfig will wrap all the
@@ -43,16 +44,17 @@ def start(config, files, placeholders):
over and add missing documentation on.
"""
if files:
- config.plain['file_list'] = list(files)
- dyc = DYC(config.plain, placeholders=placeholders)
+ config.plain["file_list"] = list(files)
+ dyc = DYC(config.plain, placeholders=placeholders, skip_confirm=skip_confirm)
dyc.prepare()
dyc.process_methods()
dyc.process_top()
+ dyc.process_classes()
@main.command()
@click.option(
- '--watch', help='Add default placeholder when watching', is_flag=True, default=False
+ "--watch", help="Add default placeholder when watching", is_flag=True, default=False
)
@config
def diff(config, watch):
@@ -64,7 +66,7 @@ def diff(config, watch):
else:
diff = Diff(config.plain)
uncommitted = diff.uncommitted
- paths = [idx.get('path') for idx in uncommitted]
+ paths = [idx.get("path") for idx in uncommitted]
if len(uncommitted):
dyc = DYC(config.plain)
dyc.prepare(files=paths)
diff --git a/dyc/events.py b/dyc/events.py
index aefa67f..e39fbe7 100644
--- a/dyc/events.py
+++ b/dyc/events.py
@@ -27,14 +27,14 @@ def dispatch(self, event):
diff = Diff(self.config.plain)
uncommitted = diff.uncommitted
paths = [
- idx.get('path')
+ idx.get("path")
for idx in uncommitted
- if './{}'.format(idx.get('path')) == event.src_path
+ if "./{}".format(idx.get("path")) == event.src_path
]
filtered = [
idx
for idx in uncommitted
- if './{}'.format(idx.get('path')) == event.src_path
+ if "./{}".format(idx.get("path")) == event.src_path
]
if len(filtered):
dyc = DYC(self.config.plain, placeholders=True)
@@ -57,12 +57,12 @@ def start(cls, config):
observer = Observer()
event_handler = WatchEvent()
event_handler.config = config
- observer.schedule(event_handler, '.', recursive=True)
+ observer.schedule(event_handler, ".", recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
- print('Quitting..')
+ print("Quitting..")
observer.join()
diff --git a/dyc/main.py b/dyc/main.py
index c80c7c1..81ba604 100644
--- a/dyc/main.py
+++ b/dyc/main.py
@@ -9,13 +9,15 @@
from .utils import get_extension
from .methods import MethodBuilder
from .top import TopBuilder
+from .classes import ClassBuilder
from .base import Processor
class DYC(Processor):
- def __init__(self, config, details=None, placeholders=False):
+ def __init__(self, config, details=None, placeholders=False, skip_confirm=False):
self.config = config
self.placeholders = placeholders
+ self.skip_confirm = skip_confirm
def process_methods(self, diff_only=False, changes=[]):
"""
@@ -45,7 +47,10 @@ def process_methods(self, diff_only=False, changes=[]):
method_cnf = fmt.get("method", {})
method_cnf["arguments"] = fmt.get("arguments")
builder = MethodBuilder(
- filename, method_cnf, placeholders=self.placeholders
+ filename,
+ method_cnf,
+ placeholders=self.placeholders,
+ skip_confirm=self.skip_confirm,
)
builder.initialize(change=change)
builder.prompts()
@@ -54,10 +59,24 @@ def process_methods(self, diff_only=False, changes=[]):
def process_classes(self):
"""
- Main method that documents Classes in a file. Still TODO
+ Main method that documents Classes in a file.
"""
- # self.classes = ClassesBuilder()
- pass
+ print("\nProcessing Classes\n\r")
+ for filename in self.file_list:
+ extension = get_extension(filename)
+ fmt = self.formats.get(extension)
+ classes_cnf = fmt.get("class", {})
+ classes_cnf["parents"] = fmt.get("parents")
+ builder = ClassBuilder(
+ filename,
+ classes_cnf,
+ placeholders=self.placeholders,
+ skip_confirm=self.skip_confirm,
+ )
+ builder.initialize()
+ builder.prompts()
+ builder.apply()
+ builder.clear(filename)
def process_top(self, diff_only=False):
"""
@@ -69,7 +88,12 @@ def process_top(self, diff_only=False):
extension = get_extension(filename)
fmt = self.formats.get(extension)
top_cnf = fmt.get("top", {})
- builder = TopBuilder(filename, top_cnf, placeholders=self.placeholders)
+ builder = TopBuilder(
+ filename,
+ top_cnf,
+ placeholders=self.placeholders,
+ skip_confirm=self.skip_confirm,
+ )
builder.prompts()
builder.apply()
builder.clear(filename)
diff --git a/dyc/parser.py b/dyc/parser.py
index b8b6558..3e75bba 100644
--- a/dyc/parser.py
+++ b/dyc/parser.py
@@ -17,7 +17,7 @@ def __init__(self):
except AttributeError:
click.echo(
click.style(
- '`dyc.yaml` Missing or Incorrectly formatted. USING default settings',
- fg='cyan',
+ "`dyc.yaml` Missing or Incorrectly formatted. USING default settings",
+ fg="cyan",
)
)
diff --git a/dyc/utils.py b/dyc/utils.py
index 09e5502..887e4f5 100644
--- a/dyc/utils.py
+++ b/dyc/utils.py
@@ -49,7 +49,7 @@ def __init__(self, default=""):
self.default = default
def get_value(self, key, args, kwds):
- """"""
+ """ """
if isinstance(key, str):
return kwds.get(key, self.default)
else:
@@ -186,4 +186,4 @@ def is_comment(line, comments):
str line: The line string in a file
list comments: A list of potential comment keywords
"""
- return line.lstrip(' ')[0] in comments
+ return line.lstrip(" ")[0] in comments
diff --git a/setup.py b/setup.py
index a44f63b..15824b3 100644
--- a/setup.py
+++ b/setup.py
@@ -21,5 +21,5 @@
"pre-commit==1.18.1",
],
entry_points={"console_scripts": ["dyc=dyc.dyc:main"]},
- package_data={'': ['*.yaml']},
+ package_data={"": ["*.yaml"]},
)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_case_files/class_test_1.in b/tests/test_case_files/class_test_1.in
new file mode 100644
index 0000000..c543add
--- /dev/null
+++ b/tests/test_case_files/class_test_1.in
@@ -0,0 +1,17 @@
+N
+y
+y
+y
+y
+y
+MyClass Description
+MyClass1 Description
+Parent1 Description
+MyClass2 Description
+Parent1 Description
+Parent2 Description
+MyClass3 Description
+Parent1 Description
+MyClass4 Description
+Parent1 Description
+Parent2 Description
diff --git a/tests/test_case_files/class_test_1.py b/tests/test_case_files/class_test_1.py
new file mode 100755
index 0000000..f719a28
--- /dev/null
+++ b/tests/test_case_files/class_test_1.py
@@ -0,0 +1,20 @@
+class MyClass:
+
+ x = 1
+
+ class MyClass1(Parent1):
+
+ y = 1
+
+ class MyClass2(Parent1, Parent2):
+
+ z = 1
+
+ class MyClass3(Parent1):
+
+ a = 1
+
+
+class MyClass4(Parent1, Parent2):
+
+ b = 1
diff --git a/tests/test_case_files/class_test_1_correct.py b/tests/test_case_files/class_test_1_correct.py
new file mode 100644
index 0000000..f17a85f
--- /dev/null
+++ b/tests/test_case_files/class_test_1_correct.py
@@ -0,0 +1,53 @@
+class MyClass:
+ """
+ MyClass Description
+ """
+
+ x = 1
+
+ class MyClass1(Parent1):
+ """
+ MyClass1 Description
+
+ Inheritance
+ -----------
+ Parent1: Parent1 Description
+ """
+
+ y = 1
+
+ class MyClass2(Parent1, Parent2):
+ """
+ MyClass2 Description
+
+ Inheritance
+ -----------
+ Parent1: Parent1 Description
+ Parent2: Parent2 Description
+ """
+
+ z = 1
+
+ class MyClass3(Parent1):
+ """
+ MyClass3 Description
+
+ Inheritance
+ -----------
+ Parent1: Parent1 Description
+ """
+
+ a = 1
+
+
+class MyClass4(Parent1, Parent2):
+ """
+ MyClass4 Description
+
+ Inheritance
+ -----------
+ Parent1: Parent1 Description
+ Parent2: Parent2 Description
+ """
+
+ b = 1
diff --git a/tests/test_classes.py b/tests/test_classes.py
new file mode 100644
index 0000000..54066b1
--- /dev/null
+++ b/tests/test_classes.py
@@ -0,0 +1,110 @@
+from dyc.classes import (
+ ClassBuilder,
+ ClassDetails,
+ ClassInterface,
+ ClassFormatter,
+)
+from dyc.utils import get_extension
+from dyc.dyc import start
+import click
+from click.testing import CliRunner
+import pytest
+
+
+class TestClassInterface:
+ def testSetDocstring(self):
+ @click.command()
+ def runner():
+ interface = ClassInterface(
+ "plain", "name", 8, 11, "empty_file", ["x", "y"], {}, 4, False, False
+ )
+ interface._prompt_docstring()
+ print(interface.class_docstring)
+ return
+
+ runn = CliRunner()
+ # input = ["N", "y", "y", "Class 1 Descritposf"]
+ result = runn.invoke(runner, input="test")
+ assert result.output.split("\n")[2] == "test"
+
+
+class TestClassDetails:
+ def testExtract(self):
+ detail = ClassDetails("(~, y, z)", {})
+ detail.extract()
+ assert detail.parents == ["~", "y", "z"]
+
+ def testSanitize(self):
+ detail = ClassDetails("(~invalid, y, z)", {})
+ detail.extract()
+ assert detail.parents == ["~invalid", "y", "z"]
+ sanitized = list(detail.sanitize())
+ assert sanitized == ["y", "z"]
+
+ detail_2 = ClassDetails("(valid, inval!d, also invalid)", {})
+ detail_2.extract()
+ assert detail_2.parents == ["valid", "inval!d", "also invalid"]
+ sanitized = list(detail_2.sanitize())
+ assert sanitized == ["valid"]
+
+
+class TestClassBuilder:
+ def testIsClass(self):
+ builder = ClassBuilder(
+ "filename", {"keywords": ["class"]}, placeholders=False, skip_confirm=False
+ )
+ assert builder._is_class("class") == True
+ assert builder._is_class("classes = 5") == False
+
+ def testGetName(self):
+ builder = ClassBuilder(
+ "filename", {"keywords": ["class"]}, placeholders=False, skip_confirm=False
+ )
+ assert builder._get_name("class test") == "test"
+ assert builder._get_name("class test()") == "test"
+ assert builder._get_name("class test(\n):") == "test"
+
+ def testPrompts(self):
+ builder = ClassBuilder(
+ "filename", {"keywords": ["class"]}, placeholders=False, skip_confirm=False
+ )
+ assert builder.details == dict()
+ assert builder.prompts() == None
+
+ def testExtractClasses(self):
+ builder = ClassBuilder(
+ "filename", {"keywords": ["class"]}, placeholders=False, skip_confirm=False
+ )
+ assert builder.extract_classes("class test(x,y,z)") == ["x", "y", "z"]
+ assert builder.extract_classes("class test:") == []
+
+ # IN PROGRESS BELOW
+ # CURRENT STATUS: PASSES ON PYTHON 3.8.5, BUT FAILS ON PYTHON 2.7.15
+
+ # @pytest.mark.parametrize("filename", ["tests/test_case_files/class_test_1.py"])
+ # def testProcess(self, filename):
+ # # Get oracle file from the filename of the input file
+ # oracle_file = filename.split(".")[0] + "_correct.py"
+
+ # # Save original contents of test file to a string to rewrite later
+ # with open(filename, "r") as file_obj:
+ # test_file_orig_contents = file_obj.read()
+
+ # # Save the user input
+ # user_input_file = filename.split(".")[0] + ".in"
+ # with open(user_input_file, "r") as user_input_obj:
+ # user_input = user_input_obj.read()
+
+ # runner = CliRunner()
+ # result = runner.invoke(start, ["--skip-confirm", filename], input=user_input)
+
+ # # Compare to Oracle
+ # with open(filename, "r") as file_obj, open(oracle_file, "r") as oracle_obj:
+ # # Replace dumb whitespace
+ # assert file_obj.read().replace(" ", "") == oracle_obj.read().replace(
+ # " ", ""
+ # )
+
+ # # Write back original contents
+ # with open(filename, "w") as file_obj:
+ # file_obj.write(test_file_orig_contents)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 026fb92..267ce6f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -17,15 +17,15 @@ def test_tabs(self):
"""
"""Test tabs functionality"""
- text = '\t\tHello'
- expected = '\t\t'
+ text = "\t\tHello"
+ expected = "\t\t"
got = get_leading_whitespace(text)
assert expected == got
def test_whitespace(self):
"""Test whitespace functionality"""
- space = ' '
- text = '{space}Such a long whitespace'.format(space=space)
+ space = " "
+ text = "{space}Such a long whitespace".format(space=space)
expected = space
got = get_leading_whitespace(text)
assert expected == got
@@ -40,7 +40,7 @@ def test_should_return_none_if_not_found(self):
----------
"""
- random_path = '/path/to/non/existing/file.yaml'
+ random_path = "/path/to/non/existing/file.yaml"
expected = None
got = read_yaml(random_path)
assert expected == got
@@ -55,7 +55,7 @@ def test_tabs(self):
----------
"""
- assert get_indent('tab') == '\t'
+ assert get_indent("tab") == "\t"
def test_2_spaces(self):
"""
@@ -65,7 +65,7 @@ def test_2_spaces(self):
----------
"""
- assert get_indent('2 spaces') == ' '
+ assert get_indent("2 spaces") == " "
def test_falsy_value(self):
"""
@@ -75,7 +75,7 @@ def test_falsy_value(self):
----------
"""
- assert get_indent(False) == ''
+ assert get_indent(False) == ""
def test_default_4_spaces(self):
"""
@@ -86,7 +86,7 @@ def test_default_4_spaces(self):
----------
"""
- assert get_indent(None) == ' '
+ assert get_indent(None) == " "
class TestGetExtension:
@@ -98,8 +98,8 @@ def test_existing_extension_valid(self):
----------
"""
- ext = 'file.puk'
- expected = 'puk'
+ ext = "file.puk"
+ expected = "puk"
got = get_extension(ext)
assert expected == got
@@ -112,8 +112,8 @@ def test_non_existing_extension(self):
----------
"""
- ext = 'file'
- expected = ''
+ ext = "file"
+ expected = ""
got = get_extension(ext)
assert expected == got
@@ -127,7 +127,7 @@ def test_wrong_extension_type(self):
"""
exts = [dict(), False, True, [], 123]
- expected = ''
+ expected = ""
for ext in exts:
got = get_extension(ext)
assert expected == got
@@ -136,10 +136,10 @@ def test_wrong_extension_type(self):
class TestIsComment:
def test_valid_comments(self):
"""Testing valid comments"""
- text = '# Hello World'
- assert is_comment(text, ['#']) == True
+ text = "# Hello World"
+ assert is_comment(text, ["#"]) == True
def test_invalid_comments(self):
"""Testing invalid comments"""
- text = '# Hello World'
- assert is_comment(text, ['//']) == False
+ text = "# Hello World"
+ assert is_comment(text, ["//"]) == False