From 3cd18bf2a73ca0e774073f7d9e6af3adc2ef3168 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Sun, 16 May 2021 17:40:38 +0300 Subject: [PATCH 1/7] =?UTF-8?q?The=20initial=20creation=20of=20the=20main?= =?UTF-8?q?=20components=20=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + codemate/__init__.py | 4 + codemate/block.py | 365 +++++++++++++++++++++++++++++++++++++++++ codemate/exceptions.py | 17 ++ codemate/file.py | 56 +++++++ codemate/method.py | 80 +++++++++ codemate/structure.py | 129 +++++++++++++++ codemate/utils.py | 17 ++ requirements.txt | 2 + 9 files changed, 673 insertions(+) create mode 100644 codemate/__init__.py create mode 100644 codemate/block.py create mode 100644 codemate/exceptions.py create mode 100644 codemate/file.py create mode 100644 codemate/method.py create mode 100644 codemate/structure.py create mode 100644 codemate/utils.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index b6e4761..bb1b755 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +#Pycharm +.idea/ \ No newline at end of file diff --git a/codemate/__init__.py b/codemate/__init__.py new file mode 100644 index 0000000..0c6ebe9 --- /dev/null +++ b/codemate/__init__.py @@ -0,0 +1,4 @@ +from codemate.block import Block +from codemate.file import File +from codemate.method import ClassMethod, Method, StaticMethod +from codemate.structure import Class, Function diff --git a/codemate/block.py b/codemate/block.py new file mode 100644 index 0000000..a4d2326 --- /dev/null +++ b/codemate/block.py @@ -0,0 +1,365 @@ +import re +from functools import partial +from typing import List, Set + +import black +import isort + +from codemate.exceptions import InputError, PythonSyntaxError +from codemate.utils import remove_indentation + + +class Block: + """ + A generator of a Python block syntax. + + The block syntax structure: + + * Documentation - Order is kept, when using block parsing it will be striped. + * Imports - Sorted and modified by isort - https://github.com/PyCQA/isort. + * General syntax lines - Order is kept, when using block parsing it will be striped. + + The only way to control the structure is by using the following methods: + + * add_line + * add_lines + * add_syntax_block + * add_variable + * extend + * insert + * syntax + + Methods for adding documentation: + + * add_doc_line + * add_doc_lines + * add_doc_block + + Methods for adding imports: + + * add_import + * add_imports + * add_specific_import + + Methods for adding Python syntax: + + * add_syntax_line + * add_syntax_lines + * add_syntax_block + * add_variable + * extend + * insert + + Methods for getting the generated syntax: + + * syntax + * use_black + + Other Methods: + + * validate + * parse_line + + Args: + indentation (int): Determines how many spaces are used in the syntax indentation. + """ + + def __init__( + self, + indentation: int = 4, + ) -> None: + self._indentation = indentation * " " + + self._docs: List[str] = [] + self._imports: Set[str] = set() + self._lines: List[str] = [] + + def parse_block(self, block: str, new_line: int = 0, indent: int = 0) -> str: + """ + Parsing a given block to be in the proper indentation and place. + + Args: + block (str): A string that represents multiple line . + new_line (int): How many empty new lines to insert before the block. + indent (indent): How many indentations to insert before each line. + + Returns: + str: The parsed line. + """ + syntax = "\n" * new_line + syntax += re.sub("(^|\n)", f"\\1{self._indentation * indent}", block) + return syntax + + def add_doc_line(self, line: str, indent: int = 0) -> "Block": + """ + Adds a documentation line to the Python block syntax. + + Args: + line (str): Documentation line of the block. + indent (int): How much to indent the syntax. + + Returns: + Block: The block instance. + """ + doc = self.parse_block(line, indent=indent) + self._docs.append(doc) + return self + + def add_doc_lines(self, *lines: str, indent: int = 0) -> "Block": + """ + Adds documentation lines to the Python block syntax. + + Args: + *lines (Collection[str]): Documentation lines of the block. + indent (int): How much to indent the syntax. + + Returns: + Block: The block instance. + """ + add_doc_line = partial(self.add_doc_line, indent=indent) + tuple(map(add_doc_line, lines)) + return self + + def add_doc_block(self, block: str, indent: int = 0) -> "Block": + """ + Adds documentation block of lines to the Python block syntax. + + Args: + block (str): Documentation block. + indent (int): How much to indent the syntax. + + Returns: + Block: The block instance. + """ + lines = remove_indentation(block).strip().split("\n") + self.add_doc_lines(*lines, indent=indent) + return self + + def add_import(self, module: str) -> "Block": + """ + Adds an import syntax line to the Python block syntax. + + Args: + module (str): The module name that we want to import. + + Returns: + Block: The block instance. + """ + self._imports.add(f"import {module}") + return self + + def add_imports(self, *modules: str) -> "Block": + """ + Adds imports syntax lines to the Python block syntax. + + Args: + modules (Collection[str]): The modules names that we want to import. + + Returns: + Block: The block instance. + + Raises: + ValueError: When one of the provided modules is an empty string. + """ + for module in modules: + if module: + self.add_import(module) + else: + raise ValueError("Module name can't be empty") + return self + + def add_specific_import(self, module: str, *components: str) -> "Block": + """ + Adds a specific import syntax to the Python block syntax. + + Args: + module (str): The module name that we want to import. + *components (Tuple[str]): The components that we want to import directly from + the given module. + + Returns: + Block: The block instance. + """ + if components: + self._imports.add(f"from {module} import {', '.join(components)}") + return self + + def add_syntax_line(self, line: str, indent: int = 0) -> "Block": + """ + Adds a Python line syntax to the Python block syntax. + + Args: + line (str): The Python line syntax that we want to insert. + indent (int): How much to indent the line. + + Returns: + Block: The block instance. + """ + syntax = self.parse_block(line, indent=indent) + self._lines.append(syntax) + return self + + def add_syntax_lines(self, *lines: str, indent: int = 0) -> "Block": + """ + Adds multiple Python lines syntax to the Python block syntax. + + Args: + *lines (Collection[str]): The Python lines syntax that we want to insert. + indent (int): How much to indent the lines. + + Returns: + Block: The block instance. + """ + add_syntax_line = partial(self.add_syntax_line, indent=indent) + tuple(map(add_syntax_line, lines)) + return self + + def add_syntax_block(self, block: str, indent: int = 0) -> "Block": + """ + Adds a block of Python lines syntax to the Python block syntax. + + Args: + block (str): A Python syntax block. + indent (int): How much to indent the lines. + + Returns: + Block: The block instance. + """ + lines = remove_indentation(block).strip().split("\n") + self.add_syntax_lines(*lines, indent=indent) + return self + + def add_variable(self, name: str, **kwargs: str) -> "Block": + """ + Adds a variable Python syntax to the class. + + Args: + name (str): The name of the variable. + + Keyword Args: + type (:obj:`str`, optional): The type of the variable. + value (:obj:`str`, optional): The value of the variable. + + Returns: + Class: The class instance. + """ + syntax = name + if not (kwargs["type"] or kwargs["value"]): + message = "Class variable must have at least 'type' or 'value'" + raise PythonSyntaxError(message) + if kwargs["type"]: + syntax += f": {kwargs['type']}" + if kwargs["value"]: + syntax += f" = {kwargs['value']}" + self.add_syntax_line(syntax) + return self + + def extend(self, block: "Block") -> "Block": + """ + Adds other Python block syntax to the current Python block syntax. + + Args: + block (Block): The block that we want to add. + + Returns: + Block: The block instance. + """ + self._imports.update(block._imports) # pylint: disable=protected-access + self._lines.extend(block._lines) # pylint: disable=protected-access + return self + + def insert(self, block: "Block") -> "Block": + """ + Insert as is other Python block syntax to the current Python block syntax. + + Args: + block (Block): The block that we want to add. + + Returns: + Block: The block instance. + """ + self._lines.append(block.syntax()) + return self + + def _format_docs(self, indent: int) -> str: + format_line = partial(self.parse_block, new_line=1, indent=indent) + syntax = self.parse_block('"""', indent=indent) + syntax += "".join(format_line(doc) for doc in self._docs) + syntax += format_line('"""') + return syntax + + def _format_imports(self, indent: int) -> str: + format_line = partial(self.parse_block, new_line=1, indent=indent) + syntax = "".join(format_line(import_) for import_ in self._imports) + return isort.code(syntax) + + def syntax(self, indent: int = 0, imports: bool = True) -> str: + """ + Convert the block structure to Python syntax. + + Args: + indent (int): How much to indent the block syntax. + imports (bool): Whether to add imports or not to the block syntax. + + Returns: + str: The block syntax. + """ + format_line = partial(self.parse_block, new_line=1, indent=indent) + syntax = "" + if self._docs: + syntax += self._format_docs(indent) + if imports: + syntax += self._format_imports(indent) + syntax += "".join(format_line(str(line)) for line in self._lines) + return syntax + + def use_black(self) -> str: + """ + Convert the block structure to python syntax formatted by Black. + + References: + * Black docs - https://github.com/psf/black + * The solution - https://stackoverflow.com/a/57653302 + + Returns: + str: The block syntax. + """ + return black.format_str(self.syntax(), mode=black.FileMode()) + + def validate(self) -> bool: + """ + Checks if the generated syntax is valid. + If not raises 'InputError' error that wraps black.InvalidInput error. + + Returns: + bool: True the generated syntax is valid. + + Raises: + InputError: When the generated Python code isn't valid by black. + """ + try: + self.use_black() + except black.InvalidInput as error: + raise InputError(error) from None + else: + return True + + def __repr__(self): + class_name = type(self) + return f"{class_name}({vars(self)})" + + def __str__(self) -> str: + return self.syntax() + + def __contains__(self, value: str) -> bool: + """ + x.__contains__(y) <==> y in x. + + Raises: + ValueError: When the provided input isn't instance of string. + """ + if not isinstance(value, str): + type_name = type(value).__name__ + error = f"Argument 'value' should be instance of 'str' not '{type_name}'" + raise ValueError(error) + return value in str(self) diff --git a/codemate/exceptions.py b/codemate/exceptions.py new file mode 100644 index 0000000..f71b038 --- /dev/null +++ b/codemate/exceptions.py @@ -0,0 +1,17 @@ +import black + + +class GenerationError(Exception): + """Represents an exception while generating the Python syntax""" + + +class PythonSyntaxError(GenerationError): + """Represents an exception in the python syntax""" + + +class InputError(GenerationError, black.InvalidInput): + """Raised when the generated Python code isn't valid by black""" + + +class SaveFileError(GenerationError, OSError): + """Raised when the generated Python code file can't be created""" diff --git a/codemate/file.py b/codemate/file.py new file mode 100644 index 0000000..5d194bb --- /dev/null +++ b/codemate/file.py @@ -0,0 +1,56 @@ +from datetime import datetime +from typing import Optional + +from codemate.block import Block +from codemate.exceptions import SaveFileError + + +def generate_header() -> str: + """ + Generates a file header. + + Returns: + str: The generated header of a file. + """ + syntax = " Warning generated file ".center(90, "-") + date = datetime.now().isoformat() + syntax += "\n" + syntax += f"Generated at: {date}" + syntax += "\n" + syntax += "".center(90, "-") + return syntax + + +class File(Block): + """ + Creates a Python file syntax. + + Args: + header (Optional[str]): A block string that represents the files header + """ + + def __init__(self, header: Optional[str] = generate_header()) -> None: + super().__init__() + if header: + self.add_doc_block(block=header) + + def save(self, path: str, use_black=True) -> None: + """ + Save the generated Python file in a given location. + + Args: + path (str): The path to the location that we want to save the file at. + use_black (bool): When true black linter will be used to format the generated + Python code. + + Raises: + SaveFileError: When the generated Python code file can't be created. + """ + try: + with open(path, "w") as file: + if use_black: + file.write(self.use_black()) + else: + file.write(self.syntax()) + except OSError as error: + raise SaveFileError("Can't create the generated file") from error diff --git a/codemate/method.py b/codemate/method.py new file mode 100644 index 0000000..52875b8 --- /dev/null +++ b/codemate/method.py @@ -0,0 +1,80 @@ +from typing import Collection, Optional + +from codemate.structure import Function + + +class Method(Function): + """ + Generates a python method syntax. + + Args: + name (str): The name of the function. + arguments (Collection[str]): The inputs of the function. + is_async (bool): Represents whether async keyword should be added. + return_value (Optional[str]): The type of the function return value. + """ + + def __init__( + self, + name: str, + arguments: Collection[str] = (), + is_async: bool = False, + return_value: Optional[str] = None, + ) -> None: + super().__init__( + name=name, + arguments=("self", *arguments), + is_async=is_async, + return_value=return_value, + ) + + +class ClassMethod(Function): + """ + Generates a python class method syntax. + + Args: + name (str): The name of the function. + arguments (Collection[str]): The inputs of the function. + is_async (bool): Represents whether async keyword should be added. + return_value (Optional[str]): The type of the function return value. + """ + + def __init__( + self, + name: str, + arguments: Collection[str] = (), + is_async: bool = False, + return_value: Optional[str] = None, + ) -> None: + super().__init__( + name=name, + arguments=("cls", *arguments), + is_async=is_async, + return_value=return_value, + ) + self.add_decorator("classmethod") + + +class StaticMethod(Function): + """ + Generates a python method syntax. + + Args: + name (str): The name of the function. + arguments (Collection[str]): The inputs of the function. + is_async (bool): Represents whether async keyword should be added. + return_value (Optional[str]): The type of the function return value. + """ + + def __init__( + self, + name: str, + arguments: Collection[str] = (), + is_async: bool = False, + return_value: Optional[str] = None, + ) -> None: + super().__init__( + name=name, arguments=arguments, is_async=is_async, return_value=return_value + ) + self.add_decorator("staticmethod") diff --git a/codemate/structure.py b/codemate/structure.py new file mode 100644 index 0000000..b95051d --- /dev/null +++ b/codemate/structure.py @@ -0,0 +1,129 @@ +from abc import abstractmethod +from collections import Counter +from functools import partial +from typing import Collection, List, Optional + +from codemate.block import Block + + +class Structure(Block): + """ + Creates an abstract Python structure syntax. + + Args: + name(str): The name of the structure. + """ + + def __init__( + self, + name: str, + ) -> None: + super().__init__() + self._name = name + self._decorators: List[str] = [] + + def add_decorator(self, line: str) -> "Structure": + """ + Adds a line that represent a Python decorator syntax to the structure, + in LIFO order. + + Args: + line (str): The syntax line that should be inserted as a decorator. + + Returns: + Class: The class instance. + """ + self._decorators.insert(0, f"@{line}") + return self + + def _format_decorators(self, indent: int) -> str: + format_line = partial(self.parse_block, new_line=1, indent=indent) + syntax = "".join(format_line(line) for line in self._decorators) + return syntax.strip() + + @abstractmethod + def _format_signature(self, indent: int) -> str: + raise NotImplementedError + + def syntax(self, indent: int = 0, imports: bool = True) -> str: + """ + Convert the structure to Python syntax. + + Args: + indent (int): How much to indent the class content. + imports (bool): Whether to add imports or not. + + Returns: + str: The structure Python syntax. + """ + syntax = "" + if self._decorators: + syntax += self._format_decorators(indent) + syntax += "\n" + syntax += self._format_signature(indent) + syntax += self.parse_block(super().syntax(indent + 1)) + return syntax + + +class Function(Structure): + """ + Generates a Python function syntax. + + Args: + name (str): The name of the function. + arguments (Collection[str]): The inputs of the function. + is_async (bool): Represents whether async keyword should be added. + return_value (Optional[str]): The type of the function return value. + """ + + def __init__( + self, + name: str, + arguments: Collection[str] = (), + is_async: bool = False, + return_value: Optional[str] = None, + ) -> None: + super().__init__(name) + self._arguments = arguments + self._is_async = is_async + self._return_value = return_value + + def _format_signature(self, indent: int) -> str: + # Counter is used to remove duplications of arguments + args_syntax = ", ".join(Counter(self._arguments)) + async_prefix = "async " if self._is_async else "" + return_value = f" -> {self._return_value}" if self._return_value else "" + signature = f"{async_prefix}def {self._name}({args_syntax}){return_value}:" + return self.parse_block(signature, indent=indent) + + +class Class(Structure): + """ + Creates a python class syntax. + + Args: + name(str): The name of the class. + inheritance (Collection[str]): The classes that this class inherits from. + """ + + def __init__( + self, + name: str, + metaclass: Optional[str] = None, + inheritance: Collection[str] = (), + ) -> None: + super().__init__(name) + self._metaclass = metaclass + self._inheritance = inheritance + + def _format_signature(self, indent: int) -> str: + signature = f"class {self._name}" + # Counter is used to remove duplications of arguments + inheritance = ", ".join(Counter(self._inheritance)) + if inheritance: + inheritance += "," + metaclass = f"metaclass={self._metaclass}" if self._metaclass else "" + if metaclass and inheritance: + signature += f"({inheritance}{metaclass})" + signature += ":" + return self.parse_block(signature, indent=indent) diff --git a/codemate/utils.py b/codemate/utils.py new file mode 100644 index 0000000..930de1b --- /dev/null +++ b/codemate/utils.py @@ -0,0 +1,17 @@ +import re + + +def remove_indentation(content: str) -> str: + """ + Removes indentation from a given string that contains multiple lines. + It removes spaces before new lines by the first line spaces at the beginning. + + Args: + content(str): The sting that we want to clean from the indentation. + + Returns: + str: The unindented content. + """ + indentation = next(iter(re.findall("^\n*( *)", content) or []), "") + unindented = re.subn(f"(\n){indentation}", r"\1", content)[0].strip() + return unindented diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f3400ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +black>=20.8b1,<21.0 +isort[requirements_deprecated_finder,pipfile_deprecated_finder] \ No newline at end of file From 15efe7968a7b2dc0a0b20e98d0eb0805ac86943e Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Sun, 16 May 2021 17:46:39 +0300 Subject: [PATCH 2/7] =?UTF-8?q?Added=20dev=20tools,=20requirements,=20conf?= =?UTF-8?q?iguration,=20docs=20=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 4 + .pre-commit-config.yaml | 23 ++ README.md | 11 +- dev_requirements.txt | 5 + pylintrc | 533 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 dev_requirements.txt create mode 100644 pylintrc diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..408f3d3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 90 +ignore = E203, E501, W503 +exclude = __init__.py \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a53f2b2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: + - repo: https://github.com/psf/black + rev: stable + hooks: + - id: black + language_version: python3.6 + - repo: https://github.com/pycqa/pylint + rev: "" + hooks: + - id: pylint + - repo: https://github.com/pre-commit/mirrors-mypy + rev: "" + hooks: + - id: mypy + - repo: https://github.com/pycqa/flake8 + rev: "" + hooks: + - id: flake8 + - repo: https://github.com/pycqa/isort + rev: 5.8.0 + hooks: + - id: isort + name: isort (python) \ No newline at end of file diff --git a/README.md b/README.md index d6f04a1..8afa88c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# codemate +# CodeMate + Python package, syntax generator, easy to use, OOP API + +## Development + +Use pre-commit: + +* `pre-commit install` - run on every commit. +* `pre-commit run --all-files` - run manually on the repository. + diff --git a/dev_requirements.txt b/dev_requirements.txt new file mode 100644 index 0000000..9e3885b --- /dev/null +++ b/dev_requirements.txt @@ -0,0 +1,5 @@ +black>=20.8b1,<21.0 +mypy==0.782 +flake8>=3.9.2,<4.0.0 +pre-commit>=2.12.1,<3.0.0 +pylint>=2.8.2,<3.0.0 \ No newline at end of file diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..1b4fb00 --- /dev/null +++ b/pylintrc @@ -0,0 +1,533 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=missing-module-docstring, + unsubscriptable-object, # Due to - https://github.com/PyCQA/pylint/issues/2377 + + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +# notes= + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=7 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception From ff16e472b917d86e6f6c55b33d3ff9a1291df8a2 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Wed, 19 May 2021 10:29:05 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Created=20github=20?= =?UTF-8?q?workflows,=20updated=20linters=20configuration,=20replaced=20re?= =?UTF-8?q?quirements=20files=20with=20pyproject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 2 +- .github/workflows/test.yml | 36 ++++++++++++++++++++++++++++ .gitignore | 2 +- .pre-commit-config.yaml | 2 +- codemate/__init__.py | 6 +++++ dev_requirements.txt | 5 ---- mypy.ini | 4 ++++ pyproject.toml | 49 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 -- scripts/lint.sh | 10 ++++++++ scripts/test.sh | 6 +++++ 11 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 dev_requirements.txt create mode 100644 mypy.ini create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 scripts/lint.sh create mode 100644 scripts/test.sh diff --git a/.flake8 b/.flake8 index 408f3d3..b22ebf1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length = 90 ignore = E203, E501, W503 -exclude = __init__.py \ No newline at end of file +exclude = __init__.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f16035d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Test + +on: + push: + pull_request: + types: [opened, synchronize] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v2 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test + - name: Install Flit + if: steps.cache.outputs.cache-hit != 'true' + run: pip install flit + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: flit install --symlink + - name: Test + run: bash scripts/test.sh +# - name: Upload coverage +# uses: codecov/codecov-action@v1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index bb1b755..f0a5964 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,4 @@ dmypy.json .pyre/ #Pycharm -.idea/ \ No newline at end of file +.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a53f2b2..7cd313c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,4 +20,4 @@ repos: rev: 5.8.0 hooks: - id: isort - name: isort (python) \ No newline at end of file + name: isort (python) diff --git a/codemate/__init__.py b/codemate/__init__.py index 0c6ebe9..e961701 100644 --- a/codemate/__init__.py +++ b/codemate/__init__.py @@ -1,4 +1,10 @@ +""" +Python syntax generator based on Object-Oriented Programing, type hints, and simplicity. +""" + from codemate.block import Block from codemate.file import File from codemate.method import ClassMethod, Method, StaticMethod from codemate.structure import Class, Function + +__version__ = "0.1.0" diff --git a/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index 9e3885b..0000000 --- a/dev_requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -black>=20.8b1,<21.0 -mypy==0.782 -flake8>=3.9.2,<4.0.0 -pre-commit>=2.12.1,<3.0.0 -pylint>=2.8.2,<3.0.0 \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d3005b2 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] + +[mypy-isort.*] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bd85733 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "codemate" +author = "Dor Abu" +author-email = "dor.abu@cyberproof.com" +home-page = "https://github.com/Cyberproof/codemate" +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", +] +description-file = "README.md" +requires-python = ">=3.6" + +[tool.flit.metadata.requires-extra] +test = [ + "pytest ==5.4.3", + "pytest-cov ==2.10.0", + "mypy==0.782", + "flake8>=3.9.2,<4.0.0", + "pylint>=2.8.2,<3.0.0", +] +doc = [ +] +dev = [ + "mypy==0.782", + "flake8>=3.9.2,<4.0.0", + "pre-commit>=2.12.1,<3.0.0", + "pylint>=2.8.2,<3.0.0", +] +all = [ + "black>=20.8b1,<21.0", + "isort[requirements_deprecated_finder,pipfile_deprecated_finder]>=5.8.0,<6.0.0", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f3400ee..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -black>=20.8b1,<21.0 -isort[requirements_deprecated_finder,pipfile_deprecated_finder] \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100644 index 0000000..91b5459 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e +set -x + +mypy codemate +flake8 codemate +black codemate --check +isort codemate --check-only +pylint codemate \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..2bc138e --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -e +set -x + +bash ./scripts/lint.sh From 1668991f7588c67a9ff2c25735d34f7426a9b233 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Tue, 22 Jun 2021 18:05:38 +0300 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Improved=20components=20impleme?= =?UTF-8?q?ntations,=20updated=20linters=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coveragerc | 2 ++ .gitignore | 7 +++++++ .pre-commit-config.yaml | 17 +++++++++++------ codemate/block.py | 35 ++++++++++++++++++++++++----------- codemate/file.py | 2 +- codemate/structure.py | 18 ++++++++++-------- pylintrc | 3 ++- pyproject.toml | 4 ++-- scripts/lint.sh | 10 +++++----- 9 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..15b79ff --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = tests/*, */__init__.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index f0a5964..0ffc37b 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -130,3 +131,9 @@ dmypy.json #Pycharm .idea/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cd313c..6e039e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,22 @@ repos: - repo: https://github.com/psf/black - rev: stable + rev: 21.5b1 hooks: - id: black language_version: python3.6 - - repo: https://github.com/pycqa/pylint - rev: "" + - repo: local hooks: - - id: pylint + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "" + rev: "v0.812" hooks: - id: mypy - repo: https://github.com/pycqa/flake8 - rev: "" + rev: "3.9.2" hooks: - id: flake8 - repo: https://github.com/pycqa/isort @@ -21,3 +24,5 @@ repos: hooks: - id: isort name: isort (python) + + diff --git a/codemate/block.py b/codemate/block.py index a4d2326..a5cddc5 100644 --- a/codemate/block.py +++ b/codemate/block.py @@ -87,7 +87,7 @@ def parse_block(self, block: str, new_line: int = 0, indent: int = 0) -> str: str: The parsed line. """ syntax = "\n" * new_line - syntax += re.sub("(^|\n)", f"\\1{self._indentation * indent}", block) + syntax += re.sub("(^|\n)(.)", f"\\1{self._indentation * indent}\\2", block) return syntax def add_doc_line(self, line: str, indent: int = 0) -> "Block": @@ -244,12 +244,12 @@ def add_variable(self, name: str, **kwargs: str) -> "Block": Class: The class instance. """ syntax = name - if not (kwargs["type"] or kwargs["value"]): + if not (kwargs.get("type") or kwargs.get("value")): message = "Class variable must have at least 'type' or 'value'" raise PythonSyntaxError(message) - if kwargs["type"]: + if kwargs.get("type"): syntax += f": {kwargs['type']}" - if kwargs["value"]: + if kwargs.get("value"): syntax += f" = {kwargs['value']}" self.add_syntax_line(syntax) return self @@ -257,6 +257,7 @@ def add_variable(self, name: str, **kwargs: str) -> "Block": def extend(self, block: "Block") -> "Block": """ Adds other Python block syntax to the current Python block syntax. + Ignoring the other's docs, copying the imports and adding the syntax. Args: block (Block): The block that we want to add. @@ -270,7 +271,8 @@ def extend(self, block: "Block") -> "Block": def insert(self, block: "Block") -> "Block": """ - Insert as is other Python block syntax to the current Python block syntax. + Inserts as is other Python block syntax to the current Python block syntax. + Inserting the docs and syntax as is and copying the imports. Args: block (Block): The block that we want to add. @@ -278,7 +280,8 @@ def insert(self, block: "Block") -> "Block": Returns: Block: The block instance. """ - self._lines.append(block.syntax()) + self._imports.update(block._imports) # pylint: disable=protected-access + self._lines.append(block.syntax(imports=False)) return self def _format_docs(self, indent: int) -> str: @@ -286,12 +289,13 @@ def _format_docs(self, indent: int) -> str: syntax = self.parse_block('"""', indent=indent) syntax += "".join(format_line(doc) for doc in self._docs) syntax += format_line('"""') + syntax += "\n" return syntax def _format_imports(self, indent: int) -> str: format_line = partial(self.parse_block, new_line=1, indent=indent) syntax = "".join(format_line(import_) for import_ in self._imports) - return isort.code(syntax) + return isort.code(syntax.strip("\n")) def syntax(self, indent: int = 0, imports: bool = True) -> str: """ @@ -308,9 +312,18 @@ def syntax(self, indent: int = 0, imports: bool = True) -> str: syntax = "" if self._docs: syntax += self._format_docs(indent) - if imports: + if imports and self._imports: + if syntax: + syntax += "\n" syntax += self._format_imports(indent) - syntax += "".join(format_line(str(line)) for line in self._lines) + if self._lines: + if syntax: + syntax += "\n" + syntax += "".join(format_line(str(line)) for line in self._lines).strip( + "\n" + ) + if syntax and syntax[-1] != "\n": + return syntax + "\n" return syntax def use_black(self) -> str: @@ -328,7 +341,7 @@ def use_black(self) -> str: def validate(self) -> bool: """ - Checks if the generated syntax is valid. + Checks if the generated syntax structure is valid. If not raises 'InputError' error that wraps black.InvalidInput error. Returns: @@ -345,7 +358,7 @@ def validate(self) -> bool: return True def __repr__(self): - class_name = type(self) + class_name = getattr(type(self), "__name__", type(self)) return f"{class_name}({vars(self)})" def __str__(self) -> str: diff --git a/codemate/file.py b/codemate/file.py index 5d194bb..ab7273b 100644 --- a/codemate/file.py +++ b/codemate/file.py @@ -34,7 +34,7 @@ def __init__(self, header: Optional[str] = generate_header()) -> None: if header: self.add_doc_block(block=header) - def save(self, path: str, use_black=True) -> None: + def save(self, path: str, use_black: bool = True) -> None: """ Save the generated Python file in a given location. diff --git a/codemate/structure.py b/codemate/structure.py index b95051d..5eb5d0f 100644 --- a/codemate/structure.py +++ b/codemate/structure.py @@ -25,7 +25,7 @@ def __init__( def add_decorator(self, line: str) -> "Structure": """ Adds a line that represent a Python decorator syntax to the structure, - in LIFO order. + in FIFO order. Args: line (str): The syntax line that should be inserted as a decorator. @@ -33,7 +33,7 @@ def add_decorator(self, line: str) -> "Structure": Returns: Class: The class instance. """ - self._decorators.insert(0, f"@{line}") + self._decorators.append(f"@{line}") return self def _format_decorators(self, indent: int) -> str: @@ -61,7 +61,9 @@ def syntax(self, indent: int = 0, imports: bool = True) -> str: syntax += self._format_decorators(indent) syntax += "\n" syntax += self._format_signature(indent) - syntax += self.parse_block(super().syntax(indent + 1)) + block_content = super().syntax(indent + 1, imports) + syntax += "\n" + syntax += self.parse_block(block_content) return syntax @@ -103,27 +105,27 @@ class Class(Structure): Args: name(str): The name of the class. - inheritance (Collection[str]): The classes that this class inherits from. + inherit (Collection[str]): The classes that this class inherits from. """ def __init__( self, name: str, metaclass: Optional[str] = None, - inheritance: Collection[str] = (), + inherit: Collection[str] = (), ) -> None: super().__init__(name) self._metaclass = metaclass - self._inheritance = inheritance + self._inherit = inherit def _format_signature(self, indent: int) -> str: signature = f"class {self._name}" # Counter is used to remove duplications of arguments - inheritance = ", ".join(Counter(self._inheritance)) + inheritance = ", ".join(Counter(self._inherit)) if inheritance: inheritance += "," metaclass = f"metaclass={self._metaclass}" if self._metaclass else "" - if metaclass and inheritance: + if metaclass or inheritance: signature += f"({inheritance}{metaclass})" signature += ":" return self.parse_block(signature, indent=indent) diff --git a/pylintrc b/pylintrc index 1b4fb00..deac541 100644 --- a/pylintrc +++ b/pylintrc @@ -67,6 +67,7 @@ confidence= # --disable=W". disable=missing-module-docstring, unsubscriptable-object, # Due to - https://github.com/PyCQA/pylint/issues/2377 + duplicate-code, Due to - https://github.com/PyCQA/pylint/issues/214 # Enable the message, report, category or checker with the given id(s). You can @@ -253,7 +254,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=90 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/pyproject.toml b/pyproject.toml index bd85733..4a60c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,8 +29,8 @@ requires-python = ">=3.6" [tool.flit.metadata.requires-extra] test = [ - "pytest ==5.4.3", - "pytest-cov ==2.10.0", + "pytest==5.4.3", + "pytest-cov==2.10.0", "mypy==0.782", "flake8>=3.9.2,<4.0.0", "pylint>=2.8.2,<3.0.0", diff --git a/scripts/lint.sh b/scripts/lint.sh index 91b5459..b0441f0 100644 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,8 +3,8 @@ set -e set -x -mypy codemate -flake8 codemate -black codemate --check -isort codemate --check-only -pylint codemate \ No newline at end of file +mypy codemate tests +flake8 codemate tests +black codemate tests --check +isort codemate tests --check-only +pylint codemate tests \ No newline at end of file From a5e388527f7a9498d906d9af09c5dbdc977e11f2 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Tue, 22 Jun 2021 18:08:05 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=94=8D=20Added=20unit=20tests=20and?= =?UTF-8?q?=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 + scripts/test.sh | 1 + tests/__init__.py | 0 tests/block/__init__.py | 0 tests/block/test_add_doc.py | 76 ++++++++++++++++ tests/block/test_add_import.py | 57 ++++++++++++ tests/block/test_add_syntax.py | 81 +++++++++++++++++ tests/block/test_general.py | 146 ++++++++++++++++++++++++++++++ tests/block/test_parse_block.py | 63 +++++++++++++ tests/class_/__init__.py | 0 tests/class_/test_general.py | 154 ++++++++++++++++++++++++++++++++ tests/examples/__init__.py | 1 + tests/examples/block.py | 87 ++++++++++++++++++ tests/examples/class_.py | 37 ++++++++ tests/examples/file.py | 133 +++++++++++++++++++++++++++ tests/examples/function.py | 75 ++++++++++++++++ tests/examples/method.py | 110 +++++++++++++++++++++++ tests/file/__init__.py | 0 tests/file/test_general.py | 21 +++++ tests/function/__init__.py | 0 tests/function/test_general.py | 65 ++++++++++++++ tests/utils.py | 20 +++++ 22 files changed, 1128 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/block/__init__.py create mode 100644 tests/block/test_add_doc.py create mode 100644 tests/block/test_add_import.py create mode 100644 tests/block/test_add_syntax.py create mode 100644 tests/block/test_general.py create mode 100644 tests/block/test_parse_block.py create mode 100644 tests/class_/__init__.py create mode 100644 tests/class_/test_general.py create mode 100644 tests/examples/__init__.py create mode 100644 tests/examples/block.py create mode 100644 tests/examples/class_.py create mode 100644 tests/examples/file.py create mode 100644 tests/examples/function.py create mode 100644 tests/examples/method.py create mode 100644 tests/file/__init__.py create mode 100644 tests/file/test_general.py create mode 100644 tests/function/__init__.py create mode 100644 tests/function/test_general.py create mode 100644 tests/utils.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f16035d..b500013 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,5 +32,6 @@ jobs: run: flit install --symlink - name: Test run: bash scripts/test.sh +# todo consider # - name: Upload coverage # uses: codecov/codecov-action@v1 \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index 2bc138e..11f38c7 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,3 +4,4 @@ set -e set -x bash ./scripts/lint.sh +pytest --cov=codemate --cov=tests --cov-report=term-missing --cov-report=xml tests ${@} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/block/__init__.py b/tests/block/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/block/test_add_doc.py b/tests/block/test_add_doc.py new file mode 100644 index 0000000..98b3c15 --- /dev/null +++ b/tests/block/test_add_doc.py @@ -0,0 +1,76 @@ +# pylint: disable=missing-function-docstring +# flake8: noqa: E101, W191, W291, W293 + +from codemate import Block + +DOC_TEMPLATE = '"""\n{content}\n"""\n' + +DOC_LINE = "Checks: the following - DOC line@!#$%^&*()" + +DOC_LINES = ( + "Checks: the following - DOC line@!#$%^&*()", + "n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 ", + "n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 \n\n", + "\t\t\n\n\t\t\n\n", + "dasdsadwt3et435t457658697809-9654142341`4q23rewr2$!$$%@%^$%^&*^&()", +) + +DOC_BLOCK = "\n".join(DOC_LINES) + +DOC_COMPLEX = ''' + """ + Checks: the following - DOC line@!#$%^&*() + n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 + n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 + + + + + + + + dasdsadwt3et435t457658697809-9654142341`4q23rewr2$!$$%@%^$%^&*^&() + Checks: the following - DOC line@!#$%^&*() + Checks: the following - DOC line@!#$%^&*() + n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 + n2j31k4nbj3hb5t4hrjb 4bn `j34kn24iu2o1hj124n331jbn3jk1b2 + + + + + + + + dasdsadwt3et435t457658697809-9654142341`4q23rewr2$!$$%@%^$%^&*^&() + Checks: the following - DOC line@!#$%^&*() + """ +'''.lstrip( + "\n" +) + + +def test_single_line(): + block = Block() + block.add_doc_line(DOC_LINE) + assert DOC_TEMPLATE.format(content=DOC_LINE) == block.syntax() + + +def test_multiple_lines(): + block = Block() + block.add_doc_lines(*DOC_LINES) + assert DOC_TEMPLATE.format(content=DOC_BLOCK) == block.syntax() + + +def test_single_block(): + block = Block() + block.add_doc_block(DOC_BLOCK) + assert DOC_TEMPLATE.format(content=DOC_BLOCK) == block.syntax() + + +def test_multiple_block_and_lines(): + block = Block() + block.add_doc_block(DOC_BLOCK) + block.add_doc_line(DOC_LINE) + block.add_doc_lines(*DOC_LINES) + block.add_doc_line(DOC_LINE) + assert DOC_COMPLEX == block.syntax(indent=1) diff --git a/tests/block/test_add_import.py b/tests/block/test_add_import.py new file mode 100644 index 0000000..3e14917 --- /dev/null +++ b/tests/block/test_add_import.py @@ -0,0 +1,57 @@ +# pylint: disable=missing-function-docstring +import isort + +from codemate import Block + +SINGLE_IMPORT = "math" + +SINGLE_IMPORT_RESULT: str = isort.code("import math") + +MULTIPLE_IMPORTS = ("math", "sys", "math", "datetime") + +MULTIPLE_IMPORTS_RESULT: str = isort.code( + """ +import math +import sys +import datetime +""".strip() +) + +SINGLE_SPECIFIC_IMPORT = ("math", "sin") + +SINGLE_SPECIFIC_IMPORT_RESULT: str = isort.code("from math import sin") + +IMPORTS_COMPLEX_RESULT = """ + import datetime + import math + import sys + from math import sin +""".lstrip( + "\n" +) + + +def test_single_import(): + block = Block() + block.add_import(SINGLE_IMPORT) + assert SINGLE_IMPORT_RESULT == block.syntax() + + +def test_multiple_imports(): + block = Block() + block.add_imports(*MULTIPLE_IMPORTS) + assert MULTIPLE_IMPORTS_RESULT == block.syntax() + + +def test_specific_import(): + block = Block() + block.add_specific_import(*SINGLE_SPECIFIC_IMPORT) + assert SINGLE_SPECIFIC_IMPORT_RESULT == block.syntax() + + +def test_complex(): + block = Block() + block.add_imports(*MULTIPLE_IMPORTS) + block.add_import(SINGLE_IMPORT) + block.add_specific_import(*SINGLE_SPECIFIC_IMPORT) + assert IMPORTS_COMPLEX_RESULT == block.syntax(indent=1) diff --git a/tests/block/test_add_syntax.py b/tests/block/test_add_syntax.py new file mode 100644 index 0000000..8a185f8 --- /dev/null +++ b/tests/block/test_add_syntax.py @@ -0,0 +1,81 @@ +# pylint: disable=missing-function-docstring + +from codemate import Block + +SYNTAX_LINE = "print('hello world')" + +SYNTAX_LINE_RESULT = f"{SYNTAX_LINE}\n" + +SYNTAX_LINES = ( + "greet = True", + "if greet:", + " print('hello world')", + "else:", + " pass", +) + +SYNTAX_LINES_RESULT = "\n".join(SYNTAX_LINES) + "\n" + +SYNTAX_BLOCK = """ +multi = lambda: x: x*9 +number = multi(9) +print(number) +""".strip( + "\n" +) + +SYNTAX_BLOCK_RESULT = SYNTAX_BLOCK + "\n" + +SINGLE_VARIABLE = dict(name="round", type="int", value="50") + +SINGLE_VARIABLE_RESULT = "round: int = 50\n" + +SYNTAX_COMPLEX_RESULT = """ + round: int = 50 + multi = lambda: x: x*9 + number = multi(9) + print(number) + greet = True + if greet: + print('hello world') + else: + pass + round: int = 50 + print('hello world') +""".lstrip( + "\n" +) + + +def test_single_line(): + block = Block() + block.add_syntax_line(SYNTAX_LINE) + assert SYNTAX_LINE_RESULT == block.syntax() + + +def test_multiple_lines(): + block = Block() + block.add_syntax_lines(*SYNTAX_LINES) + assert SYNTAX_LINES_RESULT == block.syntax() + + +def test_single_block(): + block = Block() + block.add_syntax_block(SYNTAX_BLOCK) + assert SYNTAX_BLOCK_RESULT == block.syntax() + + +def test_single_variable(): + block = Block() + block.add_variable(**SINGLE_VARIABLE) + assert SINGLE_VARIABLE_RESULT == block.syntax() + + +def test_complex(): + block = Block() + block.add_variable(**SINGLE_VARIABLE) + block.add_syntax_block(SYNTAX_BLOCK) + block.add_syntax_lines(*SYNTAX_LINES) + block.add_variable(**SINGLE_VARIABLE) + block.add_syntax_line(SYNTAX_LINE) + assert SYNTAX_COMPLEX_RESULT == block.syntax(indent=1) diff --git a/tests/block/test_general.py b/tests/block/test_general.py new file mode 100644 index 0000000..c96ee74 --- /dev/null +++ b/tests/block/test_general.py @@ -0,0 +1,146 @@ +# pylint: disable=missing-function-docstring +from copy import deepcopy + +from codemate import Block +from codemate.exceptions import InputError +from tests import examples + +OTHER_BLOCK = Block() +OTHER_BLOCK.add_import("math") +OTHER_BLOCK.add_doc_line("Testing two blocks features") +OTHER_BLOCK.add_syntax_lines("x = math.log2(8) ** 9", "LOGGER.debug(x)") + +BLACK_OTHER_BLOCK = deepcopy(OTHER_BLOCK) +BLACK_OTHER_BLOCK.add_syntax_line( + "result = " + "{num:num2 for num in range(100) if num * 10 + 10 < 1000 for num2 in range(num)}" +) # complex line + +VALIDATE_OTHER_BLOCK = deepcopy(OTHER_BLOCK) +VALIDATE_OTHER_BLOCK.add_syntax_line("bk45jhb123h5jb13hjvb 4h12das") # invalid line + +EXTEND_RESULT = ( + examples.block.get_syntax() + + """ +x = math.log2(8) ** 9 +LOGGER.debug(x) +""".lstrip() +) + +INSERT_RESULT = ( + examples.block.get_syntax() + + """ +\"\"\" +Testing two blocks features +\"\"\" + +x = math.log2(8) ** 9 +LOGGER.debug(x) +""".lstrip() +) + +BLACK_RESULT = ( + examples.block.get_syntax() + + """ +\"\"\" +Testing two blocks features +\"\"\" + +x = math.log2(8) ** 9 +LOGGER.debug(x) +result = { + num: num2 for num in range(100) if num * 10 + 10 < 1000 for num2 in range(num) +} +""".lstrip() +) + + +TO_STRING_RESULT = """ +\"\"\" +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +\"\"\" + +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") +""".lstrip() + +CONTAINS_VALUE_TEST = """ +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. +""".strip() + + +def test_extend(): + block = examples.block.get_example() + block.extend(OTHER_BLOCK) + assert EXTEND_RESULT == block.syntax() + + +def test_insert(): + block = examples.block.get_example() + block.insert(OTHER_BLOCK) + assert INSERT_RESULT == block.syntax() + + +def test_black(): + block = examples.block.get_example() + block.insert(BLACK_OTHER_BLOCK) + assert BLACK_RESULT == block.use_black() + + +# noinspection PyBroadException +def test_validate(): + block = examples.block.get_example() + block.insert(BLACK_OTHER_BLOCK) + assert block.validate() + block = examples.block.get_example() + block.insert(VALIDATE_OTHER_BLOCK) + try: + block.validate() + except InputError: + pass + except Exception: # pylint: disable=broad-except + assert False, "Should raise InputError" + else: + assert False, "Should raise InputError" + + +def test_repr(): + block = examples.block.get_example() + # The content is based on hash functions, therefore it is modified every iteration. + # We will test only the stable wrapper of the representation. + representation = repr(block) + assert representation[0:6] == "Block(" and representation[-1] == ")" + + +def test_to_string(): + block = examples.block.get_example() + assert TO_STRING_RESULT == str(block) + + +def test_contains(): + block = examples.block.get_example() + assert CONTAINS_VALUE_TEST in block diff --git a/tests/block/test_parse_block.py b/tests/block/test_parse_block.py new file mode 100644 index 0000000..e3c9062 --- /dev/null +++ b/tests/block/test_parse_block.py @@ -0,0 +1,63 @@ +# pylint: disable=missing-function-docstring + +from codemate import Block + +SYNTAX_BLOCK = """ +def add(x: int, y:int): + return x + y + +print(add(1,1)) +""".strip( + "\n" +) + +INDENTED_BLOCK = """ + def add(x: int, y:int): + return x + y + + print(add(1,1)) +""".strip( + "\n" +) + +NEW_LINE_BLOCK = """ +def add(x: int, y:int): + return x + y + +print(add(1,1)) +""".rstrip( + "\n" +) + +INDENTED_NEW_LINE_BLOCK = """ + def add(x: int, y:int): + return x + y + + print(add(1,1)) +""".rstrip( + "\n" +) + + +def test_default(): + block = Block() + assert SYNTAX_BLOCK == block.parse_block(SYNTAX_BLOCK) + + +def test_indented_block(): + block = Block() + assert INDENTED_BLOCK == block.parse_block(INDENTED_BLOCK) + assert INDENTED_BLOCK == block.parse_block(SYNTAX_BLOCK, indent=1) + + +def test_new_line_block(): + block = Block() + assert NEW_LINE_BLOCK == block.parse_block(NEW_LINE_BLOCK) + assert NEW_LINE_BLOCK == block.parse_block(SYNTAX_BLOCK, new_line=1) + + +def test_indented_new_line_block(): + block = Block() + assert INDENTED_NEW_LINE_BLOCK == block.parse_block(INDENTED_NEW_LINE_BLOCK) + result = block.parse_block(SYNTAX_BLOCK, indent=1, new_line=1) + assert INDENTED_NEW_LINE_BLOCK == result diff --git a/tests/class_/__init__.py b/tests/class_/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/class_/test_general.py b/tests/class_/test_general.py new file mode 100644 index 0000000..38890f7 --- /dev/null +++ b/tests/class_/test_general.py @@ -0,0 +1,154 @@ +# pylint: disable=missing-function-docstring +from tests import examples + +CLASS_WITH_METHODS_RESULT = """ +class APIWrapper(Sized,): + \"\"\" + A class that represents a raper for a defined API structure. + \"\"\" + + from collections import Sized + from logging import Logger + from typing import List + def __init__(self, logger:Logger=LOGGER) -> None: + self._logger = logger + self._size = 0 + + @timer + def get_x(self, item_id:str) -> List[int]: + pass + + @timer + def get_y(self, item_id:str) -> str: + pass + + @timer + def post_x(self, item_id:str) -> bool: + pass + + @timer + def post_y(self, item_id:str) -> bool: + pass + + def __len__(self) -> int: + return self._size + + @classmethod + def set_base(cls) -> int: + pass + + @staticmethod + @timer + def calc(key:str, value:int) -> int: + pass +""".lstrip( + "\n" +) + +COMPLEX_RESULT = """ +\"\"\" +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +\"\"\" + +import time +from collections import Sized +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger +from typing import Callable, List + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") + + +def timer(func: Callable) -> Callable: + \"\"\" + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + \"\"\" + + def decorator(*args, **kwargs): + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + + return decorator + + +class APIWrapper( + Sized, +): + \"\"\" + A class that represents a raper for a defined API structure. + \"\"\" + + def __init__(self, logger: Logger = LOGGER) -> None: + self._logger = logger + self._size = 0 + + @timer + def get_x(self, item_id: str) -> List[int]: + pass + + @timer + def get_y(self, item_id: str) -> str: + pass + + @timer + def post_x(self, item_id: str) -> bool: + pass + + @timer + def post_y(self, item_id: str) -> bool: + pass + + def __len__(self) -> int: + return self._size + + @classmethod + def set_base(cls) -> int: + pass + + @staticmethod + @timer + def calc(key: str, value: int) -> int: + pass +""".lstrip() + + +def test_class_with_methods(): + methods = examples.method.get_example() + class_ = examples.class_.get_example() + class_.insert(methods) + assert CLASS_WITH_METHODS_RESULT == class_.syntax() + + +def test_complex(): + block_ = examples.block.get_example() + function_ = examples.function.get_example() + block_.insert(function_) + methods = examples.method.get_example() + class_ = examples.class_.get_example() + class_.insert(methods) + block_.insert(class_) + assert COMPLEX_RESULT == block_.use_black() diff --git a/tests/examples/__init__.py b/tests/examples/__init__.py new file mode 100644 index 0000000..67ce0af --- /dev/null +++ b/tests/examples/__init__.py @@ -0,0 +1 @@ +from tests.examples import block, class_, file, function, method diff --git a/tests/examples/block.py b/tests/examples/block.py new file mode 100644 index 0000000..fb39500 --- /dev/null +++ b/tests/examples/block.py @@ -0,0 +1,87 @@ +from copy import deepcopy +from functools import partial + +from codemate import Block +from tests import utils + +_BLOCK = Block() +_BLOCK.add_doc_lines( + " Example ".center(90, "-"), + "This is an example of how to use this Python package to generate easily and safely", + "Python syntax.", +) +_BLOCK.add_doc_line("") +_BLOCK.add_doc_block( + """ + The use cases for using this pack may be one of the following: + * Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf + * Generate adapters between the code to systems I/O. + + Easily generating python code without the need to care for styling and indentation. +""".strip( + "\n" + ) +) +_BLOCK.add_doc_line("") +_BLOCK.add_doc_line("".center(90, "-")) +_BLOCK.add_specific_import("logging", "getLogger") +_BLOCK.add_specific_import("logging", "INFO, StreamHandler") +_BLOCK.add_specific_import("logging", "DEBUG", "Logger") +_BLOCK.add_variable("LOGGER", type="Logger", value="getLogger(__name__)") +_BLOCK.add_syntax_block( + """ + LOGGER.setLevel(DEBUG) + _channel = StreamHandler() + _channel.setLevel(INFO) + LOGGER.addHandler(_channel) + LOGGER.debug("✨So far so good✨") +""".strip( + "\n" + ) +) + + +_SYNTAX = """ +\"\"\" +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +\"\"\" + +import math +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") +""".lstrip() + + +def get_example() -> Block: + """ + Factory function that returns a copy of an example of Block object for tests and POCs. + + Returns: + Block: The copy of the Block object instance. + """ + return deepcopy(_BLOCK) + + +get_syntax = partial(utils.get_syntax, postfix=_SYNTAX) diff --git a/tests/examples/class_.py b/tests/examples/class_.py new file mode 100644 index 0000000..a692f08 --- /dev/null +++ b/tests/examples/class_.py @@ -0,0 +1,37 @@ +from copy import deepcopy +from functools import partial + +from codemate import Class +from tests import utils + +# Note: +# This generated code must be part of an upper block to pass the imports to greater level + +_CLASS = Class(name="APIWrapper", inherit=("Sized",)) +_CLASS.add_doc_line("A class that represents a raper for a defined API structure.") +_CLASS.add_specific_import("collections", "Sized") +_CLASS.add_specific_import("logging", "Logger") + +_SYNTAX = """ +class APIWrapper(Sized,): + \"\"\" + A class that represents a raper for a defined API structure. + \"\"\" + + from collections import Sized + from logging import Logger +""".lstrip() + + +def get_example() -> Class: + """ + Factory class that returns a copy of an example of Class object for tests + and POCs. + + Returns: + Class: The copy of the Function object instance. + """ + return deepcopy(_CLASS) + + +get_syntax = partial(utils.get_syntax, postfix=_SYNTAX) diff --git a/tests/examples/file.py b/tests/examples/file.py new file mode 100644 index 0000000..9a00025 --- /dev/null +++ b/tests/examples/file.py @@ -0,0 +1,133 @@ +from copy import deepcopy +from functools import partial + +from codemate import File +from tests import utils +from tests.examples.block import get_example as get_block_example +from tests.examples.class_ import get_example as get_class_example +from tests.examples.function import get_example as get_function_example +from tests.examples.method import get_example as get_method_example + +_HEADER = """ +--------------------------------- Warning generated file --------------------------------- +Generated by: foo +------------------------------------------------------------------------------------------ +""".strip() + +_File = File(header=_HEADER) +_File.insert(get_block_example()) +_File.insert(get_function_example()) +_METHODS = get_method_example() +_CLASS = get_class_example() +_CLASS.insert(_METHODS) +_File.insert(_CLASS) + +_SYNTAX = """ +\"\"\" +--------------------------------- Warning generated file --------------------------------- +Generated by: foo +------------------------------------------------------------------------------------------ +\"\"\" + +import time +from collections import Sized +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger +from typing import Callable, List + +\"\"\" +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +\"\"\" + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") + + +def timer(func: Callable) -> Callable: + \"\"\" + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + \"\"\" + + def decorator(*args, **kwargs): + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + + return decorator + + +class APIWrapper( + Sized, +): + \"\"\" + A class that represents a raper for a defined API structure. + \"\"\" + + def __init__(self, logger: Logger = LOGGER) -> None: + self._logger = logger + self._size = 0 + + @timer + def get_x(self, item_id: str) -> List[int]: + pass + + @timer + def get_y(self, item_id: str) -> str: + pass + + @timer + def post_x(self, item_id: str) -> bool: + pass + + @timer + def post_y(self, item_id: str) -> bool: + pass + + def __len__(self) -> int: + return self._size + + @classmethod + def set_base(cls) -> int: + pass + + @staticmethod + @timer + def calc(key: str, value: int) -> int: + pass +""".lstrip() + + +def get_example() -> File: + """ + Factory function that returns a copy of an example of Function object for tests + and POCs. + + Returns: + Function: The copy of the Function object instance. + """ + return deepcopy(_File) + + +get_syntax = partial(utils.get_syntax, postfix=_SYNTAX) diff --git a/tests/examples/function.py b/tests/examples/function.py new file mode 100644 index 0000000..9a09c46 --- /dev/null +++ b/tests/examples/function.py @@ -0,0 +1,75 @@ +from copy import deepcopy +from functools import partial + +from codemate import Function +from tests import utils + +# Note: +# This generated code must be part of an upper block to pass the imports to greater level + +_FUNCTION = Function( + name="timer", arguments=("func:Callable",), return_value="Callable" +) +_FUNCTION.add_doc_block( + """ + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. +""".strip( + "\n" + ) +) +_FUNCTION.add_specific_import("typing", "Callable") +_INNER_DECORATOR = Function(name="decorator", arguments=("*args", "**kwargs")) +_INNER_DECORATOR.add_import("time") +_INNER_DECORATOR.add_syntax_block( + """ + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value +""".strip( + "\n" + ) +) +_FUNCTION.insert(_INNER_DECORATOR) +_FUNCTION.add_syntax_line("return decorator") + +_SYNTAX = """ +def timer(func:Callable) -> Callable: + \"\"\" + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + \"\"\" + + import time + from typing import Callable + def decorator(*args, **kwargs): + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + + return decorator +""".lstrip() + + +def get_example() -> Function: + """ + Factory function that returns a copy of an example of Function object for tests + and POCs. + + Returns: + Function: The copy of the Function object instance. + """ + return deepcopy(_FUNCTION) + + +get_syntax = partial(utils.get_syntax, postfix=_SYNTAX) diff --git a/tests/examples/method.py b/tests/examples/method.py new file mode 100644 index 0000000..ccfe2f4 --- /dev/null +++ b/tests/examples/method.py @@ -0,0 +1,110 @@ +from copy import deepcopy +from functools import partial +from typing import List + +from codemate import Block, ClassMethod, Function, Method, StaticMethod +from tests import utils + +_API_STRUCTURE = [ + {"operation_name": "get_x", "return_value": "List[int]"}, + {"operation_name": "get_y", "return_value": "str"}, + {"operation_name": "post_x", "return_value": "bool"}, + {"operation_name": "post_y", "return_value": "bool"}, +] + +_METHODS: List[Function] = [] + +_INIT = Method("__init__", arguments=("logger:Logger=LOGGER",), return_value="None") +_INIT.add_specific_import("logging", "Logger") +_INIT.add_specific_import("typing", "List") +_INIT.add_variable("self._logger", value="logger") +_INIT.add_variable("self._size", value="0") + +_METHODS.append(_INIT) + +for _operation in _API_STRUCTURE: + _name, _return_value = _operation.values() + _method = Method(_name, arguments=("item_id:str",), return_value=_return_value) + _method.add_decorator("timer") + _method.add_syntax_line("pass") + _METHODS.append(_method) + +_LEN = Method("__len__", return_value="int") +_LEN.add_syntax_line("return self._size") + +_METHODS.append(_LEN) + +_CLASS_METHOD = ClassMethod("set_base", return_value="int") +_CLASS_METHOD.add_syntax_line("pass") + +_METHODS.append(_CLASS_METHOD) + +_STATIC_METHOD = StaticMethod( + "calc", + arguments=( + "key:str", + "value:int", + ), + return_value="int", +) +_STATIC_METHOD.add_decorator("timer") +_STATIC_METHOD.add_syntax_line("pass") + +_METHODS.append(_STATIC_METHOD) + +_BLOCK = Block() + +for _item in _METHODS: + _BLOCK.insert(_item) + +_SYNTAX = """ +from typing import List + +from logging import Logger + +def __init__(self, logger:Logger=LOGGER) -> None: + self._logger = logger + self._size = 0 + +@timer +def get_x(self, item_id:str) -> List[int]: + pass + +@timer +def get_y(self, item_id:str) -> str: + pass + +@timer +def post_x(self, item_id:str) -> bool: + pass + +@timer +def post_y(self, item_id:str) -> bool: + pass + +def __len__(self) -> int: + return self._size + +@classmethod +def set_base(cls) -> int: + pass + +@staticmethod +@timer +def calc(key:str, value:int) -> int: + pass +""".lstrip() + + +def get_example() -> Block: + """ + Factory class that returns a copy of an example of Class object for tests + and POCs. + + Returns: + Method: The copy of the Function object instance. + """ + return deepcopy(_BLOCK) + + +get_syntax = partial(utils.get_syntax, postfix=_SYNTAX) diff --git a/tests/file/__init__.py b/tests/file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/file/test_general.py b/tests/file/test_general.py new file mode 100644 index 0000000..935f507 --- /dev/null +++ b/tests/file/test_general.py @@ -0,0 +1,21 @@ +# pylint: disable=missing-function-docstring +import os +import tempfile + +from codemate import File +from tests import examples + + +def test_save_file(): + # If an exception hasn't beaning raised, the test has passed + with tempfile.TemporaryDirectory() as tmp_dirname: + file = File() + path = os.path.join(tmp_dirname, "tmp.py") + file.save(path) + path = os.path.join(tmp_dirname, "tmp.py") + file.save(path, use_black=False) + + +def test_complex_file(): + file = examples.file.get_example() + assert examples.file.get_syntax() == file.use_black() diff --git a/tests/function/__init__.py b/tests/function/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/function/test_general.py b/tests/function/test_general.py new file mode 100644 index 0000000..6a79915 --- /dev/null +++ b/tests/function/test_general.py @@ -0,0 +1,65 @@ +# pylint: disable=missing-function-docstring +from tests import examples + +INITIATION_RESULT = examples.function.get_syntax() + +COMPLEX_RESULT = """ +\"\"\" +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +\"\"\" + +import time +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger +from typing import Callable + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") + + +def timer(func: Callable) -> Callable: + \"\"\" + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + \"\"\" + + def decorator(*args, **kwargs): + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + + return decorator +""".lstrip() + + +def test_initiation(): + function_ = examples.function.get_example() + assert INITIATION_RESULT == function_.syntax() + + +def test_complex(): + block_ = examples.block.get_example() + function_ = examples.function.get_example() + block_.insert(function_) + assert COMPLEX_RESULT == block_.use_black() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..70cec91 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,20 @@ +import black + + +def get_syntax(use_black: bool = False, prefix: str = "", postfix: str = "") -> str: + """ + Factory function that returns a copy of the example expected syntax. + + Args: + use_black (bool): If sets to True, the black formatter is used on the syntax. + Otherwise, the syntax return as is. + prefix (str): The syntax prefix sting. + postfix (str): The syntax postfix sting. + + Returns: + str: The formatted syntax. + """ + syntax = prefix + postfix + if use_black: + return black.format_str(syntax, mode=black.FileMode()) + return syntax From 3c1535928fb129e9c2bb5c24ef9e98d2fb4cb1b5 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Tue, 22 Jun 2021 18:12:17 +0300 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=8E=93=20Improved=20the=20documentati?= =?UTF-8?q?on,=20and=20added=20more=20examples=20in=20the=20ReadMe=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 859 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 858 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8afa88c..b465ac5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,868 @@ # CodeMate +

