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