├── .editorconfig ├── .github ├── doccoverage.svg └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── cdd ├── __init__.py ├── __main__.py ├── argparse_function │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ └── emit_utils.py ├── class_ │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ ├── emit_utils.py │ │ ├── parse_utils.py │ │ └── shared_utils.py ├── compound │ ├── __init__.py │ ├── doctrans.py │ ├── doctrans_utils.py │ ├── exmod.py │ ├── exmod_utils.py │ ├── gen.py │ ├── gen_utils.py │ ├── openapi │ │ ├── __init__.py │ │ ├── emit.py │ │ ├── gen_openapi.py │ │ ├── gen_routes.py │ │ ├── parse.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── emit_openapi_utils.py │ │ │ ├── emit_utils.py │ │ │ └── parse_utils.py │ └── sync_properties.py ├── docstring │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ ├── emit_utils.py │ │ └── parse_utils.py ├── function │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ ├── emit_utils.py │ │ └── parse_utils.py ├── json_schema │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ ├── emit_utils.py │ │ ├── parse_utils.py │ │ └── shared_utils.py ├── pydantic │ ├── __init__.py │ ├── emit.py │ └── parse.py ├── routes │ ├── __init__.py │ ├── emit │ │ ├── __init__.py │ │ ├── bottle.py │ │ └── bottle_constants_utils.py │ └── parse │ │ ├── __init__.py │ │ ├── bottle.py │ │ ├── bottle_utils.py │ │ ├── fastapi.py │ │ └── fastapi_utils.py ├── shared │ ├── __init__.py │ ├── ast_cst_utils.py │ ├── ast_utils.py │ ├── conformance.py │ ├── cst.py │ ├── cst_utils.py │ ├── defaults_utils.py │ ├── docstring_parsers.py │ ├── docstring_utils.py │ ├── emit │ │ ├── __init__.py │ │ ├── file.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ └── emitter_utils.py │ ├── parse │ │ ├── __init__.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ └── parser_utils.py │ ├── pkg_utils.py │ ├── pure_utils.py │ ├── source_transformer.py │ └── types.py ├── sqlalchemy │ ├── __init__.py │ ├── emit.py │ ├── parse.py │ └── utils │ │ ├── __init__.py │ │ ├── emit_utils.py │ │ ├── parse_utils.py │ │ └── shared_utils.py └── tests │ ├── __init__.py │ ├── mocks │ ├── __init__.py │ ├── argparse.py │ ├── classes.py │ ├── cst.py │ ├── cstify.py │ ├── docstrings.py │ ├── doctrans.py │ ├── eval.py │ ├── exmod.py │ ├── fastapi_routes.py │ ├── gen.py │ ├── ir.py │ ├── json_schema.py │ ├── methods.py │ ├── openapi.py │ ├── openapi_emit_utils.py │ ├── pydantic.py │ ├── routes.py │ └── sqlalchemy.py │ ├── test_argparse_function │ ├── __init__.py │ ├── test_emit_argparse.py │ └── test_parse_argparse.py │ ├── test_ast_equality.py │ ├── test_class │ ├── __init__.py │ ├── test_emit_class_.py │ └── test_parse_class.py │ ├── test_cli │ ├── __init__.py │ ├── test_cli.py │ ├── test_cli_doctrans.py │ ├── test_cli_exmod.py │ ├── test_cli_gen.py │ ├── test_cli_gen_routes.py │ ├── test_cli_openapi.py │ ├── test_cli_sync.py │ └── test_cli_sync_properties.py │ ├── test_compound │ ├── __init__.py │ ├── test_doctrans.py │ ├── test_doctrans_utils.py │ ├── test_exmod.py │ ├── test_exmod_utils.py │ ├── test_gen.py │ ├── test_gen_routes.py │ ├── test_gen_utils.py │ ├── test_openapi_bulk.py │ ├── test_openapi_emit_utils.py │ ├── test_openapi_sub.py │ └── test_sync_properties.py │ ├── test_docstring │ ├── __init__.py │ ├── test_emit_docstring.py │ ├── test_parse_docstring.py │ └── test_parse_docstring_utils.py │ ├── test_emit │ ├── __init__.py │ ├── test_emit_file.py │ ├── test_emitter_utils.py │ └── test_emitters.py │ ├── test_function │ ├── __init__.py │ ├── test_emit_function.py │ └── test_parse_function.py │ ├── test_json_schema │ ├── __init__.py │ ├── test_emit_json_schema.py │ ├── test_emit_json_schema_utils.py │ ├── test_parse_json_schema.py │ └── test_parse_json_schema_utils.py │ ├── test_marshall_docstring.py │ ├── test_parse │ ├── __init__.py │ ├── test_parser_utils.py │ └── test_parsers.py │ ├── test_pydantic │ ├── __init__.py │ ├── test_emit_pydantic.py │ └── test_parse_pydantic.py │ ├── test_routes │ ├── __init__.py │ ├── test_bottle_route_emit.py │ ├── test_bottle_route_parse.py │ ├── test_fastapi_routes_parse.py │ ├── test_route_emit.py │ └── test_route_parse.py │ ├── test_setup.py │ ├── test_shared │ ├── __init__.py │ ├── test_ast_cst_utils.py │ ├── test_ast_utils.py │ ├── test_conformance.py │ ├── test_cst.py │ ├── test_cst_utils.py │ ├── test_default_utils.py │ ├── test_docstring_utils.py │ ├── test_pkg_utils.py │ ├── test_pure_utils.py │ └── test_source_transformer.py │ ├── test_sqlalchemy │ ├── __init__.py │ ├── test_emit_sqlalchemy.py │ ├── test_emit_sqlalchemy_utils.py │ ├── test_parse_sqlalchemy.py │ └── test_parse_sqlalchemy_utils.py │ ├── test_utils_for_tests.py │ └── utils_for_tests.py ├── requirements.txt └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.py] 11 | max_line_length = 119 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | # Jetbrains IDEs 5 | .idea 6 | out/ 7 | .project 8 | .pydevproject 9 | .vscode 10 | 11 | # File-based project format 12 | *.iws 13 | 14 | # JIRA plugin 15 | atlassian-ide-plugin.xml 16 | 17 | # Crashlytics plugin (for Android Studio and IntelliJ) 18 | com_crashlytics_export_strings.xml 19 | crashlytics.properties 20 | crashlytics-build.properties 21 | fabric.properties 22 | 23 | ### Python ### 24 | # Byte-compiled / optimized / DLL files 25 | __pycache__/ 26 | *.py[cod] 27 | *$py.class 28 | 29 | # C extensions 30 | *.so 31 | 32 | # Distribution / packaging 33 | .Python 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | pip-wheel-metadata/ 47 | share/python-wheels/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | MANIFEST 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .nox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | .hypothesis/ 74 | .pytest_cache/ 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | local_settings.py 83 | db.sqlite3 84 | db.sqlite3-journal 85 | 86 | # Flask stuff: 87 | instance/ 88 | .webassets-cache 89 | 90 | # Scrapy stuff: 91 | .scrapy 92 | 93 | # Sphinx documentation 94 | docs/_build/ 95 | 96 | # PyBuilder 97 | target/ 98 | 99 | # Jupyter Notebook 100 | .ipynb_checkpoints 101 | 102 | # IPython 103 | profile_default/ 104 | ipython_config.py 105 | 106 | # pyenv 107 | .python-version 108 | 109 | # pipenv 110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 113 | # install all needed dependencies. 114 | #Pipfile.lock 115 | 116 | # celery beat schedule file 117 | celerybeat-schedule 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # macOS 150 | .DS_Store 151 | 152 | # Docs 153 | _docs 154 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN apk add --no-cache gcc musl-dev && \ 7 | pip install --no-cache-dir -r requirements.txt && \ 8 | pip install --no-cache-dir meta 9 | 10 | COPY . . 11 | 12 | CMD [ "python", "setup.py", "test" ] 13 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2020–2023 Samuel Marks (for Offscale.io) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /cdd/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Root __init__ 5 | """ 6 | 7 | import logging 8 | from logging import Logger 9 | from logging import getLogger as get_logger 10 | 11 | __author__ = "Samuel Marks" # type: str 12 | __version__ = "0.0.99rc46" # type: str 13 | __description__ = ( 14 | "Open API to/fro routes, models, and tests. " 15 | "Convert between docstrings, classes, methods, argparse, pydantic, and SQLalchemy." 16 | ) # type: str 17 | 18 | 19 | root_logger: Logger = get_logger() 20 | logging.getLogger("blib2to3").setLevel(logging.WARNING) 21 | 22 | __all__ = [ 23 | "get_logger", 24 | "root_logger", 25 | "__description__", 26 | "__version__", 27 | ] # type: list[str] 28 | -------------------------------------------------------------------------------- /cdd/argparse_function/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Argparse function parser and emitter 3 | """ 4 | 5 | # cdd.shared.types.conforms_to_parse_protocol(cdd.argparse_function.parse) 6 | -------------------------------------------------------------------------------- /cdd/argparse_function/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Argparse function parser 3 | """ 4 | 5 | from ast import Assign, Call, FunctionDef, Return, Tuple, get_docstring 6 | from collections import OrderedDict 7 | from functools import partial 8 | from itertools import filterfalse 9 | from operator import setitem 10 | from typing import List, Optional, cast 11 | 12 | from cdd.argparse_function.utils.emit_utils import _parse_return, parse_out_param 13 | from cdd.shared.ast_utils import ( 14 | get_function_type, 15 | get_value, 16 | is_argparse_add_argument, 17 | is_argparse_description, 18 | ) 19 | from cdd.shared.docstring_parsers import parse_docstring 20 | from cdd.shared.types import IntermediateRepr 21 | 22 | 23 | def argparse_ast( 24 | function_def, 25 | function_type=None, 26 | function_name=None, 27 | parse_original_whitespace=False, 28 | word_wrap=False, 29 | ): 30 | """ 31 | Converts an argparse AST to our IR 32 | 33 | :param function_def: AST of argparse function_def 34 | :type function_def: ```FunctionDef``` 35 | 36 | :param function_type: Type of function, static is static or global method, others just become first arg 37 | :type function_type: ```Literal['self', 'cls', 'static']``` 38 | 39 | :param function_name: name of function_def 40 | :type function_name: ```str``` 41 | 42 | :param parse_original_whitespace: Whether to parse original whitespace or strip it out 43 | :type parse_original_whitespace: ```bool``` 44 | 45 | :param word_wrap: Whether to word-wrap. Set `DOCTRANS_LINE_LENGTH` to configure length. 46 | :type word_wrap: ```bool``` 47 | 48 | :return: a dictionary consistent with `IntermediateRepr`, defined as: 49 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 50 | IntermediateRepr = TypedDict("IntermediateRepr", { 51 | "name": Optional[str], 52 | "type": Optional[str], 53 | "doc": Optional[str], 54 | "params": OrderedDict[str, ParamVal], 55 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 56 | }) 57 | :rtype: ```dict``` 58 | """ 59 | assert isinstance( 60 | function_def, FunctionDef 61 | ), "Expected `FunctionDef` got `{node_name!r}`".format( 62 | node_name=type(function_def).__name__ 63 | ) 64 | 65 | doc_string: Optional[str] = get_docstring( 66 | function_def, clean=parse_original_whitespace 67 | ) 68 | intermediate_repr: IntermediateRepr = { 69 | "name": function_name or function_def.name, 70 | "type": function_type or get_function_type(function_def), 71 | "doc": "", 72 | "params": OrderedDict(), 73 | } 74 | ir: IntermediateRepr = parse_docstring( 75 | doc_string, 76 | word_wrap=word_wrap, 77 | emit_default_doc=True, 78 | parse_original_whitespace=parse_original_whitespace, 79 | ) 80 | 81 | # Whether a default is required, if not found in doc, infer the proper default from type 82 | require_default = False 83 | 84 | # Parse all relevant nodes from function body 85 | body: FunctionDef.body = ( 86 | function_def.body if doc_string is None else function_def.body[1:] 87 | ) 88 | for node in body: 89 | if is_argparse_add_argument(node): 90 | name, _param = parse_out_param( 91 | node, 92 | emit_default_doc=False, # require_default=require_default 93 | ) 94 | ( 95 | intermediate_repr["params"][name].update 96 | if name in intermediate_repr["params"] 97 | else partial(setitem, intermediate_repr["params"], name) 98 | )(_param) 99 | if not require_default and _param.get("default") is not None: 100 | require_default: bool = True 101 | elif isinstance(node, Assign) and is_argparse_description(node): 102 | intermediate_repr["doc"] = get_value(node.value) 103 | elif isinstance(node, Return) and isinstance(node.value, Tuple): 104 | intermediate_repr["returns"] = OrderedDict( 105 | ( 106 | _parse_return( 107 | node, 108 | intermediate_repr=ir, 109 | function_def=function_def, 110 | emit_default_doc=False, 111 | ), 112 | ) 113 | ) 114 | 115 | inner_body: List[Call] = cast( 116 | List[Call], 117 | list( 118 | filterfalse( 119 | is_argparse_description, 120 | filterfalse(is_argparse_add_argument, body), 121 | ) 122 | ), 123 | ) 124 | if inner_body: 125 | intermediate_repr["_internal"] = { 126 | "original_doc_str": ( 127 | doc_string 128 | if parse_original_whitespace 129 | else get_docstring(function_def, clean=False) 130 | ), 131 | "body": inner_body, 132 | "from_name": function_def.name, 133 | "from_type": "static", 134 | } 135 | 136 | return intermediate_repr 137 | 138 | 139 | __all__ = ["argparse_ast"] # type: list[str] 140 | -------------------------------------------------------------------------------- /cdd/argparse_function/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Argparse function parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/class_/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | class parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/class_/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | class parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/class_/utils/emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit.class_` 3 | """ 4 | 5 | import ast 6 | from ast import Attribute, Load, Name 7 | 8 | 9 | class RewriteName(ast.NodeTransformer): 10 | """ 11 | A :class:`NodeTransformer` subclass that walks the abstract syntax tree and 12 | allows modification of nodes. Here it modifies parameter names to be `self.param_name` 13 | """ 14 | 15 | def __init__(self, node_ids): 16 | """ 17 | Set parameter 18 | 19 | :param node_ids: Container of AST `id`s to match for rename 20 | :type node_ids: ```Optional[Iterator[str]]``` 21 | """ 22 | self.node_ids = node_ids 23 | 24 | def visit_Name(self, node): 25 | """ 26 | Rename parameter name with a `self.` attribute prefix 27 | 28 | :param node: The AST node 29 | :type node: ```Name``` 30 | 31 | :return: `Name` iff `Name` is not a parameter else `Attribute` 32 | :rtype: ```Union[Name, Attribute]``` 33 | """ 34 | return ( 35 | Attribute( 36 | Name("self", Load(), lineno=None, col_offset=None), 37 | node.id, 38 | Load(), 39 | lineno=None, 40 | col_offset=None, 41 | ) 42 | if not self.node_ids or node.id in self.node_ids 43 | else ast.NodeTransformer.generic_visit(self, node) 44 | ) 45 | 46 | 47 | __all__ = ["RewriteName"] # type: list[str] 48 | -------------------------------------------------------------------------------- /cdd/class_/utils/parse_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.parse.class` 3 | """ 4 | 5 | from inspect import getsource 6 | 7 | 8 | def get_source(obj): 9 | """ 10 | Call inspect.getsource and raise an error unless class definition could not be found 11 | 12 | :param obj: object to inspect 13 | :type obj: ```Any``` 14 | 15 | :return: The source 16 | :rtype: ```Optional[str]``` 17 | """ 18 | try: 19 | return getsource(obj) 20 | except OSError as e: 21 | if e.args and e.args[0] in frozenset( 22 | ( 23 | "could not find class definition", 24 | "source code not available", 25 | "could not get source code", 26 | ) 27 | ): 28 | return None 29 | raise 30 | 31 | 32 | __all__ = ["get_source"] # type: list[str] 33 | -------------------------------------------------------------------------------- /cdd/class_/utils/shared_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared utility functions for `cdd.class_` 3 | """ 4 | 5 | from ast import ClassDef, Module 6 | from typing import List, Optional, Tuple, Union 7 | 8 | from cdd.shared.pure_utils import PY_GTE_3_8 9 | from cdd.shared.types import IntermediateRepr 10 | 11 | if PY_GTE_3_8: 12 | from typing import Literal, Protocol 13 | else: 14 | from typing_extensions import Literal, Protocol 15 | 16 | 17 | class ClassEmitProtocol(Protocol): 18 | """ 19 | Protocol for class emitter 20 | """ 21 | 22 | def __call__( 23 | self, 24 | intermediate_repr: IntermediateRepr, 25 | emit_call: bool = False, 26 | class_name: Optional[str] = None, 27 | class_bases: Tuple[str] = ("object",), 28 | decorator_list: Optional[List[str]] = None, 29 | word_wrap: bool = True, 30 | docstring_format: Literal["rest", "numpydoc", "google"] = "rest", 31 | emit_original_whitespace: bool = False, 32 | emit_default_doc: bool = False, 33 | ) -> ClassDef: 34 | """ 35 | Construct a class 36 | 37 | :param intermediate_repr: a dictionary consistent with `IntermediateRepr`, defined as: 38 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 39 | IntermediateRepr = TypedDict("IntermediateRepr", { 40 | "name": Optional[str], 41 | "type": Optional[str], 42 | "doc": Optional[str], 43 | "params": OrderedDict[str, ParamVal], 44 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 45 | }) 46 | 47 | :param emit_call: Whether to emit a `__call__` method from the `_internal` IR subdict 48 | 49 | :param class_name: name of class 50 | 51 | :param class_bases: bases of class (the generated class will inherit these) 52 | 53 | :param decorator_list: List of decorators 54 | 55 | :param word_wrap: Whether to word-wrap. Set `DOCTRANS_LINE_LENGTH` to configure length. 56 | 57 | :param docstring_format: Format of docstring 58 | 59 | :param emit_original_whitespace: Whether to emit original whitespace or strip it out (in docstring) 60 | 61 | :param emit_default_doc: Whether help/docstring should include 'With default' text 62 | 63 | :return: Class AST 64 | """ 65 | 66 | 67 | class ClassParserProtocol(Protocol): 68 | """ 69 | Protocol for class parser 70 | """ 71 | 72 | def __call__( 73 | self, 74 | class_def: Union[Module, ClassDef], 75 | class_name: Optional[str] = None, 76 | merge_inner_function: Optional[str] = None, 77 | infer_type: bool = False, 78 | parse_original_whitespace: bool = False, 79 | word_wrap: bool = True, 80 | ) -> IntermediateRepr: 81 | """ 82 | Converts an AST to our IR 83 | 84 | :param class_def: Class AST or Module AST with a ClassDef inside 85 | 86 | :param class_name: Name of `class`. If None, gives first found. 87 | 88 | :param merge_inner_function: Name of inner function to merge. If None, merge nothing. 89 | 90 | :param infer_type: Whether to try inferring the typ (from the default) 91 | 92 | :param parse_original_whitespace: Whether to parse original whitespace or strip it out 93 | 94 | :param word_wrap: Whether to word-wrap. Set `DOCTRANS_LINE_LENGTH` to configure length. 95 | 96 | :return: a dictionary consistent with `IntermediateRepr`, defined as: 97 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 98 | IntermediateRepr = TypedDict("IntermediateRepr", { 99 | "name": Optional[str], 100 | "type": Optional[str], 101 | "doc": Optional[str], 102 | "params": OrderedDict[str, ParamVal], 103 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 104 | }) 105 | """ 106 | 107 | 108 | __all__ = ["ClassEmitProtocol", "ClassParserProtocol"] # type: list[str] 109 | -------------------------------------------------------------------------------- /cdd/compound/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | `cdd.compound` contains modules that combine multiple others 3 | 4 | This is used for OpenAPI, CLI subcommands, and similar. 5 | """ 6 | -------------------------------------------------------------------------------- /cdd/compound/doctrans.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper to traverse the AST of the input file, extract the docstring out, parse and format to intended style, and emit 3 | """ 4 | 5 | from ast import Module, fix_missing_locations 6 | from copy import deepcopy 7 | from operator import attrgetter 8 | from typing import List, NamedTuple 9 | 10 | from cdd.compound.doctrans_utils import DocTrans, doctransify_cst, has_type_annotations 11 | from cdd.shared.ast_utils import cmp_ast 12 | from cdd.shared.cst import cst_parse 13 | from cdd.shared.source_transformer import ast_parse 14 | 15 | 16 | def doctrans(filename, docstring_format, type_annotations, no_word_wrap): 17 | """ 18 | Transform the docstrings found within provided filename to intended docstring_format 19 | 20 | :param filename: Python file to convert docstrings within. Edited in place. 21 | :type filename: ```str``` 22 | 23 | :param docstring_format: Format of docstring 24 | :type docstring_format: ```Literal['rest', 'numpydoc', 'google']``` 25 | 26 | :param type_annotations: True to have type annotations (3.6+), False to place in docstring 27 | :type type_annotations: ```bool``` 28 | 29 | :param no_word_wrap: Whether word-wrap is disabled (on emission). 30 | :type no_word_wrap: ```Optional[Literal[True]]``` 31 | """ 32 | with open(filename, "rt") as f: 33 | original_source: str = f.read() 34 | node: Module = ast_parse(original_source, skip_docstring_remit=False) 35 | original_module: Module = deepcopy(node) 36 | 37 | node: Module = fix_missing_locations( 38 | DocTrans( 39 | docstring_format=docstring_format, 40 | word_wrap=no_word_wrap is None, 41 | type_annotations=type_annotations, 42 | existing_type_annotations=has_type_annotations(node), 43 | whole_ast=original_module, 44 | ).visit(node) 45 | ) 46 | 47 | if not cmp_ast(node, original_module): 48 | cst_list: List[NamedTuple] = list(cst_parse(original_source)) 49 | 50 | # Carefully replace only docstrings, function return annotations, assignment and annotation assignments. 51 | # Maintaining all other existing whitespace, comments, &etc. 52 | doctransify_cst(cst_list, node) 53 | 54 | with open(filename, "wt") as f: 55 | f.write("".join(map(attrgetter("value"), cst_list))) 56 | 57 | 58 | __all__ = ["doctrans"] # type: list[str] 59 | -------------------------------------------------------------------------------- /cdd/compound/openapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parsers and emitters for OpenAPI 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/compound/openapi/emit.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAPI emitter function(s) 3 | """ 4 | 5 | from collections import deque 6 | 7 | from cdd.compound.openapi.utils.emit_openapi_utils import ( 8 | components_paths_from_name_model_route_id_crud, 9 | ) 10 | from cdd.tests.mocks.json_schema import server_error_schema 11 | 12 | 13 | def openapi(name_model_route_id_cruds): 14 | """ 15 | Emit OpenAPI dict 16 | 17 | :param name_model_route_id_cruds: Collection of (name, model, route, id, crud) 18 | :type name_model_route_id_cruds: ```Iterable[NameModelRouteIdCrud]``` 19 | 20 | :return: OpenAPI dict 21 | :rtype: ```dict``` 22 | """ 23 | paths, components = {}, { 24 | "requestBodies": {}, 25 | "schemas": { 26 | "ServerError": { 27 | k: v for k, v in server_error_schema.items() if not k.startswith("$") 28 | } 29 | }, 30 | } 31 | 32 | deque( 33 | map( 34 | lambda name_model_route_id_crud: components_paths_from_name_model_route_id_crud( 35 | components, paths, *name_model_route_id_crud 36 | ), 37 | name_model_route_id_cruds, 38 | ), 39 | maxlen=0, 40 | ) 41 | 42 | return { 43 | "openapi": "3.0.0", 44 | "info": {"version": "0.0.1", "title": "REST API"}, 45 | # "servers": [{"url": "https://example.io/v1"}], 46 | "components": components, 47 | "paths": paths, 48 | } 49 | 50 | 51 | __all__ = ["openapi"] # type: list[str] 52 | -------------------------------------------------------------------------------- /cdd/compound/openapi/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAPI parsers 3 | """ 4 | 5 | from json import loads 6 | from typing import List, Optional 7 | 8 | from yaml import safe_load 9 | 10 | from cdd.compound.openapi.utils.parse_utils import extract_entities 11 | 12 | 13 | def openapi(openapi_str, routes_dict, summary): 14 | """ 15 | OpenAPI parser 16 | 17 | :param openapi_str: The OpenAPI str 18 | :type openapi_str: ```str``` 19 | 20 | :param routes_dict: Has keys ("route", "name", "method") 21 | :type routes_dict: ```dict``` 22 | 23 | :param summary: summary string (used as fallback) 24 | :type summary: ```str``` 25 | 26 | :return: OpenAPI dictionary 27 | """ 28 | entities: List[str] = extract_entities(openapi_str) 29 | 30 | non_error_entity: Optional[str] = None 31 | 32 | for entity in entities: 33 | openapi_str: str = openapi_str.replace( 34 | "$ref: ```{entity}```".format(entity=entity), 35 | "{{'$ref': '#/components/schemas/{entity}'}}".format(entity=entity), 36 | ) 37 | if entity != "ServerError": 38 | non_error_entity: str = entity 39 | openapi_d: dict = (loads if openapi_str.startswith("{") else safe_load)(openapi_str) 40 | if non_error_entity is not None: 41 | openapi_d["summary"] = "{located} `{entity}` object.".format( 42 | located="A", entity=non_error_entity 43 | ) 44 | if routes_dict["method"] in frozenset(("post", "patch")): 45 | openapi_d["requestBody"] = { 46 | "$ref": "#/components/requestBodies/{entity}Body".format( 47 | entity=non_error_entity 48 | ), 49 | "required": True, 50 | } 51 | else: 52 | openapi_d["summary"] = summary 53 | if "responses" in openapi_d: 54 | openapi_d["responses"] = {k: v or {} for k, v in openapi_d["responses"].items()} 55 | return openapi_d 56 | 57 | 58 | __all__ = ["openapi"] # type: list[str] 59 | -------------------------------------------------------------------------------- /cdd/compound/openapi/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAPI parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/compound/openapi/utils/emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit.sqlalchemy` 3 | """ 4 | 5 | import cdd.sqlalchemy.utils.emit_utils 6 | 7 | cdd.sqlalchemy.utils.emit_utils.typ2column_type.update( 8 | { 9 | "bool": "Boolean", 10 | "dict": "JSON", 11 | "float": "Float", 12 | "int": "Integer", 13 | "str": "String", 14 | "string": "String", 15 | "int64": "BigInteger", 16 | "Optional[dict]": "JSON", 17 | } 18 | ) 19 | 20 | __all__ = [] # type: list[str] 21 | -------------------------------------------------------------------------------- /cdd/compound/openapi/utils/parse_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.parse.openapi` 3 | """ 4 | 5 | 6 | def extract_entities(openapi_str): 7 | """ 8 | Extract entities from an OpenAPI string, where entities are defines as anything within "```" 9 | 10 | :param openapi_str: The OpenAPI str 11 | :type openapi_str: ```str``` 12 | 13 | :return: Entities 14 | :rtype: ```list[str]``` 15 | """ 16 | entities, ticks, space, stack = [], 0, 0, [] 17 | 18 | def add_then_clear_stack(): 19 | """ 20 | Join entity, if non-empty add to entities. Clear stack. 21 | """ 22 | entity: str = "".join(stack) 23 | if entity: 24 | entities.append(entity) 25 | stack.clear() 26 | 27 | for idx, ch in enumerate(openapi_str): 28 | if ch.isspace(): 29 | space += 1 30 | add_then_clear_stack() 31 | ticks: int = 0 32 | elif ticks > 2: 33 | ticks, space = 0, 0 34 | stack and add_then_clear_stack() 35 | stack.append(ch) 36 | elif ch == "`": 37 | ticks += 1 38 | elif stack: 39 | stack.append(ch) 40 | add_then_clear_stack() 41 | return entities 42 | 43 | 44 | __all__ = ["extract_entities"] # type: list[str] 45 | -------------------------------------------------------------------------------- /cdd/docstring/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | docstring parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/docstring/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Docstring parser 3 | """ 4 | 5 | import cdd.shared.docstring_parsers 6 | from cdd.shared.types import IntermediateRepr 7 | 8 | 9 | def docstring( 10 | doc_string, 11 | infer_type=False, 12 | return_tuple=False, 13 | parse_original_whitespace=False, 14 | emit_default_prop=True, 15 | emit_default_doc=True, 16 | ): 17 | """ 18 | Converts a docstring to an AST 19 | 20 | :param doc_string: docstring portion 21 | :type doc_string: ```Union[str, Dict]``` 22 | 23 | :param infer_type: Whether to try inferring the typ (from the default) 24 | :type infer_type: ```bool``` 25 | 26 | :param return_tuple: Whether to return a tuple, or just the intermediate_repr 27 | :type return_tuple: ```bool``` 28 | 29 | :param parse_original_whitespace: Whether to parse original whitespace or strip it out 30 | :type parse_original_whitespace: ```bool``` 31 | 32 | :param emit_default_prop: Whether to include the default dictionary property. 33 | :type emit_default_prop: ```bool``` 34 | 35 | :param emit_default_doc: Whether help/docstring should include 'With default' text 36 | :type emit_default_doc: ```bool``` 37 | 38 | :return: intermediate_repr, whether it returns or not 39 | :rtype: ```Optional[Union[dict, Tuple[dict, bool]]]``` 40 | """ 41 | assert isinstance( 42 | doc_string, str 43 | ), "Expected `str` got `{doc_string_type!r}`".format( 44 | doc_string_type=type(doc_string).__name__ 45 | ) 46 | parsed: IntermediateRepr = ( 47 | doc_string 48 | if isinstance(doc_string, dict) 49 | else cdd.shared.docstring_parsers.parse_docstring( 50 | doc_string, 51 | infer_type=infer_type, 52 | emit_default_prop=emit_default_prop, 53 | emit_default_doc=emit_default_doc, 54 | parse_original_whitespace=parse_original_whitespace, 55 | ) 56 | ) 57 | 58 | if return_tuple: 59 | return parsed, ( 60 | "returns" in parsed 61 | and parsed["returns"] is not None 62 | and "return_type" in (parsed.get("returns") or iter(())) 63 | ) 64 | 65 | return parsed 66 | 67 | 68 | __all__ = ["docstring"] # type: list[str] 69 | -------------------------------------------------------------------------------- /cdd/docstring/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | docstring parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/docstring/utils/emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit.docstring` 3 | """ 4 | 5 | import cdd.shared.ast_utils 6 | from cdd.shared.defaults_utils import extract_default 7 | from cdd.shared.pure_utils import simple_types, unquote 8 | 9 | 10 | def interpolate_defaults( 11 | param, default_search_announce=None, require_default=False, emit_default_doc=True 12 | ): 13 | """ 14 | Correctly set the 'default' and 'doc' parameters 15 | 16 | :param param: Name, dict with keys: 'typ', 'doc', 'default' 17 | :type param: ```tuple[str, dict]``` 18 | 19 | :param default_search_announce: Default text(s) to look for. If None, uses default specified in default_utils. 20 | :type default_search_announce: ```Optional[Union[str, Iterable[str]]]``` 21 | 22 | :param require_default: Whether a default is required, if not found in doc, infer the proper default from type 23 | :type require_default: ```bool``` 24 | 25 | :param emit_default_doc: Whether help/docstring should include 'With default' text 26 | :type emit_default_doc: ```bool``` 27 | 28 | :return: Name, dict with keys: 'typ', 'doc', 'default' 29 | :rtype: ```tuple[str, dict]``` 30 | """ 31 | name, _param = param 32 | del param 33 | if "doc" in _param: 34 | doc, default = extract_default( 35 | _param["doc"], 36 | typ=_param.get("typ"), 37 | default_search_announce=default_search_announce, 38 | emit_default_doc=emit_default_doc, 39 | ) 40 | _param["doc"] = doc 41 | if default is not None: 42 | _param["default"] = unquote(default) 43 | if require_default and _param.get("default") is None: 44 | # if ( 45 | # "typ" in _param 46 | # and _param["typ"] not in frozenset(("Any", "object")) 47 | # and not _param["typ"].startswith("Optional") 48 | # ): 49 | # _param["typ"] = "Optional[{}]".format(_param["typ"]) 50 | _param["default"] = ( 51 | simple_types[_param["typ"]] 52 | if _param.get("typ", memoryview) in simple_types 53 | else cdd.shared.ast_utils.NoneStr 54 | ) 55 | 56 | return name, _param 57 | 58 | 59 | __all__ = ["interpolate_defaults"] # type: list[str] 60 | -------------------------------------------------------------------------------- /cdd/function/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | function parser and emitter module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/function/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | function parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/function/utils/emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit.function_utils` 3 | """ 4 | 5 | import ast 6 | from ast import Expr, FunctionDef, Return, arguments 7 | 8 | import cdd.shared.ast_utils 9 | from cdd.class_.utils.emit_utils import RewriteName 10 | from cdd.shared.docstring_utils import emit_param_str 11 | from cdd.shared.pure_utils import ( 12 | code_quoted, 13 | indent_all_but_first, 14 | multiline, 15 | none_types, 16 | ) 17 | 18 | 19 | def make_call_meth(body, return_type, param_names, docstring_format, word_wrap): 20 | """ 21 | Construct a `__call__` method from the provided `body` 22 | 23 | :param body: The body, probably from a `FunctionDef.body` 24 | :type body: ```list[AST]``` 25 | 26 | :param return_type: The return type of the parent symbol (probably class). Used to fill in `__call__` return. 27 | :type return_type: ```Optional[str]``` 28 | 29 | :param param_names: Container of AST `id`s to match for rename 30 | :type param_names: ```Optional[Iterator[str]]``` 31 | 32 | :param docstring_format: Format of docstring 33 | :type docstring_format: ```Literal['rest', 'numpydoc', 'google']``` 34 | 35 | :param word_wrap: Whether to word-wrap. Set `DOCTRANS_LINE_LENGTH` to configure length. 36 | :type word_wrap: ```bool``` 37 | 38 | :return: Internal function for `__call__` 39 | :rtype: ```FunctionDef``` 40 | """ 41 | body_len: int = len(body) 42 | if body_len and isinstance(body, dict): 43 | body = list( 44 | filter( 45 | None, 46 | ( 47 | ( 48 | None 49 | if body.get("doc") in none_types 50 | else Expr( 51 | cdd.shared.ast_utils.set_value( 52 | emit_param_str( 53 | ( 54 | "return_type", 55 | { 56 | "doc": multiline( 57 | indent_all_but_first(body["doc"]) 58 | ) 59 | }, 60 | ), 61 | style=docstring_format, 62 | word_wrap=word_wrap, 63 | purpose="function", 64 | ) 65 | ), 66 | lineno=None, 67 | col_offset=None, 68 | ) 69 | ), 70 | ( 71 | RewriteName(param_names).visit( 72 | Return( 73 | cdd.shared.ast_utils.get_value( 74 | ast.parse(return_type.strip("`")).body[0] 75 | ), 76 | expr=None, 77 | ) 78 | ) 79 | if code_quoted(body["default"]) 80 | else Return( 81 | cdd.shared.ast_utils.set_value(body["default"]), expr=None 82 | ) 83 | ), 84 | ), 85 | ) 86 | ) 87 | 88 | return ( 89 | ast.fix_missing_locations( 90 | FunctionDef( 91 | args=arguments( 92 | args=[cdd.shared.ast_utils.set_arg("self")], 93 | defaults=[], 94 | kw_defaults=[], 95 | kwarg=None, 96 | kwonlyargs=[], 97 | posonlyargs=[], 98 | vararg=None, 99 | arg=None, 100 | ), 101 | body=body, 102 | decorator_list=[], 103 | type_params=[], 104 | name="__call__", 105 | returns=None, 106 | arguments_args=None, 107 | identifier_name=None, 108 | stmt=None, 109 | lineno=None, 110 | **cdd.shared.ast_utils.maybe_type_comment 111 | ) 112 | ) 113 | if body 114 | else None 115 | ) 116 | 117 | 118 | __all__ = ["make_call_meth"] # type: list[str] 119 | -------------------------------------------------------------------------------- /cdd/function/utils/parse_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.parse.function` 3 | """ 4 | 5 | from ast import Constant, Return, Tuple 6 | from collections import OrderedDict 7 | from typing import Optional 8 | 9 | import cdd.shared.ast_utils 10 | import cdd.shared.source_transformer 11 | from cdd.shared.pure_utils import PY_GTE_3_8, rpartial 12 | 13 | if PY_GTE_3_8: 14 | from cdd.shared.pure_utils import FakeConstant as Str 15 | 16 | Num = Str 17 | else: 18 | from ast import Num, Str 19 | 20 | 21 | def _interpolate_return(function_def, intermediate_repr): 22 | """ 23 | Interpolate the return value into the IR. 24 | 25 | :param function_def: function definition 26 | :type function_def: ```FunctionDef``` 27 | 28 | :param intermediate_repr: a dictionary consistent with `IntermediateRepr`, defined as: 29 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 30 | IntermediateRepr = TypedDict("IntermediateRepr", { 31 | "name": Optional[str], 32 | "type": Optional[str], 33 | "doc": Optional[str], 34 | "params": OrderedDict[str, ParamVal], 35 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 36 | }) 37 | :type intermediate_repr: ```dict``` 38 | 39 | :return: a dictionary consistent with `IntermediateRepr`, defined as: 40 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 41 | IntermediateRepr = TypedDict("IntermediateRepr", { 42 | "name": Optional[str], 43 | "type": Optional[str], 44 | "doc": Optional[str], 45 | "params": OrderedDict[str, ParamVal], 46 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 47 | }) 48 | :rtype: ```dict``` 49 | """ 50 | return_ast: Optional[Return] = next( 51 | filter(rpartial(isinstance, Return), function_def.body[::-1]), None 52 | ) 53 | if return_ast is not None and return_ast.value is not None: 54 | if intermediate_repr.get("returns") is None: 55 | intermediate_repr["returns"] = OrderedDict((("return_type", {}),)) 56 | 57 | if ( 58 | "typ" in intermediate_repr["returns"]["return_type"] 59 | and "[" not in intermediate_repr["returns"]["return_type"]["typ"] 60 | ): 61 | del intermediate_repr["returns"]["return_type"]["typ"] 62 | intermediate_repr["returns"]["return_type"]["default"] = ( 63 | lambda default: ( 64 | "({})".format(default) 65 | if isinstance(return_ast.value, Tuple) 66 | and (not default.startswith("(") or not default.endswith(")")) 67 | else ( 68 | lambda default_: ( 69 | default_ 70 | if isinstance( 71 | default_, 72 | (str, int, float, complex, Num, Str, Constant), 73 | ) 74 | else "```{}```".format(default) 75 | ) 76 | )( 77 | cdd.shared.ast_utils.get_value( 78 | cdd.shared.ast_utils.get_value(return_ast) 79 | ) 80 | ) 81 | ) 82 | )(cdd.shared.source_transformer.to_code(return_ast.value).rstrip("\n")) 83 | if hasattr(function_def, "returns") and function_def.returns is not None: 84 | intermediate_repr["returns"] = intermediate_repr.get("returns") or OrderedDict( 85 | (("return_type", {}),) 86 | ) 87 | intermediate_repr["returns"]["return_type"]["typ"] = ( 88 | cdd.shared.source_transformer.to_code(function_def.returns).rstrip("\n") 89 | ) 90 | 91 | return intermediate_repr 92 | 93 | 94 | __all__ = ["_interpolate_return"] # type: list[str] 95 | -------------------------------------------------------------------------------- /cdd/json_schema/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSON-schema parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/json_schema/emit.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSON schema emitter 3 | """ 4 | 5 | from collections import OrderedDict 6 | from functools import partial 7 | from json import dump 8 | from operator import add 9 | 10 | from cdd.docstring.emit import docstring 11 | from cdd.json_schema.utils.emit_utils import param2json_schema_property 12 | from cdd.shared.pure_utils import SetEncoder, deindent 13 | 14 | 15 | def json_schema( 16 | intermediate_repr, 17 | identifier=None, 18 | emit_original_whitespace=False, 19 | emit_default_doc=False, 20 | word_wrap=False, 21 | ): 22 | """ 23 | Construct a JSON schema dict 24 | 25 | :param intermediate_repr: a dictionary consistent with `IntermediateRepr`, defined as: 26 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 27 | IntermediateRepr = TypedDict("IntermediateRepr", { 28 | "name": Optional[str], 29 | "type": Optional[str], 30 | "doc": Optional[str], 31 | "params": OrderedDict[str, ParamVal], 32 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 33 | }) 34 | :type intermediate_repr: ```dict``` 35 | 36 | :param identifier: The `$id` of the schema 37 | :type identifier: ```str``` 38 | 39 | :param emit_original_whitespace: Whether to emit original whitespace (in top-level `description`) or strip it out 40 | :type emit_original_whitespace: ```bool``` 41 | 42 | :param emit_default_doc: Whether help/docstring should include 'With default' text 43 | :type emit_default_doc: ```bool``` 44 | 45 | :param word_wrap: Whether to word-wrap. Set `DOCTRANS_LINE_LENGTH` to configure length. 46 | :type word_wrap: ```bool``` 47 | 48 | :return: JSON Schema dict 49 | :rtype: ```dict``` 50 | """ 51 | del emit_default_doc, word_wrap 52 | assert isinstance( 53 | intermediate_repr, dict 54 | ), "Expected `dict` got `{type_name}`".format( 55 | type_name=type(intermediate_repr).__name__ 56 | ) 57 | if "$id" in intermediate_repr and "params" not in intermediate_repr: 58 | return intermediate_repr # Somehow this function got JSON schema as input 59 | if identifier is None: 60 | identifier: str = intermediate_repr.get( 61 | "$id", 62 | "https://offscale.io/{}.schema.json".format( 63 | intermediate_repr.get("name", "INFERRED") 64 | ), 65 | ) 66 | required = [] 67 | _param2json_schema_property = partial(param2json_schema_property, required=required) 68 | properties = dict( 69 | map(_param2json_schema_property, intermediate_repr["params"].items()) 70 | ) 71 | 72 | return { 73 | "$id": identifier, 74 | "$schema": "https://json-schema.org/draft/2020-12/schema", 75 | "description": ( 76 | deindent( 77 | add( 78 | *map( 79 | partial( 80 | docstring, 81 | emit_default_doc=True, 82 | emit_original_whitespace=emit_original_whitespace, 83 | emit_types=True, 84 | ), 85 | ( 86 | { 87 | "doc": intermediate_repr["doc"], 88 | "params": OrderedDict(), 89 | "returns": None, 90 | }, 91 | { 92 | "doc": "", 93 | "params": OrderedDict(), 94 | "returns": intermediate_repr["returns"], 95 | }, 96 | ), 97 | ) 98 | ) 99 | ).lstrip("\n") 100 | or None 101 | ), 102 | "type": "object", 103 | "properties": properties, 104 | "required": required, 105 | } 106 | 107 | 108 | def json_schema_file(input_mapping, output_filename): 109 | """ 110 | Emit `input_mapping`—as JSON schema—into `output_filename` 111 | 112 | :param input_mapping: Import location of mapping/2-tuple collection. 113 | :type input_mapping: ```Dict[str, AST]``` 114 | 115 | :param output_filename: Output file to write to 116 | :type output_filename: ```str``` 117 | """ 118 | schemas_it = (json_schema(v) for k, v in input_mapping.items()) 119 | schemas = ( 120 | {"schemas": list(schemas_it)} if len(input_mapping) > 1 else next(schemas_it) 121 | ) 122 | with open(output_filename, "a") as f: 123 | dump(schemas, f, cls=SetEncoder) 124 | 125 | 126 | __all__ = ["json_schema", "json_schema_file"] 127 | -------------------------------------------------------------------------------- /cdd/json_schema/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSON schema parser 3 | """ 4 | 5 | from collections import OrderedDict 6 | from copy import deepcopy 7 | from functools import partial 8 | from typing import FrozenSet 9 | 10 | from cdd.docstring.parse import docstring 11 | from cdd.json_schema.utils.parse_utils import json_schema_property_to_param 12 | from cdd.shared.types import IntermediateRepr 13 | 14 | 15 | def json_schema(json_schema_dict, parse_original_whitespace=False): 16 | """ 17 | Parse a JSON schema into the IR 18 | 19 | :param json_schema_dict: A valid JSON schema as a Python dict 20 | :type json_schema_dict: ```dict``` 21 | 22 | :param parse_original_whitespace: Whether to parse original whitespace or strip it out 23 | :type parse_original_whitespace: ```bool``` 24 | 25 | :return: IR representation of the given JSON schema 26 | :rtype: ```dict``` 27 | """ 28 | # I suppose a JSON-schema validation routine could be executed here 29 | schema: dict = deepcopy(json_schema_dict) 30 | 31 | required: FrozenSet[str] = ( 32 | frozenset(schema["required"]) if schema.get("required") else frozenset() 33 | ) 34 | _json_schema_property_to_param = partial( 35 | json_schema_property_to_param, required=required 36 | ) 37 | 38 | ir: IntermediateRepr = docstring( 39 | json_schema_dict.get("description", ""), 40 | emit_default_doc=False, 41 | parse_original_whitespace=parse_original_whitespace, 42 | ) 43 | ir.update( 44 | { 45 | "params": ( 46 | OrderedDict( 47 | map(_json_schema_property_to_param, schema["properties"].items()) 48 | ) 49 | if "properties" in schema 50 | else OrderedDict() 51 | ), 52 | "name": json_schema_dict.get( 53 | "name", 54 | json_schema_dict.get("id", json_schema_dict.get("title", ir["name"])), 55 | ), 56 | } 57 | ) 58 | return ir 59 | 60 | 61 | __all__ = ["json_schema"] # type: list[str] 62 | -------------------------------------------------------------------------------- /cdd/json_schema/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | JSON-schema parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/json_schema/utils/emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit.json_schema` 3 | """ 4 | 5 | import ast 6 | from ast import AST, Set 7 | from typing import Dict 8 | 9 | import cdd.shared.ast_utils 10 | from cdd.json_schema.utils.parse_utils import json_type2typ 11 | from cdd.shared.pure_utils import none_types 12 | 13 | 14 | def param2json_schema_property(param, required): 15 | """ 16 | Turn a param into a JSON schema property 17 | 18 | :param param: Name, dict with keys: 'typ', 'doc', 'default' 19 | :type param: ```tuple[str, dict]``` 20 | 21 | :param required: Required parameters. This function may push to the list. 22 | :type required: ```list[str]``` 23 | 24 | :return: JSON schema property. Also, may push to `required`. 25 | :rtype: ```dict``` 26 | """ 27 | name, _param = param 28 | del param 29 | if _param.get("doc"): 30 | _param["description"] = _param.pop("doc") 31 | if _param.get("typ") == "datetime": 32 | del _param["typ"] 33 | _param.update({"type": "string", "format": "date-time"}) 34 | required.append(name) 35 | elif _param.get("typ") in typ2json_type: 36 | _param["type"] = typ2json_type[_param.pop("typ")] 37 | required.append(name) 38 | elif _param.get("typ", ast) is not ast: 39 | _param["type"] = _param.pop("typ") 40 | if _param["type"].startswith("Optional["): 41 | _param["type"] = _param["type"][len("Optional[") : -1] 42 | if _param["type"] in typ2json_type: 43 | _param["type"] = typ2json_type[_param["type"]] 44 | # elif _param.get("typ") in typ2json_type: 45 | # _param["type"] = typ2json_type[_param.pop("typ")] 46 | else: 47 | required.append(name) 48 | 49 | if _param["type"].startswith("Literal["): 50 | parsed_typ = cdd.shared.ast_utils.get_value( 51 | ast.parse(_param["type"]).body[0] 52 | ) 53 | assert ( 54 | parsed_typ.value.id == "Literal" 55 | ), "Only basic Literal support is implemented, not {}".format( 56 | parsed_typ.value.id 57 | ) 58 | enum = sorted( 59 | map( 60 | cdd.shared.ast_utils.get_value, 61 | cdd.shared.ast_utils.get_value(parsed_typ.slice).elts, 62 | ) 63 | ) 64 | _param.update( 65 | { 66 | "pattern": "|".join(enum), 67 | "type": typ2json_type[type(enum[0]).__name__], 68 | } 69 | ) 70 | if _param.get("default", False) in none_types: 71 | del _param["default"] # Will be inferred as `null` from the type 72 | elif isinstance(_param.get("default"), AST): 73 | _param["default"] = cdd.shared.ast_utils.ast_type_to_python_type( 74 | _param["default"] 75 | ) 76 | if isinstance(_param.get("choices"), Set): 77 | _param["pattern"] = "|".join( 78 | sorted(map(str, cdd.shared.ast_utils.Set_to_set(_param.pop("choices")))) 79 | ) 80 | return name, _param 81 | 82 | 83 | typ2json_type: Dict[str, str] = {v: k for k, v in json_type2typ.items()} 84 | 85 | __all__ = ["param2json_schema_property"] 86 | -------------------------------------------------------------------------------- /cdd/json_schema/utils/parse_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.parse.json_schema` 3 | """ 4 | 5 | from typing import Dict 6 | 7 | import cdd.shared.ast_utils 8 | from cdd.shared.pure_utils import namespaced_pascal_to_upper_camelcase, none_types 9 | 10 | 11 | def json_schema_property_to_param(param, required): 12 | """ 13 | Convert a JSON schema property to a param 14 | 15 | :param param: Name, dict with keys: 'typ', 'doc', 'default' 16 | :type param: ```tuple[str, dict]``` 17 | 18 | :param required: Names of all required parameters 19 | :type required: ```FrozenSet[str]``` 20 | 21 | :return: Name, dict with keys: 'typ', 'doc', 'default' 22 | :rtype: ```tuple[str, dict]``` 23 | """ 24 | name, _param = param 25 | del param 26 | if name.endswith("kwargs"): 27 | _param["typ"] = "Optional[dict]" 28 | # elif "enum" in _param: 29 | # _param["typ"] = "Literal{}".format(_param.pop("enum")) 30 | # del _param["type"] 31 | if "description" in _param: 32 | _param["doc"] = _param.pop("description") 33 | 34 | if _param.get("type"): 35 | _param["typ"] = json_type2typ[_param.pop("type")] 36 | 37 | if _param.get("pattern"): 38 | maybe_enum = _param["pattern"].split("|") 39 | if all(filter(str.isalpha, maybe_enum)): 40 | _param["typ"] = "Literal[{}]".format( 41 | ", ".join(map("'{}'".format, maybe_enum)) 42 | ) 43 | del _param["pattern"] 44 | 45 | def transform_ref_fk_set(ref, foreign_key): 46 | """ 47 | Transform $ref to upper camel case and add to the foreign key 48 | 49 | :param ref: JSON ref 50 | :type ref: ```str``` 51 | 52 | :param foreign_key: Foreign key structure (pass by reference) 53 | :type foreign_key: ```dict``` 54 | 55 | :return: $ref without the namespace and in upper camel case 56 | :rtype: ```str``` 57 | """ 58 | entity = namespaced_pascal_to_upper_camelcase( 59 | ref.rpartition("/")[2].replace(".", "__") 60 | ) 61 | foreign_key["fk"] = entity 62 | return entity 63 | 64 | fk = {"fk": None} 65 | if "anyOf" in _param: 66 | _param["typ"] = list( 67 | map( 68 | lambda typ: ( 69 | ( 70 | transform_ref_fk_set(typ["$ref"], fk) 71 | if "$ref" in typ 72 | else typ["type"] 73 | ) 74 | if isinstance(typ, dict) 75 | else typ 76 | ), 77 | _param.pop("anyOf"), 78 | ) 79 | ) 80 | 81 | if len(_param["typ"]) > 1 and "string" in _param["typ"]: 82 | del _param["typ"][_param["typ"].index("string")] 83 | _param["typ"] = ( 84 | _param["typ"][0] 85 | if len(_param["typ"]) == 1 86 | else "Union[{}]".format(",".join(_param["typ"])) 87 | ) 88 | elif "$ref" in _param: 89 | _param["typ"] = transform_ref_fk_set(_param.pop("$ref"), fk) 90 | 91 | if fk["fk"] is not None: 92 | fk_val = fk.pop("fk") 93 | fk_prefix = fk_val if fk_val.startswith("[FK(") else "[FK({})]".format(fk_val) 94 | _param["doc"] = ( 95 | "{} {}".format(fk_prefix, _param["doc"]) if _param.get("doc") else fk_prefix 96 | ) 97 | 98 | if ( 99 | name not in required 100 | and _param.get("typ") 101 | and "Optional[" not in _param["typ"] 102 | # Could also parse out a `Union` for `None` 103 | or _param.pop("nullable", False) 104 | ): 105 | _param["typ"] = "Optional[{}]".format(_param["typ"]) 106 | if _param.get("default", False) in none_types: 107 | _param["default"] = cdd.shared.ast_utils.NoneStr 108 | 109 | return name, _param 110 | 111 | 112 | # https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.4.2.1 113 | json_type2typ: Dict[str, str] = { 114 | "boolean": "bool", 115 | "string": "str", 116 | "object": "dict", 117 | "array": "list", 118 | "int": "integer", 119 | "integer": "int", 120 | "float": ( 121 | "number" 122 | ), # <- Actually a problem, maybe `literal_eval` on default then `type()` or just `type(default)`? 123 | "number": "float", 124 | "null": "NoneType", 125 | } 126 | 127 | 128 | __all__ = ["json_schema_property_to_param", "json_type2typ"] 129 | -------------------------------------------------------------------------------- /cdd/json_schema/utils/shared_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared utility functions for JSON schema 3 | """ 4 | 5 | from typing import Any, Dict, List 6 | 7 | from cdd.shared.pure_utils import PY_GTE_3_8 8 | 9 | if PY_GTE_3_8: 10 | from typing import Literal, TypedDict 11 | else: 12 | from typing_extensions import Literal, TypedDict 13 | 14 | JSON_property = TypedDict( 15 | "JSON_property", 16 | {"description": str, "type": str, "default": Any, "x_typ": Any}, 17 | total=False, 18 | ) 19 | JSON_schema = TypedDict( 20 | "JSON_schema", 21 | { 22 | "$id": str, 23 | "$schema": str, 24 | "description": str, 25 | "type": Literal["object"], 26 | "properties": Dict[str, JSON_property], 27 | "required": List[str], 28 | }, 29 | ) 30 | 31 | __all__ = ["JSON_schema"] # type: list[str] 32 | -------------------------------------------------------------------------------- /cdd/pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pydantic parser and emitter utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/pydantic/emit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic `class` emitter 3 | 4 | https://pydantic-docs.helpmanual.io/usage/schema/ 5 | """ 6 | 7 | from functools import partial 8 | 9 | import cdd.class_.emit 10 | from cdd.class_.utils.shared_utils import ClassEmitProtocol 11 | 12 | pydantic: ClassEmitProtocol = partial( 13 | cdd.class_.emit.class_, class_bases=("BaseModel",) 14 | ) 15 | 16 | __all__ = ["pydantic"] # type: list[str] 17 | -------------------------------------------------------------------------------- /cdd/pydantic/parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic `class` parser 3 | 4 | https://pydantic-docs.helpmanual.io/usage/schema/ 5 | """ 6 | 7 | from functools import partial 8 | 9 | import cdd.class_.parse 10 | from cdd.class_.utils.shared_utils import ClassParserProtocol 11 | 12 | pydantic: ClassParserProtocol = partial(cdd.class_.parse.class_, infer_type=True) 13 | 14 | __all__ = ["pydantic"] # type: list[str] 15 | -------------------------------------------------------------------------------- /cdd/routes/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Routes for parsing/emitting. Currently, Bottle, and aimed for OpenAPI. 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/routes/emit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module of route emitters 3 | """ 4 | 5 | EMITTERS = ["bottle"] # type: list[str] 6 | 7 | __all__ = ["EMITTERS"] # type: list[str] 8 | -------------------------------------------------------------------------------- /cdd/routes/emit/bottle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Emit constant strings with interpolated values for route generation 3 | """ 4 | 5 | from cdd.routes.emit.bottle_constants_utils import ( 6 | create_helper_variants, 7 | create_route_variants, 8 | delete_route_variants, 9 | read_route_variants, 10 | ) 11 | 12 | 13 | def create(app, name, route, variant=2): 14 | """ 15 | Create the `create` route 16 | 17 | :param app: Variable name (Bottle App) 18 | :type app: ```str``` 19 | 20 | :param name: Name of entity 21 | :type name: ```str``` 22 | 23 | :param route: The path of the resource 24 | :type route: ```str``` 25 | 26 | :param variant: Number of variant 27 | :type variant: ```int``` 28 | 29 | :return: Create route variant with interpolated values 30 | :rtype: ```str``` 31 | """ 32 | return create_route_variants[variant].format(app=app, name=name, route=route) 33 | 34 | 35 | def create_util(name, route, variant=1): 36 | """ 37 | Create utility function that the `create` emitter above uses 38 | 39 | :param name: Name of entity 40 | :type name: ```str``` 41 | 42 | :param route: The path of the resource 43 | :type route: ```str``` 44 | 45 | :param variant: Number of variant 46 | :type variant: ```int``` 47 | 48 | :return: Create route variant with interpolated values 49 | :rtype: ```str``` 50 | """ 51 | return create_helper_variants[variant].format(name=name, route=route) 52 | 53 | 54 | def read(app, name, route, primary_key, variant=0): 55 | """ 56 | Create the `read` route 57 | 58 | :param app: Variable name (Bottle App) 59 | :type app: ```str``` 60 | 61 | :param name: Name of entity 62 | :type name: ```str``` 63 | 64 | :param route: The path of the resource 65 | :type route: ```str``` 66 | 67 | :param primary_key: The id 68 | :type primary_key: ```Any``` 69 | 70 | :param variant: Number of variant 71 | :type variant: ```int``` 72 | 73 | :return: Create route variant with interpolated values 74 | :rtype: ```str``` 75 | """ 76 | return read_route_variants[variant].format( 77 | app=app, name=name, route=route, id=primary_key 78 | ) 79 | 80 | 81 | def destroy(app, name, route, primary_key, variant=0): 82 | """ 83 | Create the `destroy` route 84 | 85 | :param app: Variable name (Bottle App) 86 | :type app: ```str``` 87 | 88 | :param name: Name of entity 89 | :type name: ```str``` 90 | 91 | :param route: The path of the resource 92 | :type route: ```str``` 93 | 94 | :param primary_key: The id 95 | :type primary_key: ```Any``` 96 | 97 | :param variant: Number of variant 98 | :type variant: ```int``` 99 | 100 | :return: Create route variant with interpolated values 101 | :rtype: ```str``` 102 | """ 103 | return delete_route_variants[variant].format( 104 | app=app, name=name, route=route, id=primary_key 105 | ) 106 | 107 | 108 | __all__ = ["create", "create_util", "read", "destroy"] # type: list[str] 109 | -------------------------------------------------------------------------------- /cdd/routes/parse/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module of route parsers 3 | """ 4 | 5 | PARSERS = ["bottle", "fastapi"] # type: list[str] 6 | 7 | __all__ = ["PARSERS"] # type: list[str] 8 | -------------------------------------------------------------------------------- /cdd/routes/parse/bottle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parsers for routes 3 | """ 4 | 5 | import ast 6 | from ast import FunctionDef 7 | from importlib import import_module 8 | from inspect import getsource 9 | from types import FunctionType 10 | from typing import FrozenSet, Optional, cast 11 | 12 | import cdd.compound.openapi.parse 13 | from cdd.shared.ast_utils import get_value 14 | from cdd.shared.docstring_parsers import parse_docstring 15 | from cdd.shared.pure_utils import PY_GTE_3_8 16 | from cdd.shared.types import IntermediateRepr 17 | 18 | Literal = getattr( 19 | import_module("typing" if PY_GTE_3_8 else "typing_extensions"), "Literal" 20 | ) 21 | 22 | methods_literal_type = Literal["patch", "post", "put", "get", "delete", "trace"] 23 | methods: FrozenSet[methods_literal_type] = frozenset( 24 | ("patch", "post", "put", "get", "delete", "trace") 25 | ) 26 | 27 | 28 | def bottle(function_def): 29 | """ 30 | Parse bottle API 31 | 32 | :param function_def: Function definition of a bottle route, like `@api.get("/api") def root(): return "/"` 33 | :type function_def: ```Union[FunctionDef, FunctionType]``` 34 | 35 | :return: OpenAPI representation of the given route 36 | :rtype: ```dict``` 37 | """ 38 | if isinstance(function_def, FunctionType): 39 | # Dynamic function, i.e., this isn't source code; and is in your memory 40 | function_def: FunctionDef = cast( 41 | FunctionDef, ast.parse(getsource(function_def)).body[0] 42 | ) 43 | 44 | assert isinstance( 45 | function_def, FunctionDef 46 | ), "Expected `FunctionDef` got `{type_name}`".format( 47 | type_name=type(function_def).__name__ 48 | ) 49 | app_decorator = next( 50 | filter( 51 | lambda call: call.func.attr in methods, 52 | function_def.decorator_list, 53 | ) 54 | ) 55 | route: str = get_value(app_decorator.args[0]) 56 | name: str = app_decorator.func.value.id 57 | method: methods_literal_type = app_decorator.func.attr 58 | 59 | route_dict = {"route": route, "name": name, "method": method} 60 | doc_str: Optional[str] = ast.get_docstring(function_def, clean=True) 61 | if doc_str is not None: 62 | ir: IntermediateRepr = parse_docstring(doc_str) 63 | yml_start_str, yml_end_str = "```yml", "```" 64 | yml_start: int = ir["doc"].find(yml_start_str) 65 | # if yml_start < 0: 66 | # return route_dict 67 | openapi_str: str = ir["doc"][ 68 | yml_start 69 | + len(yml_start_str) : ir["doc"].rfind(yml_end_str) 70 | - len(yml_end_str) 71 | + 2 72 | ] 73 | return cdd.compound.openapi.parse.openapi( 74 | openapi_str, route_dict, ir["doc"][:yml_start].rstrip() 75 | ) 76 | # return route_dict 77 | 78 | 79 | __all__ = ["bottle", "methods"] # type: list[str] 80 | -------------------------------------------------------------------------------- /cdd/routes/parse/bottle_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parser utils for routes 3 | """ 4 | 5 | from ast import Call, FunctionDef 6 | 7 | from cdd.routes.parse.bottle import methods 8 | from cdd.shared.ast_utils import get_value 9 | from cdd.shared.pure_utils import rpartial 10 | 11 | 12 | def get_route_meta(mod): 13 | """ 14 | Get the (func_name, app_name, route_path, http_method)s 15 | 16 | :param mod: Parsed AST containing routes 17 | :type mod: ```Module``` 18 | 19 | :return: Iterator of tuples of (func_name, app_name, route_path, http_method) 20 | :rtype: ```Iterator[tuple[str, str, str, str]]``` 21 | """ 22 | return map( 23 | lambda func: ( 24 | func.name, 25 | *next( 26 | map( 27 | lambda call: ( 28 | call.func.value.id, 29 | get_value(call.args[0]), 30 | call.func.attr, 31 | ), 32 | filter( 33 | lambda call: call.args and call.func.attr in methods, 34 | filter(rpartial(isinstance, Call), func.decorator_list), 35 | ), 36 | ) 37 | ), 38 | ), 39 | filter(rpartial(isinstance, FunctionDef), mod.body), 40 | ) 41 | 42 | 43 | __all__ = ["get_route_meta"] # type: list[str] 44 | -------------------------------------------------------------------------------- /cdd/routes/parse/fastapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastAPI route parser 3 | """ 4 | 5 | from cdd.routes.parse.fastapi_utils import parse_fastapi_responses 6 | from cdd.shared.ast_utils import get_value 7 | 8 | 9 | def fastapi(fastapi_route): 10 | """ 11 | Parse a single FastAPI route 12 | 13 | :param fastapi_route: A single FastAPI route 14 | :type fastapi_route: ```AsyncFunctionDef``` 15 | 16 | :return: Pair of (str, dict) consisting of API path to a dictionary of form 17 | { Literal["post","get","put","patch"]: { 18 | "requestBody": { "$ref": str, "required": boolean }, 19 | "responses": { number: { "content": {string: {"schema": {"$ref": string}, 20 | "description": string} } } }, 21 | "summary": string 22 | } 23 | } 24 | :rtype: ```tuple[str, dict]``` 25 | """ 26 | method: str = fastapi_route.decorator_list[0].func.attr 27 | route = get_value(fastapi_route.decorator_list[0].args[0]) 28 | return route, { 29 | method: { 30 | "responses": parse_fastapi_responses( 31 | next( 32 | filter( 33 | lambda keyword: keyword.arg == "responses", 34 | fastapi_route.decorator_list[0].keywords, 35 | ) 36 | ) 37 | ) 38 | } 39 | } 40 | 41 | 42 | __all__ = ["fastapi"] # type: list[str] 43 | -------------------------------------------------------------------------------- /cdd/routes/parse/fastapi_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastAPI utils 3 | """ 4 | 5 | from functools import partial 6 | 7 | from cdd.shared.ast_utils import Dict_to_dict, get_value 8 | 9 | 10 | def model_handler(key, model_name, location, mime_type): 11 | """ 12 | Create fully-qualified model name from unqualified name 13 | 14 | :param key: Key name 15 | :type key: ```str``` 16 | 17 | :param model_name: Not fully-qualified model name or a `{"$ref": string}` dict 18 | :type model_name: ```str|dict``` 19 | 20 | :param location: Full-qualified parent path 21 | :type location: ```str``` 22 | 23 | :param mime_type: MIME type 24 | :type mime_type: ```str``` 25 | 26 | :return: Tuple["content", JSON ref to model name, of form `{"$ref": string}`] 27 | :rtype: ```tuple[Union[str,"content"], dict]``` 28 | """ 29 | return ( 30 | (key, model_name) 31 | if isinstance(model_name, dict) 32 | else ( 33 | "content", 34 | {mime_type: {"schema": {"$ref": "{}{}".format(location, model_name)}}}, 35 | ) 36 | ) 37 | 38 | 39 | parse_handlers = { 40 | "model": partial( 41 | model_handler, location="#/components/schemas/", mime_type="application/json" 42 | ) 43 | } 44 | 45 | 46 | def parse_fastapi_responses(responses): 47 | """ 48 | Parse FastAPI "responses" key 49 | 50 | :param responses: `responses` keyword value from FastAPI decorator on route 51 | :type responses: ```Dict``` 52 | 53 | :return: Transformed FastAPI "responses" 54 | :rtype: ```dict``` 55 | """ 56 | 57 | return { 58 | key: dict( 59 | ( 60 | ( 61 | lambda _v: ( 62 | (parse_handlers[k](k, _v)) if k in parse_handlers else (k, _v) 63 | ) 64 | )(get_value(v)) 65 | ) 66 | for k, v in Dict_to_dict(val).items() 67 | ) 68 | for key, val in Dict_to_dict(responses.value).items() 69 | } 70 | 71 | 72 | __all__ = ["parse_fastapi_responses"] # type: list[str] 73 | -------------------------------------------------------------------------------- /cdd/shared/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | cdd-wide shared utilities module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/shared/cst.py: -------------------------------------------------------------------------------- 1 | """ 2 | Concrete Syntax Tree for Python 3.6+ source code 3 | """ 4 | 5 | from cdd.shared.cst_utils import cst_parser, cst_scanner 6 | 7 | 8 | def cst_parse(source): 9 | """ 10 | Parse Python source lines into a Concrete Syntax Tree 11 | 12 | :param source: Python source code 13 | :type source: ```str``` 14 | 15 | :return: List of `namedtuple`s with at least ("line_no_start", "line_no_end", "value") attributes 16 | :rtype: ```list[Any]``` 17 | """ 18 | scanned = cst_scanner(source) 19 | parsed = cst_parser(scanned) 20 | return parsed 21 | 22 | 23 | __all__ = ["cst_parse"] # type: list[str] 24 | -------------------------------------------------------------------------------- /cdd/shared/emit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Transform from string or AST representations of input, to AST, file, or str input_str. 3 | """ 4 | 5 | from typing import List 6 | 7 | # from cdd.emit.argparse_function import argparse_function 8 | # from cdd.emit.class_ import class_ 9 | # from cdd.emit.docstring import docstring 10 | # from cdd.emit.file import file 11 | # from cdd.emit.function import function 12 | # from cdd.emit.json_schema import json_schema 13 | # from cdd.emit.sqlalchemy import sqlalchemy, sqlalchemy_table 14 | 15 | EMITTERS: List[str] = [ 16 | "argparse_function", 17 | "class_", 18 | "docstring", 19 | # "file", 20 | "function", 21 | "json_schema", 22 | # "openapi", 23 | "pydantic", 24 | "routes", 25 | "sqlalchemy", 26 | "sqlalchemy_hybrid", 27 | "sqlalchemy_table", 28 | ] 29 | 30 | __all__ = ["EMITTERS"] # type: list[str] 31 | -------------------------------------------------------------------------------- /cdd/shared/emit/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | File emitter 3 | """ 4 | 5 | from ast import Module 6 | from importlib import import_module 7 | from importlib.util import find_spec 8 | 9 | import cdd.shared.source_transformer 10 | 11 | black = ( 12 | import_module("black") 13 | if find_spec("black") is not None 14 | else type( 15 | "black", 16 | tuple(), 17 | { 18 | "format_str": lambda src_contents, mode: src_contents, 19 | "Mode": ( 20 | lambda target_versions, line_length, is_pyi, string_normalization: None 21 | ), 22 | }, 23 | ) 24 | ) 25 | 26 | 27 | def file(node, filename, mode="a", skip_black=False): 28 | """ 29 | Convert AST to a file 30 | 31 | :param node: AST node 32 | :type node: ```Union[Module, ClassDef, FunctionDef]``` 33 | 34 | :param filename: emit to this file 35 | :type filename: ```str``` 36 | 37 | :param mode: Mode to open the file in, defaults to append 38 | :type mode: ```str``` 39 | 40 | :param skip_black: Whether to skip formatting with black 41 | :type skip_black: ```bool``` 42 | 43 | :return: None 44 | :rtype: ```NoneType``` 45 | """ 46 | if not isinstance(node, Module): 47 | node: Module = Module(body=[node], type_ignores=[], stmt=None) 48 | src: str = cdd.shared.source_transformer.to_code(node) 49 | if not skip_black: 50 | src = black.format_str( 51 | src, 52 | mode=black.Mode( 53 | target_versions=set(), 54 | line_length=119, 55 | is_pyi=False, 56 | string_normalization=False, 57 | ), 58 | ) 59 | with open(filename, mode) as f: 60 | f.write(src) 61 | 62 | 63 | __all__ = ["file"] # type: list[str] 64 | -------------------------------------------------------------------------------- /cdd/shared/emit/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.emit` 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/shared/emit/utils/emitter_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions which produce intermediate_repr from various different inputs 3 | """ 4 | 5 | import ast 6 | from importlib import import_module 7 | 8 | 9 | def get_internal_body(target_name, target_type, intermediate_repr): 10 | """ 11 | Get the internal body from our IR 12 | 13 | :param target_name: name of target. If both `target_name` and `target_type` match internal body extract, then emit 14 | :type target_name: ```str``` 15 | 16 | :param target_type: Type of target, static is static or global method, others just become first arg 17 | :type target_type: ```Literal['self', 'cls', 'static']``` 18 | 19 | :param intermediate_repr: a dictionary consistent with `IntermediateRepr`, defined as: 20 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 21 | IntermediateRepr = TypedDict("IntermediateRepr", { 22 | "name": Optional[str], 23 | "type": Optional[str], 24 | "doc": Optional[str], 25 | "params": OrderedDict[str, ParamVal], 26 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 27 | }) 28 | :type intermediate_repr: ```dict``` 29 | 30 | :return: Internal body or an empty tuple 31 | :rtype: ```Union[list, tuple]``` 32 | """ 33 | return ( 34 | intermediate_repr["_internal"]["body"] 35 | if intermediate_repr.get("_internal", {}).get("body") 36 | and intermediate_repr["_internal"]["from_name"] == target_name 37 | and intermediate_repr["_internal"]["from_type"] == target_type 38 | else tuple() 39 | ) 40 | 41 | 42 | def ast_parse_fix(s): 43 | """ 44 | Hack to resolve unbalanced parentheses SyntaxError acquired from PyTorch parsing 45 | TODO: remove 46 | 47 | :param s: String to parse 48 | :type s: ```str``` 49 | 50 | :return: Value 51 | """ 52 | balanced: bool = (s.count("[") + s.count("]")) & 1 == 0 53 | return ast.parse(s if balanced else "{}]".format(s)).body[0].value 54 | 55 | 56 | # def normalise_intermediate_representation(intermediate_repr): 57 | # """ 58 | # Normalise the intermediate representation. Performs: 59 | # - Move header and footer of docstring to same place—and with same whitespace—as original docstring 60 | # 61 | # :param intermediate_repr: a dictionary of form 62 | # { "name": Optional[str], 63 | # "type": Optional[str], 64 | # "doc": Optional[str], 65 | # "params": OrderedDict[str, {'typ': str, 'doc': Optional[str], 'default': Any}] 66 | # "returns": Optional[OrderedDict[Literal['return_type'], 67 | # {'typ': str, 'doc': Optional[str], 'default': Any}),)]] } 68 | # :type intermediate_repr: ```dict``` 69 | # 70 | # :return: a dictionary of form 71 | # { "name": Optional[str], 72 | # "type": Optional[str], 73 | # "doc": Optional[str], 74 | # "params": OrderedDict[str, {'typ': str, 'doc': Optional[str], 'default': Any}] 75 | # "returns": Optional[OrderedDict[Literal['return_type'], 76 | # {'typ': str, 'doc': Optional[str], 'default': Any}),)]] } 77 | # :rtype: ```dict``` 78 | # """ 79 | # current_doc_str = intermediate_repr["doc"] 80 | # original_doc_str = intermediate_repr.get("_internal", {"original_doc_str": None})[ 81 | # "original_doc_str" 82 | # ] 83 | # intermediate_repr["doc"] = ensure_doc_args_whence_original( 84 | # current_doc_str=current_doc_str, original_doc_str=original_doc_str 85 | # ) 86 | # return intermediate_repr 87 | 88 | 89 | def get_emitter(emit_name): 90 | """ 91 | Get emitter function specialised for output `node` 92 | 93 | :param emit_name: Which type to emit. 94 | :type emit_name: ```Literal["argparse", "class", "function", "json_schema", 95 | "pydantic", "sqlalchemy", "sqlalchemy_table", "sqlalchemy_hybrid"]``` 96 | 97 | :return: Function which returns intermediate_repr 98 | :rtype: ```Callable[[...], dict]```` 99 | """ 100 | emit_name: str = {"class": "class_"}.get(emit_name, emit_name) 101 | return getattr( 102 | import_module( 103 | ".".join( 104 | ( 105 | "cdd", 106 | ( 107 | "sqlalchemy" 108 | if emit_name 109 | in frozenset(("sqlalchemy_hybrid", "sqlalchemy_table")) 110 | else emit_name 111 | ), 112 | "emit", 113 | ) 114 | ) 115 | ), 116 | emit_name, 117 | ) 118 | 119 | 120 | __all__ = [ 121 | "ast_parse_fix", 122 | "get_internal_body", 123 | "get_emitter", 124 | # "normalise_intermediate_representation", 125 | ] # type: list[str] 126 | -------------------------------------------------------------------------------- /cdd/shared/parse/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Transform from string or AST representations of input, to intermediate_repr, a dictionary consistent 3 | with `IntermediateRepr`, defined as: 4 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 5 | IntermediateRepr = TypedDict("IntermediateRepr", { 6 | "name": Optional[str], 7 | "type": Optional[str], 8 | "doc": Optional[str], 9 | "params": OrderedDict[str, ParamVal], 10 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 11 | }) 12 | """ 13 | 14 | from ast import AnnAssign, Assign, AsyncFunctionDef, ClassDef, FunctionDef 15 | from typing import List 16 | 17 | PARSERS: List[str] = [ 18 | "argparse_function", 19 | "class_", 20 | "docstring", 21 | "function", 22 | "json_schema", 23 | # "openapi", 24 | "pydantic", 25 | "routes", 26 | "sqlalchemy", 27 | "sqlalchemy_hybrid", 28 | "sqlalchemy_table", 29 | ] 30 | 31 | kind2instance_type = { 32 | "argparse": (FunctionDef,), 33 | "argparse_function": (FunctionDef,), 34 | "class": (ClassDef,), 35 | "class_": (ClassDef,), 36 | "function": (FunctionDef, AsyncFunctionDef), 37 | "method": (FunctionDef, AsyncFunctionDef), 38 | "pydantic": (ClassDef,), 39 | "sqlalchemy_hybrid": (ClassDef,), 40 | "sqlalchemy_table": (Assign, AnnAssign), 41 | "sqlalchemy": (ClassDef,), 42 | } 43 | 44 | __all__ = ["PARSERS", "kind2instance_type"] # type: list[str] 45 | -------------------------------------------------------------------------------- /cdd/shared/parse/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for `cdd.parse` 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/shared/pkg_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | pkg_utils 3 | """ 4 | 5 | from cdd.shared.pure_utils import PY_GTE_3_12 6 | 7 | if PY_GTE_3_12: 8 | from sysconfig import get_paths 9 | 10 | get_python_lib = lambda prefix="", *args, **kwargs: get_paths(*args, **kwargs)[ 11 | prefix or "purelib" 12 | ] 13 | else: 14 | from distutils.sysconfig import get_python_lib 15 | 16 | 17 | def relative_filename(filename, remove_hints=tuple()): 18 | """ 19 | Remove all the paths which are not relevant 20 | 21 | :param filename: Filename 22 | :type filename: ```str``` 23 | 24 | :param remove_hints: Hints as to what can be removed 25 | :type remove_hints: ```tuple[str, ...]``` 26 | 27 | :return: Relative `os.path` (if derived) else original 28 | :rtype: ```str``` 29 | """ 30 | _filename: str = filename.casefold() 31 | lib = get_python_lib(), get_python_lib(prefix="") # type: tuple[str, str] 32 | return next( 33 | map( 34 | lambda elem: filename[len(elem) + 1 :], 35 | filter( 36 | lambda elem: _filename.startswith(elem.casefold()), remove_hints + lib 37 | ), 38 | ), 39 | filename, 40 | ) 41 | 42 | 43 | __all__ = ["get_python_lib", "relative_filename"] # type: list[str] 44 | -------------------------------------------------------------------------------- /cdd/shared/source_transformer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Source transformer module. Uses astor on Python < 3.9 3 | """ 4 | 5 | from ast import AsyncFunctionDef, ClassDef, FunctionDef, Module, get_docstring, parse 6 | from importlib import import_module 7 | from sys import version_info 8 | from typing import Optional 9 | 10 | import cdd.shared.ast_utils 11 | from cdd.shared.pure_utils import reindent, tab 12 | 13 | unparse = ( 14 | getattr(import_module("astor"), "to_source") 15 | if version_info[:2] < (3, 9) 16 | else getattr(import_module("ast"), "unparse") 17 | ) 18 | 19 | 20 | def to_code(node): 21 | """ 22 | Convert the AST input to Python source string 23 | 24 | :param node: AST node 25 | :type node: ```AST``` 26 | 27 | :return: Python source 28 | :rtype: ```str``` 29 | """ 30 | # ^Not `to_code = getattr…` so docstring can be included^ 31 | return unparse(node) 32 | 33 | 34 | def ast_parse( 35 | source, 36 | filename="", 37 | mode="exec", 38 | skip_annotate=False, 39 | skip_docstring_remit=False, 40 | ): 41 | """ 42 | Convert the AST input to Python source string 43 | 44 | :param source: Python source 45 | :type source: ```str``` 46 | 47 | :param filename: Filename being parsed 48 | :type filename: ```str``` 49 | 50 | :param mode: 'exec' to compile a module, 'single' to compile a single (interactive) statement, 51 | or 'eval' to compile an expression. 52 | :type mode: ```Literal['exec', 'single', 'eval']``` 53 | 54 | :param skip_annotate: Don't run `annotate_ancestry` 55 | :type skip_annotate: ```bool``` 56 | 57 | :param skip_docstring_remit: Don't parse & emit the docstring as a replacement for current docstring 58 | :type skip_docstring_remit: ```bool``` 59 | 60 | :return: AST node 61 | :rtype: ```AST``` 62 | """ 63 | parsed_ast = parse(source, filename=filename, mode=mode) 64 | if not skip_annotate: 65 | cdd.shared.ast_utils.annotate_ancestry(parsed_ast, filename=filename) 66 | setattr(parsed_ast, "__file__", filename) 67 | if not skip_docstring_remit and isinstance( 68 | parsed_ast, (Module, ClassDef, FunctionDef, AsyncFunctionDef) 69 | ): 70 | docstring: Optional[str] = get_docstring(parsed_ast, clean=True) 71 | if docstring is None: 72 | return parsed_ast 73 | 74 | # Reindent docstring 75 | parsed_ast.body[0].value.value = "\n{tab}{docstring}\n{tab}".format( 76 | tab=tab, docstring=reindent(docstring) 77 | ) 78 | return parsed_ast 79 | 80 | 81 | __all__ = ["ast_parse", "to_code"] # type: list[str] 82 | -------------------------------------------------------------------------------- /cdd/shared/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared types 3 | """ 4 | 5 | from ast import AST 6 | from typing import Any, List, Optional 7 | 8 | from cdd.shared.pure_utils import PY_GTE_3_8, PY_GTE_3_9, PY_GTE_3_11 9 | 10 | if PY_GTE_3_8: 11 | if PY_GTE_3_9: 12 | from collections import OrderedDict 13 | else: 14 | from typing import OrderedDict 15 | from typing import TypedDict 16 | 17 | if PY_GTE_3_11: 18 | from typing import Required 19 | else: 20 | from typing_extensions import Required 21 | else: 22 | from typing_extensions import OrderedDict, Required, TypedDict 23 | 24 | 25 | # class Parse(Protocol): 26 | # def add(self, a: int, b: int) -> int: 27 | # return a + b 28 | # 29 | # 30 | # def conforms_to_parse_protocol(parse: Parse): 31 | # pass 32 | 33 | 34 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 35 | Internal = TypedDict( 36 | "Internal", 37 | { 38 | "original_doc_str": Optional[str], 39 | "body": List[AST], 40 | "from_name": Optional[str], 41 | "from_type": str, 42 | }, 43 | total=False, 44 | ) 45 | IntermediateRepr = TypedDict( 46 | "IntermediateRepr", 47 | { 48 | "name": Required[Optional[str]], 49 | "type": Optional[str], 50 | "_internal": Internal, 51 | "doc": Required[Optional[str]], 52 | "params": Required[OrderedDict[str, ParamVal]], 53 | "returns": Required[ 54 | Optional[OrderedDict[str, ParamVal]] 55 | ], # OrderedDict[Literal["return_type"] 56 | }, 57 | total=False, 58 | ) 59 | 60 | __all__ = ["IntermediateRepr", "Internal", "ParamVal"] # type: list[str] 61 | -------------------------------------------------------------------------------- /cdd/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SQLalchemy parsers and emitters module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/sqlalchemy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SQLalchemy parsers and emitters utility module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Prefix tests with test_ 3 | to have them run through the unittest discover or `python setup.py test` mechanism 4 | """ 5 | -------------------------------------------------------------------------------- /cdd/tests/mocks/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared by the mocks. Currently unused, but has some imports mocked for later use… 3 | """ 4 | 5 | from ast import AST 6 | from ast import parse as ast_parse 7 | from typing import List 8 | 9 | from cdd.shared.pure_utils import PY_GTE_3_8 10 | 11 | imports_header: str = ( 12 | """ 13 | from {package} import Literal 14 | from typing import Optional, Tuple, Union 15 | 16 | try: 17 | import tensorflow as tf 18 | import numpy as np 19 | except ImportError: 20 | tf = type('TensorFlow', tuple(), {{ 'data': type('Dataset', tuple(), {{ "Dataset": None }}) }} ) 21 | np = type('numpy', tuple(), {{ 'ndarray': None, 'empty': lambda _: _ }}) 22 | """.format( 23 | package="typing" if PY_GTE_3_8 else "typing_extensions" 24 | ) 25 | ) 26 | 27 | imports_header_ast: List[AST] = ast_parse(imports_header).body 28 | 29 | __all__ = ["imports_header", "imports_header_ast"] # type: list[str] 30 | -------------------------------------------------------------------------------- /cdd/tests/mocks/cstify.py: -------------------------------------------------------------------------------- 1 | # pragma: no cover 2 | # flake8: noqa 3 | # === Copyright 2022 under CC0 4 | """Module docstring goes here""" 5 | 6 | from operator import add 7 | 8 | 9 | class C(object): 10 | """My cls""" 11 | 12 | @staticmethod 13 | def add1(foo): 14 | """ 15 | :param foo: a foo 16 | :type foo: ```int``` 17 | 18 | :return: foo + 1 19 | :rtype: ```int``` 20 | """ 21 | 22 | """foo""" 23 | 24 | def g(): 25 | """foo : bar ; can""" 26 | pass 27 | 28 | def h(): # stuff 29 | pass 30 | 31 | def adder(a: int, b: int) -> int: 32 | """ 33 | :param a: First arg 34 | 35 | :param b: Second arg 36 | 37 | :return: first + second arg 38 | """ 39 | # fmt: off 40 | res: \ 41 | int \ 42 | = a + b 43 | return res 44 | 45 | r = add(foo, 1) or adder(foo, 1) 46 | if r: 47 | None 48 | elif r: 49 | True 50 | False 51 | # ([5,5] @ [5,5]) *\ 52 | -5 / 7**6 + 6.0 - 6e1 & 1 + 2.34j 53 | r <<= 5 54 | print(r) 55 | else: 56 | pass 57 | # fmt: on 58 | # That^ incremented `foo` by 1 59 | return r 60 | 61 | 62 | # from contextlib import ContextDecorator 63 | 64 | # with ContextDecorator(): 65 | # pass 66 | 67 | 68 | def f(): 69 | return 1 70 | -------------------------------------------------------------------------------- /cdd/tests/mocks/eval.py: -------------------------------------------------------------------------------- 1 | """ eval.py for testing with `sync_properties` """ 2 | 3 | import sys 4 | 5 | if sys.version_info >= (3, 9): 6 | FrozenSet = frozenset 7 | else: 8 | from typing import FrozenSet 9 | 10 | import cdd.tests.mocks 11 | 12 | _attr_within: FrozenSet[str] = frozenset(("mocks",)) 13 | 14 | get_modules = tuple( 15 | attr for attr in dir(cdd.tests) if not attr.startswith("_") and attr in _attr_within 16 | ) # type: tuple[str, ...] 17 | 18 | __all__ = ["get_modules"] # type: list[str] 19 | -------------------------------------------------------------------------------- /cdd/tests/mocks/gen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gen mocks 3 | """ 4 | 5 | from ast import Import, ImportFrom, alias 6 | 7 | from cdd.shared.source_transformer import to_code 8 | 9 | import_star_from_input_ast: ImportFrom = ImportFrom( 10 | module="input", 11 | names=[ 12 | alias( 13 | name="input_map", 14 | asname=None, 15 | identifier=None, 16 | identifier_name=None, 17 | ), 18 | alias( 19 | name="Foo", 20 | asname=None, 21 | identifier=None, 22 | identifier_name=None, 23 | ), 24 | ], 25 | level=1, 26 | identifier=None, 27 | ) 28 | import_star_from_input_str: str = to_code(import_star_from_input_ast) 29 | import_gen_test_module_ast: Import = Import( 30 | names=[ 31 | alias( 32 | name="gen_test_module", 33 | asname=None, 34 | identifier=None, 35 | identifier_name=None, 36 | ) 37 | ], 38 | alias=None, 39 | ) 40 | import_gen_test_module_str: str = "{}\n".format( 41 | to_code(import_gen_test_module_ast).rstrip("\n") 42 | ) 43 | 44 | __all__ = [ 45 | "import_gen_test_module_ast", 46 | "import_gen_test_module_str", 47 | "import_star_from_input_ast", 48 | "import_star_from_input_str", 49 | ] # type: list[str] 50 | -------------------------------------------------------------------------------- /cdd/tests/mocks/json_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mocks for JSON Schema 3 | """ 4 | 5 | from copy import deepcopy 6 | 7 | from cdd.json_schema.utils.shared_utils import JSON_schema 8 | from cdd.tests.mocks.docstrings import docstring_header_and_return_no_nl_str 9 | 10 | config_schema: JSON_schema = { 11 | "$id": "https://offscale.io/config.schema.json", 12 | "$schema": "https://json-schema.org/draft/2020-12/schema", 13 | "description": docstring_header_and_return_no_nl_str, 14 | "type": "object", 15 | "properties": { 16 | "dataset_name": { 17 | "description": "[PK] name of dataset.", 18 | "type": "string", 19 | "default": "mnist", 20 | }, 21 | "tfds_dir": { 22 | "description": "directory to look for models in.", 23 | "type": "string", 24 | "default": "~/tensorflow_datasets", 25 | }, 26 | "K": { 27 | "description": "backend engine, e.g., `np` or `tf`.", 28 | "type": "string", 29 | "pattern": "np|tf", 30 | "default": "np", 31 | }, 32 | "as_numpy": { 33 | "description": "Convert to numpy ndarrays", 34 | "type": "boolean", 35 | }, 36 | "data_loader_kwargs": { 37 | "description": "pass this as arguments to data_loader function", 38 | "type": "object", 39 | }, 40 | }, 41 | "required": ["dataset_name", "tfds_dir", "K"], 42 | } 43 | 44 | config_schema_with_sql_types: JSON_schema = deepcopy(config_schema) 45 | for param, typ in ( 46 | ("dataset_name", "String"), 47 | ("tfds_dir", "String"), 48 | ("as_numpy", "Boolean"), 49 | ("data_loader_kwargs", "JSON"), 50 | ): 51 | config_schema_with_sql_types["properties"][param]["x_typ"] = {"sql": {"type": typ}} 52 | 53 | 54 | server_error_schema: JSON_schema = { 55 | "$id": "https://offscale.io/error_json.schema.json", 56 | "$schema": "https://json-schema.org/draft/2020-12/schema", 57 | "description": "Error schema", 58 | "type": "object", 59 | "properties": { 60 | "error": {"description": "Name of the error", "type": "string"}, 61 | "error_description": { 62 | "description": "Description of the error", 63 | "type": "string", 64 | }, 65 | "error_code": { 66 | "description": "Code of the error (usually is searchable in a KB for further information)", 67 | "type": "string", 68 | }, 69 | "status_code": { 70 | "description": "Status code (usually for HTTP)", 71 | "type": "number", 72 | }, 73 | }, 74 | "required": ["error", "error_description"], 75 | } 76 | 77 | 78 | __all__ = [ 79 | "config_schema", 80 | "server_error_schema", 81 | "config_schema_with_sql_types", 82 | ] # type: list[str] 83 | -------------------------------------------------------------------------------- /cdd/tests/mocks/openapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAPI mocks 3 | """ 4 | 5 | from copy import deepcopy 6 | 7 | from cdd.compound.openapi.utils.emit_openapi_utils import OpenApiType 8 | from cdd.tests.mocks.json_schema import ( 9 | config_schema, 10 | config_schema_with_sql_types, 11 | server_error_schema, 12 | ) 13 | from cdd.tests.mocks.routes import route_config 14 | 15 | openapi_dict: OpenApiType = { 16 | "openapi": "3.0.0", 17 | "info": {"title": "REST API", "version": "0.0.1"}, 18 | "components": { 19 | "requestBodies": { 20 | "ConfigBody": { 21 | "content": { 22 | "application/json": { 23 | "schema": { 24 | "$ref": "#/components/schemas/{name}".format( 25 | name=route_config["name"] 26 | ) 27 | } 28 | } 29 | }, 30 | "description": "A `{name}` object.".format(name=route_config["name"]), 31 | "required": True, 32 | } 33 | }, 34 | "schemas": { 35 | name: {k: v for k, v in schema.items() if not k.startswith("$")} 36 | for name, schema in { 37 | route_config["name"]: config_schema, 38 | "ServerError": server_error_schema, 39 | }.items() 40 | }, 41 | }, 42 | "paths": { 43 | route_config["route"]: { 44 | "post": { 45 | "requestBody": { 46 | "$ref": "#/components/requestBodies/{name}Body".format( 47 | name=route_config["name"] 48 | ), 49 | "required": True, 50 | }, 51 | "responses": { 52 | "201": { 53 | "content": { 54 | "application/json": { 55 | "schema": { 56 | "$ref": "#/components/schemas/{name}".format( 57 | name=route_config["name"] 58 | ) 59 | } 60 | } 61 | }, 62 | "description": "A `{name}` object.".format( 63 | name=route_config["name"] 64 | ), 65 | }, 66 | "400": { 67 | "content": { 68 | "application/json": { 69 | "schema": {"$ref": "#/components/schemas/ServerError"} 70 | } 71 | }, 72 | "description": "A `ServerError` object.", 73 | }, 74 | }, 75 | "summary": "A `{name}` object.".format(name=route_config["name"]), 76 | } 77 | }, 78 | "{route_config[route]}/{{{route_config[primary_key]}}}".format( 79 | route_config=route_config 80 | ): { 81 | "delete": { 82 | "responses": {"204": {}}, 83 | "summary": "Delete one `{name}`".format(name=route_config["name"]), 84 | }, 85 | "get": { 86 | "responses": { 87 | "200": { 88 | "content": { 89 | "application/json": { 90 | "schema": { 91 | "$ref": "#/components/schemas/{name}".format( 92 | name=route_config["name"] 93 | ) 94 | } 95 | } 96 | }, 97 | "description": "A `{name}` object.".format( 98 | name=route_config["name"] 99 | ), 100 | }, 101 | "404": { 102 | "content": { 103 | "application/json": { 104 | "schema": {"$ref": "#/components/schemas/ServerError"} 105 | } 106 | }, 107 | "description": "A `ServerError` object.", 108 | }, 109 | }, 110 | "summary": "A `{name}` object.".format(name=route_config["name"]), 111 | }, 112 | "parameters": [ 113 | { 114 | "description": "Primary key of target `{name}`".format( 115 | name=route_config["name"] 116 | ), 117 | "in": "path", 118 | "name": route_config["primary_key"], 119 | "required": True, 120 | "schema": {"type": "string"}, 121 | } 122 | ], 123 | }, 124 | }, 125 | } 126 | 127 | openapi_dict_with_sql_types: OpenApiType = deepcopy(openapi_dict) 128 | openapi_dict_with_sql_types["components"]["schemas"] = { 129 | name: {k: v for k, v in schema.items() if not k.startswith("$")} 130 | for name, schema in { 131 | route_config["name"]: config_schema_with_sql_types, 132 | "ServerError": server_error_schema, 133 | }.items() 134 | } 135 | 136 | 137 | __all__ = ["openapi_dict", "openapi_dict_with_sql_types"] # type: list[str] 138 | -------------------------------------------------------------------------------- /cdd/tests/mocks/openapi_emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | OpenAPI emit_utils 3 | """ 4 | 5 | from ast import Assign, Call, Load, Name, Store, keyword 6 | 7 | from cdd.shared.ast_utils import set_value 8 | 9 | column_fk: Assign = Assign( 10 | targets=[Name(id="column_name", ctx=Store(), lineno=None, col_offset=None)], 11 | value=Call( 12 | func=Name(id="Column", ctx=Load(), lineno=None, col_offset=None), 13 | args=[ 14 | Name(id="TableName0", ctx=Load(), lineno=None, col_offset=None), 15 | Call( 16 | func=Name(id="ForeignKey", ctx=Load(), lineno=None, col_offset=None), 17 | args=[set_value("TableName0")], 18 | keywords=[], 19 | lineno=None, 20 | col_offset=None, 21 | ), 22 | ], 23 | keywords=[keyword(arg="nullable", value=set_value(True))], 24 | lineno=None, 25 | col_offset=None, 26 | ), 27 | type_comment=None, 28 | expr=None, 29 | lineno=None, 30 | ) 31 | 32 | column_fk_gold: Assign = Assign( 33 | targets=[Name(id="column_name", ctx=Store(), lineno=None, col_offset=None)], 34 | value=Call( 35 | func=Name(id="Column", ctx=Load(), lineno=None, col_offset=None), 36 | args=[ 37 | Name(id="Integer", ctx=Load(), lineno=None, col_offset=None), 38 | Call( 39 | func=Name(id="ForeignKey", ctx=Load(), lineno=None, col_offset=None), 40 | args=[set_value("table_name0.id")], 41 | keywords=[], 42 | lineno=None, 43 | col_offset=None, 44 | ), 45 | ], 46 | keywords=[keyword(arg="nullable", value=set_value(True), identifier=None)], 47 | lineno=None, 48 | col_offset=None, 49 | ), 50 | type_comment=None, 51 | expr=None, 52 | lineno=None, 53 | ) 54 | 55 | id_column: Assign = Assign( 56 | targets=[Name(id="id", ctx=Store(), lineno=None, col_offset=None)], 57 | value=Call( 58 | func=Name(id="Column", ctx=Load(), lineno=None, col_offset=None), 59 | args=[Name(id="Integer", ctx=Load(), lineno=None, col_offset=None)], 60 | keywords=[ 61 | keyword(arg="primary_key", value=set_value(True)), 62 | keyword( 63 | arg="server_default", 64 | value=Call( 65 | func=Name(id="Identity", ctx=Load(), lineno=None, col_offset=None), 66 | args=[], 67 | keywords=[], 68 | lineno=None, 69 | col_offset=None, 70 | ), 71 | ), 72 | ], 73 | lineno=None, 74 | col_offset=None, 75 | ), 76 | type_comment=None, 77 | expr=None, 78 | lineno=None, 79 | ) 80 | 81 | __all__ = ["column_fk", "column_fk_gold", "id_column"] # type: list[str] 82 | -------------------------------------------------------------------------------- /cdd/tests/mocks/pydantic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic mocks 3 | """ 4 | 5 | from ast import AnnAssign, ClassDef, Index, Load, Name, Store, Subscript 6 | 7 | from cdd.shared.ast_utils import maybe_type_comment, set_value 8 | 9 | pydantic_class_str: str = """ 10 | class Cat(BaseModel): 11 | pet_type: Literal['cat'] 12 | cat_name: str 13 | """ 14 | 15 | pydantic_class_cls_def: ClassDef = ClassDef( 16 | bases=[Name(ctx=Load(), id="BaseModel", lineno=None, col_offset=None)], 17 | body=[ 18 | AnnAssign( 19 | annotation=Subscript( 20 | ctx=Load(), 21 | slice=Index(value=set_value("cat")), 22 | value=Name(ctx=Load(), id="Literal", lineno=None, col_offset=None), 23 | lineno=None, 24 | col_offset=None, 25 | ), 26 | simple=1, 27 | target=Name(ctx=Store(), id="pet_type", lineno=None, col_offset=None), 28 | value=None, 29 | expr=None, 30 | expr_target=None, 31 | expr_annotation=None, 32 | lineno=None, 33 | col_offset=None, 34 | **maybe_type_comment 35 | ), 36 | AnnAssign( 37 | annotation=Name("str", Load(), lineno=None, col_offset=None), 38 | simple=1, 39 | target=Name(ctx=Store(), id="cat_name", lineno=None, col_offset=None), 40 | value=None, 41 | expr=None, 42 | expr_target=None, 43 | expr_annotation=None, 44 | lineno=None, 45 | col_offset=None, 46 | **maybe_type_comment 47 | ), 48 | ], 49 | decorator_list=[], 50 | type_params=[], 51 | keywords=[], 52 | name="Cat", 53 | lineno=None, 54 | col_offset=None, 55 | ) 56 | 57 | __all__ = ["pydantic_class_str", "pydantic_class_cls_def"] # type: list[str] 58 | -------------------------------------------------------------------------------- /cdd/tests/mocks/routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock routes 3 | """ 4 | 5 | import cdd.routes.emit.bottle 6 | from cdd.shared.pure_utils import tab 7 | 8 | route_config = { 9 | "app": "rest_api", 10 | "name": "Config", 11 | "route": "/api/config", 12 | "variant": -1, 13 | } # type: dict[str, Union[str, int]] 14 | 15 | create_route: str = cdd.routes.emit.bottle.create(**route_config) 16 | 17 | route_config["primary_key"] = "dataset_name" 18 | 19 | read_route: str = cdd.routes.emit.bottle.read(**route_config) 20 | destroy_route: str = cdd.routes.emit.bottle.destroy(**route_config) 21 | 22 | route_mock_prelude: str = ( 23 | 'rest_api = type("App", tuple(),\n' 24 | "{sep}{{ method: lambda h: lambda g=None: g \n" 25 | '{sep} for method in ("get", "post", "put", "delete") }})\n'.format(sep=tab * 4) 26 | ) 27 | 28 | route_prelude: str = ( 29 | "from bottle import Bottle, request, response\n\n" 30 | "rest_api = Bottle(catchall=False, autojson=True)\n" 31 | ) 32 | 33 | __all__ = [ 34 | "create_route", 35 | "read_route", 36 | "destroy_route", 37 | "route_config", 38 | "route_prelude", 39 | "route_mock_prelude", 40 | ] # type: list[str] 41 | -------------------------------------------------------------------------------- /cdd/tests/test_argparse_function/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for argparse parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_argparse_function/test_emit_argparse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.argparse` 3 | """ 4 | 5 | from copy import deepcopy 6 | from unittest import TestCase 7 | 8 | import cdd.argparse_function.emit 9 | import cdd.argparse_function.parse 10 | import cdd.class_.emit 11 | import cdd.class_.parse 12 | import cdd.docstring.emit 13 | import cdd.function.emit 14 | import cdd.function.parse 15 | import cdd.json_schema.emit 16 | import cdd.shared.emit.file 17 | import cdd.sqlalchemy.emit 18 | from cdd.shared.pure_utils import deindent 19 | from cdd.shared.types import IntermediateRepr 20 | from cdd.tests.mocks.argparse import ( 21 | argparse_func_action_append_ast, 22 | argparse_func_ast, 23 | argparse_func_torch_nn_l1loss_ast, 24 | argparse_func_with_body_ast, 25 | argparse_function_google_keras_tensorboard_ast, 26 | ) 27 | from cdd.tests.mocks.classes import ( 28 | class_ast, 29 | class_google_keras_tensorboard_ast, 30 | class_nargs_ast, 31 | ) 32 | from cdd.tests.mocks.ir import class_torch_nn_l1loss_ir 33 | from cdd.tests.utils_for_tests import reindent_docstring, run_ast_test, unittest_main 34 | 35 | 36 | class TestEmitArgparse(TestCase): 37 | """Tests emission""" 38 | 39 | def test_to_argparse(self) -> None: 40 | """ 41 | Tests whether `to_argparse` produces `argparse_func_ast` given `class_ast` 42 | """ 43 | run_ast_test( 44 | self, 45 | gen_ast=cdd.argparse_function.emit.argparse_function( 46 | cdd.class_.parse.class_(class_ast), 47 | emit_default_doc=False, 48 | ), 49 | gold=argparse_func_ast, 50 | ) 51 | 52 | def test_to_argparse_func_nargs(self) -> None: 53 | """ 54 | Tests whether an argparse function is generated with `action="append"` set properly 55 | """ 56 | run_ast_test( 57 | self, 58 | gen_ast=cdd.argparse_function.emit.argparse_function( 59 | cdd.class_.parse.class_(class_nargs_ast), 60 | emit_default_doc=False, 61 | function_name="set_cli_action_append", 62 | ), 63 | gold=argparse_func_action_append_ast, 64 | ) 65 | 66 | def test_to_argparse_google_keras_tensorboard(self) -> None: 67 | """ 68 | Tests whether `to_argparse` produces `argparse_function_google_tf_tensorboard_ast` 69 | given `class_google_tf_tensorboard_ast` 70 | """ 71 | ir: IntermediateRepr = cdd.class_.parse.class_( 72 | class_google_keras_tensorboard_ast, merge_inner_function="__init__" 73 | ) 74 | run_ast_test( 75 | self, 76 | gen_ast=cdd.argparse_function.emit.argparse_function( 77 | ir, 78 | emit_default_doc=False, 79 | word_wrap=False, 80 | ), 81 | gold=argparse_function_google_keras_tensorboard_ast, 82 | ) 83 | 84 | def test_from_argparse_with_extra_body_to_argparse_with_extra_body(self) -> None: 85 | """Tests if this can make the roundtrip from a full argparse function to a full argparse function""" 86 | 87 | ir: IntermediateRepr = cdd.argparse_function.parse.argparse_ast( 88 | argparse_func_with_body_ast 89 | ) 90 | func = cdd.argparse_function.emit.argparse_function( 91 | ir, emit_default_doc=False, word_wrap=True 92 | ) 93 | run_ast_test( 94 | self, *map(reindent_docstring, (func, argparse_func_with_body_ast)) 95 | ) 96 | 97 | def test_from_torch_ir_to_argparse(self) -> None: 98 | """Tests if emission of class from torch IR is as expected""" 99 | ir = deepcopy(class_torch_nn_l1loss_ir) 100 | ir["doc"] = deindent(ir["doc"], 1) 101 | func = cdd.argparse_function.emit.argparse_function( 102 | ir, 103 | emit_default_doc=False, 104 | wrap_description=False, 105 | word_wrap=False, 106 | ) 107 | run_ast_test( 108 | self, 109 | func, 110 | argparse_func_torch_nn_l1loss_ast, 111 | ) 112 | 113 | 114 | unittest_main() 115 | -------------------------------------------------------------------------------- /cdd/tests/test_argparse_function/test_parse_argparse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the argparse parser 3 | """ 4 | 5 | from ast import FunctionDef 6 | from copy import deepcopy 7 | from unittest import TestCase 8 | 9 | import cdd.argparse_function.emit 10 | import cdd.argparse_function.parse 11 | from cdd.shared.pure_utils import tab 12 | from cdd.shared.source_transformer import to_code 13 | from cdd.shared.types import IntermediateRepr 14 | from cdd.tests.mocks.argparse import argparse_func_ast 15 | from cdd.tests.mocks.ir import intermediate_repr_no_default_doc 16 | from cdd.tests.utils_for_tests import unittest_main 17 | 18 | 19 | class TestParseArgparse(TestCase): 20 | """ 21 | Tests whether the intermediate representation is consistent when parsed from different inputs. 22 | 23 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 24 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 25 | IntermediateRepr = TypedDict("IntermediateRepr", { 26 | "name": Optional[str], 27 | "type": Optional[str], 28 | "doc": Optional[str], 29 | "params": OrderedDict[str, ParamVal], 30 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 31 | }) 32 | """ 33 | 34 | def test_from_argparse_ast(self) -> None: 35 | """ 36 | Tests whether `argparse_ast` produces `intermediate_repr_no_default_doc` 37 | from `argparse_func_ast`""" 38 | ir: IntermediateRepr = cdd.argparse_function.parse.argparse_ast( 39 | argparse_func_ast 40 | ) 41 | del ir["_internal"] # Not needed for this test 42 | _intermediate_repr_no_default_doc = deepcopy(intermediate_repr_no_default_doc) 43 | _intermediate_repr_no_default_doc["name"] = "set_cli_args" 44 | self.assertDictEqual(ir, _intermediate_repr_no_default_doc) 45 | 46 | def test_from_argparse_ast_empty(self) -> None: 47 | """ 48 | Tests `argparse_ast` empty condition 49 | """ 50 | self.assertEqual( 51 | to_code( 52 | cdd.argparse_function.emit.argparse_function( 53 | cdd.argparse_function.parse.argparse_ast( 54 | FunctionDef( 55 | body=[], 56 | name=None, 57 | arguments_args=None, 58 | identifier_name=None, 59 | stmt=None, 60 | ) 61 | ), 62 | emit_default_doc=True, 63 | ) 64 | ).rstrip("\n"), 65 | "def set_cli_args(argument_parser):\n" 66 | "{tab}{body}".format( 67 | tab=tab, 68 | body=tab.join( 69 | ( 70 | '"""\n', 71 | "Set CLI arguments\n", 72 | "\n", 73 | ":param argument_parser: argument parser\n", 74 | ":type argument_parser: ```ArgumentParser```\n", 75 | "\n", 76 | ":return: argument_parser\n", 77 | ":rtype: ```ArgumentParser```\n", 78 | '"""\n', 79 | "argument_parser.description = ''\n", 80 | "return argument_parser", 81 | ) 82 | ), 83 | ), 84 | ) 85 | 86 | 87 | unittest_main() 88 | -------------------------------------------------------------------------------- /cdd/tests/test_ast_equality.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for AST equality 3 | """ 4 | 5 | import ast 6 | from unittest import TestCase 7 | 8 | from cdd.tests.mocks.argparse import argparse_func_ast, argparse_func_str 9 | from cdd.tests.mocks.classes import class_ast, class_str 10 | from cdd.tests.utils_for_tests import reindent_docstring, run_ast_test, unittest_main 11 | 12 | 13 | class TestAstEquality(TestCase): 14 | """ 15 | Tests whether the AST generated matches the mocked one expected 16 | """ 17 | 18 | def test_argparse_func(self) -> None: 19 | """Tests whether the `argparse_func_str` correctly produces `argparse_func_ast`""" 20 | run_ast_test( 21 | self, 22 | *map( 23 | reindent_docstring, 24 | (ast.parse(argparse_func_str).body[0], argparse_func_ast), 25 | ) 26 | ) 27 | 28 | def test_class(self) -> None: 29 | """Tests whether the `class_str` correctly produces `class_ast`""" 30 | run_ast_test(self, class_str, class_ast) 31 | 32 | 33 | unittest_main() 34 | -------------------------------------------------------------------------------- /cdd/tests/test_class/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for class parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_class/test_emit_class_.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.class_` 3 | """ 4 | 5 | from ast import FunctionDef 6 | from typing import cast 7 | from unittest import TestCase 8 | 9 | import cdd.argparse_function.emit 10 | import cdd.argparse_function.parse 11 | import cdd.class_.emit 12 | import cdd.docstring.emit 13 | import cdd.docstring.parse 14 | import cdd.function.emit 15 | import cdd.function.parse 16 | import cdd.json_schema.emit 17 | import cdd.shared.emit.file 18 | import cdd.sqlalchemy.emit 19 | from cdd.shared.ast_utils import annotate_ancestry, find_in_ast 20 | from cdd.shared.pure_utils import rpartial 21 | from cdd.shared.types import IntermediateRepr 22 | from cdd.tests.mocks.argparse import argparse_func_action_append_ast, argparse_func_ast 23 | from cdd.tests.mocks.classes import class_ast, class_nargs_ast 24 | from cdd.tests.mocks.docstrings import docstring_no_nl_str 25 | from cdd.tests.mocks.methods import class_with_method_and_body_types_ast 26 | from cdd.tests.utils_for_tests import reindent_docstring, run_ast_test, unittest_main 27 | 28 | 29 | class TestEmitClass(TestCase): 30 | """Tests emission""" 31 | 32 | def test_to_class_from_argparse_ast(self) -> None: 33 | """ 34 | Tests whether `class_` produces `class_ast` given `argparse_func_ast` 35 | """ 36 | 37 | ir: IntermediateRepr = cdd.argparse_function.parse.argparse_ast( 38 | argparse_func_ast 39 | ) 40 | gen_ast = cdd.class_.emit.class_( 41 | ir, 42 | emit_default_doc=True, 43 | class_name="ConfigClass", 44 | ) 45 | 46 | run_ast_test( 47 | self, 48 | gen_ast=gen_ast, 49 | gold=class_ast, 50 | ) 51 | 52 | def test_to_class_from_argparse_action_append_ast(self) -> None: 53 | """ 54 | Tests whether a class from an argparse function with `nargs` set 55 | """ 56 | run_ast_test( 57 | self, 58 | cdd.class_.emit.class_( 59 | cdd.argparse_function.parse.argparse_ast( 60 | argparse_func_action_append_ast 61 | ), 62 | class_name="ConfigClass", 63 | ), 64 | gold=class_nargs_ast, 65 | ) 66 | 67 | def test_to_class_from_docstring_str(self) -> None: 68 | """ 69 | Tests whether `class_` produces `class_ast` given `docstring_str` 70 | """ 71 | run_ast_test( 72 | self, 73 | cdd.class_.emit.class_( 74 | cdd.docstring.parse.docstring( 75 | docstring_no_nl_str, emit_default_doc=True 76 | ), 77 | emit_default_doc=True, 78 | class_name="ConfigClass", 79 | ), 80 | gold=class_ast, 81 | ) 82 | 83 | def test_from_class_with_body_in_method_to_method_with_body(self) -> None: 84 | """Tests if this can make the roundtrip from a full function to a full function""" 85 | annotate_ancestry(class_with_method_and_body_types_ast) 86 | 87 | function_def: FunctionDef = cast( 88 | FunctionDef, 89 | reindent_docstring( 90 | next( 91 | filter( 92 | rpartial(isinstance, FunctionDef), 93 | class_with_method_and_body_types_ast.body, 94 | ) 95 | ) 96 | ), 97 | ) 98 | 99 | ir: IntermediateRepr = cdd.function.parse.function( 100 | find_in_ast( 101 | "C.function_name".split("."), 102 | class_with_method_and_body_types_ast, 103 | ), 104 | ) 105 | gen_ast = cdd.function.emit.function( 106 | ir, 107 | emit_default_doc=False, 108 | function_name="function_name", 109 | function_type="self", 110 | indent_level=0, 111 | emit_separating_tab=True, 112 | emit_as_kwonlyargs=False, 113 | ) 114 | 115 | # emit.file(gen_ast, os.path.join(os.path.dirname(__file__), 116 | # "delme{extsep}py".format(extsep=extsep)), mode="wt") 117 | 118 | run_ast_test( 119 | self, 120 | gen_ast=gen_ast, 121 | gold=function_def, 122 | ) 123 | 124 | 125 | unittest_main() 126 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for python-cdd's Command Line Interface (CLI) 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI (__main__.py) """ 2 | 3 | import os 4 | from argparse import ArgumentParser 5 | from importlib.machinery import SourceFileLoader 6 | from importlib.util import module_from_spec, spec_from_loader 7 | from os.path import extsep 8 | from unittest import TestCase 9 | from unittest.mock import MagicMock, patch 10 | 11 | from cdd import __description__, __version__ 12 | from cdd.__main__ import _build_parser 13 | from cdd.shared.pure_utils import PY3_8 14 | from cdd.tests.utils_for_tests import run_cli_test, unittest_main 15 | 16 | 17 | class TestCli(TestCase): 18 | """Test class for __main__.py""" 19 | 20 | def test_build_parser(self) -> None: 21 | """Test that `_build_parser` produces a parser object""" 22 | parser: ArgumentParser = _build_parser() 23 | self.assertIsInstance(parser, ArgumentParser) 24 | self.assertEqual(parser.description, __description__) 25 | 26 | def test_version(self) -> None: 27 | """Tests CLI interface gives version""" 28 | run_cli_test( 29 | self, 30 | ["--version"], 31 | exit_code=0, 32 | output=__version__, 33 | output_checker=lambda output: output[output.rfind(" ") + 1 :][:-1], 34 | ) 35 | 36 | def test_name_main(self) -> None: 37 | """Test the `if __name__ == '__main___'` block""" 38 | 39 | argparse_mock: MagicMock = MagicMock() 40 | 41 | loader: SourceFileLoader = SourceFileLoader( 42 | "__main__", 43 | os.path.join( 44 | os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 45 | "__main__{extsep}py".format(extsep=extsep), 46 | ), 47 | ) 48 | with patch("argparse.ArgumentParser._print_message", argparse_mock), patch( 49 | "sys.argv", [] 50 | ), self.assertRaises(SystemExit) as e: 51 | loader.exec_module(module_from_spec(spec_from_loader(loader.name, loader))) 52 | self.assertEqual(e.exception.code, SystemExit(2).code) 53 | 54 | self.assertEqual( 55 | (lambda output: output[(output.rfind(" ") + 1) :][:-1])( 56 | (argparse_mock.call_args.args if PY3_8 else argparse_mock.call_args[0])[ 57 | 0 58 | ] 59 | ), 60 | "command", 61 | ) 62 | 63 | 64 | unittest_main() 65 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_doctrans.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI doctrans subparser (__main__.py) """ 2 | 3 | from os import path 4 | from tempfile import TemporaryDirectory 5 | from unittest import TestCase 6 | from unittest.mock import patch 7 | 8 | from cdd.tests.utils_for_tests import mock_function, run_cli_test, unittest_main 9 | 10 | 11 | class TestCliDocTrans(TestCase): 12 | """Test class for __main__.py""" 13 | 14 | def test_doctrans_fails_with_wrong_args(self) -> None: 15 | """Tests CLI interface wrong args failure case""" 16 | 17 | run_cli_test( 18 | self, 19 | ["doctrans", "--wrong"], 20 | exit_code=2, 21 | output="the following arguments are required: --filename, --format\n", 22 | ) 23 | 24 | def test_doctrans_fails_with_file_missing(self) -> None: 25 | """Tests CLI interface file missing failure case""" 26 | 27 | with patch("cdd.__main__.doctrans", mock_function): 28 | self.assertTrue( 29 | run_cli_test( 30 | self, 31 | [ 32 | "doctrans", 33 | "--filename", 34 | "foo", 35 | "--format", 36 | "google", 37 | "--no-type-annotations", 38 | ], 39 | exit_code=2, 40 | output="--filename must be an existent file. Got: 'foo'\n", 41 | ), 42 | ) 43 | 44 | def test_doctrans_succeeds(self) -> None: 45 | """Tests CLI interface gets all the way to the doctrans call without error""" 46 | 47 | with TemporaryDirectory() as tempdir: 48 | filename: str = path.join(tempdir, "foo") 49 | open(filename, "a").close() 50 | with patch("cdd.__main__.doctrans", mock_function): 51 | self.assertTrue( 52 | run_cli_test( 53 | self, 54 | [ 55 | "doctrans", 56 | "--filename", 57 | filename, 58 | "--format", 59 | "numpydoc", 60 | "--type-annotations", 61 | ], 62 | exit_code=None, 63 | output=None, 64 | ), 65 | ) 66 | 67 | 68 | unittest_main() 69 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_exmod.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI exmod subparser (__main__.py) """ 2 | 3 | from unittest import TestCase 4 | from unittest.mock import MagicMock, patch 5 | 6 | from cdd.tests.utils_for_tests import run_cli_test, unittest_main 7 | 8 | 9 | class TestCliExMod(TestCase): 10 | """Test class for __main__.py""" 11 | 12 | def test_exmod_fails(self) -> None: 13 | """Tests CLI interface exmod failure cases""" 14 | run_cli_test( 15 | self, 16 | ["exmod", "--wrong"], 17 | exit_code=2, 18 | output="the following arguments are required: -m/--module, --emit, -o/--output-directory\n", 19 | ) 20 | 21 | def test_exmod_is_called(self) -> None: 22 | """Tests CLI interface exmod function gets called""" 23 | with patch("cdd.__main__.exmod", new_callable=MagicMock()): 24 | self.assertTrue( 25 | run_cli_test( 26 | self, 27 | [ 28 | "exmod", 29 | "--module", 30 | "foo", 31 | "--emit", 32 | "argparse", 33 | "--output-directory", 34 | "foo", 35 | ], 36 | exit_code=None, 37 | output=None, 38 | ), 39 | ) 40 | 41 | 42 | unittest_main() 43 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_gen.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI gen subparser (__main__.py) """ 2 | 3 | import os 4 | from os.path import extsep 5 | from tempfile import TemporaryDirectory 6 | from unittest import TestCase 7 | from unittest.mock import patch 8 | 9 | from cdd.tests.utils_for_tests import mock_function, run_cli_test, unittest_main 10 | 11 | 12 | class TestCliGen(TestCase): 13 | """Test class for __main__.py""" 14 | 15 | def test_gen_fails(self) -> None: 16 | """Tests CLI interface failure cases""" 17 | run_cli_test( 18 | self, 19 | ["gen", "--wrong"], 20 | exit_code=2, 21 | output="the following arguments are required: --name-tpl, --input-mapping, --emit, -o/--output-filename\n", 22 | ) 23 | 24 | def test_existent_file_fails(self) -> None: 25 | """Tests nonexistent file throws the right error""" 26 | with TemporaryDirectory() as tempdir: 27 | filename: str = os.path.join( 28 | tempdir, 29 | "delete_this_1{__file___basename}".format( 30 | __file___basename=os.path.basename(__file__) 31 | ), 32 | ) 33 | open(filename, "a").close() 34 | 35 | run_cli_test( 36 | self, 37 | [ 38 | "gen", 39 | "--name-tpl", 40 | "{name}Config", 41 | "--input-mapping", 42 | "cdd.pure_utils.simple_types", 43 | "--emit", 44 | "class", 45 | "--output-filename", 46 | filename, 47 | ], 48 | exception=OSError, 49 | exit_code=2, 50 | output="File exists and this is a destructive operation. Delete/move {filename!r} then rerun.".format( 51 | filename=filename 52 | ), 53 | ) 54 | 55 | def test_gen(self) -> None: 56 | """Tests CLI interface gets all the way to the gen call without error""" 57 | with TemporaryDirectory() as tempdir: 58 | output_filename: str = os.path.join( 59 | tempdir, "classes{extsep}py".format(extsep=extsep) 60 | ) 61 | 62 | with patch("cdd.__main__.gen", mock_function): 63 | run_cli_test( 64 | self, 65 | [ 66 | "gen", 67 | "--name-tpl", 68 | "{name}Config", 69 | "--input-mapping", 70 | "cdd.pure_utils.simple_types", 71 | "--emit", 72 | "class", 73 | "--output-filename", 74 | output_filename, 75 | ], 76 | exit_code=None, 77 | output=None, 78 | ) 79 | 80 | 81 | unittest_main() 82 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_gen_routes.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI gen_routes subparser (__main__.py) """ 2 | 3 | from unittest import TestCase 4 | from unittest.mock import patch 5 | 6 | from cdd.tests.utils_for_tests import mock_function, run_cli_test, unittest_main 7 | 8 | 9 | class TestCliGenRoutes(TestCase): 10 | """Test class for __main__.py""" 11 | 12 | def test_gen_routes_fails(self) -> None: 13 | """Tests CLI interface failure cases""" 14 | run_cli_test( 15 | self, 16 | ["gen_routes", "--wrong"], 17 | exit_code=2, 18 | output="the following arguments are required: --crud, --model-path, --model-name, --routes-path\n", 19 | ) 20 | 21 | def test_gen_routes(self) -> None: 22 | """Tests CLI interface gets all the way to the gen_routes call without error""" 23 | 24 | with patch( 25 | "cdd.__main__.gen_routes", lambda *args, **kwargs: (True,) * 2 26 | ), patch("cdd.__main__.upsert_routes", mock_function): 27 | self.assertTrue( 28 | run_cli_test( 29 | self, 30 | [ 31 | "gen_routes", 32 | "--crud", 33 | "CRD", 34 | "--model-path", 35 | "cdd.tests.mocks.sqlalchemy", 36 | "--routes-path", 37 | "/api/config", 38 | "--model-name", 39 | "Config", 40 | ], 41 | exit_code=None, 42 | output=None, 43 | ), 44 | ) 45 | 46 | 47 | unittest_main() 48 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_openapi.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI openapi subparser (__main__.py) """ 2 | 3 | from unittest import TestCase 4 | from unittest.mock import patch 5 | 6 | from cdd.tests.utils_for_tests import mock_function, run_cli_test, unittest_main 7 | 8 | 9 | class TestOpenApi(TestCase): 10 | """Test class for __main__.py""" 11 | 12 | def test_gen_routes_fails(self) -> None: 13 | """Tests CLI interface failure cases""" 14 | run_cli_test( 15 | self, 16 | ["openapi", "--wrong"], 17 | exit_code=2, 18 | output="the following arguments are required: --model-paths, --routes-paths\n", 19 | ) 20 | 21 | def test_openapi(self) -> None: 22 | """Tests CLI interface gets all the way to the `openapi` call without error""" 23 | 24 | with patch("cdd.__main__.openapi_bulk", mock_function): 25 | self.assertTrue( 26 | run_cli_test( 27 | self, 28 | [ 29 | "openapi", 30 | "--app-name", 31 | "app", 32 | "--model-paths", 33 | "cdd.tests.mocks.sqlalchemy", 34 | "--routes-paths", 35 | "cdd.tests.mocks.routes", 36 | ], 37 | exit_code=None, 38 | output=None, 39 | ), 40 | ) 41 | 42 | 43 | unittest_main() 44 | -------------------------------------------------------------------------------- /cdd/tests/test_cli/test_cli_sync_properties.py: -------------------------------------------------------------------------------- 1 | """ Tests for CLI sync_properties subparser (__main__.py) """ 2 | 3 | import os 4 | from os.path import extsep 5 | from tempfile import TemporaryDirectory 6 | from unittest import TestCase 7 | from unittest.mock import patch 8 | 9 | from cdd.tests.utils_for_tests import mock_function, run_cli_test, unittest_main 10 | 11 | 12 | class TestCliSyncProperties(TestCase): 13 | """Test class for __main__.py""" 14 | 15 | def test_sync_properties_fails(self) -> None: 16 | """Tests CLI interface failure cases""" 17 | run_cli_test( 18 | self, 19 | ["sync_properties", "--wrong"], 20 | exit_code=2, 21 | output="the following arguments are required:" 22 | " --input-filename, --input-param, --output-filename, --output-param\n", 23 | ) 24 | 25 | def test_non_existent_file_fails(self) -> None: 26 | """Tests nonexistent file throws the right error""" 27 | with TemporaryDirectory() as tempdir: 28 | filename: str = os.path.join( 29 | tempdir, 30 | "delete_this_1{}".format(os.path.basename(__file__)), 31 | ) 32 | 33 | run_cli_test( 34 | self, 35 | [ 36 | "sync_properties", 37 | "--input-file", 38 | filename, 39 | "--input-param", 40 | "Foo.g.f", 41 | "--output-file", 42 | filename, 43 | "--output-param", 44 | "f.h", 45 | ], 46 | exit_code=2, 47 | output="--input-file must be an existent file. Got: {filename!r}\n".format( 48 | filename=filename 49 | ), 50 | ) 51 | 52 | with TemporaryDirectory() as tempdir: 53 | input_filename: str = os.path.join( 54 | tempdir, 55 | "input_filename{extsep}py".format(extsep=extsep), 56 | ) 57 | output_filename: str = os.path.join( 58 | tempdir, 59 | "output_filename{extsep}py".format(extsep=extsep), 60 | ) 61 | open(input_filename, "wt").close() 62 | 63 | run_cli_test( 64 | self, 65 | [ 66 | "sync_properties", 67 | "--input-file", 68 | input_filename, 69 | "--input-param", 70 | "Foo.g.f", 71 | "--output-file", 72 | output_filename, 73 | "--output-param", 74 | "f.h", 75 | ], 76 | exit_code=2, 77 | output="--output-file must be an existent file. Got: {output_filename!r}\n".format( 78 | output_filename=output_filename 79 | ), 80 | ) 81 | 82 | def test_sync_properties(self) -> None: 83 | """Tests CLI interface gets all the way to the sync_properties call without error""" 84 | with TemporaryDirectory() as tempdir: 85 | input_filename: str = os.path.join( 86 | tempdir, "class_{extsep}py".format(extsep=extsep) 87 | ) 88 | output_filename: str = os.path.join( 89 | tempdir, "method{extsep}py".format(extsep=extsep) 90 | ) 91 | open(input_filename, "wt").close() 92 | open(output_filename, "wt").close() 93 | 94 | with patch("cdd.__main__.sync_properties", mock_function): 95 | self.assertTrue( 96 | run_cli_test( 97 | self, 98 | [ 99 | "sync_properties", 100 | "--input-file", 101 | input_filename, 102 | "--input-param", 103 | "Foo.g.f", 104 | "--output-file", 105 | output_filename, 106 | "--output-param", 107 | "f.h", 108 | ], 109 | exit_code=None, 110 | output=None, 111 | ), 112 | ) 113 | 114 | 115 | unittest_main() 116 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for compound parsers and emitters 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_doctrans.py: -------------------------------------------------------------------------------- 1 | """ Tests for doctrans """ 2 | 3 | from copy import deepcopy 4 | from os import path 5 | from tempfile import TemporaryDirectory 6 | from unittest import TestCase 7 | from unittest.mock import patch 8 | 9 | from cdd.compound.doctrans import doctrans 10 | from cdd.shared.ast_utils import annotate_ancestry 11 | from cdd.shared.source_transformer import to_code 12 | from cdd.tests.mocks.doctrans import function_type_annotated 13 | from cdd.tests.mocks.methods import return_ast 14 | from cdd.tests.utils_for_tests import unittest_main 15 | 16 | 17 | class TestDocTrans(TestCase): 18 | """Test class for doctrans.py""" 19 | 20 | def test_doctrans_append(self) -> None: 21 | """Tests doctrans""" 22 | 23 | with TemporaryDirectory() as temp_dir, patch( 24 | "cdd.compound.doctrans_utils.DocTrans", 25 | lambda **kwargs: type( 26 | "DocTrans", tuple(), {"visit": lambda *args: return_ast} 27 | )(), 28 | ): 29 | filename: str = path.join(temp_dir, "foo") 30 | with open(filename, "wt") as f: 31 | f.write("5*5") 32 | self.assertIsNone( 33 | doctrans( 34 | filename=filename, 35 | no_word_wrap=None, 36 | docstring_format="numpydoc", 37 | type_annotations=True, 38 | ) 39 | ) 40 | 41 | def test_doctrans_replace(self) -> None: 42 | """Tests doctrans""" 43 | 44 | with TemporaryDirectory() as temp_dir: 45 | filename: str = path.join( 46 | temp_dir, "fun{extsep}py".format(extsep=path.extsep) 47 | ) 48 | original_node = annotate_ancestry(deepcopy(function_type_annotated)) 49 | with open(filename, "wt") as f: 50 | f.write(to_code(original_node)) 51 | self.assertIsNone( 52 | doctrans( 53 | filename=filename, 54 | no_word_wrap=None, 55 | docstring_format="rest", 56 | type_annotations=False, 57 | ) 58 | ) 59 | # with open(filename, "rt") as f: 60 | # src = f.read() 61 | # new_node = ast_parse(src, skip_docstring_remit=True).body[0] 62 | # run_ast_test( 63 | # self, new_node, gold=function_type_in_docstring, skip_black=True 64 | # ) 65 | 66 | 67 | unittest_main() 68 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_exmod_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for exmod_utils """ 2 | 3 | from ast import Module 4 | from collections import deque 5 | from io import StringIO 6 | from os import path 7 | from os.path import extsep 8 | from tempfile import TemporaryDirectory 9 | from unittest import TestCase 10 | from unittest.mock import MagicMock, patch 11 | 12 | from cdd.compound.exmod_utils import ( 13 | _emit_symbol, 14 | emit_file_on_hierarchy, 15 | get_module_contents, 16 | ) 17 | from cdd.shared.pure_utils import INIT_FILENAME, quote, rpartial 18 | from cdd.shared.types import IntermediateRepr 19 | from cdd.tests.utils_for_tests import unittest_main 20 | 21 | 22 | class TestExmodUtils(TestCase): 23 | """Test class for emitter_utils""" 24 | 25 | def test_emit_file_on_hierarchy_dry_run(self) -> None: 26 | """Test that `emit_file_on_hierarchy` works with dry_run""" 27 | 28 | ir: IntermediateRepr = {"name": "YEP", "doc": None} 29 | with patch( 30 | "cdd.compound.exmod_utils.EXMOD_OUT_STREAM", new_callable=StringIO 31 | ) as f: 32 | emit_file_on_hierarchy( 33 | name_orig_ir=("", "foo_dir", ir), 34 | emit_name="argparse", 35 | module_name="", 36 | new_module_name="", 37 | mock_imports=True, 38 | filesystem_layout=None, 39 | extra_modules_to_all=None, 40 | output_directory="", 41 | first_output_directory="", 42 | no_word_wrap=None, 43 | dry_run=True, 44 | ) 45 | self.assertEqual(ir["name"], "YEP") 46 | self.assertListEqual( 47 | deque(map(rpartial(str.split, "\t"), f.getvalue().splitlines()), maxlen=1)[ 48 | 0 49 | ], 50 | ["write", quote("{name}{sep}py".format(name=ir["name"], sep=extsep), "'")], 51 | ) 52 | 53 | def test_emit_file_on_hierarchy(self) -> None: 54 | """Test `emit_file_on_hierarchy`""" 55 | 56 | ir: IntermediateRepr = {"name": "YEP", "doc": None} 57 | with patch( 58 | "cdd.compound.exmod_utils.EXMOD_OUT_STREAM", new_callable=StringIO 59 | ), TemporaryDirectory() as tempdir: 60 | open(path.join(tempdir, INIT_FILENAME), "a").close() 61 | emit_file_on_hierarchy( 62 | ("foo.bar", "foo_dir", ir), 63 | "argparse", 64 | "", 65 | "", 66 | True, 67 | filesystem_layout="as_input", 68 | output_directory=tempdir, 69 | first_output_directory=tempdir, 70 | no_word_wrap=None, 71 | dry_run=False, 72 | extra_modules_to_all=None, 73 | ) 74 | self.assertTrue(path.isdir(tempdir)) 75 | 76 | def test__emit_symbols_isfile_emit_filename_true(self) -> None: 77 | """Test `_emit_symbol` when `isfile_emit_filename is True`""" 78 | with patch( 79 | "cdd.compound.exmod_utils.EXMOD_OUT_STREAM", new_callable=StringIO 80 | ), patch( 81 | "cdd.shared.ast_utils.merge_modules", MagicMock() 82 | ) as func__merge_modules, patch( 83 | "cdd.shared.ast_utils.merge_assignment_lists", MagicMock() 84 | ) as func__merge_assignment_lists, patch( 85 | "cdd.shared.ast_utils.infer_imports", MagicMock() 86 | ) as func__infer_imports, patch( 87 | "cdd.shared.ast_utils.deduplicate_sorted_imports", MagicMock() 88 | ) as func__deduplicate_sorted_imports: 89 | _emit_symbol( 90 | name_orig_ir=("", "", dict()), 91 | emit_name="argparse", 92 | module_name="module_name", 93 | emit_filename="emit_filename", 94 | existent_mod=Module( 95 | body=[], 96 | type_ignores=[], 97 | stmt=None, 98 | ), 99 | init_filepath="", 100 | intermediate_repr={"name": None, "doc": None}, 101 | isfile_emit_filename=True, 102 | name="", 103 | mock_imports=True, 104 | extra_modules_to_all=None, 105 | no_word_wrap=None, 106 | first_output_directory=path.join("foo", "module_name"), 107 | dry_run=True, 108 | ) 109 | func__merge_modules.assert_called_once() 110 | func__merge_assignment_lists.assert_called_once() 111 | func__infer_imports.assert_called_once() 112 | func__deduplicate_sorted_imports.assert_called_once() 113 | 114 | def test_get_module_contents_empty(self) -> None: 115 | """`get_module_contents`""" 116 | self.assertDictEqual(get_module_contents(None, "nonexistent", {}), {}) 117 | 118 | 119 | unittest_main() 120 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_gen_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for gen_utils """ 2 | 3 | from ast import Assign, List, Load, Module, Name, Store 4 | from os import path 5 | from tempfile import TemporaryDirectory 6 | from unittest import TestCase 7 | 8 | import cdd.shared.source_transformer 9 | from cdd.compound.gen_utils import ( 10 | file_to_input_mapping, 11 | gen_module, 12 | get_input_mapping_from_path, 13 | ) 14 | from cdd.tests.mocks.classes import class_ast 15 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 16 | 17 | 18 | def f(s): 19 | """ 20 | :param s: str 21 | :type s: ```str``` 22 | """ 23 | return s 24 | 25 | 26 | class TestGenUtils(TestCase): 27 | """Test class for cdd.gen_utils""" 28 | 29 | def test_file_to_input_mapping_else_condition(self) -> None: 30 | """Test that `file_to_input_mapping` else condition works""" 31 | with TemporaryDirectory() as temp_dir: 32 | filename: str = path.join(temp_dir, "foo{}py".format(path.extsep)) 33 | with open(filename, "wt") as f: 34 | f.write(cdd.shared.source_transformer.to_code(class_ast)) 35 | input_mapping = file_to_input_mapping(filename, "infer") 36 | self.assertEqual(len(input_mapping.keys()), 1) 37 | self.assertIn(class_ast.name, input_mapping) 38 | run_ast_test(self, input_mapping[class_ast.name], class_ast) 39 | 40 | def test_get_input_mapping_from_path(self) -> None: 41 | """test `get_input_mapping_from_path`""" 42 | self.assertEqual(f(""), "") 43 | name_to_node = get_input_mapping_from_path( 44 | "function", "cdd.tests.test_compound", "test_gen_utils" 45 | ) 46 | self.assertEqual(len(name_to_node), 1) 47 | self.assertIn("f", name_to_node) 48 | self.assertIsInstance(name_to_node["f"], dict) 49 | 50 | def test_gen_module_when_emit_and_infer_imports(self) -> None: 51 | """ 52 | Tests that `emit_and_infer_imports` works when `emit_and_infer_imports` is True 53 | """ 54 | run_ast_test( 55 | self, 56 | gen_module( 57 | decorator_list=[], 58 | emit_and_infer_imports=True, 59 | emit_call=False, 60 | emit_default_doc=False, 61 | emit_name="class", 62 | functions_and_classes=None, 63 | imports=None, 64 | input_mapping_it={}, 65 | name_tpl="{name}Foo", 66 | no_word_wrap=True, 67 | parse_name="class", 68 | prepend=None, 69 | ), 70 | Module( 71 | body=[ 72 | Assign( 73 | targets=[ 74 | Name( 75 | id="__all__", ctx=Store(), lineno=None, col_offset=None 76 | ) 77 | ], 78 | value=List( 79 | elts=[], 80 | ctx=Load(), 81 | expr=None, 82 | ), 83 | type_comment=None, 84 | expr=None, 85 | lineno=None, 86 | ) 87 | ], 88 | stmt=None, 89 | type_ignores=[], 90 | ), 91 | ) 92 | 93 | 94 | unittest_main() 95 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_openapi_bulk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests OpenAPI 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from cdd.compound.openapi.emit import openapi 8 | from cdd.compound.openapi.utils.emit_openapi_utils import NameModelRouteIdCrud 9 | from cdd.tests.mocks.json_schema import config_schema 10 | from cdd.tests.mocks.openapi import openapi_dict 11 | from cdd.tests.mocks.routes import route_config 12 | from cdd.tests.utils_for_tests import unittest_main 13 | 14 | 15 | class TestOpenApi(TestCase): 16 | """Tests whether `NameModelRouteIdCrud` can construct a `dict`""" 17 | 18 | def test_openapi_emitter(self) -> None: 19 | """ 20 | Tests whether `openapi.emit` produces `openapi_dict` given `NameModelRouteIdCrud` 21 | """ 22 | self.assertDictEqual( 23 | openapi( 24 | ( 25 | NameModelRouteIdCrud( 26 | name=route_config["name"], 27 | model=config_schema, 28 | route=route_config["route"], 29 | id=route_config["primary_key"], 30 | crud="CRD", 31 | ), 32 | ) 33 | ), 34 | openapi_dict, 35 | ) 36 | 37 | 38 | unittest_main() 39 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_openapi_emit_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests OpenAPI emit_utils 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from cdd.tests.utils_for_tests import unittest_main 8 | 9 | 10 | class TestOpenApiEmitUtils(TestCase): 11 | """Tests whether `openapi` can construct a `dict`""" 12 | 13 | 14 | unittest_main() 15 | -------------------------------------------------------------------------------- /cdd/tests/test_compound/test_openapi_sub.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests OpenAPI bulk 3 | """ 4 | 5 | from functools import partial 6 | from os import path 7 | from os.path import extsep 8 | from tempfile import TemporaryDirectory 9 | from unittest import TestCase 10 | 11 | from cdd.compound.openapi.gen_openapi import openapi_bulk 12 | from cdd.shared.pure_utils import INIT_FILENAME 13 | from cdd.tests.mocks.openapi import openapi_dict_with_sql_types 14 | from cdd.tests.mocks.routes import ( 15 | create_route, 16 | destroy_route, 17 | read_route, 18 | route_mock_prelude, 19 | ) 20 | from cdd.tests.mocks.sqlalchemy import ( 21 | config_tbl_with_comments_str, 22 | sqlalchemy_imports_str, 23 | ) 24 | from cdd.tests.utils_for_tests import unittest_main 25 | 26 | 27 | class TestOpenApiBulk(TestCase): 28 | """Tests whether `openapi` can construct a `dict`""" 29 | 30 | def test_openapi_bulk(self) -> None: 31 | """ 32 | Tests whether `openapi_bulk` produces `openapi_dict` given `model_paths` and `routes_paths` 33 | """ 34 | with TemporaryDirectory() as tempdir: 35 | temp_dir_join = partial(path.join, tempdir) 36 | open(temp_dir_join(INIT_FILENAME), "a").close() 37 | 38 | models_filename: str = temp_dir_join( 39 | "models{extsep}py".format(extsep=extsep) 40 | ) 41 | routes_filename: str = temp_dir_join( 42 | "routes{extsep}py".format(extsep=extsep) 43 | ) 44 | 45 | with open(models_filename, "wt") as f: 46 | f.write( 47 | "\n".join((sqlalchemy_imports_str, config_tbl_with_comments_str)) 48 | ) 49 | 50 | with open(routes_filename, "wt") as f: 51 | f.write( 52 | "\n".join( 53 | (route_mock_prelude, create_route, read_route, destroy_route) 54 | ) 55 | ) 56 | 57 | gen, gold = ( 58 | openapi_bulk( 59 | app_name="rest_api", 60 | model_paths=(models_filename,), 61 | routes_paths=(routes_filename,), 62 | ), 63 | openapi_dict_with_sql_types, 64 | ) 65 | 66 | self.assertEqual( 67 | *map( 68 | lambda d: d["components"]["schemas"]["Config"]["description"], 69 | (gen, gold), 70 | ) 71 | ) 72 | 73 | self.assertDictEqual(gen, gold) 74 | 75 | 76 | unittest_main() 77 | -------------------------------------------------------------------------------- /cdd/tests/test_docstring/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for docstring parsers and emitters 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_docstring/test_emit_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.docstring` 3 | """ 4 | 5 | import ast 6 | from ast import Expr, FunctionDef, arguments 7 | from copy import deepcopy 8 | from functools import partial 9 | from unittest import TestCase 10 | 11 | import cdd.argparse_function.emit 12 | import cdd.argparse_function.parse 13 | import cdd.class_.emit 14 | import cdd.class_.parse 15 | import cdd.docstring.emit 16 | import cdd.function.emit 17 | import cdd.function.parse 18 | import cdd.json_schema.emit 19 | import cdd.shared.emit.file 20 | import cdd.sqlalchemy.emit 21 | from cdd.shared.pure_utils import omit_whitespace, reindent 22 | from cdd.shared.types import IntermediateRepr 23 | from cdd.tests.mocks.classes import class_ast 24 | from cdd.tests.mocks.docstrings import ( 25 | docstring_google_str, 26 | docstring_google_tf_ops_losses__safe_mean_str, 27 | docstring_no_default_no_nl_str, 28 | docstring_no_nl_str, 29 | docstring_numpydoc_str, 30 | ) 31 | from cdd.tests.mocks.ir import ( 32 | function_google_tf_ops_losses__safe_mean_ir, 33 | intermediate_repr, 34 | ) 35 | from cdd.tests.mocks.methods import function_google_tf_ops_losses__safe_mean_ast 36 | from cdd.tests.utils_for_tests import unittest_main 37 | 38 | 39 | class TestEmitDocstring(TestCase): 40 | """Tests emission""" 41 | 42 | def test_to_docstring(self) -> None: 43 | """ 44 | Tests whether `docstring` produces indented `docstring_str` given `class_ast` 45 | """ 46 | self.assertEqual( 47 | cdd.docstring.emit.docstring( 48 | cdd.class_.parse.class_(class_ast), emit_default_doc=True 49 | ), 50 | reindent(docstring_no_nl_str, 1), 51 | ) 52 | 53 | def test_to_docstring_emit_default_doc_false(self) -> None: 54 | """ 55 | Tests whether `docstring` produces `docstring_str` given `class_ast` 56 | """ 57 | ir: IntermediateRepr = cdd.class_.parse.class_(class_ast) 58 | self.assertEqual( 59 | cdd.docstring.emit.docstring(ir, emit_default_doc=False), 60 | reindent(docstring_no_default_no_nl_str, 1), 61 | ) 62 | 63 | def test_to_numpy_docstring(self) -> None: 64 | """ 65 | Tests whether `docstring` produces `docstring_numpydoc_str` when `docstring_format` is 'numpydoc' 66 | """ 67 | self.assertEqual( 68 | docstring_numpydoc_str, 69 | cdd.docstring.emit.docstring( 70 | deepcopy(intermediate_repr), docstring_format="numpydoc" 71 | ), 72 | ) 73 | 74 | def test_to_google_docstring(self) -> None: 75 | """ 76 | Tests whether `docstring` produces `docstring_google_str` when `docstring_format` is 'google' 77 | """ 78 | self.assertEqual( 79 | docstring_google_str, 80 | cdd.docstring.emit.docstring( 81 | deepcopy(intermediate_repr), docstring_format="google" 82 | ), 83 | ) 84 | 85 | def test_to_google_docstring_no_types(self) -> None: 86 | """ 87 | Tests whether a Google docstring is correctly generated sans types 88 | """ 89 | 90 | self.assertEqual( 91 | *map( 92 | omit_whitespace, 93 | ( 94 | docstring_google_tf_ops_losses__safe_mean_str, 95 | cdd.docstring.emit.docstring( 96 | deepcopy(function_google_tf_ops_losses__safe_mean_ir), 97 | docstring_format="google", 98 | emit_original_whitespace=True, 99 | emit_default_doc=False, 100 | word_wrap=True, 101 | ), 102 | ), 103 | ) 104 | ) 105 | 106 | def test_to_docstring_use_original_when_whitespace_only_changes(self) -> None: 107 | """ 108 | Tests whether original docstring is used when whitespace only changes are made 109 | """ 110 | 111 | self.assertEqual( 112 | *map( 113 | partial(ast.get_docstring, clean=True), 114 | map( 115 | lambda doc_str: FunctionDef( 116 | name="_", 117 | args=arguments( 118 | posonlyargs=[], 119 | args=[], 120 | kwonlyargs=[], 121 | kw_defaults=[], 122 | defaults=[], 123 | vararg=None, 124 | kwarg=None, 125 | ), 126 | body=[Expr(doc_str, lineno=None, col_offset=None)], 127 | decorator_list=[], 128 | type_params=[], 129 | lineno=None, 130 | returns=None, 131 | ), 132 | ( 133 | cdd.docstring.emit.docstring( 134 | cdd.function.parse.function( 135 | function_google_tf_ops_losses__safe_mean_ast 136 | ), 137 | docstring_format="google", 138 | emit_original_whitespace=True, 139 | emit_default_doc=False, 140 | ), 141 | docstring_google_tf_ops_losses__safe_mean_str, 142 | ), 143 | ), 144 | ) 145 | ) 146 | 147 | 148 | unittest_main() 149 | -------------------------------------------------------------------------------- /cdd/tests/test_docstring/test_parse_docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the parsers 3 | """ 4 | 5 | from collections import OrderedDict 6 | from unittest import TestCase 7 | 8 | import cdd.docstring.parse 9 | from cdd.shared.types import IntermediateRepr 10 | from cdd.tests.mocks.docstrings import ( 11 | docstring_keras_rmsprop_class_str, 12 | docstring_keras_rmsprop_method_str, 13 | docstring_reduction_v2_str, 14 | ) 15 | from cdd.tests.mocks.ir import ( 16 | docstring_keras_rmsprop_class_ir, 17 | docstring_keras_rmsprop_method_ir, 18 | ) 19 | from cdd.tests.utils_for_tests import unittest_main 20 | 21 | 22 | class TestParseDocstring(TestCase): 23 | """ 24 | Tests whether the intermediate representation is consistent when parsed from different inputs. 25 | 26 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 27 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 28 | IntermediateRepr = TypedDict("IntermediateRepr", { 29 | "name": Optional[str], 30 | "type": Optional[str], 31 | "doc": Optional[str], 32 | "params": OrderedDict[str, ParamVal], 33 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 34 | }) 35 | """ 36 | 37 | def test_from_docstring_docstring_reduction_v2_str(self) -> None: 38 | """ 39 | Test that the non-matching docstring doesn't fill out params 40 | """ 41 | ir: IntermediateRepr = cdd.docstring.parse.docstring(docstring_reduction_v2_str) 42 | self.assertEqual(ir["params"], OrderedDict()) 43 | self.assertEqual(ir["returns"], None) 44 | 45 | def test_from_docstring_keras_rmsprop_class_str(self) -> None: 46 | """Tests IR from docstring_keras_rmsprop_class_str""" 47 | 48 | self.assertDictEqual( 49 | cdd.docstring.parse.docstring(docstring_keras_rmsprop_class_str), 50 | docstring_keras_rmsprop_class_ir, 51 | ) 52 | 53 | def test_from_docstring_keras_rmsprop_class_method_str(self) -> None: 54 | """Tests IR from docstring_keras_rmsprop_method_str""" 55 | 56 | self.assertDictEqual( 57 | cdd.docstring.parse.docstring(docstring_keras_rmsprop_method_str), 58 | docstring_keras_rmsprop_method_ir, 59 | ) 60 | 61 | 62 | unittest_main() 63 | -------------------------------------------------------------------------------- /cdd/tests/test_docstring/test_parse_docstring_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for cdd.docstring.utils.parse_utils 3 | """ 4 | 5 | from collections import deque 6 | from unittest import TestCase 7 | 8 | import cdd.shared.docstring_parsers 9 | from cdd.docstring.utils.parse_utils import parse_adhoc_doc_for_typ 10 | from cdd.shared.pure_utils import pp 11 | from cdd.tests.mocks.docstrings import docstring_google_keras_tensorboard_return_str 12 | from cdd.tests.mocks.ir import class_google_keras_tensorboard_ir 13 | from cdd.tests.utils_for_tests import unittest_main 14 | 15 | 16 | class TestParseDocstringUtils(TestCase): 17 | """ 18 | Tests whether the intermediate representation is consistent when parsed from different inputs. 19 | 20 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 21 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 22 | IntermediateRepr = TypedDict("IntermediateRepr", { 23 | "name": Optional[str], 24 | "type": Optional[str], 25 | "doc": Optional[str], 26 | "params": OrderedDict[str, ParamVal], 27 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 28 | }) 29 | """ 30 | 31 | def test_parse_adhoc_doc_for_typ(self) -> None: 32 | """ 33 | Test that `parse_adhoc_doc_for_typ` works for various found-in-wild Keras variants 34 | """ 35 | pp( 36 | parse_adhoc_doc_for_typ( 37 | "Dictionary of `{str: object}` pairs, where the `str` key is the object name.", 38 | name="", 39 | default_is_none=False, 40 | ) 41 | ) 42 | deque( 43 | map( 44 | lambda output_input: self.assertEqual( 45 | output_input[0], 46 | parse_adhoc_doc_for_typ( 47 | output_input[1], name="", default_is_none=False 48 | ), 49 | ), 50 | ( 51 | ( 52 | "str", 53 | class_google_keras_tensorboard_ir["params"]["log_dir"]["doc"], 54 | ), 55 | ( 56 | "int", 57 | class_google_keras_tensorboard_ir["params"]["histogram_freq"][ 58 | "doc" 59 | ], 60 | ), 61 | ("Union[list,tuple]", "A list/tuple"), 62 | ( 63 | 'Union[Literal["batch", "epoch"], int]', 64 | class_google_keras_tensorboard_ir["params"]["update_freq"][ 65 | "doc" 66 | ], 67 | ), 68 | ( 69 | "int", 70 | "Explicit `int64`-castable monotonic step value for this summary.", 71 | ), 72 | ( 73 | "bool", 74 | cdd.shared.docstring_parsers.parse_docstring( 75 | docstring_google_keras_tensorboard_return_str 76 | )["returns"]["return_type"]["typ"], 77 | ), 78 | ( 79 | "Literal['auto', 'min', 'max']", 80 | "String. One of `{'auto', 'min', 'max'}`. In `'min'` mode,", 81 | ), 82 | ( 83 | 'Union[Literal["epoch"], int, bool]', 84 | '`"epoch"`, integer, or `False`.' 85 | 'When set to `"epoch" the callback saves the checkpoint at the end of each epoch.', 86 | ), 87 | ("Optional[int]", "Int or None, defaults to None."), 88 | ( 89 | "Literal['bfloat16', 'float16', 'float32', 'float64']", 90 | "String; `'bfloat16'`, `'float16'`, `'float32'`, or `'float64'`.", 91 | ), 92 | ("List[str]", "List of string."), 93 | ("Mapping[str, object]", "Dictionary of `{str: object}` pairs."), 94 | (None, ""), 95 | ), 96 | ), 97 | maxlen=0, 98 | ) 99 | 100 | 101 | unittest_main() 102 | -------------------------------------------------------------------------------- /cdd/tests/test_emit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the `cdd.emit` module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_emit/test_emit_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.file` 3 | """ 4 | 5 | import ast 6 | import os 7 | from importlib.util import find_spec 8 | from os.path import extsep 9 | from tempfile import TemporaryDirectory 10 | from unittest import TestCase 11 | 12 | import cdd.argparse_function.emit 13 | import cdd.argparse_function.parse 14 | import cdd.class_.emit 15 | import cdd.docstring.emit 16 | import cdd.function.emit 17 | import cdd.json_schema.emit 18 | import cdd.shared.emit.file 19 | import cdd.sqlalchemy.emit 20 | from cdd.shared.ast_utils import get_value, set_value 21 | from cdd.shared.pure_utils import emit_separating_tabs 22 | from cdd.tests.mocks.classes import class_ast 23 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 24 | 25 | 26 | class TestEmitFile(TestCase): 27 | """Tests emission""" 28 | 29 | def test_to_file(self) -> None: 30 | """ 31 | Tests whether `file` constructs a file, and fills it with the right content 32 | """ 33 | 34 | with TemporaryDirectory() as tempdir: 35 | filename: str = os.path.join( 36 | tempdir, "delete_me{extsep}py".format(extsep=extsep) 37 | ) 38 | try: 39 | cdd.shared.emit.file.file(class_ast, filename, skip_black=True) 40 | 41 | with open(filename, "rt") as f: 42 | ugly = f.read() 43 | 44 | os.remove(filename) 45 | 46 | cdd.shared.emit.file.file(class_ast, filename, skip_black=False) 47 | 48 | with open(filename, "rt") as f: 49 | blacked = f.read() 50 | 51 | self.assertNotEqual( 52 | ugly + "" if find_spec("black") is not None else "\t", blacked 53 | ) 54 | ugly_mod = ast.parse(ugly) 55 | black_mod = ast.parse(blacked) 56 | for mod in ugly_mod, black_mod: 57 | mod.body[0].body[0].value = set_value( 58 | emit_separating_tabs(get_value(mod.body[0].body[0].value)) 59 | ) 60 | run_ast_test(self, ugly_mod, black_mod) 61 | 62 | finally: 63 | if os.path.isfile(filename): 64 | os.remove(filename) 65 | 66 | 67 | unittest_main() 68 | -------------------------------------------------------------------------------- /cdd/tests/test_emit/test_emitter_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for emitter_utils """ 2 | 3 | from ast import Attribute, Call, Expr, Load, Name, Subscript, keyword 4 | from collections import OrderedDict 5 | from copy import deepcopy 6 | from operator import itemgetter 7 | from unittest import TestCase 8 | 9 | from cdd.argparse_function.utils.emit_utils import parse_out_param 10 | from cdd.docstring.utils.emit_utils import interpolate_defaults 11 | from cdd.shared.ast_utils import NoneStr, get_value, set_value 12 | from cdd.tests.mocks.argparse import ( 13 | argparse_add_argument_ast, 14 | argparse_func_ast, 15 | as_numpy_argparse_call, 16 | ) 17 | from cdd.tests.mocks.ir import intermediate_repr 18 | from cdd.tests.utils_for_tests import unittest_main 19 | 20 | 21 | class TestEmitterUtils(TestCase): 22 | """Test class for emitter_utils""" 23 | 24 | def test_parse_out_param(self) -> None: 25 | """Test that parse_out_param parses out the right dict""" 26 | # Sanity check 27 | self.assertEqual( 28 | get_value(argparse_func_ast.body[5].value.args[0]), "--as_numpy" 29 | ) 30 | 31 | self.assertDictEqual( 32 | *map( 33 | itemgetter("as_numpy"), 34 | ( 35 | OrderedDict((parse_out_param(argparse_func_ast.body[5]),)), 36 | intermediate_repr["params"], 37 | ), 38 | ) 39 | ) 40 | 41 | def test_parse_out_param_default(self) -> None: 42 | """Test that parse_out_param sets default when required and unset""" 43 | 44 | self.assertDictEqual( 45 | parse_out_param(argparse_add_argument_ast)[1], 46 | {"default": 0, "doc": None, "typ": "int"}, 47 | ) 48 | 49 | self.assertDictEqual( 50 | parse_out_param(as_numpy_argparse_call, require_default=True)[1], 51 | { 52 | "default": NoneStr, 53 | "doc": "Convert to numpy ndarrays", 54 | "typ": "Optional[bool]", 55 | }, 56 | ) 57 | 58 | def test_parse_out_param_fails(self) -> None: 59 | """Test that parse_out_param throws NotImplementedError when unsupported type given""" 60 | self.assertRaises( 61 | NotImplementedError, 62 | lambda: parse_out_param( 63 | Expr( 64 | Call( 65 | args=[set_value("--num")], 66 | func=Attribute( 67 | Name( 68 | "argument_parser", Load(), lineno=None, col_offset=None 69 | ), 70 | "add_argument", 71 | Load(), 72 | lineno=None, 73 | col_offset=None, 74 | ), 75 | keywords=[ 76 | keyword( 77 | arg="type", 78 | value=Subscript( 79 | expr_context_ctx=None, 80 | expr_slice=None, 81 | expr_value=None, 82 | lineno=None, 83 | col_offset=None, 84 | ), 85 | identifier=None, 86 | ), 87 | keyword( 88 | arg="required", 89 | value=set_value(True), 90 | identifier=None, 91 | ), 92 | ], 93 | expr=None, 94 | expr_func=None, 95 | lineno=None, 96 | col_offset=None, 97 | ), 98 | lineno=None, 99 | col_offset=None, 100 | ) 101 | ), 102 | ) 103 | 104 | def test_interpolate_defaults(self) -> None: 105 | """Test that interpolate_defaults corrects sets the default property""" 106 | param = "K", deepcopy(intermediate_repr["params"]["K"]) 107 | param_with_correct_default = deepcopy(param[1]) # type: ParamVal 108 | del param[1]["default"] 109 | self.assertDictEqual(interpolate_defaults(param)[1], param_with_correct_default) 110 | 111 | 112 | unittest_main() 113 | -------------------------------------------------------------------------------- /cdd/tests/test_emit/test_emitters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test for the `cdd.emit` module 3 | """ 4 | 5 | from os import path 6 | from unittest import TestCase 7 | 8 | from cdd.shared.emit import EMITTERS 9 | from cdd.shared.pure_utils import all_dunder_for_module 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestEmitters(TestCase): 14 | """ 15 | Tests the `cdd.emit` module magic `__all__` 16 | """ 17 | 18 | def test_emitters_root(self) -> None: 19 | """Confirm that emitter names are up-to-date""" 20 | self.assertListEqual( 21 | EMITTERS, 22 | all_dunder_for_module( 23 | path.dirname(path.dirname(path.dirname(__file__))), 24 | ( 25 | "sqlalchemy_hybrid", 26 | "sqlalchemy_table", 27 | ), 28 | ), 29 | ) 30 | 31 | 32 | unittest_main() 33 | -------------------------------------------------------------------------------- /cdd/tests/test_function/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for function parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_json_schema/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for JSON-schema parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_json_schema/test_emit_json_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.json_schema` 3 | """ 4 | 5 | from copy import deepcopy 6 | from json import load 7 | from operator import itemgetter 8 | from os import path 9 | from tempfile import TemporaryDirectory 10 | from unittest import TestCase 11 | 12 | import cdd.argparse_function.emit 13 | import cdd.argparse_function.parse 14 | import cdd.class_.emit 15 | import cdd.docstring.emit 16 | import cdd.function.emit 17 | import cdd.function.parse 18 | import cdd.json_schema.emit 19 | import cdd.shared.emit.file 20 | import cdd.sqlalchemy.emit 21 | from cdd.compound.gen_utils import get_input_mapping_from_path 22 | from cdd.json_schema.utils.shared_utils import JSON_schema 23 | from cdd.tests.mocks.ir import intermediate_repr_no_default_sql_doc 24 | from cdd.tests.mocks.json_schema import config_schema 25 | from cdd.tests.utils_for_tests import unittest_main 26 | 27 | 28 | class TestEmitJsonSchema(TestCase): 29 | """Tests emission""" 30 | 31 | def test_to_json_schema(self) -> None: 32 | """ 33 | Tests that `emit.json_schema` with `intermediate_repr_no_default_doc` produces `config_schema` 34 | """ 35 | gen_config_schema: JSON_schema = cdd.json_schema.emit.json_schema( 36 | deepcopy(intermediate_repr_no_default_sql_doc), 37 | "https://offscale.io/config.schema.json", 38 | emit_original_whitespace=True, 39 | ) 40 | self.assertEqual( 41 | *map(itemgetter("description"), (gen_config_schema, config_schema)) 42 | ) 43 | self.assertDictEqual( 44 | gen_config_schema, 45 | config_schema, 46 | ) 47 | 48 | def test_to_json_schema_early_exit(self) -> None: 49 | """ 50 | Tests that `emit.json_schema` has an early exit 51 | """ 52 | ir: dict = {"$id": "https://offscale.io/config.schema.json"} 53 | gen_config_schema: dict = cdd.json_schema.emit.json_schema(deepcopy(ir), "") 54 | self.assertDictEqual(ir, gen_config_schema) 55 | 56 | def test_to_json_schema_file(self) -> None: 57 | """ 58 | Tests that `emit.json_schema` with `intermediate_repr_no_default_doc` produces `config_schema` 59 | """ 60 | with TemporaryDirectory() as temp_dir: 61 | temp_file: str = path.join(temp_dir, "foo{}py".format(path.extsep)) 62 | cdd.json_schema.emit.json_schema_file( 63 | get_input_mapping_from_path( 64 | "function", "cdd.tests.test_compound", "test_gen_utils" 65 | ), 66 | temp_file, 67 | ) 68 | with open(temp_file, "rt") as f: 69 | temp_json_schema = load(f) 70 | self.assertDictEqual( 71 | temp_json_schema, 72 | { 73 | "$id": "https://offscale.io/f.schema.json", 74 | "$schema": "https://json-schema.org/draft/2020-12/schema", 75 | "description": ":rtype: ```str```", 76 | "properties": {"s": {"description": "str", "type": "string"}}, 77 | "required": ["s"], 78 | "type": "object", 79 | }, 80 | ) 81 | 82 | 83 | unittest_main() 84 | -------------------------------------------------------------------------------- /cdd/tests/test_json_schema/test_parse_json_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the JSON schema parser 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | import cdd.json_schema.emit 8 | import cdd.json_schema.parse 9 | from cdd.tests.mocks.ir import intermediate_repr_no_default_sql_doc 10 | from cdd.tests.mocks.json_schema import config_schema 11 | from cdd.tests.utils_for_tests import unittest_main 12 | 13 | 14 | class TestParseJsonSchema(TestCase): 15 | """ 16 | Tests whether the intermediate representation is consistent when parsed from different inputs. 17 | 18 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 19 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 20 | IntermediateRepr = TypedDict("IntermediateRepr", { 21 | "name": Optional[str], 22 | "type": Optional[str], 23 | "doc": Optional[str], 24 | "params": OrderedDict[str, ParamVal], 25 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 26 | }) 27 | """ 28 | 29 | def test_from_json_schema(self) -> None: 30 | """ 31 | Tests that `parse.json_schema` produces `intermediate_repr_no_default_sql_doc` properly 32 | """ 33 | self.assertDictEqual( 34 | cdd.json_schema.parse.json_schema(config_schema), 35 | intermediate_repr_no_default_sql_doc, 36 | ) 37 | 38 | 39 | unittest_main() 40 | -------------------------------------------------------------------------------- /cdd/tests/test_json_schema/test_parse_json_schema_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the JSON schema parser 3 | """ 4 | 5 | from copy import deepcopy 6 | from unittest import TestCase 7 | 8 | import cdd.json_schema.utils.parse_utils 9 | from cdd.tests.mocks.json_schema import server_error_schema 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestParseJsonSchemaUtils(TestCase): 14 | """ 15 | Tests whether the intermediate representation is consistent when parsed from different inputs. 16 | 17 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 18 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 19 | IntermediateRepr = TypedDict("IntermediateRepr", { 20 | "name": Optional[str], 21 | "type": Optional[str], 22 | "doc": Optional[str], 23 | "params": OrderedDict[str, ParamVal], 24 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 25 | }) 26 | """ 27 | 28 | def test_json_schema_property_to_param_anyOf(self) -> None: 29 | """ 30 | Tests that `json_schema_property_to_param` works with `anyOf` 31 | """ 32 | 33 | mock = "address", { 34 | "anyOf": [{"$ref": "#/components/schemas/address"}], 35 | "doc": "The customer's address.", 36 | "nullable": True, 37 | } # type: tuple[str, dict] 38 | 39 | res = cdd.json_schema.utils.parse_utils.json_schema_property_to_param( 40 | mock, {mock[0]: False} 41 | ) # type: tuple[str, dict] 42 | 43 | self.assertEqual(res[0], "address") 44 | self.assertDictEqual( 45 | res[1], {"typ": "Optional[Address]", "doc": mock[1]["doc"]} 46 | ) 47 | 48 | def test_json_schema_property_to_param_ref(self) -> None: 49 | """ 50 | Tests that `json_schema_property_to_param` works with `$ref` as type 51 | """ 52 | 53 | mock = "tax", { 54 | "$ref": "#/components/schemas/customer_tax" 55 | } # type: tuple[str, dict] 56 | 57 | res = cdd.json_schema.utils.parse_utils.json_schema_property_to_param( 58 | mock, {} 59 | ) # type: tuple[str, dict] 60 | 61 | self.assertEqual(res[0], "tax") 62 | self.assertDictEqual( 63 | res[1], {"doc": "[FK(CustomerTax)]", "typ": "Optional[CustomerTax]"} 64 | ) 65 | 66 | def test_json_schema_property_to_param_default_none(self) -> None: 67 | """ 68 | Tests that `json_schema_property_to_param` works with `$ref` as type 69 | """ 70 | 71 | mock = (lambda k: (k, deepcopy(server_error_schema["properties"][k])))( 72 | "error_description" 73 | ) 74 | mock[1]["default"] = None 75 | 76 | res = cdd.json_schema.utils.parse_utils.json_schema_property_to_param( 77 | mock, {mock[0]: True} 78 | ) # type: tuple[str, dict] 79 | 80 | self.assertEqual(res[0], mock[0]) 81 | self.assertDictEqual(res[1], mock[1]) 82 | 83 | res = cdd.json_schema.utils.parse_utils.json_schema_property_to_param( 84 | mock, {} 85 | ) # type: tuple[str, dict] 86 | self.assertEqual(res[0], mock[0]) 87 | self.assertDictEqual(res[1], mock[1]) 88 | 89 | def test_json_schema_property_to_param_removes_string_from_anyOf(self) -> None: 90 | """Tests that `json_schema_property_to_param` removes `string` from `anyOf`""" 91 | param = ("foo", {"anyOf": ["string", "can"], "typ": ["string", "can", "haz"]}) 92 | cdd.json_schema.utils.parse_utils.json_schema_property_to_param(param, {}) 93 | self.assertDictEqual(param[1], {"typ": "Optional[can]"}) 94 | 95 | 96 | unittest_main() 97 | -------------------------------------------------------------------------------- /cdd/tests/test_parse/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the `cdd.parse` module 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_parse/test_parsers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test for the `cdd.parse` module 3 | """ 4 | 5 | from os import path 6 | from unittest import TestCase 7 | 8 | from cdd.shared.parse import PARSERS 9 | from cdd.shared.pure_utils import all_dunder_for_module 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestParsers(TestCase): 14 | """ 15 | Tests the `cdd.parse` module magic `__all__` 16 | """ 17 | 18 | def test_parsers_root(self) -> None: 19 | """Confirm that emitter names are up-to-date""" 20 | self.assertListEqual( 21 | PARSERS, 22 | all_dunder_for_module( 23 | path.dirname(path.dirname(path.dirname(__file__))), 24 | ( 25 | "sqlalchemy_hybrid", 26 | "sqlalchemy_table", 27 | ), 28 | ), 29 | ) 30 | 31 | 32 | unittest_main() 33 | -------------------------------------------------------------------------------- /cdd/tests/test_pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for pydantic parser and emitter 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_pydantic/test_emit_pydantic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.pydantic` 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | import cdd.pydantic.emit 8 | from cdd.tests.mocks.ir import pydantic_ir 9 | from cdd.tests.mocks.pydantic import pydantic_class_cls_def 10 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 11 | 12 | 13 | class TestEmitPyDantic(TestCase): 14 | """Tests emission""" 15 | 16 | def test_to_pydantic(self) -> None: 17 | """ 18 | Tests whether `pydantic` produces `pydantic_class_cls_def` given `pydantic_ir` 19 | """ 20 | run_ast_test( 21 | self, 22 | gen_ast=cdd.pydantic.emit.pydantic(pydantic_ir), 23 | gold=pydantic_class_cls_def, 24 | ) 25 | 26 | 27 | unittest_main() 28 | -------------------------------------------------------------------------------- /cdd/tests/test_pydantic/test_parse_pydantic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the `pydantic` parser 3 | """ 4 | 5 | import ast 6 | from unittest import TestCase 7 | 8 | import cdd.pydantic.parse 9 | from cdd.shared.types import IntermediateRepr 10 | from cdd.tests.mocks.ir import pydantic_ir 11 | from cdd.tests.mocks.pydantic import pydantic_class_cls_def, pydantic_class_str 12 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 13 | 14 | 15 | class TestParsePydantic(TestCase): 16 | """ 17 | Tests whether the intermediate representation is consistent when parsed from different inputs. 18 | 19 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 20 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 21 | IntermediateRepr = TypedDict("IntermediateRepr", { 22 | "name": Optional[str], 23 | "type": Optional[str], 24 | "doc": Optional[str], 25 | "params": OrderedDict[str, ParamVal], 26 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 27 | }) 28 | """ 29 | 30 | def test_from_pydantic_roundtrip(self) -> None: 31 | """ 32 | Tests pydantic roundtrip of mocks 33 | """ 34 | run_ast_test( 35 | self, ast.parse(pydantic_class_str).body[0], pydantic_class_cls_def 36 | ) 37 | 38 | def test_from_pydantic(self) -> None: 39 | """ 40 | Tests whether `pydantic` produces `pydantic_ir` 41 | from `pydantic_class_cls_def` 42 | """ 43 | ir: IntermediateRepr = cdd.pydantic.parse.pydantic(pydantic_class_cls_def) 44 | del ir["_internal"] # Not needed for this test 45 | self.assertDictEqual(ir, pydantic_ir) 46 | 47 | 48 | unittest_main() 49 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test routes for route tests 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/test_bottle_route_emit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests route emission 3 | """ 4 | 5 | from copy import deepcopy 6 | from unittest import TestCase 7 | 8 | from cdd.routes.emit.bottle_constants_utils import ( 9 | create_route_variants, 10 | delete_route_variants, 11 | read_route_variants, 12 | ) 13 | from cdd.tests.mocks.routes import create_route, destroy_route, read_route, route_config 14 | from cdd.tests.utils_for_tests import unittest_main 15 | 16 | 17 | class TestBottleRouteEmit(TestCase): 18 | """Tests `routes.emit`""" 19 | 20 | @classmethod 21 | def setUpClass(cls) -> None: 22 | """ 23 | Setup a couple of class-wide config variables 24 | """ 25 | cls.config = deepcopy(route_config) 26 | del cls.config["primary_key"] 27 | cls.config_with_id = deepcopy(route_config) 28 | cls.config_with_id["id"] = cls.config_with_id.pop("primary_key") 29 | 30 | def test_create(self) -> None: 31 | """ 32 | Tests whether `create_route` produces the right `create_route_variants` 33 | """ 34 | self.assertEqual(create_route_variants[-1].format(**self.config), create_route) 35 | 36 | def test_read(self) -> None: 37 | """ 38 | Tests whether `read_route` produces the right `read_route_variants` 39 | """ 40 | self.assertEqual( 41 | read_route_variants[-1].format(**self.config_with_id), read_route 42 | ) 43 | 44 | def test_delete(self) -> None: 45 | """ 46 | Tests whether `destroy_route` produces the right `delete_route_variants` 47 | """ 48 | self.assertEqual( 49 | delete_route_variants[-1].format(**self.config_with_id), destroy_route 50 | ) 51 | 52 | 53 | unittest_main() 54 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/test_bottle_route_parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests route parsing 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | import cdd.routes.emit.bottle 8 | import cdd.routes.emit.bottle_constants_utils 9 | import cdd.routes.parse.bottle 10 | from cdd.tests.mocks.openapi import openapi_dict 11 | from cdd.tests.mocks.routes import ( 12 | create_route, 13 | destroy_route, 14 | read_route, 15 | route_config, 16 | route_mock_prelude, 17 | ) 18 | from cdd.tests.utils_for_tests import inspectable_compile, unittest_main 19 | 20 | 21 | class TestBottleRouteParse(TestCase): 22 | """Tests `routes.parse`""" 23 | 24 | route_id_url: str = "{route_config[route]}/{{{route_config[primary_key]}}}".format( 25 | route_config=route_config 26 | ) 27 | 28 | def test_create(self) -> None: 29 | """ 30 | Tests whether `create_route` is produced by `emit.route` 31 | """ 32 | _create_route = inspectable_compile(route_mock_prelude + create_route).create 33 | self.assertDictEqual( 34 | cdd.routes.parse.bottle.bottle(_create_route), 35 | openapi_dict["paths"][route_config["route"]]["post"], 36 | ) 37 | 38 | def test_create_util(self) -> None: 39 | """ 40 | Tests whether `create_util` is produced by `create_util` 41 | """ 42 | self.assertEqual( 43 | cdd.routes.emit.bottle.create_util( 44 | name=route_config["name"], route=route_config["route"] 45 | ), 46 | cdd.routes.emit.bottle_constants_utils.create_helper_variants[-1].format( 47 | name=route_config["name"], route=route_config["route"] 48 | ), 49 | ) 50 | 51 | def test_read(self) -> None: 52 | """ 53 | Tests whether `read_route` is produced by `emit.route` 54 | """ 55 | _read_route = inspectable_compile(route_mock_prelude + read_route).read 56 | self.assertDictEqual( 57 | cdd.routes.parse.bottle.bottle(_read_route), 58 | openapi_dict["paths"][self.route_id_url]["get"], 59 | ) 60 | 61 | def test_delete(self) -> None: 62 | """ 63 | Tests whether `destroy_route` is produced by `emit.route` 64 | """ 65 | _destroy_route = inspectable_compile(route_mock_prelude + destroy_route).destroy 66 | self.assertDictEqual( 67 | cdd.routes.parse.bottle.bottle(_destroy_route), 68 | openapi_dict["paths"][self.route_id_url]["delete"], 69 | ) 70 | 71 | 72 | unittest_main() 73 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/test_fastapi_routes_parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the FastAPI route parser 3 | """ 4 | 5 | from ast import parse 6 | from copy import deepcopy 7 | from unittest import TestCase 8 | 9 | import cdd.routes.parse.fastapi 10 | from cdd.tests.mocks.fastapi_routes import ( 11 | fastapi_post_create_config_async_func, 12 | fastapi_post_create_config_str, 13 | ) 14 | from cdd.tests.mocks.openapi import openapi_dict 15 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 16 | 17 | 18 | class TestFastApiRoutesParse(TestCase): 19 | """ 20 | Tests FastAPI route parser 21 | """ 22 | 23 | def test_from_fastapi_post_create_config(self) -> None: 24 | """ 25 | Tests whether `cdd.routes.parse.fastapi` produces `fastapi_post_create_config_str` 26 | from `fastapi_post_create_config_async_func` 27 | """ 28 | 29 | # Roundtrip sanity 30 | run_ast_test( 31 | self, 32 | parse(fastapi_post_create_config_str).body[0], 33 | fastapi_post_create_config_async_func, 34 | ) 35 | 36 | fastapi_func_resp = cdd.routes.parse.fastapi.fastapi( 37 | fastapi_post_create_config_async_func 38 | ) 39 | self.assertEqual(fastapi_func_resp[0], "/api/config") 40 | 41 | mock_api_config = deepcopy(openapi_dict["paths"]["/api/config"]) 42 | del mock_api_config["post"]["summary"], mock_api_config["post"]["requestBody"] 43 | mock_api_config["post"]["responses"].update( 44 | { 45 | 404: mock_api_config["post"]["responses"].pop("400"), 46 | 201: mock_api_config["post"]["responses"].pop("201"), 47 | } 48 | ) 49 | 50 | self.assertDictEqual(fastapi_func_resp[1], mock_api_config) 51 | 52 | 53 | unittest_main() 54 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/test_route_emit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests `cdd.routes.emit` EMITTERS 3 | """ 4 | 5 | from os import path 6 | from unittest import TestCase 7 | 8 | from cdd.routes.emit import EMITTERS 9 | from cdd.shared.pure_utils import all_dunder_for_module 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestRoutesEmit(TestCase): 14 | """Tests `cdd.routes.emit`""" 15 | 16 | def test_routes_emit_root(self) -> None: 17 | """Confirm that route emitter names are up-to-date""" 18 | self.assertListEqual( 19 | EMITTERS, 20 | all_dunder_for_module( 21 | path.join( 22 | path.dirname(path.dirname(path.dirname(__file__))), "routes", "emit" 23 | ), 24 | iter(()), 25 | path_validator=path.isfile, 26 | ), 27 | ) 28 | 29 | 30 | unittest_main() 31 | -------------------------------------------------------------------------------- /cdd/tests/test_routes/test_route_parse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests `cdd.routes.parse` PARSERS 3 | """ 4 | 5 | from itertools import filterfalse 6 | from operator import itemgetter 7 | from os import listdir, path 8 | from unittest import TestCase 9 | 10 | from cdd.routes.parse import PARSERS 11 | from cdd.tests.utils_for_tests import unittest_main 12 | 13 | 14 | class TestRoutesParse(TestCase): 15 | """Tests `cdd.routes.parse`""" 16 | 17 | def test_routes_parse_root(self) -> None: 18 | """Confirm that route parser names are up-to-date""" 19 | module_directory: str = path.join( 20 | path.dirname(path.dirname(path.dirname(__file__))), "routes", "parse" 21 | ) 22 | self.assertListEqual( 23 | PARSERS, 24 | # all_dunder_for_module(module_directory, iter(())) 25 | sorted( 26 | filterfalse( 27 | lambda name: name.startswith("_") or name.endswith("_utils"), 28 | map(itemgetter(0), map(path.splitext, listdir(module_directory))), 29 | ) 30 | ), 31 | ) 32 | 33 | 34 | unittest_main() 35 | -------------------------------------------------------------------------------- /cdd/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for setup.py 3 | """ 4 | 5 | from importlib.machinery import SourceFileLoader 6 | from importlib.util import module_from_spec, spec_from_loader 7 | from operator import methodcaller 8 | from os import path 9 | from os.path import extsep 10 | from sys import modules 11 | from unittest import TestCase 12 | from unittest.mock import patch 13 | 14 | from cdd.tests.utils_for_tests import mock_function, unittest_main 15 | 16 | 17 | class TestSetupPy(TestCase): 18 | """ 19 | Tests whether docstrings are parsed out—and emitted—correctly 20 | """ 21 | 22 | @classmethod 23 | def setUpClass(cls) -> None: 24 | """Construct the setup_py module""" 25 | cls.mod = cls.import_setup_py() 26 | 27 | @staticmethod 28 | def import_setup_py(): 29 | """ 30 | Function which imports setup.py as a module 31 | 32 | :return: setup.py as a module 33 | :rtype: ```Union[module, ModuleSpec]``` 34 | """ 35 | modname: str = "setup_py" 36 | loader = SourceFileLoader( 37 | modname, 38 | path.join( 39 | path.dirname(path.dirname(path.dirname(__file__))), 40 | "setup{extsep}py".format(extsep=extsep), 41 | ), 42 | ) 43 | modules[modname] = module_from_spec(spec_from_loader(loader.name, loader)) 44 | loader.exec_module(modules[modname]) 45 | return modules[modname] 46 | 47 | def test_properties(self) -> None: 48 | """ 49 | Tests whether 'setup.py' has correct properties 50 | """ 51 | self.assertEqual(getattr(self.mod, "package_name"), "cdd") 52 | self.assertEqual(self.mod.__name__, "setup_py") 53 | 54 | def test_main(self) -> None: 55 | """ 56 | Tests that no errors occur in `main` function call (up to `setup()`, which is tested in setuptools) 57 | """ 58 | with patch("setup_py.setup", mock_function): 59 | self.assertIsNone(self.mod.main()) 60 | 61 | def test_setup_py_main(self) -> None: 62 | """ 63 | Tests that `__name__ == __main__` calls the `main` function via `setup_py_main` call 64 | """ 65 | 66 | with patch("setup_py.main", mock_function), patch( 67 | "setup_py.__name__", "__main__" 68 | ): 69 | self.assertIsNone(self.mod.setup_py_main()) 70 | 71 | 72 | unittest_main() 73 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for shared functions and classes 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/test_cst.py: -------------------------------------------------------------------------------- 1 | """ Tests for cst """ 2 | 3 | from os import extsep, path 4 | from unittest import TestCase 5 | 6 | from cdd.shared.cst import cst_parse 7 | from cdd.tests.mocks.cst import cstify_cst 8 | from cdd.tests.utils_for_tests import unittest_main 9 | 10 | 11 | class TestCst(TestCase): 12 | """Test class for cst""" 13 | 14 | def test_cstify_file(self) -> None: 15 | """Tests that `handle_multiline_comment` can do its namesake""" 16 | with open( 17 | path.join( 18 | path.dirname(__file__), 19 | "../mocks", 20 | "cstify{extsep}py".format(extsep=extsep), 21 | ), 22 | "rt", 23 | ) as f: 24 | cst = cst_parse(f.read()) 25 | 26 | self.assertTupleEqual(cst, cstify_cst) 27 | 28 | 29 | unittest_main() 30 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/test_cst_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for cst_utils """ 2 | 3 | from unittest import TestCase 4 | 5 | from cdd.shared.cst_utils import ( 6 | DictExprStatement, 7 | GenExprStatement, 8 | ListCompStatement, 9 | SetExprStatement, 10 | UnchangingLine, 11 | infer_cst_type, 12 | ) 13 | from cdd.tests.utils_for_tests import unittest_main 14 | 15 | 16 | class TestCstUtils(TestCase): 17 | """Test class for cst_utils""" 18 | 19 | def test_infer_cst_type(self) -> None: 20 | """Test that `infer_cst_type` can infer the right CST type""" 21 | for input_str, expected_type in ( 22 | ("foo", UnchangingLine), 23 | ("()", GenExprStatement), 24 | ("[i for i in ()]", ListCompStatement), 25 | ("{i for i in ()}", SetExprStatement), 26 | ("{i:i for i in ()}", DictExprStatement), 27 | ): 28 | self.assertEqual( 29 | expected_type, 30 | infer_cst_type( 31 | input_str, 32 | words=tuple(filter(None, map(str.strip, input_str.split(" ")))), 33 | ), 34 | ) 35 | 36 | 37 | unittest_main() 38 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/test_default_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for default utils """ 2 | 3 | from unittest import TestCase 4 | 5 | from cdd.shared.defaults_utils import extract_default, set_default_doc 6 | from cdd.tests.utils_for_tests import unittest_main 7 | 8 | 9 | class TestDefaultUtils(TestCase): 10 | """Test class for default utils""" 11 | 12 | def test_extract_default(self) -> None: 13 | """Tests that `extract_default` produces the expected output""" 14 | sample: str = "This defaults to foo." 15 | self.assertTupleEqual(extract_default(sample), (sample, "foo")) 16 | self.assertTupleEqual( 17 | extract_default(sample, emit_default_doc=False), ("This", "foo") 18 | ) 19 | 20 | def test_extract_default_middle(self) -> None: 21 | """Tests that `extract_default` produces the expected output""" 22 | sample: str = "Why would you. Have this defaults to something. In the middle?" 23 | default: str = "something" 24 | self.assertTupleEqual(extract_default(sample), (sample, default)) 25 | 26 | self.assertTupleEqual( 27 | extract_default(sample, rstrip_default=False, emit_default_doc=False), 28 | ("Why would you. Have this. In the middle?", default), 29 | ) 30 | self.assertTupleEqual( 31 | extract_default(sample, rstrip_default=True, emit_default_doc=False), 32 | ("Why would you. Have thisIn the middle?", default), 33 | ) 34 | 35 | def test_extract_default_with_dot(self) -> None: 36 | """Tests that `extract_default` works when there is a `.` in the default""" 37 | sample: str = "This. defaults to (np.empty(0), np.empty(0))" 38 | self.assertTupleEqual( 39 | extract_default(sample, emit_default_doc=False), 40 | ("This.", "(np.empty(0), np.empty(0))"), 41 | ) 42 | 43 | def test_extract_default_with_int(self) -> None: 44 | """Tests that `extract_default` works for an integer default""" 45 | sample: str = "learning rate. Defaults to 0001." 46 | self.assertTupleEqual( 47 | extract_default(sample, emit_default_doc=False), 48 | ("learning rate.", 1), 49 | ) 50 | 51 | def test_extract_default_with_float(self) -> None: 52 | """Tests that `extract_default` works when there is a `.` in the default referring to a decimal place""" 53 | sample: str = "learning rate. Defaults to 0.001." 54 | self.assertTupleEqual( 55 | extract_default(sample, emit_default_doc=False), 56 | ("learning rate.", 0.001), 57 | ) 58 | 59 | def test_extract_default_with_bool(self) -> None: 60 | """Tests that `extract_default` works for an integer default""" 61 | sample = ( 62 | "Boolean. Whether to apply AMSGrad variant of this algorithm from" 63 | 'the paper "On the Convergence of Adam and beyond". Defaults to `True`.' 64 | ) 65 | self.assertTupleEqual( 66 | extract_default(sample, emit_default_doc=True), 67 | (sample, True), 68 | ) 69 | 70 | def test_extract_default_with_parens(self) -> None: 71 | """Tests that `extract_default` works when wrapped in parentheses""" 72 | sample: str = "learning rate (default: 1)" 73 | self.assertTupleEqual( 74 | extract_default(sample, emit_default_doc=True), 75 | (sample, 1), 76 | ) 77 | 78 | sample = ( 79 | "tolerance_change (float): termination tolerance on function\n" 80 | " value/parameter changes (default: 1e-9)." 81 | ) 82 | self.assertTupleEqual( 83 | extract_default(sample, emit_default_doc=True), 84 | (sample, 1e-9), 85 | ) 86 | 87 | def test_extract_default_with_many_parens(self) -> None: 88 | """Tests that `extract_default` works when default parses to an AST type""" 89 | sample: str = ( 90 | "betas (Tuple[float, float], optional): coefficients used for computing\n" 91 | " running averages of gradient and its square (default: (0.9, 0.999))" 92 | ) 93 | default: str = "(0.9, 0.999)" 94 | self.assertTupleEqual( 95 | extract_default(sample, emit_default_doc=True), 96 | (sample, default), 97 | ) 98 | self.assertTupleEqual( 99 | extract_default(sample, emit_default_doc=False), 100 | ( 101 | "betas (Tuple[float, float], optional): coefficients used for computing\n" 102 | " running averages of gradient and its square", 103 | default, 104 | ), 105 | ) 106 | 107 | def test_extract_default_with_ast_default(self) -> None: 108 | """Tests that `extract_default` works when default parses to an AST type""" 109 | sample = ( 110 | "maximal number of function evaluations per optimization\n" 111 | " step (default: max_iter * 1.25)." 112 | ) 113 | self.assertTupleEqual( 114 | extract_default(sample, emit_default_doc=True), 115 | (sample, "max_iter * 1.25"), 116 | ) 117 | 118 | def test_set_default_doc_none(self) -> None: 119 | """Tests that `set_default_doc` does nop whence no doc in param""" 120 | name_param = "foo", {} 121 | self.assertTupleEqual(set_default_doc(name_param), name_param) 122 | 123 | 124 | unittest_main() 125 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/test_pkg_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for pkg utils """ 2 | 3 | from operator import eq 4 | from os import path 5 | from site import getsitepackages 6 | from unittest import TestCase 7 | 8 | from cdd.shared.pkg_utils import get_python_lib, relative_filename 9 | from cdd.shared.pure_utils import rpartial 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestPkgUtils(TestCase): 14 | """Test class for pkg utils""" 15 | 16 | def test_relative_filename(self) -> None: 17 | """Tests relative_filename ident""" 18 | expect: str = "gaffe" 19 | self.assertEqual(relative_filename(expect), expect) 20 | 21 | def test_get_python_lib(self) -> None: 22 | """Tests that `get_python_lib` works""" 23 | python_lib: str = get_python_lib() 24 | site_packages: str = getsitepackages()[0] 25 | site_packages: str = next( 26 | filter( 27 | rpartial(eq, python_lib), 28 | ( 29 | lambda two_dir_above: ( 30 | site_packages, 31 | two_dir_above, 32 | path.join(site_packages, "Lib", "site-packages"), 33 | path.join(two_dir_above, "python3", "dist-packages"), 34 | ) 35 | )(path.dirname(path.dirname(site_packages))), 36 | ), 37 | site_packages, 38 | ) 39 | self.assertEqual(site_packages, python_lib) 40 | 41 | 42 | unittest_main() 43 | -------------------------------------------------------------------------------- /cdd/tests/test_shared/test_source_transformer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for source_transformer 3 | """ 4 | 5 | from ast import FunctionDef, Pass, arguments 6 | from unittest import TestCase 7 | 8 | from cdd.shared.pure_utils import tab 9 | from cdd.shared.source_transformer import to_code 10 | from cdd.tests.utils_for_tests import unittest_main 11 | 12 | 13 | class TestSourceTransformer(TestCase): 14 | """ 15 | Tests for source_transformer 16 | """ 17 | 18 | def test_to_code(self) -> None: 19 | """ 20 | Tests to_source in Python 3.9 and < 3.9 21 | """ 22 | func_def: FunctionDef = FunctionDef( 23 | name="funcy", 24 | args=arguments( 25 | posonlyargs=[], 26 | args=[], 27 | kwonlyargs=[], 28 | kw_defaults=[], 29 | defaults=[], 30 | vararg=None, 31 | kwarg=None, 32 | ), 33 | body=[Pass()], 34 | decorator_list=[], 35 | type_params=[], 36 | lineno=None, 37 | returns=None, 38 | ) 39 | 40 | self.assertEqual( 41 | to_code(func_def).rstrip("\n"), 42 | "def funcy():\n" "{tab}pass".format(tab=tab), 43 | ) 44 | 45 | 46 | unittest_main() 47 | -------------------------------------------------------------------------------- /cdd/tests/test_sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for SQLalchemy parsers and emitters 3 | """ 4 | -------------------------------------------------------------------------------- /cdd/tests/test_sqlalchemy/test_emit_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `cdd.emit.sqlalchemy` 3 | """ 4 | 5 | from copy import deepcopy 6 | from platform import system 7 | from unittest import TestCase 8 | 9 | import cdd.argparse_function.emit 10 | import cdd.argparse_function.parse 11 | import cdd.class_.emit 12 | import cdd.docstring.emit 13 | import cdd.function.emit 14 | import cdd.json_schema.emit 15 | import cdd.shared.emit.file 16 | import cdd.sqlalchemy.emit 17 | from cdd.shared.types import IntermediateRepr 18 | from cdd.tests.mocks.ir import ( 19 | intermediate_repr_empty, 20 | intermediate_repr_no_default_sql_doc, 21 | ) 22 | from cdd.tests.mocks.sqlalchemy import ( 23 | config_decl_base_ast, 24 | config_hybrid_ast, 25 | config_tbl_with_comments_ast, 26 | empty_with_inferred_pk_column_assign, 27 | ) 28 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 29 | 30 | 31 | class TestEmitSqlAlchemy(TestCase): 32 | """Tests emission""" 33 | 34 | def test_to_sqlalchemy_table(self) -> None: 35 | """ 36 | Tests that `emit.sqlalchemy_table` with `intermediate_repr_no_default_sql_doc` produces `config_tbl_ast` 37 | """ 38 | gen_ast = cdd.sqlalchemy.emit.sqlalchemy_table( 39 | deepcopy(intermediate_repr_no_default_sql_doc), name="config_tbl" 40 | ) 41 | run_ast_test( 42 | self, 43 | gen_ast=gen_ast, 44 | gold=config_tbl_with_comments_ast, 45 | ) 46 | 47 | def test_to_sqlalchemy_hybrid(self) -> None: 48 | """ 49 | Tests that `emit.sqlalchemy_table` with `intermediate_repr_no_default_sql_doc` produces `config_tbl_ast` 50 | """ 51 | for class_name in (None, "Config"): 52 | ir: IntermediateRepr = deepcopy(intermediate_repr_no_default_sql_doc) 53 | ir["name"] = "Config" 54 | gen_ast = cdd.sqlalchemy.emit.sqlalchemy_hybrid( 55 | ir, 56 | table_name="config_tbl", 57 | class_name=class_name, 58 | emit_repr=False, 59 | emit_create_from_attr=False, 60 | ) 61 | run_ast_test( 62 | self, 63 | gen_ast=gen_ast, 64 | gold=config_hybrid_ast, 65 | ) 66 | 67 | def test_to_sqlalchemy_table_with_inferred_pk(self) -> None: 68 | """ 69 | Tests that `emit.sqlalchemy_table` with `intermediate_repr_no_default_sql_doc` produces `config_tbl_ast` 70 | """ 71 | sql_table = cdd.sqlalchemy.emit.sqlalchemy_table( 72 | deepcopy(intermediate_repr_empty), name="empty_with_inferred_pk_tbl" 73 | ) 74 | run_ast_test( 75 | self, 76 | sql_table, 77 | gold=empty_with_inferred_pk_column_assign, 78 | ) 79 | 80 | def test_to_sqlalchemy(self) -> None: 81 | """ 82 | Tests that `emit.sqlalchemy` with `intermediate_repr_no_default_sql_doc` produces `config_tbl_ast` 83 | """ 84 | system() in frozenset(("Darwin", "Linux")) and print("test_to_sqlalchemy") 85 | 86 | ir: IntermediateRepr = deepcopy(intermediate_repr_no_default_sql_doc) 87 | ir["name"] = "Config" 88 | gen_ast = cdd.sqlalchemy.emit.sqlalchemy( 89 | ir, 90 | # class_name="Config", 91 | table_name="config_tbl", 92 | ) 93 | run_ast_test( 94 | self, 95 | gen_ast=gen_ast, 96 | gold=config_decl_base_ast, 97 | ) 98 | 99 | 100 | unittest_main() 101 | -------------------------------------------------------------------------------- /cdd/tests/test_sqlalchemy/test_parse_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the Intermediate Representation produced by the SQLalchemy parsers 3 | """ 4 | 5 | import ast 6 | from copy import deepcopy 7 | from unittest import TestCase 8 | 9 | import cdd.sqlalchemy.parse 10 | from cdd.shared.types import IntermediateRepr 11 | from cdd.tests.mocks.ir import ( 12 | intermediate_repr_no_default_sql_with_sql_types, 13 | intermediate_repr_node_pk, 14 | ) 15 | from cdd.tests.mocks.sqlalchemy import ( 16 | config_decl_base_ast, 17 | config_decl_base_str, 18 | config_tbl_with_comments_ast, 19 | config_tbl_with_comments_str, 20 | foreign_sqlalchemy_tbls_mod, 21 | foreign_sqlalchemy_tbls_str, 22 | node_pk_tbl_ass, 23 | ) 24 | from cdd.tests.utils_for_tests import run_ast_test, unittest_main 25 | 26 | 27 | class TestParseSqlAlchemy(TestCase): 28 | """ 29 | Tests whether the intermediate representation is consistent when parsed from different inputs. 30 | 31 | IR is a dictionary consistent with `IntermediateRepr`, defined as: 32 | ParamVal = TypedDict("ParamVal", {"typ": str, "doc": Optional[str], "default": Any}) 33 | IntermediateRepr = TypedDict("IntermediateRepr", { 34 | "name": Optional[str], 35 | "type": Optional[str], 36 | "doc": Optional[str], 37 | "params": OrderedDict[str, ParamVal], 38 | "returns": Optional[OrderedDict[Literal["return_type"], ParamVal]], 39 | }) 40 | """ 41 | 42 | def test_from_sqlalchemy_table(self) -> None: 43 | """ 44 | Tests that `parse.sqlalchemy_table` produces `intermediate_repr_no_default_sql_doc` properly 45 | """ 46 | 47 | # Sanity check 48 | run_ast_test( 49 | self, 50 | config_tbl_with_comments_ast, 51 | gold=ast.parse(config_tbl_with_comments_str).body[0], 52 | ) 53 | 54 | for variant in ( 55 | config_tbl_with_comments_str, 56 | config_tbl_with_comments_str.replace( 57 | "config_tbl =", "config_tbl: Table =", 1 58 | ), 59 | config_tbl_with_comments_str.replace("config_tbl =", "", 1).lstrip(), 60 | ): 61 | ir: IntermediateRepr = cdd.sqlalchemy.parse.sqlalchemy_table( 62 | ast.parse(variant).body[0] 63 | ) 64 | self.assertEqual(ir["name"], "config_tbl") 65 | ir["name"] = None 66 | self.assertDictEqual(ir, intermediate_repr_no_default_sql_with_sql_types) 67 | 68 | def test_from_sqlalchemy(self) -> None: 69 | """ 70 | Tests that `parse.sqlalchemy` produces `intermediate_repr_no_default_sql_doc` properly 71 | """ 72 | 73 | # Sanity check 74 | run_ast_test( 75 | self, 76 | config_decl_base_ast, 77 | gold=ast.parse(config_decl_base_str).body[0], 78 | ) 79 | 80 | for ir in ( 81 | cdd.sqlalchemy.parse.sqlalchemy(deepcopy(config_decl_base_ast)), 82 | cdd.sqlalchemy.parse.sqlalchemy_hybrid(deepcopy(config_decl_base_ast)), 83 | ): 84 | self.assertEqual(ir["name"], "config_tbl") 85 | ir["name"] = None 86 | self.assertDictEqual(ir, intermediate_repr_no_default_sql_with_sql_types) 87 | 88 | def test_from_sqlalchemy_with_foreign_rel(self) -> None: 89 | """Test from SQLalchemy with a foreign key relationship""" 90 | # Sanity check 91 | run_ast_test( 92 | self, 93 | foreign_sqlalchemy_tbls_mod, 94 | gold=ast.parse(foreign_sqlalchemy_tbls_str), 95 | ) 96 | ir: IntermediateRepr = cdd.sqlalchemy.parse.sqlalchemy_table( 97 | deepcopy(node_pk_tbl_ass) 98 | ) 99 | self.assertDictEqual(ir, intermediate_repr_node_pk) 100 | 101 | 102 | unittest_main() 103 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Note: no dependencies are required on Python >3.10; `black` is optional; `pyyaml` is mostly optional 2 | https://api.github.com/repos/offscale/astor/zipball/empty-annassign#egg=astor ; python_version < "3.9" 3 | black 4 | pyyaml 5 | setuptools 6 | typing-extensions ; python_version < "3.11" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | setup.py implementation, interesting because it parsed the first __init__.py and 5 | extracts the `__author__` and `__version__` 6 | """ 7 | 8 | import sys 9 | from ast import Assign, Name, parse 10 | from functools import partial 11 | from operator import attrgetter 12 | from os import path 13 | from os.path import extsep 14 | 15 | from setuptools import find_packages, setup 16 | 17 | if sys.version_info[:2] >= (3, 12): 18 | from ast import Del as Str 19 | else: 20 | from ast import Str 21 | 22 | if sys.version_info[0] == 2: 23 | from itertools import ifilter as filter 24 | from itertools import imap as map 25 | 26 | if sys.version_info[:2] > (3, 7): 27 | from ast import Constant 28 | else: 29 | from ast import expr 30 | 31 | # Constant. Will never be used in Python =< 3.8 32 | Constant = type("Constant", (expr,), {}) 33 | 34 | 35 | package_name = "cdd" 36 | 37 | with open( 38 | path.join(path.dirname(__file__), "README{extsep}md".format(extsep=extsep)), 39 | "rt", 40 | encoding="utf-8", 41 | ) as fh: 42 | long_description = fh.read() 43 | 44 | 45 | def main(): 46 | """Main function for setup.py; this actually does the installation""" 47 | with open( 48 | path.join( 49 | path.abspath(path.dirname(__file__)), 50 | package_name, 51 | "__init__{extsep}py".format(extsep=extsep), 52 | ) 53 | ) as f: 54 | parsed_init = parse(f.read()) 55 | 56 | __author__, __version__, __description__ = map( 57 | lambda node: node.value if isinstance(node, Constant) else node.s, 58 | filter( 59 | lambda node: isinstance(node, (Constant, Str)), 60 | map( 61 | attrgetter("value"), 62 | filter( 63 | lambda node: isinstance(node, Assign) 64 | and any( 65 | filter( 66 | lambda name: isinstance(name, Name) 67 | and name.id 68 | in frozenset( 69 | ("__author__", "__version__", "__description__") 70 | ), 71 | node.targets, 72 | ) 73 | ), 74 | parsed_init.body, 75 | ), 76 | ), 77 | ), 78 | ) 79 | 80 | setup( 81 | name=package_name, 82 | author=__author__, 83 | author_email="807580+SamuelMarks@users.noreply.github.com", 84 | version=__version__, 85 | url="https://github.com/offscale/{}".format(package_name), 86 | description=__description__, 87 | long_description=long_description, 88 | long_description_content_type="text/markdown", 89 | classifiers=[ 90 | "Development Status :: 3 - Alpha", 91 | "Environment :: Console", 92 | "Intended Audience :: Developers", 93 | "License :: OSI Approved", 94 | "License :: OSI Approved :: Apache Software License", 95 | "License :: OSI Approved :: MIT License", 96 | "Natural Language :: English", 97 | "Operating System :: OS Independent", 98 | "Programming Language :: Python :: 3 :: Only", 99 | "Programming Language :: Python :: 3.6", 100 | "Programming Language :: Python :: 3.7", 101 | "Programming Language :: Python :: 3.8", 102 | "Programming Language :: Python :: 3.9", 103 | "Programming Language :: Python :: 3.10", 104 | "Programming Language :: Python :: 3.11", 105 | "Programming Language :: Python :: 3.12", 106 | "Programming Language :: Python :: 3.13", 107 | "Programming Language :: Python :: Implementation", 108 | "Programming Language :: Python :: Implementation :: CPython", 109 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", 110 | "Topic :: Software Development", 111 | "Topic :: Software Development :: Build Tools", 112 | "Topic :: Software Development :: Code Generators", 113 | "Topic :: Software Development :: Compilers", 114 | "Topic :: Software Development :: Pre-processors", 115 | ], 116 | license="(Apache-2.0 OR MIT)", 117 | license_files=["LICENSE-APACHE", "LICENSE-MIT"], 118 | install_requires=["pyyaml"], 119 | test_suite="{}{}tests".format(package_name, path.extsep), 120 | packages=find_packages(), 121 | python_requires=">=3.6", 122 | ) 123 | 124 | 125 | def setup_py_main(): 126 | """Calls main if `__name__ == '__main__'`""" 127 | if __name__ == "__main__": 128 | main() 129 | 130 | 131 | setup_py_main() 132 | --------------------------------------------------------------------------------