+ + Test + +

+ Python package, syntax generator, easy to use, OOP API +## Goals + +Easily generating python code without the need to care for styling and typo's. + +## Use Cases + +The use cases for using this pack may be one of the following: + +* Generate Python clients by protocols: + + * OpenAPI + + * AsyncAPI + + * ProtoBuf + +* Generate adapters between the code to services I/O. + +## Documentation + +Here will be described the usage of the pack features to generate Python code, +by a simple and convenient API: + +* Block +* Function +* Class +* Method +* File + +All of the components inherit from the Block component, +from that we receive the simplicity of the API. + +## Block component + +The Block component is the base of all of the components, it generates a python syntax +in a new context block. + +The Block structure (each section is optional): + +1. Docs +2. Imports +3. General Syntax + +### Initialization + +In the initiation of the Block, the default indentation may be changed, it will set how many +spaces to add before each line multiplied by the indentation. + +Each method that inserts/parse lines support indentation as input. + +```python +from codemate import Block + +block = Block(indentation=4) + +``` + +To generate the code use the following line: + +`print(block.syntax())` + +### Adding syntax + +Syntax lines are referred as general Python code lines, they are inserted as is, +in the given order. + +```python +from codemate import Block + +block = Block() + +# Adding multiple line as block +block.add_syntax_block(""" +import math + +x = math.sqrt(9) +if x > 3: +""") + +# Adding a single line +block.add_syntax_line("print(x)", indent=1) + +# Adding a variable +block.add_variable( + name="y", + type="float", + value="x * 3" +) + +# Adding multiple lines +block.add_syntax_lines( + "z = x * y", + "print(z)" +) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +import math + +x = math.sqrt(9) +if x > 3: + print(x) +y: float = x * 3 +z = x * y +print(z) + +``` + +### Adding imports + +Specifying syntax lines as imports line provide syntax typo safety, sorting with isort +and inserting after the docs section and before the syntax. Order and content +are modified. + +```python +from codemate import Block + +block = Block() + +# Adding a single import +block.add_import("codemate") + +# Adding specific import from a module +block.add_specific_import( + "codemate", + "Block", + "Function", + "Method", + "ClassMethod", + "StaticMethod", + "Class", + "File", +) + +# Adding multiple imports +block.add_imports( + "typing", + "codemate", + "typing" +) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +import typing + +import codemate +from codemate import (Block, Class, ClassMethod, File, Function, Method, + StaticMethod) + + +``` + +### Adding docs + +Specifying syntax lines as doc line inserts it before the imports section and the syntax +section. + +```python +from codemate import Block + +block = Block() + +# Adding a doc line +block.add_doc_line("Adding a single documentation line to the block") + +# Adding multiple doc lines +block.add_doc_lines( + "", + "Adding multiple,", + "documentation lines,", + "each one of them is in a new line.", + "", +) + +# Adding multiple imports +block.add_doc_block(""" + Adding a block of documentation, + for adding paragraphs easily. +""") + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +""" +Adding a single documentation line to the block + +Adding multiple, +documentation lines, +each one of them is in a new line. + +Adding a block of documentation, +for adding paragraphs easily. +""" + +``` + +### Extension + +Adds other Python block syntax to the current Python block syntax. Ignoring the +other's docs, copying the imports and adding the syntax. + +```python +from codemate import Block + +other = Block() + +other.add_import("math") + +other.add_doc_line("Other doc") + +other.add_syntax_block(""" +# other syntax +x = math.sqrt(9) +if x > 3: +""") + +other.add_syntax_line("print(x)") + +block = Block() + +block.add_doc_line("Extend example") + +block.add_syntax_line("pass") + +block.extend(other) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +""" +Extend example +""" + +import math + +pass +# other syntax +x = math.sqrt(9) +if x > 3: +print(x) + +``` + +### Insertion + +Inserts as is other Python block syntax to the current Python block syntax. Inserting +the docs and syntax as is and copying the imports. + +```python +from codemate import Block + +other = Block() + +other.add_import("math") + +other.add_doc_line("Other doc") + +other.add_syntax_block(""" +# other syntax +x = math.sqrt(9) +if x > 3: +""") + +other.add_syntax_line("print(x)") + +block = Block() + +block.add_doc_line("Insert example") + +block.add_syntax_line("pass") + +block.insert(other) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +""" +Insert example +""" + +import math + +pass +""" +Other doc +""" + +# other syntax +x = math.sqrt(9) +if x > 3: +print(x) + +``` + +### Using Black + +Will use the [black](https://github.com/psf/black) linter to format the Python syntax. + +** Note it modifies the content. + +```python +from codemate import Block + +block = Block() + +# complex syntax +block.add_syntax_block(""" + data = [{'userId':'3998e9a3dsdsa', 'related': {'tmp':'example'}, + 'emails':['gopo@fake.com', 'tmp@tmp.io', 'ab@ab.io']}] + print(data) +""") + +``` + +Generating the syntax using `print(block.use_black())`, we will receive: + +```python +data = [ + { + "userId": "3998e9a3dsdsa", + "related": {"tmp": "example"}, + "emails": ["gopo@fake.com", "tmp@tmp.io", "ab@ab.io"], + } +] +print(data) + +``` + +### Validation + +Checks if the generated syntax structure is valid. + +```python +from codemate import Block + +block = Block() + +# complex syntax +block.add_syntax_block(""" + data = ["structure error here -> missing ']'" + print(data) +""") + +# Will raise an exception, the reason is that ']' is missing in the 'data' variable set. +block.validate() + +``` + +## Function component + +The Function generates a python function syntax, moreover this is the base of the methods +objects. + +### Initialization + +```python +from codemate import Function + +# Simple function +simple_func = Function(name="simple") +simple_func.add_syntax_line("pass") + +# Function with arguments +with_arguments_func = Function(name="with_arguments", arguments=("foo:int", "bar")) +with_arguments_func.add_syntax_line("pass") + +# Async function +async_func = Function(name="async", arguments=("foo:int", "bar")) +async_func.add_syntax_line("pass") + +# Function with return value type hint +with_rv_func = Function(name="with_rv", return_value="str") +with_rv_func.add_syntax_line("pass") + +``` + +Generating the syntax using `print({function instance}.syntax())`, we will receive: + +```python +def simple(): + pass + +# -------------------------------------------- +def with_arguments(foo:int, bar): + pass + +# -------------------------------------------- +def async(foo:int, bar): + pass + +# -------------------------------------------- +def with_rv() -> str: + pass + +``` + +### Decorators + +```python +from codemate import Function, Block + +block = Block() +block.add_specific_import("functools", "lru_cache") + +function = Function(name="foo") +function.add_syntax_line("pass") +function.add_decorator("lru_cache()") + +block.insert(function) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +from functools import lru_cache + +@lru_cache() +def foo(): + pass + +``` + +## Class component + +The Class generates a python class syntax. + +### Initialization + +```python +from codemate import Class + +# Simple class +simple_class = Class(name="Simple") +simple_class.add_syntax_line("pass") + +# Class with metaclass +with_meta_class = Class(name="WithMeta", metaclass="Simple") +with_meta_class.add_syntax_line("pass") + +# Class with inheritance +with_inheritance_class = Class(name="WithInheritance", inherit=("str", "WithMeta")) +with_inheritance_class.add_syntax_line("pass") + +``` + +Generating the syntax using `print({class instance}.syntax())`, we will receive: + +```python +class Simple: + pass + +# -------------------------------------------- +class WithMeta(metaclass=Simple): + pass + +# -------------------------------------------- +class WithInheritance(str, WithMeta,): + pass + +``` + +### Decorators + +```python +from codemate import Class, Function, Block + +block = Block() + +decorator = Function(name="class_decorator", arguments=("cls",)) +decorator.add_syntax_line("pass") +block.insert(decorator) + +class_ = Class(name="Bar") +class_.add_syntax_line("pass") +class_.add_decorator("class_decorator") +block.insert(class_) + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +def class_decorator(cls): + pass + +@class_decorator +class Bar: + pass + +``` + +## Method component + +The Method, ClassMethod, and StaticMethod components inherit from Function and already +contain the relevant adaptation. + +## File component + +File component is a Block component with relevant capabilities to make Python files +generation simple. + +### Initialization + +```python +from codemate import File + +# A file with default header +default = File() + +# A file with customized header +customized = File(header="-------- Customized Header --------") + +``` + +Generating the syntax using `print({file instance}.syntax())`, we will receive: + +```python +""" +--------------------------------- Warning generated file --------------------------------- +Generated at: 2021-06-22T15:30:42.222423 +------------------------------------------------------------------------------------------ +""" + +# -------------------------------------------- +""" +-------- Customized Header -------- +""" + +``` + +### Saving the file + +We can save the generated file and specific whether to use black linter on the content +or not. + +```python +from codemate import Class, Function, Block + +from codemate import File + +file = File() + +# Without black linter +file.save(path="without_black.py") + +# With black linter +file.save(path="with_black.py", use_black=True) +``` + +## Use case example + +An example of how to use the components in this Python package to generate a client by +API. + +```python +from typing import Tuple + +from codemate import Class, ClassMethod, File, Function, Method, StaticMethod + +DECORATOR_NAME = "timer" + +API_STRUCTURE = [ + {"operation_name": "get_x", "return_value": "List[int]"}, + {"operation_name": "get_y", "return_value": "str"}, + {"operation_name": "post_x", "return_value": "bool"}, + {"operation_name": "post_y", "return_value": "bool"}, +] + + +def _set_file_docs(file: File) -> None: + file.add_doc_lines( + " Example ".center(90, "-"), + "This is an example of how to use this Python package to generate easily and safely", + "Python syntax.", + ) + file.add_doc_line("") + file.add_doc_block( + """ + The use cases for using this pack may be one of the following: + * Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf + * Generate adapters between the code to systems I/O. + + Easily generating python code without the need to care for styling and indentation. + """.strip( + "\n" + ) + ) + file.add_doc_line("") + file.add_doc_line("".center(90, "-")) + + +def _create_file_base() -> File: + file = File() + _set_file_docs(file) + file.add_specific_import("logging", "getLogger") + file.add_specific_import("logging", "INFO, StreamHandler") + file.add_specific_import("logging", "DEBUG", "Logger") + file.add_variable("LOGGER", type="Logger", value="getLogger(__name__)") + file.add_syntax_block( + """ + LOGGER.setLevel(DEBUG) + _channel = StreamHandler() + _channel.setLevel(INFO) + LOGGER.addHandler(_channel) + LOGGER.debug("✨So far so good✨") + """.strip( + "\n" + ) + ) + return file + + +def _create_inner_function() -> Function: + function = Function(name="decorator", arguments=("*args", "**kwargs")) + function.add_import("time") + function.add_syntax_block( + """ + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + """.strip( + "\n" + ) + ) + return function + + +def _create_decorator(): + function = Function( + name=DECORATOR_NAME, arguments=("func:Callable",), return_value="Callable" + ) + function.add_doc_block( + """ + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + """.strip( + "\n" + ) + ) + function.add_specific_import("typing", "Callable") + inner_function = _create_inner_function() + function.insert(inner_function) + function.add_syntax_line("return decorator") + return function + + +def _create_init() -> Method: + method = Method( + "__init__", arguments=("logger:Logger=LOGGER",), return_value="None" + ) + method.add_specific_import("logging", "Logger") + method.add_specific_import("typing", "List") + method.add_variable("self._logger", value="logger") + method.add_variable("self._size", value="0") + return method + + +def _create_operations() -> Tuple[Method, ...]: + methods = [] + for _operation in API_STRUCTURE: + _name, _return_value = _operation.values() + _method = Method(_name, arguments=("item_id:str",), return_value=_return_value) + _method.add_decorator("timer") + _method.add_syntax_line("pass") + methods.append(_method) + return tuple(methods) + + +def _create_calc_method() -> StaticMethod: + method = StaticMethod( + "calc", + arguments=( + "key:str", + "value:int", + ), + return_value="int", + ) + method.add_decorator("timer") + method.add_syntax_line("pass") + return method + + +def _create_methods() -> Tuple[Function, ...]: + methods = [] + methods.append(_create_init()) + methods.extend(_create_operations()) + len_ = Method("__len__", return_value="int") + len_.add_syntax_line("return self._size") + methods.append(len_) + set_base_method = ClassMethod("set_base", return_value="int") + set_base_method.add_syntax_line("pass") + methods.append(set_base_method) + methods.append(_create_calc_method()) + return tuple(methods) + + +def _create_class() -> Class: + class_ = Class(name="APIWrapper", inherit=("Sized",)) + class_.add_doc_line("A class that represents a raper for a defined API structure.") + class_.add_specific_import("collections", "Sized") + class_.add_specific_import("logging", "Logger") + for method in _create_methods(): + class_.insert(method) + return class_ + + +def main(): + """ + An example of how to use the components in this Python package to generate a client + by API. + """ + file = _create_file_base() + file.insert(_create_decorator()) + file.insert(_create_class()) + return file + +``` + +Generating the syntax using `print(block.syntax())`, we will receive: + +```python +""" +--------------------------------- Warning generated file --------------------------------- +Generated at: 2021-06-22T17:46:00.593553 +------------------------------------------------------------------------------------------ +---------------------------------------- Example ----------------------------------------- +This is an example of how to use this Python package to generate easily and safely +Python syntax. + +The use cases for using this pack may be one of the following: +* Generate Python clients by protocols: + * OpenAPI + * AsyncAPI + * ProtoBuf +* Generate adapters between the code to systems I/O. + +Easily generating python code without the need to care for styling and indentation. + +------------------------------------------------------------------------------------------ +""" + +import time +from collections import Sized +from logging import DEBUG, INFO, Logger, StreamHandler, getLogger +from typing import Callable, List + +LOGGER: Logger = getLogger(__name__) +LOGGER.setLevel(DEBUG) +_channel = StreamHandler() +_channel.setLevel(INFO) +LOGGER.addHandler(_channel) +LOGGER.debug("✨So far so good✨") + + +def timer(func: Callable) -> Callable: + """ + A decorator that times the execution of the wrapped function. + + Args: + func (Callable): The wrapped function. + """ + + def decorator(*args, **kwargs): + start = time.perf_counter() + return_value = func(*args, **kwargs) + end = time.perf_counter() + name = getattr(func, "__name__", "UnKnown") + LOGGER.info(f"The execution of '{name}' took {end - start:0.4f} seconds") + return return_value + + return decorator + + +class APIWrapper( + Sized, +): + """ + A class that represents a raper for a defined API structure. + """ + + def __init__(self, logger: Logger = LOGGER) -> None: + self._logger = logger + self._size = 0 + + @timer + def get_x(self, item_id: str) -> List[int]: + pass + + @timer + def get_y(self, item_id: str) -> str: + pass + + @timer + def post_x(self, item_id: str) -> bool: + pass + + @timer + def post_y(self, item_id: str) -> bool: + pass + + def __len__(self) -> int: + return self._size + + @classmethod + def set_base(cls) -> int: + pass + + @staticmethod + @timer + def calc(key: str, value: int) -> int: + pass + +``` + ## Development -Use pre-commit: +Use the following command to execute the linters and the unit tests. + +Make sure that the linters and the unit tests pass and that the unit tests coverage +is above 90%: + +`scripts/test.sh` + +Use "pytest-cov" to only execute the unit tests: + +`pytest --cov=codemate --cov=tests` + +Use the following command to make sure that the linters are passing: + +`scripts/lint.sh` + +Use "pre-commit" to run the active and passive linters: * `pre-commit install` - run on every commit. * `pre-commit run --all-files` - run manually on the repository. + From 86046fd8bd55cb6d356b1f1ce3dc10a299225826 Mon Sep 17 00:00:00 2001 From: "dor.abu" Date: Tue, 6 Jul 2021 10:30:33 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=A1=20PR=20comments=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codemate/block.py | 16 +++++----------- codemate/utils.py | 2 +- tests/block/test_add_import.py | 2 +- tests/utils.py | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/codemate/block.py b/codemate/block.py index a5cddc5..a93bada 100644 --- a/codemate/block.py +++ b/codemate/block.py @@ -117,7 +117,7 @@ def add_doc_lines(self, *lines: str, indent: int = 0) -> "Block": Block: The block instance. """ add_doc_line = partial(self.add_doc_line, indent=indent) - tuple(map(add_doc_line, lines)) + list(map(add_doc_line, lines)) return self def add_doc_block(self, block: str, indent: int = 0) -> "Block": @@ -150,22 +150,16 @@ def add_import(self, module: str) -> "Block": def add_imports(self, *modules: str) -> "Block": """ - Adds imports syntax lines to the Python block syntax. + Adds imports syntax lines to the Python block syntax, + Ignores empty modules names. Args: modules (Collection[str]): The modules names that we want to import. Returns: Block: The block instance. - - Raises: - ValueError: When one of the provided modules is an empty string. """ - for module in modules: - if module: - self.add_import(module) - else: - raise ValueError("Module name can't be empty") + list(map(self.add_import, filter(bool, modules))) return self def add_specific_import(self, module: str, *components: str) -> "Block": @@ -357,7 +351,7 @@ def validate(self) -> bool: else: return True - def __repr__(self): + def __repr__(self) -> str: class_name = getattr(type(self), "__name__", type(self)) return f"{class_name}({vars(self)})" diff --git a/codemate/utils.py b/codemate/utils.py index 930de1b..0b20382 100644 --- a/codemate/utils.py +++ b/codemate/utils.py @@ -12,6 +12,6 @@ def remove_indentation(content: str) -> str: Returns: str: The unindented content. """ - indentation = next(iter(re.findall("^\n*( *)", content) or []), "") + indentation = next(iter(re.findall("^\n*( *)", content)), "") unindented = re.subn(f"(\n){indentation}", r"\1", content)[0].strip() return unindented diff --git a/tests/block/test_add_import.py b/tests/block/test_add_import.py index 3e14917..976ca6f 100644 --- a/tests/block/test_add_import.py +++ b/tests/block/test_add_import.py @@ -7,7 +7,7 @@ SINGLE_IMPORT_RESULT: str = isort.code("import math") -MULTIPLE_IMPORTS = ("math", "sys", "math", "datetime") +MULTIPLE_IMPORTS = ("math", "sys", "math", None, "datetime") MULTIPLE_IMPORTS_RESULT: str = isort.code( """ diff --git a/tests/utils.py b/tests/utils.py index 70cec91..25834e0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,7 +14,7 @@ def get_syntax(use_black: bool = False, prefix: str = "", postfix: str = "") -> Returns: str: The formatted syntax. """ - syntax = prefix + postfix + syntax = f"{prefix}{postfix}" if use_black: return black.format_str(syntax, mode=black.FileMode()) return syntax